diff --git a/.planning/phases/01-stabilize-video-pipeline/01-01-PLAN.md b/.planning/phases/01-stabilize-video-pipeline/01-01-PLAN.md index e3f6f86..914a8c8 100644 --- a/.planning/phases/01-stabilize-video-pipeline/01-01-PLAN.md +++ b/.planning/phases/01-stabilize-video-pipeline/01-01-PLAN.md @@ -25,7 +25,7 @@ must_haves: - "ROADMAP.md Phase 1 Success Criterion #2 no longer references tab re-attach" - "intel/decisions.md DEC-003 and DEC-010 carry an Amendment block" - "intel/constraints.md CON-tab-capture-binding and CON-service-worker-keepalive are RETIRED with a CON-display-capture-binding added" - - "manifest.json permissions list contains desktopCapture (not tabCapture) and drops the now-unused alarms entry" + - "manifest.json permissions list contains desktopCapture (not tabCapture) and drops the now-unused alarms entry (D-05, D-A6)" - "Every code-touching plan (02-07) sees a consistent doc baseline before it runs" artifacts: - path: ".planning/PROJECT.md" diff --git a/.planning/phases/01-stabilize-video-pipeline/01-03-PLAN.md b/.planning/phases/01-stabilize-video-pipeline/01-03-PLAN.md index 8cdb2b2..d17b58a 100644 --- a/.planning/phases/01-stabilize-video-pipeline/01-03-PLAN.md +++ b/.planning/phases/01-stabilize-video-pipeline/01-03-PLAN.md @@ -18,12 +18,14 @@ requirements_addressed: must_haves: truths: - - "`src/offscreen/recorder.ts` exists and exports the symbols Plan 02 tests against: addChunk, trimAged, getBuffer, resetBuffer, assertCodecSupported, VIDEO_BUFFER_DURATION_MS" + - "`src/offscreen/recorder.ts` exists at the canonical source path as a real TypeScript module — strict type-check, source maps, IDE support (D-06)" + - "`src/offscreen/recorder.ts` exports the symbols Plan 02 tests against: addChunk, trimAged, getBuffer, resetBuffer, assertCodecSupported, VIDEO_BUFFER_DURATION_MS" - "Running `npx vitest run tests/offscreen/ring-buffer.test.ts tests/offscreen/codec-check.test.ts` exits 0 with all tests green" - "Buffer holds at most: 1 header chunk + every chunk with arrival timestamp newer than now-30_000ms" - "Codec strictly bound to `video/webm;codecs=vp9` at 400 000 bps with `MediaRecorder.isTypeSupported` gate; no fallback chain (D-20)" - - "`MediaRecorder.start(2000)` is called on session start (timeslice = 2000 ms per SPEC §4.1)" - - "On `MediaStreamTrack.onended`, the buffer is cleared and a `RECORDING_ERROR` of `'user-stopped-sharing'` is emitted to SW" + - "Capture is `navigator.mediaDevices.getDisplayMedia()` invoked from inside the offscreen document (D-01); SW-side `chrome.tabCapture.getMediaStreamId` is removed in Plan 05" + - "Single continuous `MediaRecorder` runs for the whole session with `mediaRecorder.start(2000)` so chunks land on cluster boundaries per SPEC §4.1 (D-09)" + - "One-time source picker fires on session start (operator picks screen/window once); on `MediaStreamTrack.onended` the buffer is cleared and a `RECORDING_ERROR` of `'user-stopped-sharing'` is emitted to SW so the popup can re-prompt next interaction (D-03)" - "Restart-segments fallback (D-13) is pre-staged as a commented-out skeleton at the bottom of recorder.ts so Plan 07's fallback path doesn't require a re-plan" - "`src/offscreen/index.html` exists at the source path and references `./recorder.ts`" - "`src/shared/logger.ts` has an `OffscreenLogger` class with `[OS:...]` prefix" @@ -72,8 +74,10 @@ handshake — that is Plan 04. To keep this plan inside its context budget, the port-side code in `recorder.ts` is left as a small import-time placeholder that Plan 04 fills in. The Plan-02 port + handshake tests therefore remain RED until Plan 04 lands, which is the intended choreography per the -wave-1 dependency graph (Plans 03 and 04 run in parallel; Plan 02 wrote both -sets of RED tests in advance). +sequential wave structure (Plan 03 lands in wave 2; Plan 04 follows in wave +3 and refactors the bootstrap section of the same file). Plan 02 wrote both +sets of RED tests in advance so Plan 03 and Plan 04 each have a discrete +RED→GREEN cycle to flip. Purpose: REQ-video-ring-buffer's load-bearing logic lives here. The ring buffer is a pure function — exactly the TDD sweet spot. The remaining @@ -155,7 +159,7 @@ export interface VideoChunk { | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-1-01 | Tampering — codec downgrade | `MediaRecorder` constructor | mitigate | `assertCodecSupported()` calls `MediaRecorder.isTypeSupported('video/webm;codecs=vp9')` BEFORE constructing the recorder; if it returns false, throws an Error and emits `RECORDING_ERROR` to SW. No vp8 / h264 / default fallback chain. The strict mode covers `MediaRecorder` itself being absent (the codec-check test mocks both cases). Grep gate: `grep -v '^#' src/offscreen/recorder.ts \| grep -cE 'codecs=(vp8\|h264)'` returns 0 (no fallback codec strings in the module). | -| T-1-03 | Information Disclosure — captured stream contains other apps' content | `getDisplayMedia` stream | accept | This is the documented residual risk per CONTEXT.md D-04. The Chrome "Sharing your screen" indicator is the user-facing signal; the operator chose to share. No code-level mitigation is possible (the API is supposed to capture the screen). Documented as accepted risk in 01-RESEARCH.md §"Security Domain". | +| T-1-03 | Information Disclosure — captured stream contains other apps' content | `getDisplayMedia` stream | accept | This is the documented residual risk per CONTEXT.md D-04 — the operator opted into the "Sharing your screen" indicator as the cost of the broader capture coverage. The Chrome "Sharing your screen" indicator is the user-facing signal; the operator chose to share. No code-level mitigation is possible (the API is supposed to capture the screen). Documented as accepted risk in 01-RESEARCH.md §"Security Domain". | | T-1-NEW-03-01 | DoS — unbounded buffer growth on a stuck timestamp | `addChunk` + `trimAged` | mitigate | The trim function uses arrival timestamp; if a clock anomaly produces a negative or stuck `now`, the buffer is bounded above by SPEC §10 #9 (50 MB RAM ceiling) anyway. Defensive belt: the recorder also exposes `getBuffer().length` to SW so a healthchecker can observe growth. No active rate-limit needed for Phase 1. | diff --git a/.planning/phases/01-stabilize-video-pipeline/01-04-PLAN.md b/.planning/phases/01-stabilize-video-pipeline/01-04-PLAN.md index e42ae6a..419f4f2 100644 --- a/.planning/phases/01-stabilize-video-pipeline/01-04-PLAN.md +++ b/.planning/phases/01-stabilize-video-pipeline/01-04-PLAN.md @@ -14,9 +14,9 @@ requirements_addressed: must_haves: truths: - - "On module import, the offscreen opens exactly one port via `chrome.runtime.connect({ name: 'video-keepalive' })` AND emits exactly one `OFFSCREEN_READY` message via `chrome.runtime.sendMessage`" + - "On module import, the offscreen opens exactly one port via `chrome.runtime.connect({ name: 'video-keepalive' })` AND emits exactly one `OFFSCREEN_READY` message via `chrome.runtime.sendMessage` — this is the long-lived port keepalive that replaces `chrome.alarms` for SW idle-timer resets (D-17)" - "Both the port-connect and the OFFSCREEN_READY send happen AFTER `chrome.runtime.onMessage.addListener` registration (Pattern 4 ordering)" - - "When the open port fires its registered `onDisconnect` listener, the module synchronously reconnects (Pitfall 4 mitigation)" + - "When the open port fires its registered `onDisconnect` listener, the module synchronously reconnects (Pitfall 4 mitigation; preserves the D-17 contract across the ~5 min port-lifetime cap)" - "The port emits a `{ type: 'PING' }` postMessage on a ≤ 25 s interval (RESEARCH.md Pattern 5)" - "Pre-emptive reconnect runs every 290 s (belt-and-braces against the ~5 min port-lifetime cap)" - "The port handles incoming `REQUEST_BUFFER` messages by responding with `{ type: 'BUFFER', chunks: }`" @@ -51,10 +51,12 @@ a contract that survives SW unloads, port 5-min cap, and offscreen bootstrap races. Output: Updated `src/offscreen/recorder.ts` — only this single file is -touched. Plan 04 is an isolated unit-of-work; Plan 05 picks up the -SW-side `onConnect` handler in parallel (Plan 04 runs alongside Plan 05 in -Wave 1 with NO file overlap — Plan 04 owns offscreen-side, Plan 05 owns -SW-side). +touched. Plan 04 is an isolated unit-of-work; Plan 05 (the SW-side +`onConnect` counterparty) follows in wave 4 — Plan 04 lands first in wave 3 +so the SW-side host has a well-defined offscreen-side contract to bind +against. Plan 04 owns the offscreen-side; Plan 05 owns the SW-side; the two +files do not overlap (Plan 04 touches only `src/offscreen/recorder.ts`, +Plan 05 touches only `src/background/index.ts`). diff --git a/.planning/phases/01-stabilize-video-pipeline/01-05-PLAN.md b/.planning/phases/01-stabilize-video-pipeline/01-05-PLAN.md index a60f576..5edce24 100644 --- a/.planning/phases/01-stabilize-video-pipeline/01-05-PLAN.md +++ b/.planning/phases/01-stabilize-video-pipeline/01-05-PLAN.md @@ -14,13 +14,14 @@ requirements_addressed: must_haves: truths: - - "`src/background/index.ts` no longer contains `addVideoChunkFromBlob`, `cleanupVideoBuffer`, `setupKeepalive`, `loadChunkFromIndexedDB`, `openIndexedDB`, or any `chrome.alarms` reference (buffer ownership moves to offscreen per D-16/D-19)" - - "`src/background/index.ts` no longer calls `chrome.tabCapture.getMediaStreamId` (D-01 amendment)" + - "`src/background/index.ts` no longer contains `addVideoChunkFromBlob`, `cleanupVideoBuffer`, `setupKeepalive`, `loadChunkFromIndexedDB`, `openIndexedDB`, or any `chrome.alarms` reference — buffer ownership moves to offscreen per D-16/D-19, and the alarms-driven keepalive is DELETED per D-18 (it never actually reset the SW idle timer; the long-lived port does)" + - "`src/background/index.ts` no longer calls `chrome.tabCapture.getMediaStreamId` (D-01 amendment); video acquisition is now `getDisplayMedia` invoked from the offscreen module" - "`src/background/index.ts` no longer handles `VIDEO_CHUNK` or `VIDEO_CHUNK_SAVED` (deleted Message types in Plan 03)" - - "SW has an `onConnect` listener that filters `port.name === 'video-keepalive'` and validates `port.sender?.id === chrome.runtime.id` (T-1-04 mitigation)" + - "`src/background/index.ts` no longer contains any `chrome.tabs.onActivated` handler tied to the recording lifecycle (D-14: tab-switch re-attach is non-applicable under `getDisplayMedia`; D-15: operator tab-switching no longer interrupts recording, the buffer keeps filling regardless of active tab)" + - "SW has an `onConnect` listener that filters `port.name === 'video-keepalive'` and validates `port.sender?.id === chrome.runtime.id` (T-1-04 mitigation; this is the SW-side counterparty of the long-lived port keepalive per D-17)" - "SW has an `onMessage` `OFFSCREEN_READY` case that resolves a pending readiness Promise (Pattern 4 SW side)" - "SW's `SAVE_ARCHIVE` and `GET_VIDEO_BUFFER` handlers fetch the buffer via the port (`REQUEST_BUFFER` → wait for `BUFFER`) instead of holding their own `videoBuffer` array" - - "SW's `ensureOffscreen` uses `chrome.offscreen.Reason.DISPLAY_MEDIA` (not `USER_MEDIA`)" + - "SW's `ensureOffscreen` uses `chrome.offscreen.Reason.DISPLAY_MEDIA` (not `USER_MEDIA`) per D-02" - "SW's `onInstalled` listener calls `indexedDB.deleteDatabase('VideoRecorderDB')` once as a cleanup pass (RESEARCH.md Runtime State Inventory)" - "`npx tsc --noEmit` exits 0" artifacts: @@ -49,9 +50,11 @@ ring-buffer state and helpers (now owned by offscreen per D-16), DELETE the chrome.alarms keepalive (D-18), DELETE the IndexedDB code path (D-19), DELETE the `chrome.tabCapture.getMediaStreamId` call (D-01 amendment), DELETE the `VIDEO_CHUNK` / `VIDEO_CHUNK_SAVED` message handlers (their -message types were removed in Plan 03), and WIRE the SW-side `onConnect` -handler against the `'video-keepalive'` port that Plan 04 opens from the -offscreen. +message types were removed in Plan 03), DELETE any `chrome.tabs.onActivated` +re-attach plumbing (D-14: not applicable under the new capture API; D-15: +operator tab switches no longer interrupt the recording), and WIRE the +SW-side `onConnect` handler against the `'video-keepalive'` port that +Plan 04 opens from the offscreen. Purpose: REQ-video-ring-buffer's data flow on export is `popup → SAVE_ARCHIVE → SW → REQUEST_BUFFER (via port) → offscreen → BUFFER (via @@ -200,10 +203,12 @@ If `chrome.offscreen.Reason.DISPLAY_MEDIA` is NOT in the current `@types/chrome` (13) Delete `openIndexedDB` function — currently lines 507-520. +(14) Verify no `chrome.tabs.onActivated` listener exists in the file (D-14 / D-15: tab-switch handling is non-applicable under the new capture API). Run `grep -n "chrome.tabs.onActivated" src/background/index.ts`. If the grep returns any hits, DELETE those lines (the entire listener callback block). If the grep returns nothing, log "D-14/D-15 satisfied: no tab-switch handler found in SW" in the task summary. + After ALL these deletions, run `npx tsc --noEmit`. It MUST exit 0. If `VideoChunk` is reported as unused after the deletes, that indicates a function that needs it was inadvertently lost; STOP and audit. - npx tsc --noEmit && [ $(grep -cE "addVideoChunkFromBlob|cleanupVideoBuffer|setupKeepalive|loadChunkFromIndexedDB|openIndexedDB|getMediaStreamId|chrome\.alarms" src/background/index.ts) -eq 0 ] + npx tsc --noEmit && [ $(grep -cE "addVideoChunkFromBlob|cleanupVideoBuffer|setupKeepalive|loadChunkFromIndexedDB|openIndexedDB|getMediaStreamId|chrome\.alarms|chrome\.tabs\.onActivated" src/background/index.ts) -eq 0 ] - `npx tsc --noEmit` exits 0 @@ -215,6 +220,7 @@ After ALL these deletions, run `npx tsc --noEmit`. It MUST exit 0. If `VideoChun - `grep -v '^#' src/background/index.ts | grep -c "getMediaStreamId"` returns 0 - `grep -v '^#' src/background/index.ts | grep -c "VIDEO_CHUNK_SAVED"` returns 0 - `grep -v '^#' src/background/index.ts | grep -c "chrome.alarms"` returns 0 + - `grep -v '^#' src/background/index.ts | grep -c "chrome.tabs.onActivated"` returns 0 (D-14/D-15 mitigation) - `grep -c "DISPLAY_MEDIA" src/background/index.ts` returns 1 - `grep -c "as any" src/background/index.ts` returns 0 (CLAUDE.md rule) - File line count reduced from 536 to roughly 380-400 lines (allow ±40) @@ -435,7 +441,7 @@ Commit cadence: TWO commits. -- `src/background/index.ts` carries no buffer state, no alarms, no IndexedDB plumbing, no `tabCapture` calls +- `src/background/index.ts` carries no buffer state, no alarms, no IndexedDB plumbing, no `tabCapture` calls, no `chrome.tabs.onActivated` re-attach plumbing - SW has `onConnect` handler matching the offscreen's port (Plan 04 counterparty) - SW has `OFFSCREEN_READY` handshake handler resolving a readiness Promise - T-1-04 mitigations in place on BOTH onConnect (sender + port name) and onMessage (sender) diff --git a/.planning/phases/01-stabilize-video-pipeline/01-06-PLAN.md b/.planning/phases/01-stabilize-video-pipeline/01-06-PLAN.md index b296c1b..d4be8ae 100644 --- a/.planning/phases/01-stabilize-video-pipeline/01-06-PLAN.md +++ b/.planning/phases/01-stabilize-video-pipeline/01-06-PLAN.md @@ -17,11 +17,11 @@ requirements_addressed: 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" + - "`vite.config.ts` no longer contains the `copy-offscreen` inline plugin — the entire 200+-line block including IndexedDB plumbing, codec fallback chain, and `mediaRecorder` shadow is GONE per D-08 (orphan dead-code deletion plus inline-plugin deletion in lockstep)" + - "`vite.config.ts` declares the offscreen entry via `rollupOptions.input` per RESEARCH.md Example B; crxjs picks up the new TS entry through the HTML reference and the runtime path remains `offscreen/index.html` resolvable via `chrome.runtime.getURL(...)` per D-07" + - "Top-level `offscreen/index.ts` is DELETED (dead code per audit P2 #18; D-08 explicit DELETE target)" + - "Top-level `offscreen/index.html` is DELETED (REPLACED by the crxjs-managed `src/offscreen/index.html` from Plan 03, per D-07 — the runtime URL semantics survive the source-tree move)" + - "`npm run build` exits 0; `dist/` contains a bundled offscreen HTML at the path the SW's `chrome.runtime.getURL` argument expects (D-07 runtime-path contract)" - "No mention of `VideoRecorderDB`, `openIndexedDB`, `chromeMediaSource`, or `copy-offscreen` remains in `vite.config.ts`" artifacts: - path: "vite.config.ts" @@ -107,8 +107,8 @@ const url = chrome.runtime.getURL('offscreen/index.html'); | 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. | +| 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 (D-08). 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 (D-07 / D-08 — runtime path is preserved via the crxjs-managed `src/offscreen/index.html`). After this plan, a `find offscreen/ -type f` produces no output. Grep gate: `[ ! -d offscreen ]` exits 0. |