--- phase: 01-stabilize-video-pipeline plan: 06 type: execute wave: 5 depends_on: ["03", "05"] files_modified: - vite.config.ts - offscreen/index.ts - offscreen/index.html - src/background/index.ts autonomous: true requirements: - REQ-video-ring-buffer requirements_addressed: - REQ-video-ring-buffer must_haves: truths: - "`vite.config.ts` no longer contains the `copy-offscreen` inline plugin (the 200+-line block including IndexedDB plumbing, codec fallback chain, and `mediaRecorder` shadow is GONE)" - "`vite.config.ts` declares the offscreen entry via `rollupOptions.input` per RESEARCH.md Example B" - "Top-level `offscreen/index.ts` is DELETED (dead code per audit P2 #18)" - "Top-level `offscreen/index.html` is DELETED (replaced by the crxjs-managed `src/offscreen/index.html` from Plan 03)" - "`npm run build` exits 0; `dist/` contains a bundled offscreen HTML at the path the SW's `chrome.runtime.getURL` argument expects" - "No mention of `VideoRecorderDB`, `openIndexedDB`, `chromeMediaSource`, or `copy-offscreen` remains in `vite.config.ts`" artifacts: - path: "vite.config.ts" provides: "Minimal vite config with crxjs and offscreen entry" min_lines: 15 contains: "rollupOptions" - path: "dist/manifest.json" provides: "Build output (artifact of npm run build)" contains: "manifest_version" - path: "dist/src/offscreen/index.html" provides: "Bundled offscreen HTML at the path SW.ensureOffscreen expects (or wherever crxjs emits it — actual path verified at build time)" contains: "" key_links: - from: "vite.config.ts (rollupOptions.input.offscreen)" to: "src/offscreen/index.html" via: "rollup input declaration" pattern: "src/offscreen/index.html" - from: "vite.config.ts" to: "@crxjs/vite-plugin" via: "crx({ manifest, contentScripts: { injectCss: false } })" pattern: "crx(" --- Collapse the build pipeline. DELETE the 200+-line `copy-offscreen` inline Vite plugin block in `vite.config.ts` and the dead `offscreen/index.ts` + `offscreen/index.html` at the repo root. The crxjs plugin already handles bundling; we declare the new offscreen entry via `rollupOptions.input` pointing at the `src/offscreen/index.html` Plan 03 created. Purpose: D-08 says delete the inline plugin (which is the audit's P0 #1 root cause). D-07 says crxjs picks up the new TS entry through the HTML reference. The execution path is "minimal vite.config.ts + the existing crx() invocation + a single rollupOptions.input line — that's it." The plan also includes a build-time pathing verification — after the first `npm run build`, the executor inspects `dist/` and confirms the bundled offscreen HTML lands at the SAME path the SW's `chrome.runtime.getURL(...)` argument in Plan 05 expects. If crxjs strips the `src/` prefix (which it sometimes does), the SW URL gets adjusted by a follow-up Edit. RESEARCH.md Pitfall 5 calls this out explicitly. Output: A ~25-line `vite.config.ts`, no top-level `offscreen/` directory, and a clean `npm run build` whose output passes the path-matching check. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/phases/01-stabilize-video-pipeline/01-CONTEXT.md @.planning/phases/01-stabilize-video-pipeline/01-RESEARCH.md @.planning/phases/01-stabilize-video-pipeline/01-PATTERNS.md @vite.config.ts @offscreen/index.ts @offscreen/index.html @src/offscreen/index.html @manifest.json crxjs entry contract (per RESEARCH.md §"Pitfall 5" and discussion #919): - `rollupOptions.input` declares an HTML entry; crxjs bundles the HTML and emits its referenced TS module into `dist/assets/`. - The runtime URL that the SW passes to `chrome.offscreen.createDocument` MUST match the bundled HTML path. If `input: { offscreen: 'src/offscreen/index.html' }`, the runtime URL is typically `chrome.runtime.getURL('src/offscreen/index.html')` (crxjs preserves the input key as the output path), but real-world projects have reported the `src/` prefix being stripped. The executor MUST verify after the first `npm run build` and adjust Plan 05's `ensureOffscreen` call if needed. Plan 05's `ensureOffscreen` currently calls: ```typescript const url = chrome.runtime.getURL('offscreen/index.html'); ``` (this is leftover from the pre-amendment manifest entry that has the old root-level `offscreen/` dir). Plan 06 verifies the post-build emit path and updates this string to match. If the emit path is `src/offscreen/index.html`, the call becomes `getURL('src/offscreen/index.html')`. Plan 05 wrote the path AS-IS — Plan 06 is the right place to do the binding verification because it owns the build pipeline. ## Trust Boundaries | Boundary | Description | |----------|-------------| | build-time vite plugin → bundled output | A malicious build plugin could inject code; we're DELETING our inline plugin (the audit's P0 #1) which is itself an attack-surface reduction | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-1-NEW-06-01 | Tampering — string-injected code via inline plugin | `vite.config.ts:13-216` `copy-offscreen` plugin's `this.emitFile({ source: \`\` })` | mitigate | DELETE the entire inline plugin. The replacement is a `src/offscreen/recorder.ts` real module + a `src/offscreen/index.html` declared in `rollupOptions.input`. No more long template-literal JS in `vite.config.ts`. Grep gate: `grep -v '^#' vite.config.ts \| grep -c "this.emitFile"` returns 0. | | T-1-NEW-06-02 | Tampering — orphaned root-level offscreen | `offscreen/index.ts` + `offscreen/index.html` (dead code) | mitigate | DELETE both files. After this plan, a `find offscreen/ -type f` produces no output. Grep gate: `[ ! -d offscreen ]` exits 0. | Task 1: DELETE — top-level offscreen/ directory and inline copy-offscreen plugin vite.config.ts, offscreen/index.ts, offscreen/index.html - vite.config.ts (lines 1-227 — full file; the inline plugin spans lines 13-216) - offscreen/index.ts (60 lines — dead code) - offscreen/index.html (10 lines — replaced by src/offscreen/index.html from Plan 03) - .planning/phases/01-stabilize-video-pipeline/01-PATTERNS.md §`vite.config.ts` (lines 399-436) - .planning/phases/01-stabilize-video-pipeline/01-RESEARCH.md §"Example B" (lines 895-915) Three deletions and one rewrite. (1) Delete `offscreen/index.ts` (the orphan dead-code file): ```bash rm offscreen/index.ts ``` (2) Delete `offscreen/index.html` (it is REPLACED by the new `src/offscreen/index.html` that Plan 03 created — the path was deliberately moved into the source tree): ```bash rm offscreen/index.html ``` (3) Remove the now-empty `offscreen/` directory (verify it is empty first; if anything else lives there, STOP and surface it): ```bash [ -d offscreen ] && rmdir offscreen ``` (4) REWRITE `vite.config.ts` to be the minimal RESEARCH.md Example B form. Use the Write tool (NOT Edit — the file is being replaced). VERBATIM content: ```typescript import { defineConfig } from 'vite'; import { crx } from '@crxjs/vite-plugin'; import manifest from './manifest.json'; export default defineConfig({ plugins: [ crx({ manifest, contentScripts: { injectCss: false, }, }), ], build: { rollupOptions: { input: { offscreen: 'src/offscreen/index.html', }, }, }, }); ``` The deleted content (the entire `copy-offscreen` plugin block at lines 13-216 plus the surrounding `build.rollupOptions.output.manualChunks` shape at lines 218-226) does not survive any portion in the rewrite. After these edits, run: ```bash npx tsc --noEmit ``` It MUST exit 0 (the previous TS errors about `(error as any).message?.includes` etc. were inside `src/background/index.ts`; Plan 05 already cleared those. `vite.config.ts` is type-checked via Vite, not `tsc --noEmit`; the `npx tsc --noEmit` check here is a regression guard for the rest of the codebase). [ ! -f offscreen/index.ts ] && [ ! -f offscreen/index.html ] && [ ! -d offscreen ] && [ "$(grep -c 'copy-offscreen' vite.config.ts)" -eq 0 ] && npx tsc --noEmit - `test ! -f offscreen/index.ts` exits 0 (file deleted) - `test ! -f offscreen/index.html` exits 0 (file deleted) - `test ! -d offscreen` exits 0 (directory removed) - `grep -c "copy-offscreen" vite.config.ts` returns 0 - `grep -c "this.emitFile" vite.config.ts` returns 0 (T-1-NEW-06-01 grep gate) - `grep -c "VideoRecorderDB" vite.config.ts` returns 0 - `grep -c "openIndexedDB" vite.config.ts` returns 0 - `grep -c "chromeMediaSource" vite.config.ts` returns 0 - `grep -c "rollupOptions" vite.config.ts` returns 1 - `grep -c "src/offscreen/index.html" vite.config.ts` returns 1 - `wc -l vite.config.ts` returns ≤ 30 - `npx tsc --noEmit` exits 0 - `npx vitest run` exits 0 (regression guard) Inline plugin gone. Root-level offscreen/ gone. vite.config.ts is ~22 lines. Task 2: BUILD VERIFY — run npm run build and reconcile the offscreen path src/background/index.ts (only if path-adjustment needed) - vite.config.ts (post-Task-1 state) - src/offscreen/index.html (from Plan 03) - src/background/index.ts (line 85 area — the `chrome.runtime.getURL('offscreen/index.html')` call inherited from before Plan 05; verify with `grep -n "getURL" src/background/index.ts`) - .planning/phases/01-stabilize-video-pipeline/01-RESEARCH.md §"Pitfall 5" (lines 838-856) Run the build: ```bash rm -rf dist npm run build 2>&1 | tee /tmp/01-06-build.log ``` The build MUST exit 0. If it doesn't: - Check the log for syntax/type errors and STOP — do not proceed to the rest of this task. - Common failure modes: `chrome.offscreen.Reason.DISPLAY_MEDIA` not in current `@types/chrome` (fall back to the cast Plan 05 documented); the manifest reference to `desktopCapture` not yet landed (Plan 01 should have committed it — re-check); rollupOptions.input pointing at a non-existent file. If the build succeeds, verify the dist layout: ```bash ls -la dist/ ls -la dist/src/offscreen/ 2>/dev/null ls -la dist/offscreen/ 2>/dev/null ``` There will be ONE of two outcomes: **Outcome A — crxjs preserved `src/`**: `dist/src/offscreen/index.html` exists. In this case, the SW URL needs to be `chrome.runtime.getURL('src/offscreen/index.html')`. **Outcome B — crxjs stripped `src/`**: `dist/offscreen/index.html` exists. In this case, the SW URL needs to be `chrome.runtime.getURL('offscreen/index.html')` (which is what the current Plan-05 file already has by accident — leftover from the pre-amendment code). Identify which outcome obtains. Then edit `src/background/index.ts` line ~85 (inside `ensureOffscreen`): ```typescript const url = chrome.runtime.getURL(''); ``` If Outcome A: `` = `'src/offscreen/index.html'`. If Outcome B: `` = `'offscreen/index.html'` (leave unchanged). Either way, after the edit (or no-edit), re-run: ```bash npx tsc --noEmit npm run build ls -la dist/manifest.json node -e "const m=require('./dist/manifest.json'); console.log('permissions:', m.permissions.join(','))" ``` The last line MUST print a comma-separated permission list that includes `desktopCapture` and does NOT include `tabCapture` (Plan 01 should already have ensured this in `manifest.json`; the `dist/manifest.json` is the crxjs-bundled output that propagates the manifest as-is). Final layout verification: ```bash find dist -type f -name "*.html" -o -name "*.js" | sort ``` The output should contain (in some order): - `dist/manifest.json` - `dist/src/popup/index.html` (or `dist/popup/index.html` — same Outcome-A/B rule) - `dist//index.html` (whichever outcome) - One or more `dist/assets/*.js` files (bundled SW, content script, offscreen TS, popup TS) If the manifest.json's `background.service_worker` field doesn't resolve to an emitted file, STOP and audit. crxjs handles this automatically — there's only a problem if the manifest entry was hand-broken. npm run build && ls dist/manifest.json && [ -f dist/src/offscreen/index.html ] || [ -f dist/offscreen/index.html ] - `npm run build` exits 0 (with NO TypeScript errors in /tmp/01-06-build.log) - `dist/manifest.json` exists - Either `dist/src/offscreen/index.html` OR `dist/offscreen/index.html` exists - `dist/manifest.json` permissions list contains `"desktopCapture"` and does NOT contain `"tabCapture"` - `src/background/index.ts` `chrome.runtime.getURL(...)` argument matches whichever Outcome (A or B) the build produced - `dist/assets/` contains at least one `.js` file (the bundled SW / content / popup / offscreen scripts) Build succeeds. SW URL string matches the dist layout. Plan 07 can load `dist/` into Chrome and the offscreen will resolve correctly. After both tasks land: 1. `npm run build` — exits 0, clean output. 2. `dist/` contains a loadable extension (manifest.json + at least one HTML offscreen page + at least one JS bundle in assets/). 3. `npx tsc --noEmit && npx vitest run` — both exit 0 (regression guard for the rest of the codebase). 4. `wc -l vite.config.ts` — ≤ 30 lines. 5. No top-level `offscreen/` directory. 6. The SW URL string in `src/background/index.ts` matches the actual `dist/` emit path (Outcome A or B). Commit cadence: TWO commits. - Task 1: ONE commit (`refactor(01-06): delete inline copy-offscreen plugin and orphan offscreen/ directory`). - Task 2: ONE commit if path adjustment was needed (`fix(01-06): align ensureOffscreen URL with crxjs emit path`); ZERO commits if no path adjustment was needed (note in SUMMARY which Outcome obtained). - `vite.config.ts` is ~22 lines, contains crx() and rollupOptions only - `offscreen/` top-level directory is GONE - `dist/manifest.json` carries the post-Plan-01 amended permissions (desktopCapture, no tabCapture, no alarms) - The SW URL string and the bundled HTML path match - `npm run build` clean After completion, create `.planning/phases/01-stabilize-video-pipeline/01-06-SUMMARY.md` with: - Pre / post line count for vite.config.ts (was 227, now ~22) - Confirmation that `offscreen/` directory is gone (output of `find offscreen/ 2>&1 || echo absent`) - The outcome (A or B) for crxjs's emit path, and the exact SW URL string committed - Output of `find dist -type f | sort` after the post-Task-2 build, so Plan 07's manual smoke test knows what to load - Two commit SHAs (or one if Task 2 needed no path adjustment) - Note: "Plan 07 loads `dist/` unpacked into Chrome ≥ 116 and runs the ffprobe acceptance gate (D-12)."