Coherent 5-edit Wave 1 GREEN landing per Plan 04-02 Task 2; RED gate from
Task 1 (`tests/build/no-new-function-in-sw-chunk.test.ts` 1-hit assertion)
flips GREEN with 0 hits of `new Function` in any SW chunk
(`dist/assets/index.ts-*.js` glob).
## Threat T-04-02-01 mitigation (Elevation of Privilege — `new Function` literal)
Three layered mechanisms cooperate to drop the CSP-unsafe `new Function`
literal from the SW chunk while preserving JSZip's zip-assembly correctness
end-to-end (REVISION iter-2 WARNING 1 empirically pinned at UAT harness 33/33):
1. **Runtime polyfill prelude** at top-of-module of `src/background/index.ts`
(BEFORE the first `import`): an inline `queueMicrotask`-based polyfill
installs `globalThis.setImmediate` at SW boot. JSZip's pre-bundled
`dist/jszip.min.js` IIFE guards its internal setimmediate polyfill behind
`if(!s.setImmediate){...}`, so the upstream offending body never executes
at runtime once our prelude has installed the safe fast-path.
2. **`vite-plugin-node-polyfills` `exclude: ['setimmediate']`** in vite.config.ts:
prevents the plugin from injecting its node-stdlib-browser-aliased
setimmediate polyfill into the chunk. NOTE: this alone is insufficient
because JSZip's `dist/jszip.min.js` ships its OWN bundled-in setimmediate
(via the package.json `"browser"` field that maps `./lib/index` →
`./dist/jszip.min.js`); the plugin's `exclude` only filters the plugin's
own contributions.
3. **`resolve.alias.setimmediate`** redirects bare-specifier `setimmediate`
requires to `src/shared/setimmediate-stub.ts` (a 22-LOC TS module that
installs the same `queueMicrotask`-based polyfill via side-effect import).
This catches any future direct `import 'setimmediate'` consumer that
bypasses the prelude.
4. **`stripSetimmediateNewFunction()` Rollup post-transform plugin** in
vite.config.ts: surgically replaces the single occurrence of
`(I=new Function(""+I))` with `(I=function(){})` in any output chunk
that contains the JSZip-bundled setimmediate IIFE. The replacement is
observably equivalent in our codepath (the parent `typeof I!="function"&&`
guard means the body never runs when I is already a function — which is
the only form JSZip ever uses — AND the runtime prelude makes the entire
IIFE body unreachable regardless). Without this plugin, JSZip's
pre-bundled distribution embeds the upstream setimmediate package's
`setImmediate.js` verbatim inside its internal CJS module registry
(slot 54), unreachable by Vite's resolve.alias or the polyfill plugin's
exclude.
## Architecture decision log
**Option α (force JSZip unbundled `lib/index.js` via `resolve.alias.jszip`)
was attempted and reverted 2026-05-21** (between commits 630d40c and this).
Empirically broke UAT harness A30+ because the unbundled entry's transitive
readable-stream-browser browser-field mapping did not propagate correctly
through Vite's resolver — the async zip-write pipeline silently produced
an empty events.json. The post-transform plugin (Option β) is the
minimum-surface fix that preserves JSZip's runtime behavior verbatim while
satisfying the textual `new Function` count = 0 invariant.
## Verification
**Build / static gates:**
- `npm run build` exits 0; SW chunk `dist/assets/index.ts-DfBxWCT9.js`
(378.92 kB) contains 0 occurrences of `new Function` (was 1 in pre-fix
`index.ts-8LkXuqac.js`).
- `npx tsc --noEmit` exits 0.
- `grep -rn 'permissions.request' src/` returns 0 hits (Plan 04-02 ROADMAP
SC #4 regression pin GREEN).
- `node generate-icons.cjs` exits 0; old `generate-icons.js` no longer
exists (rename via `git mv` preserves history).
- `grep -c "exclude: \\['setimmediate'\\]" vite.config.ts` returns 1.
- `grep -c "queueMicrotask" src/background/index.ts` returns ≥1.
- `grep -c "Resolved in Phase 4 Plan 04-02" .planning/phases/01-stabilize-video-pipeline/deferred-items.md` returns ≥1.
**Test gates:**
- Focused: `npm test -- tests/build/no-new-function-in-sw-chunk.test.ts tests/build/dead-code-grep.test.ts --run` → 3/3 GREEN (Task 1's RED gate flipped GREEN).
- Full vitest: 183/183 GREEN on the clean run (180 baseline + 3 net new
from Plan 04-02 Task 1's two new files). Pre-existing intermittent flakes
per 04-01-SUMMARY Issues Encountered (blob-url-download / webm-remux /
webm-playback ffmpeg dry-run) persist across SUMMARY runs and are owned
by Plan 04-03.
**Pre-checkpoint bundle gates (per saved memory feedback-pre-checkpoint-bundle-gates.md):**
1. Tier-1 FORBIDDEN_HOOK_STRINGS: 13/13 tests GREEN; inventory unchanged at
12 strings (Plan 04-02 added no harness hooks).
2. SW CSP-safety grep: `grep -rn 'new Function\\|eval(' dist/assets/` returns
0 hits — polarity flipped from the pre-existing 1 documented exception
(the setimmediate literal). T-04-02-01 mitigation pin lands.
3. Node-globals: `Buffer.copy / .isView / .length / .push / .shift / .slice
/ .write` in SW chunk (pre-existing JSZip internals; unchanged from
04-01-SUMMARY).
4. DOM-globals: `document.createElement / .createTextNode / .documentElement
/ .F` + `window.Math / .console / .localStorage / .process` (pre-existing
JSZip text encoder fallback paths; unchanged from 04-01-SUMMARY).
5. manifest.json: present, MV3, `name: __MSG_extName__` (chrome.i18n intact).
**Empirical UAT harness (REVISION iter-2 WARNING 1):**
- `HEADLESS=1 SKIP_PROD_REBUILD=0 npm run test:uat` → 33/33 assertions
passed (verbatim `UAT harness: 33/33 assertions passed` in stdout).
Confirms JSZip's full SAVE → zip pipeline (A24-A32 inclusive, exercising
the in-memory MediaRecorder segments + base64 port wire + remux + zip
assembly + chrome.downloads + events.json + meta.json + screenshot)
operates correctly under the new bundle. The setimmediate polyfill
replacement preserves zip-write behavior end-to-end at the empirical
layer.
## Files
- **vite.config.ts**: imports `node:url` (fileURLToPath/URL) + `Plugin`
type from vite; adds `nodePolyfills.exclude: ['setimmediate']`;
adds `resolve.alias.setimmediate` → `src/shared/setimmediate-stub.ts`;
adds `stripSetimmediateNewFunction()` Rollup post-transform plugin
with full rationale comment.
- **src/background/index.ts**: 17-line top-of-module prelude inserted
BEFORE the first `import { Logger } ...` line. Inline `queueMicrotask`-based
setimmediate polyfill with typed widening cast (no `as any` per
CLAUDE.md). Reversible by `git revert`.
- **src/shared/setimmediate-stub.ts** (NEW): 50-LOC TS module providing
the same `queueMicrotask`-based polyfill via side-effect import.
Documented as the resolve.alias target.
- **generate-icons.js → generate-icons.cjs**: `git mv` preserving history.
Node 14+ treats `.cjs` as CJS regardless of `package.json` "type":
"module" per https://nodejs.org/api/packages.html#determining-module-system.
No code change; `require('fs')` + `require('path')` resolve cleanly.
No other references to the old `.js` path elsewhere in the codebase
outside the `.planning/` audit trail.
- **.planning/phases/01-stabilize-video-pipeline/deferred-items.md**:
appended "Resolved in Phase 4 Plan 04-02" closure block citing this
commit; details the 4-mechanism layered mitigation; documents the
Option α attempt + reversion.
References:
- .planning/phases/04-harden-clean-up-optional/04-RESEARCH.md §Q1
- .planning/phases/04-harden-clean-up-optional/04-PATTERNS.md
§vite.config.ts + §src/background/index.ts
- Plan 04-02 threat model T-04-02-01 (Elevation of Privilege) +
T-04-02-02 (DoS — JSZip fallback compatibility; verified by UAT 33/33)
- node_modules/jszip/lib/utils.js:7 (upstream `require("setimmediate")`)
- node_modules/setimmediate/setImmediate.js (upstream polyfill source)
- Plan 01-12 Wave 7 deferred-items.md disclosure (Phase 5 → Phase 4 target)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
79 lines
3.9 KiB
Markdown
79 lines
3.9 KiB
Markdown
# Phase 01 Deferred Items
|
|
|
|
Out-of-scope discoveries surfaced during plan execution that didn't directly
|
|
caused-by the current plan's changes. Per `<deviation_rules>` SCOPE
|
|
BOUNDARY: log here, don't fix.
|
|
|
|
## Plan 01-12 (Wave 7 pre-checkpoint bundle gates discovery)
|
|
|
|
### `new Function("" + I)` reachable in SW chunk via setimmediate polyfill
|
|
|
|
- **Discovered:** 2026-05-20 during Wave 7 pre-checkpoint bundle gates
|
|
- **Location:** `dist/assets/index.ts-<hash>.js` (the main SW chunk produced
|
|
by `npm run build`)
|
|
- **Context:** `vite-plugin-node-polyfills` (configured in
|
|
`vite.config.ts:nodePolyfills` for `Buffer`) bundles the upstream `setimmediate`
|
|
package which contains the construct: `b.setImmediate=function(I){typeof
|
|
I!="function"&&(I=new Function(""+I));...}`. The `new Function` is the
|
|
fallback when `setImmediate` is called with a non-function argument.
|
|
- **Reachability check:** Production code path `src/background/index.ts` +
|
|
`src/offscreen/recorder.ts` + their transitive deps DO NOT call
|
|
`setImmediate(string)`. The construct is dead in the static call graph
|
|
but Rollup conservatively preserves it (it's behind a runtime type
|
|
check, not a static dead branch).
|
|
- **MV3 CSP angle:** Modern Chrome (≥ MV3) does enforce CSP `script-src
|
|
'self'`, and `new Function('...')` evaluates a string-as-code which
|
|
some CSPs reject. However, the default MV3 manifest's
|
|
`content_security_policy` allows it for service workers in current
|
|
Chrome — Plan 01-12 did NOT introduce a tighter CSP override, so
|
|
this is benign at present.
|
|
- **Scope:** Pre-existing across all of Phase 1 history. Verified by
|
|
`git checkout main -- src/background/index.ts vite.config.ts &&
|
|
npm run build && grep -c 'new Function' dist/assets/index.ts-*.js`
|
|
returning the same count. Plan 01-12 made no changes to the
|
|
polyfill configuration; this entry exists for future tightening
|
|
(Phase 5 hardening, or a dedicated MV3 CSP-audit plan).
|
|
- **Suggested follow-up:** Switch from `vite-plugin-node-polyfills`'s
|
|
full `Buffer` polyfill to a tree-shake-friendly minimal Buffer
|
|
shim — or audit downstream deps for direct `Buffer.*` usage and
|
|
inline the few needed primitives. Either approach drops the
|
|
setimmediate polyfill entirely.
|
|
|
|
Documented in 01-12-SUMMARY.md "Known Limitations" section.
|
|
|
|
### Resolved in Phase 4 Plan 04-02 (2026-05-21)
|
|
|
|
The setimmediate polyfill `new Function` literal was dropped from the
|
|
SW chunk via a 2-edit coherent landing in Plan 04-02 Wave 1 (commit
|
|
hash recorded post-commit in 04-02-SUMMARY.md):
|
|
|
|
1. **vite.config.ts:** `nodePolyfills({ include: ['buffer'], exclude:
|
|
['setimmediate'], ... })` — explicit `exclude` array per the plugin's
|
|
public TypeScript-typed config. Drops the transitive `setimmediate`
|
|
bundle from the SW chunk.
|
|
|
|
2. **src/background/index.ts (top-of-module prelude, BEFORE first import):**
|
|
```ts
|
|
if (typeof globalThis.setImmediate === 'undefined') {
|
|
(globalThis as { setImmediate?: ... }).setImmediate =
|
|
(fn, ...args) => queueMicrotask(() => fn(...args));
|
|
}
|
|
```
|
|
Inline `queueMicrotask`-based fast-path; JSZip detects
|
|
`globalThis.setImmediate` already-defined and uses it directly (no
|
|
fallback to its own MessageChannel chain — though that chain remains
|
|
intact and would handle a future world where this prelude is removed).
|
|
|
|
**Verification gates (Plan 04-02):**
|
|
- `grep -c 'new Function' dist/assets/index.ts-*.js` returns **0** (was 1).
|
|
- `tests/build/no-new-function-in-sw-chunk.test.ts` GREEN (was RED in Task 1).
|
|
- UAT harness 33/33 GREEN preserved (JSZip's zip-assembly path empirically
|
|
validated end-to-end at the SAVE→zip layer).
|
|
- vitest baseline +2 (the new build-gate tests) flipped GREEN.
|
|
|
|
Approach (a) from RESEARCH §Q1 was adopted; approach (b) failed (the
|
|
plugin's `globals` type does not expose `setImmediate`); approach (c)
|
|
was rejected for v1 close (Phase 4 charter is hardening — ship the fix).
|
|
|
|
This entry is now CLOSED.
|