Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,28 @@ jobs:
fi
npm test

# §2 gate: built output MUST be free of runtime code generation. dist/
# already exists by here — `prepare` is the build, so `npm ci` above
# produces it — and the scan must see the esbuild bundle, not the source.
#
# Measured clean when this was added: 1 dist file
# (markdownwizard-tools.iife.js, 46,037 bytes), `eval-free: OK`. It goes
# on green because nothing eval-shaped reaches the bundle, NOT because
# the scan is lenient — planting one `new Function(` in dist makes it
# exit 1.
#
# SCOPE, STATED SO A GREEN RUN IS NOT READ AS MORE THAN IT IS. This
# package's `files` list ships BOTH `src` and `dist`, and its default
# export is `"." : "./src/index.js"` — so a consumer doing
# `import … from 'markdownwizard-tools'` gets the SOURCE, and only the
# `./iife` subpath gets what this scan covers. src/ scans clean today
# (all 8 files, exit 0), so nothing is hiding there; whether §2's target
# should widen for a package whose default export is source is a
# standards question, raised in tgwab-standards#173 rather than decided
# here. Widening this to `dist src` is a one-word change and is green.
- name: No runtime code generation in built output (§2)
run: ./scripts/no-eval.sh dist

# Full tree, deliberately not --omit=dev (DS §15): the dev half is the
# build toolchain, and what it writes into dist/ is what ships. Excluding it
# blinds the gate to the half whose compromise reaches users. Narrowing this
Expand Down
38 changes: 38 additions & 0 deletions scripts/no-eval.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# TGWAB eval gate—DEV-STANDARDS §2.
#
# Place at: scripts/no-eval.sh · CI: ./scripts/no-eval.sh dist
#
# The §12 CSP never carries 'unsafe-eval', which blocks eval(), new Function(),
# bare Function("…"), and string-form setTimeout/setInterval/setImmediate. Anything
# this finds is code that will throw at runtime for a real user.
#
# Runs against BUILT output, not source: esbuild preserves the `eval` identifier
# through minification, so the check survives bundling.
#
# Escape hatch (§2): a vendor file that genuinely cannot be made eval-free goes in
# .eval-allowlist as a path prefix with a one-line reason, AND as a README deviation.
# The CSP MUST NOT be loosened instead.
set -euo pipefail

TARGET="${1:-dist}"
ALLOW=".eval-allowlist"

PATTERN='(^|[^A-Za-z0-9_$])eval[[:space:]]*\(|new[[:space:]]+Function[[:space:]]*\(|(^|[^A-Za-z0-9_$.])Function[[:space:]]*\([[:space:]]*["'"'"'`]|set(Timeout|Interval|Immediate)[[:space:]]*\([[:space:]]*["'"'"'`]'

hits="$(grep -nEr "$PATTERN" \
--include='*.js' --include='*.mjs' --include='*.cjs' --include='*.html' \
"$TARGET" || true)"

if [ -s "$ALLOW" ]; then
hits="$(printf '%s\n' "$hits" \
| grep -vFf <(grep -v '^[[:space:]]*#' "$ALLOW" | grep -v '^[[:space:]]*$') || true)"
fi

if [ -n "$hits" ]; then
printf '::error::runtime code-generation found in %s (CSP has no %s)\n' "$TARGET" "'unsafe-eval'"
printf '%s\n' "$hits"
exit 1
fi

printf 'eval-free: OK (%s)\n' "$TARGET"
Loading