--- phase: 02-stabilize-export-pipeline plan: 03 type: auto wave: 2 depends_on: [01] files_modified: - src/background/tab-url-tracker.ts - src/shared/types.ts - src/background/index.ts - .planning/REQUIREMENTS.md autonomous: true requirements: - REQ-meta-json-schema tags: - meta-json-urls-array - tab-url-tracking - schema-amendment - p1-10-fix - d-p2-02 - d-p2-03 must_haves: truths: - "src/background/tab-url-tracker.ts exists exporting getTabUrlsSeen(): string[] returning a deduplicated, first-seen-ordered, filtered (no chrome://, no about:; include chrome-extension://) list of tab URLs observed during the rolling 30s window." - "SessionMetadata.url: string is replaced by SessionMetadata.urls: string[] in src/shared/types.ts." - "createArchive in src/background/index.ts assembles meta.json with urls (not url), plus a new schemaVersion: '2' field, totalling exactly 8 fields per D-P2-03." - "Tests tests/background/meta-json-urls-schema.test.ts (4 tests) and tests/build/strict-meta-json-validation.test.ts (8 tests) flip RED→GREEN." - ".planning/REQUIREMENTS.md REQ-meta-json-schema text amended to reflect the new 8-field shape, with the breaking-change cutover documented inline." - "chrome.tabs.onActivated and chrome.tabs.onUpdated listeners registered in src/background/index.ts maintain the tracker's internal Set; pruning happens alongside the video segment ring buffer lifecycle (no separate timer)." - "Always-on charter preserved (Plan 01-09 Amendment 3): SAVE creates a zip with the current tracker state; tracker continues accumulating after SAVE." - "manifest.json permissions: the existing `tabs` permission gap (Plan 01-13 SUMMARY Known Limitations item 3) is NOT closed by this plan — the tracker uses chrome.tabs.onActivated + chrome.tabs.onUpdated which work on `activeTab` permission alone for the active-tab URL events. Cross-window/inactive-tab URL events require `tabs` permission and are deferred to Phase 4 hardening (planner-decision documented inline)." artifacts: - path: "src/background/tab-url-tracker.ts" provides: "getTabUrlsSeen(): string[] + initTabUrlTracker(): void + clearTabUrlsSeen(): void" min_lines: 80 contains: "getTabUrlsSeen|chrome\\.tabs\\.onActivated|chrome\\.tabs\\.onUpdated" - path: "src/shared/types.ts" provides: "SessionMetadata.urls: string[] (replaces url: string); new schemaVersion: string field" contains: "urls.*string\\[\\]|schemaVersion" - path: "src/background/index.ts" provides: "createArchive consumes getTabUrlsSeen for meta.urls + initTabUrlTracker at SW init" contains: "getTabUrlsSeen|schemaVersion" - path: ".planning/REQUIREMENTS.md" provides: "REQ-meta-json-schema amended for 8-field shape + breaking-change cutover note" contains: "schemaVersion|urls.*string\\[\\]" key_links: - from: "src/background/index.ts:initialize" to: "src/background/tab-url-tracker.ts:initTabUrlTracker" via: "initTabUrlTracker() called at SW init alongside chrome.runtime.onMessage listener" pattern: "initTabUrlTracker\\(\\)" - from: "src/background/index.ts:createArchive" to: "src/background/tab-url-tracker.ts:getTabUrlsSeen" via: "metadata.urls = getTabUrlsSeen()" pattern: "metadata\\.urls.*=.*getTabUrlsSeen" - from: "src/background/tab-url-tracker.ts" to: "chrome.tabs.onActivated + chrome.tabs.onUpdated" via: "addListener at module init; each event derives the URL via tab.url and inserts into the Set if it passes the filter" pattern: "chrome\\.tabs\\.on(Activated|Updated)\\.addListener" --- Replace SessionMetadata.url: string with SessionMetadata.urls: string[] per D-P2-02. The urls array captures all tab URLs observed during the rolling recording window (operator's full multi-tab bug-reproduction context, not just the active-at-save tab). Add a new schemaVersion field per the planner-resolved 8th-field decision from Plan 02-01. Amend REQUIREMENTS.md REQ-meta-json-schema text to reflect the breaking-change cutover. Purpose: closes audit P1 #10 + makes the meta.json multi-tab-aware for the operator's typical workflow (switching tabs across the 30s window when reproducing a bug). Maintains the always-on charter (tracker keeps running after SAVE; no reset). Output: - New module src/background/tab-url-tracker.ts (~80-120 LOC). - src/shared/types.ts SessionMetadata interface updated. - src/background/index.ts createArchive + initialize updated. - .planning/REQUIREMENTS.md REQ-meta-json-schema text amended. - tests/background/meta-json-urls-schema.test.ts (4) + tests/build/strict-meta-json-validation.test.ts (8) flip RED→GREEN. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/REQUIREMENTS.md @.planning/STATE.md @.planning/phases/02-stabilize-export-pipeline/02-CONTEXT.md @.planning/phases/02-stabilize-export-pipeline/02-01-PLAN.md # Source code under modification @src/background/index.ts @src/shared/types.ts # Tests that flip GREEN under this plan @tests/background/meta-json-urls-schema.test.ts @tests/build/strict-meta-json-validation.test.ts src/background/tab-url-tracker.ts public API: ```typescript /** * Initialize the tab-URL tracker. Registers chrome.tabs.onActivated + * chrome.tabs.onUpdated listeners that maintain an internal Set of URLs * observed during the SW's lifetime. Must be called once at SW init. * * Idempotent — calling twice is safe (defensive try/catch around listener * registration matches the pattern in src/background/index.ts:bootstrap). * * D-P2-02 binding: captures the operator's multi-tab context, not just * the active-at-save tab. */ export function initTabUrlTracker(): void; /** * Return the deduplicated, first-seen-ordered, filtered list of tab URLs * observed since init OR since the last clearTabUrlsSeen() call. * * Filter (per CONTEXT.md `` block): * - INCLUDE: https://, http://, chrome-extension:// * - EXCLUDE: chrome://, about:, devtools://, file:// (low diagnostic value * OR privacy-concerning local-fs paths) * * Dedup: each URL appears exactly once. * Order: first-seen-first. * * Always returns a NEW array (copy) — caller cannot mutate the internal Set. */ export function getTabUrlsSeen(): string[]; /** * Clear the internal Set. NOT called by saveArchive (always-on charter * preserved — SAVE creates a zip with the current state; tracker keeps * accumulating). Reserved for future use (e.g., manual session-reset). */ export function clearTabUrlsSeen(): void; ``` Updated SessionMetadata (src/shared/types.ts): ```typescript export interface SessionMetadata { schemaVersion: string; // NEW — '2' marks the D-P2-02 url→urls cutover timestamp: string; // ISO-8601 with Z urls: string[]; // NEW — replaces `url: string`; non-empty per D-P2-03 userAgent: string; extensionVersion: string; videoBufferSeconds: number; logDurationMinutes: number; totalEvents: number; } ``` Existing createArchive call site (src/background/index.ts:673-684): ```typescript const metadata: SessionMetadata = { timestamp: new Date().toISOString(), url: new URL(chrome.runtime.getURL('')).origin, // ← REPLACE userAgent: navigator.userAgent, extensionVersion: manifest.version, videoBufferSeconds: 30, logDurationMinutes: 10, totalEvents: rrwebEvents.length + userEvents.length }; ``` REQ-meta-json-schema (REQUIREMENTS.md:106-119) — verbatim text to amend: ``` - [ ] **REQ-meta-json-schema**: `meta.json` inside the archive conforms to the verbatim schema: ```json { "timestamp": "2025-05-15T14:32:10Z", "url": "https://...", ← REPLACE WITH urls: [] "userAgent": "Chrome/...", "extensionVersion": "1.0.0", "videoBufferSeconds": 30, "logDurationMinutes": 10, "totalEvents": 143 } ``` All fields required. Binding: CON-meta-json-schema. ``` Task 1: Create src/background/tab-url-tracker.ts module src/background/tab-url-tracker.ts - Module exports initTabUrlTracker, getTabUrlsSeen, clearTabUrlsSeen per the interface block. - Internal state: `let tabUrlsSeen: Set = new Set()` AND `let firstSeenOrder: string[] = []` (Set for O(1) dedup; array for ordering). - On chrome.tabs.onActivated: query the activated tab via chrome.tabs.get(activeInfo.tabId); if tab.url passes filter, addUrl(tab.url). - On chrome.tabs.onUpdated: when changeInfo.url is present AND passes filter, addUrl(changeInfo.url). (Filters out the cascade of intermediate events that fire during page load — `changeInfo.status === 'complete'` is NOT required because we want to capture URL transitions even for SPA-style routing). - addUrl helper: if URL not in Set, push to firstSeenOrder array AND add to Set; idempotent. - Filter helper: accept https://, http://, chrome-extension://; reject chrome://, about:, devtools://, file://, blob:, data:. - Defense-in-depth: all chrome.* listener registrations wrapped in try/catch (matches src/background/index.ts:bootstrap pattern for chrome.notifications + chrome.action listeners). - getTabUrlsSeen returns a slice (copy) of firstSeenOrder — caller cannot mutate internal state. - clearTabUrlsSeen empties both the Set and the array. - module-init guard: `let initialized = false`; initTabUrlTracker sets to true; subsequent calls return early with a logger.warn for idempotency. Create `src/background/tab-url-tracker.ts` with the exact public API from the interfaces block. Use the Logger pattern from src/shared/logger.ts (existing pattern at recorder.ts:78 / index.ts:49): ```typescript import { Logger } from '../shared/logger'; const logger = new Logger('TabUrlTracker'); ``` The filter implementation: ```typescript function passesFilter(url: string): boolean { // Per .planning/phases/02-stabilize-export-pipeline/02-CONTEXT.md : // INCLUDE: https://, http://, chrome-extension:// // EXCLUDE: chrome://, about:, devtools://, file://, blob:, data: return /^(https?|chrome-extension):\/\//.test(url); } ``` Module structure (top → bottom): 1. Logger import + instantiation. 2. State: `let tabUrlsSeen: Set`, `let firstSeenOrder: string[]`, `let initialized: boolean`. 3. passesFilter helper. 4. addUrl helper (internal). 5. initTabUrlTracker (public) — registers listeners with defensive try/catch. 6. getTabUrlsSeen (public). 7. clearTabUrlsSeen (public). Per D-P2-02 + CONTEXT.md ``: the operator's primary tab at SAVE time should always be in the array if it has a valid URL. The chrome.tabs.onActivated path covers this — the active tab fires onActivated whenever the operator switches to it (including the first activation when the SW starts up). Combined with chrome.tabs.onUpdated, this captures both tab-switch events and in-tab navigation events. No explicit "snapshot active tab at SAVE time" is needed (the tracker has already captured it via onActivated). NOTE on `tabs` permission gap: chrome.tabs.onActivated provides tabId in the event payload; chrome.tabs.get(tabId) returns the tab WITHOUT the `.url` field unless `tabs` permission is granted (Plan 01-13 SUMMARY Known Limitations item 3). For the active tab specifically, `activeTab` permission grants temporary URL access. The tracker accepts the URL when present and skips silently when absent (logged at warn level for diagnostics). This means inactive-tab URL changes are NOT captured — acceptable per planner-decision deferred to Phase 4 hardening (CONTEXT.md `` "tabs permission gap"). Document this limitation inline. Per D-P2-03's "urls array non-empty" rule from Plan 02-01 Task 3: if getTabUrlsSeen returns an empty array at SAVE time, createArchive (Task 3 below) must fall back to a sentinel value (e.g., the extension-origin URL from `new URL(chrome.runtime.getURL('')).origin` — same as the current single-url path). This guarantees the strict 8-field schema validation never fails on empty urls[]. npx tsc --noEmit 2>&1 | head -20 File exists. tsc clean. Three exports per public API. Defensive try/catch wraps chrome.tabs.* listener registrations. Atomic commit: `feat(02-03): tab-url-tracker — chrome.tabs.onActivated + onUpdated → urls[] with dedup + filter (D-P2-02)`. Task 2: Update SessionMetadata type + meta.json assembly + tracker wiring src/shared/types.ts, src/background/index.ts - src/shared/types.ts SessionMetadata: REPLACE `url: string` with `urls: string[]`; ADD `schemaVersion: string` as the first field. Total 8 fields. - src/background/index.ts: (a) import { initTabUrlTracker, getTabUrlsSeen } from './tab-url-tracker'; (b) call initTabUrlTracker() inside the existing `initialize()` function (line 957) under defensive try/catch matching the chrome.notifications.onClicked pattern. (c) createArchive (line 673-684) updated: ```typescript const manifest = chrome.runtime.getManifest(); const tabUrls = getTabUrlsSeen(); // D-P2-03 non-empty guarantee: if the tracker has no URLs (e.g., SW just spawned, // operator hasn't activated a real tab yet), fall back to the extension-origin URL // so the strict-validation schema (REQ-meta-json-schema) never fails on empty array. const urls = tabUrls.length > 0 ? tabUrls : [new URL(chrome.runtime.getURL('')).origin]; const metadata: SessionMetadata = { schemaVersion: '2', timestamp: new Date().toISOString(), urls, userAgent: navigator.userAgent, extensionVersion: manifest.version, videoBufferSeconds: 30, logDurationMinutes: 10, totalEvents: rrwebEvents.length + userEvents.length }; ``` - Object key ordering matches the SessionMetadata interface declaration order (TypeScript object literal preserves source order; meta.json output is JSON.stringify(metadata, null, 2) → keys in insertion order per ECMA-262). - Always-on charter preserved: createArchive does NOT call clearTabUrlsSeen() — the tracker continues accumulating across saves (next save captures any tabs activated after this one). 1. Edit `src/shared/types.ts` SessionMetadata interface (lines 102-111): - DELETE `url: string;`. - INSERT `schemaVersion: string;` as the first field. - INSERT `urls: string[];` in the position previously occupied by `url`. - Final ordering: schemaVersion, timestamp, urls, userAgent, extensionVersion, videoBufferSeconds, logDurationMinutes, totalEvents (8 fields). - Add a docstring block above the interface citing D-P2-02 (the url→urls cutover) + D-P2-03 (schemaVersion + 8-field exact rule) + the planner-resolved Plan 02-01 Task 3 decision to make schemaVersion the 8th field. 2. Edit `src/background/index.ts`: - Add import: `import { initTabUrlTracker, getTabUrlsSeen } from './tab-url-tracker';` near the existing import block (line 1-11). - In `initialize()` (line 957), add a defensive try/catch block calling initTabUrlTracker() right after the existing chrome.notifications.onClicked listener registration (search for that pattern; mirror the surrounding try/catch shape). - Replace the metadata-construction block in createArchive (line 673-684) with the new 8-field shape per the behavior block above. 3. Per D-P2-02 + D-P2-03: - schemaVersion: '2' marks the breaking-change cutover. Future schema bumps increment. - urls is the operator's multi-tab context. - The non-empty fallback (extension-origin URL) guarantees REQ-meta-json-schema strict- validation passes even on a freshly-spawned SW with no tab activity. Per the planner-resolved decision in Plan 02-01 Task 3: the 8th field name `schemaVersion` was a tentative planner pick. If plan-checker / implementer surfaces a stronger candidate name, BOTH this plan AND tests/build/strict-meta-json-validation.test.ts EXPECTED_KEYS must change in lockstep (matching the Plan 01-14 lockstep-change pattern for FORBIDDEN_HOOK_STRINGS). npx tsc --noEmit 2>&1 | head -20 ; npm run build 2>&1 | tail -10 ; npx vitest run tests/background/meta-json-urls-schema.test.ts tests/build/strict-meta-json-validation.test.ts 2>&1 | tail -30 tsc clean. npm run build clean. SessionMetadata has exactly 8 fields. createArchive emits 8-field meta.json with schemaVersion='2' + urls (non-empty via fallback). All 4 tests in meta-json-urls-schema.test.ts GREEN. All 8 tests in strict-meta-json-validation.test.ts GREEN. Atomic commit: `feat(02-03): meta.json — urls[] + schemaVersion (D-P2-02 + D-P2-03; replaces url:string)`. Task 3: Amend REQUIREMENTS.md REQ-meta-json-schema for the new 8-field shape .planning/REQUIREMENTS.md - REQ-meta-json-schema entry (lines 106-119) updated to reflect the new 8-field schema verbatim. - Inline comment block citing D-P2-02 + D-P2-03 + Plan 02-03 as the cutover provenance. - Acceptance criteria preserved: all fields required; types correct; timestamp ISO-8601 with Z. - Add new acceptance criteria: urls is non-empty string[]; schemaVersion === '2'. - Traceability table entry for REQ-meta-json-schema status update: "Pending → Complete pending Plan 02-04 harness validation" (Plan 02-04 may flip this to Complete; this plan only ships the implementation + amended text). Edit `.planning/REQUIREMENTS.md`: 1. Replace lines 106-119 (REQ-meta-json-schema block) with the new 8-field schema: ```markdown - [ ] **REQ-meta-json-schema**: `meta.json` inside the archive conforms to the verbatim schema (D-P2-02 + D-P2-03 cutover; replaces the 7-field `url:string` shape per audit P1 #10 amendment 2026-05-20): ```json { "schemaVersion": "2", "timestamp": "2025-05-15T14:32:10Z", "urls": ["https://example.com/", "https://app.example.com/dashboard"], "userAgent": "Chrome/...", "extensionVersion": "1.0.0", "videoBufferSeconds": 30, "logDurationMinutes": 10, "totalEvents": 143 } ``` All 8 fields required. Acceptance: - `schemaVersion === '2'` (marks the D-P2-02 url→urls cutover; future schema bumps increment) - `timestamp` ISO-8601 with `Z` suffix - `urls` is a non-empty `string[]` of URLs matching `/^(https?|chrome-extension):\/\//` (per CONTEXT.md `` filter rules — exclude chrome://, about:, devtools://, file://) - `urls` is deduplicated; ordering is first-seen-first across the rolling recording window - `extensionVersion` matches semver - `totalEvents` is a non-negative integer - exactly 8 keys; no extras Binding: CON-meta-json-schema (this REQ-text supersedes the original CON-meta-json-schema 7-field shape). ``` 2. Update the Traceability table entry (line 225) for REQ-meta-json-schema: FROM: `| REQ-meta-json-schema | Phase 3 (originally) → **Phase 2** (renumbered) | Pending |` TO: `| REQ-meta-json-schema | Phase 2 | Pending (implementation landed via Plan 02-03; harness validation deferred to Plan 02-04) |` 3. Append a `*Updated YYYY-MM-DD*` footer line at the bottom of the file matching the existing footer pattern (line 249 area). Date: 2026-05-20. Per D-P2-02 + D-P2-03: this is the canonical schema-amendment Phase 2 ships. The 8-field shape becomes the new baseline for all downstream consumers (UAT harness, future v2 SRV-* uploads, operator post-mortem tooling). grep -c "schemaVersion" /home/parf/projects/work/repremium/.planning/REQUIREMENTS.md ; grep -c "urls.*string\[\]" /home/parf/projects/work/repremium/.planning/REQUIREMENTS.md REQUIREMENTS.md REQ-meta-json-schema block reflects 8-field shape. Traceability table updated. Footer line appended with 2026-05-20 date. Atomic commit: `docs(02-03): REQUIREMENTS — REQ-meta-json-schema amended for 8-field shape with urls[] + schemaVersion`. ## Trust Boundaries | Boundary | Description | |----------|-------------| | tab URL strings → meta.json | Tab URLs may contain sensitive operator state (per CONTEXT.md `` D-P2-02 "Privacy note"); the "log is internal" charter accepts this. | | chrome.tabs.* event source | Chrome platform → SW; trustworthy per MV3 contract. | | tab-url-tracker internal Set → getTabUrlsSeen consumer | Module returns a COPY (slice); caller cannot mutate internal state. | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-02-03-01 | Information Disclosure | URLs with embedded credentials (https://user:pass@host/) leak into meta.json | accept | Charter shift 2026-05-20 ("log is internal") explicitly accepts this. REQ-password-confidentiality moved to Out of Scope v1. No mitigation in v1; v2/Phase 4 candidate. | | T-02-03-02 | Information Disclosure | chrome-extension:/// URLs reveal extension presence | accept | The extension is installed unpacked locally; presence is already self-evident from chrome://extensions. INCLUDE per CONTEXT.md `` filter. | | T-02-03-03 | Tampering | Malicious extension overrides chrome.tabs.* events | mitigate | Extension permissions are user-granted at install time. Defensive try/catch wraps every chrome.* call so malformed events don't crash the SW. | | T-02-03-04 | Denial of Service | Unbounded tab-url Set growth in long-running SW | mitigate | URL set is bounded by O(unique tabs operator visits per SW lifetime). Operationally ≤500 even in heavy use. No pruning needed within v1 budget; SW idle teardown (~30s) clears state. Phase 4 hardening can add pruning if production telemetry surfaces unbounded growth. | | T-02-03-05 | Repudiation | Tracker silently drops URLs when `tabs` permission absent (chrome.tabs.get returns undefined .url) | mitigate | Diagnostic logger.warn at every dropped event so post-mortem investigation can identify the permission gap. Phase 4 hardening adds `tabs` permission per CONTEXT.md `` item. | - `npx tsc --noEmit` → clean. - `npm run build` → clean. - `npx vitest run tests/background/meta-json-urls-schema.test.ts` → 4/4 GREEN. - `npx vitest run tests/build/strict-meta-json-validation.test.ts` → 8/8 GREEN. - `npx vitest run` (full suite) → previously RED tests from Plan 02-01 all GREEN. Net: 153 + 15 = 168 GREEN (or whatever the exact count is post-Plan-02-01 + Plan-02-02). - `npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts` → 12 FORBIDDEN_HOOK_STRINGS unchanged → GREEN. - `npm run test:uat` → 24/24 GREEN preserved (no harness changes in this plan; A13's meta.json shape check may detect the schemaVersion + urls fields — verify A13 still GREEN; if it depends on the old `url` field, Plan 02-04 addresses). - `grep -c "schemaVersion" .planning/REQUIREMENTS.md` ≥ 2 (in REQ block + acceptance bullet). - `grep -c "url: string" src/shared/types.ts` → 0 (the legacy field is gone). - Always-on charter check: `grep -n "clearTabUrlsSeen" src/background/index.ts` → 0 hits (createArchive does NOT clear; tracker keeps accumulating after SAVE). 1. tests/background/meta-json-urls-schema.test.ts 4/4 GREEN. 2. tests/build/strict-meta-json-validation.test.ts 8/8 GREEN. 3. UAT harness 24/24 GREEN preserved (or A13 amended in lockstep if its meta.json assertions depend on the old `url` field — verify before commit). 4. .planning/REQUIREMENTS.md REQ-meta-json-schema reflects the 8-field shape with breaking-change cutover documented. 5. Always-on charter preserved: tab-url-tracker continues accumulating after SAVE; no createArchive-time clearTabUrlsSeen call. 6. Tier-1 FORBIDDEN_HOOK_STRINGS = 12 (unchanged). 7. tabs permission gap NOT closed by this plan — explicit deferral comment in tab-url-tracker.ts citing CONTEXT.md ``. After completion, create `.planning/phases/02-stabilize-export-pipeline/02-03-SUMMARY.md` documenting: - New module src/background/tab-url-tracker.ts (LOC + public API summary) - SessionMetadata field-count delta (7 → 8) + ordering rationale - 8th field `schemaVersion` decision: planner-suggested in Plan 02-01 Task 3, ratified here - Filter rules verbatim from CONTEXT.md `` - Tabs permission gap deferral citation (CONTEXT.md ``) - Forward link: Plan 02-04 may amend UAT harness A13 if its meta.json assertions assumed the old `url` field