Files
mokosh/.planning/phases/01-stabilize-video-pipeline/01-06-PLAN.md

15 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, requirements_addressed, must_haves
phase plan type wave depends_on files_modified autonomous requirements requirements_addressed must_haves
01-stabilize-video-pipeline 06 execute 2
03
vite.config.ts
offscreen/index.ts
offscreen/index.html
true
REQ-video-ring-buffer
REQ-video-ring-buffer
truths artifacts key_links
`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`
path provides min_lines contains
vite.config.ts Minimal vite config with crxjs and offscreen entry 15 rollupOptions
path provides contains
dist/manifest.json Build output (artifact of npm run build) manifest_version
path provides contains
dist/src/offscreen/index.html Bundled offscreen HTML at the path SW.ensureOffscreen expects (or wherever crxjs emits it — actual path verified at build time)
from to via pattern
vite.config.ts (rollupOptions.input.offscreen) src/offscreen/index.html rollup input declaration src/offscreen/index.html
from to via pattern
vite.config.ts @crxjs/vite-plugin crx({ manifest, contentScripts: { injectCss: false } }) 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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.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:

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.

<threat_model>

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.
</threat_model>
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):

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):

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):

[ -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:

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:

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 <acceptance_criteria> - 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) </acceptance_criteria> 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:
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:

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):

    const url = chrome.runtime.getURL('<PATH>');

If Outcome A: <PATH> = 'src/offscreen/index.html'.

If Outcome B: <PATH> = 'offscreen/index.html' (leave unchanged).

Either way, after the edit (or no-edit), re-run:

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:

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/<offscreen path>/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 ] <acceptance_criteria> - 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) </acceptance_criteria> 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).

<success_criteria>

  • 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 </success_criteria>
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)."