---
phase: 01-stabilize-video-pipeline
plan: 06
type: execute
wave: 2
depends_on: ["03"]
files_modified:
- vite.config.ts
- offscreen/index.ts
- offscreen/index.html
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