Drops the 2026-05-17 draft (zero commits, zero SUMMARY — virgin). Carries: - onInstalled flag-gated welcome tab (3 RED→GREEN unit tests; storage-key contract pinned per prior I-02 fix) - 5 new src/welcome/* files: welcome.html + welcome.ts + welcome.css + welcome-tokens.css + copy.ts - Design-swap-in-ready: every color via var(--mks-*); every string via COPY map; every font via var(--mks-font-*) with system fallback - vite.config.ts + vite.test.config.ts both gain welcome rollup input - manifest.json gains web_accessible_resources for welcome.html - Harness extended A15+A16+A17 (onboarding flag observability + no-re-open settle + design-swap invariant); Tier-1 forbidden-strings inventory unchanged at 10 - 4 autonomous tasks + 1 operator empirical checkpoint Deletes: REQUEST_PERMISSIONS flow (gone in 01-09); duplicate Start button (D-16-toolbar owns start path); start-path divergence Cites: D-17-onboarding (CONTEXT.md L537+), D-02 (welcome layout), D-08 (tagline), D-03 (voice register), D-16-toolbar (start ownership), brand-decisions-v1-followup-display-font.md (Plan 01-12 blocker; this plan ships TODAY with placeholders) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
68 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, tags, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | tags | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-stabilize-video-pipeline | 10 | execute | 4 |
|
|
false |
|
|
|
This complements Plan 01-09's runtime activation paths (toolbar onClicked → picker; onStartup notification at browser launch) by adding the install-time activation path. The welcome tab does NOT trigger recording itself — start remains a SINGLE path through chrome.action.onClicked in idle mode (D-16-toolbar). The welcome tab is informational + read-only; CTA copy directs the operator at the toolbar.
Design-swap-in-ready architecture (the key directive). Plan 01-12 has not yet landed; the designer follow-up on --mks-font-display Cyrillic gap is still open (brand-decisions-v1-followup-display-font.md). Plan 01-10 ships TODAY with engineering placeholders, architected so the Plan 01-12 swap is a small commit: every color via --mks-* CSS custom property in a separate welcome-tokens.css file (Plan 01-12 replaces with @import of tokens.css); every copy string via src/welcome/copy.ts map (Plan 01-12 swaps to chrome.i18n.getMessage post _locales/); every typography choice via var(--mks-font-*) with system fallback (Plan 01-12 self-hosts WOFF2 + drops the fallback dependency).
Harness integration (A15-A17). Extend the Plan 01-13 harness with three new assertions covering: onboarding flag observability (A15), subsequent-install does NOT re-open (A16), design-swap-readiness invariant (A17). Drivers register alongside driveA1..driveA14 in tests/uat/harness.test.ts. No new forbidden-hook strings — the Tier-1 grep gate inventory stays at 10.
Output:
- src/welcome/welcome.{html,ts,css} + src/welcome/welcome-tokens.css + src/welcome/copy.ts — 5 new files for the design-swap-in-ready welcome page.
- src/background/index.ts — openWelcomeIfFirstInstall helper + onInstalled wiring.
- manifest.json — web_accessible_resources array entry for the welcome page.
- vite.config.ts + vite.test.config.ts — rollupOptions.input gains welcome entry in BOTH bundles.
- tests/background/onboarding.test.ts — 3 RED→GREEN unit tests.
- tests/uat/extension-page-harness.{html,ts} + tests/uat/lib/harness-page-driver.ts + tests/uat/harness.test.ts — A15+A16+A17 harness assertions.
Cites: D-17-onboarding (CONTEXT.md line 537+), D-02 (welcome layout — brand-decisions-v1.md), D-08 (tagline — brand-decisions-v1.md), D-03 (Sober voice register — brand-decisions-v1.md), D-16-toolbar (toolbar owns start — 01-09-SUMMARY.md).
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/REQUIREMENTS.md @.planning/phases/01-stabilize-video-pipeline/01-CONTEXT.md @.planning/phases/01-stabilize-video-pipeline/01-09-SUMMARY.md @.planning/phases/01-stabilize-video-pipeline/01-11-SUMMARY.md @.planning/phases/01-stabilize-video-pipeline/01-13-SUMMARY.md @.planning/intel/brand-decisions-v1.md @.planning/intel/brand-decisions-v1-followup-display-font.md @.planning/intel/design-incoming/system/bundle/mokosh-handoff/tokens.css @.planning/debug/resolved/01-09-save-stops-recording.md @src/background/index.ts @src/popup/index.html @src/popup/index.ts @src/shared/logger.ts @manifest.json @vite.config.ts @vite.test.config.ts @tests/background/onstartup-notification.test.ts @tests/uat/extension-page-harness.html @tests/uat/extension-page-harness.ts @tests/uat/lib/harness-page-driver.ts @tests/uat/harness.test.ts @tests/background/no-test-hooks-in-prod-bundle.test.ts Key Chrome API surfaces and project conventions the executor needs.- chrome.runtime.onInstalled =============================
Signature (from @types/chrome): chrome.runtime.onInstalled.addListener( callback: (details: chrome.runtime.InstalledDetails) => void): void interface InstalledDetails { reason: 'install' | 'update' | 'chrome_update' | 'shared_module_update'; previousVersion?: string; id?: string; }
CURRENT handler in src/background/index.ts (line 724+, post-01-09): chrome.runtime.onInstalled.addListener((details) => { logger.log('Extension installed/updated:', details.reason); try { indexedDB.deleteDatabase('VideoRecorderDB'); logger.log('Cleaned up orphaned VideoRecorderDB (if present)'); } catch (e) { logger.warn('IDB cleanup failed:', e); } initialize(); });
This plan EXTENDS the handler by appending a fire-and-forget call to openWelcomeIfFirstInstall(details) AFTER initialize(). The existing synchronous handler stays synchronous; the helper is async-IIFE-style with its own try/catch.
- chrome.storage.local =======================
Signature: chrome.storage.local.get(key: string): Promise<{[key: string]: unknown}> chrome.storage.local.set(items: {[key: string]: unknown}): Promise
Used pattern (helper): const stored = await chrome.storage.local.get(ONBOARDING_FLAG); if (stored[ONBOARDING_FLAG] === true) { return; } // open tab ... await chrome.storage.local.set({ [ONBOARDING_FLAG]: true, 'installed-at': Date.now(), });
storage permission is ALREADY in manifest.json:11. No change needed.
- chrome.tabs.create =====================
Signature: chrome.tabs.create({url: string, active?: boolean}): Promise<chrome.tabs.Tab>
For an extension-internal URL: chrome.runtime.getURL('src/welcome/welcome.html') returns chrome-extension:///src/welcome/welcome.html. This URL is ONLY navigable if the path is declared in web_accessible_resources (MV3 requirement).
- manifest.json web_accessible_resources (MV3) ================================================
Current manifest.json HAS NO web_accessible_resources block (verified via Read at plan time). This plan adds one.
Shape (JSON): "web_accessible_resources": [ { "resources": ["src/welcome/welcome.html"], "matches": ["<all_urls>"] } ]
Insertion point: after "host_permissions" block (lines 15-17), before "background" block. Verify storage permission still present at line 11.
- vite.config.ts rollupOptions.input ======================================
Current state (vite.config.ts:52-55): rollupOptions: { input: { offscreen: 'src/offscreen/index.html' }, },
Add welcome entry: rollupOptions: { input: { offscreen: 'src/offscreen/index.html', welcome: 'src/welcome/welcome.html', }, },
Mirror in vite.test.config.ts at the same key path (line 82-92) so dist-test/ also emits the welcome page. The harness A15/A16/A17 navigates against the test bundle.
- src/welcome/welcome.html structure ======================================
- src/welcome/copy.ts shape =============================
// src/welcome/copy.ts — single source-of-truth for welcome-page copy. // Plan 01-10 D-17-onboarding ships with placeholder Russian strings // per D-03 Sober voice register. Plan 01-12 swaps to chrome.i18n.getMessage // (post _locales/) OR hot-swaps to brand-team final 8 strings (Brief §02). // STABLE KEYS — do NOT rename; Plan 01-12 binds to these. // // D-08 tagline locked: "«Тридцать секунд назад, всегда под рукой.»" + // "Thirty seconds ago, always at hand."
export const COPY: Readonly<Record<string, string>> = Object.freeze({ 'welcome.page.title': 'Добро пожаловать в Mokosh', 'welcome.hero.title': 'Mokosh', 'welcome.hero.tagline.ru': '«Тридцать секунд назад, всегда под рукой.»', 'welcome.hero.tagline.en': 'Thirty seconds ago, always at hand.', 'welcome.body.explainer.line1': 'Mokosh непрерывно записывает последние 30 секунд экрана и 10 минут ' + 'логов вашего браузера.', 'welcome.body.explainer.line2': 'Когда возникает баг, вы одним кликом сохраняете архив для службы ' + 'поддержки. Данные не отправляются никуда — только локально.', 'welcome.body.cta.toolbar': 'Чтобы начать запись, нажмите иконку AI Call Recorder на панели ' + 'инструментов браузера (правый верхний угол).', 'welcome.footer.privacy': 'Mokosh не отправляет данные на серверы. Архив создаётся ' + 'локально по вашему запросу и остаётся на вашем компьютере.', });
- src/welcome/welcome-tokens.css shape (placeholder) ======================================================
/* welcome-tokens.css — Plan 01-10 D-17-onboarding placeholder palette.
- Mirrors tokens.css semantic names. Plan 01-12 replaces with
- @import url('./tokens.css'); OR direct deletion + canonical link in
- welcome.html. Variable NAMES match tokens.css exactly; only VALUES
- differ. */
:root { --mks-surface: #fafafa; --mks-surface-raised: #ffffff; --mks-surface-sunken: #ececec; --mks-border: #e0e0e0; --mks-fg-1: #181b2a; --mks-fg-2: #5b5f76; --mks-fg-3: #7a7e94; --mks-rec: #b2543d; --mks-success: #5a7349; --mks-font-display: 'Iowan Old Style', 'Apple Garamond', 'Baskerville', 'Times New Roman', 'Droid Serif', Times, serif; --mks-font-ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; --mks-welcome-max-w: 720px; --mks-space-4: 16px; --mks-space-8: 32px; --mks-space-12: 48px; }
NOTE on welcome-tokens.css hex literals: this file IS allowed to contain hex literals — it IS the engineering-grade placeholder palette source. Plan 01-12 replaces this file entirely; until then the hex values provide the actual color rendering. The A17 grep gate excludes welcome-tokens.css from its scan; only welcome.css must be hex-literal-free.
- src/welcome/welcome.ts shape (filter-pipeline form; no
continue) =====================================================================
import { Logger } from '../shared/logger'; import { COPY } from './copy';
const logger = new Logger('Welcome');
function populateCopy(): void { const els = Array.from(document.querySelectorAll('[data-mokosh-key]')); const pairs = els .map((el) => ({ el, key: el.getAttribute('data-mokosh-key') })) .filter((p): p is { el: HTMLElement; key: string } => typeof p.key === 'string') .map((p) => ({ ...p, value: COPY[p.key] })) .filter((p): p is { el: HTMLElement; key: string; value: string } => typeof p.value === 'string'); for (const { el, value } of pairs) { if (el.tagName === 'TITLE') { document.title = value; } else { el.textContent = value; } } const missing = els.length - pairs.length; if (missing > 0) { logger.warn('populateCopy: ' + String(missing) + ' [data-mokosh-key] elements had missing COPY entries'); } }
function init(): void { populateCopy(); logger.log('welcome page ready'); }
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); }
- src/welcome/welcome.css invariant ======================================
KEY INVARIANT (A17 contract): regex /#[0-9a-fA-F]{3,8}/ must match ZERO times in welcome.css (NOT welcome-tokens.css; that one is the source). Every color = var(--mks-); every font = var(--mks-font-). Layout: max-width via var(--mks-welcome-max-w). BEM-ish class names match the HTML.
- Harness A15-A17 architecture =================================
Pattern mirrors A14 (most recent addition):
- Page-side: assertA15/A16/A17 in extension-page-harness.ts (added to window.__mokoshHarness global surface).
- Host-side: driveA15/A16/A17 in lib/harness-page-driver.ts — standard page.evaluate wrapper.
- Orchestrator: registered in drivers array in harness.test.ts.
A15 — onboarding flag observability: Approach: read chrome.storage.local.get(['onboarding-completed', 'installed-at']) and assert both keys present + correct types. Under puppeteer.launch({enableExtensions: [DIST_TEST_DIR]}) with a fresh user-data-dir, the extension load IS the install event; the helper fires; storage gets the flag set. If the harness's launchHarnessBrowser uses a persistent user-data-dir between runs, the flag may already be set from a previous run — still GREEN (the contract is "first install set the flag", not "this run set the flag").
A16 — subsequent-install does NOT re-open: Approach: snapshot chrome.tabs.query({}) tab URLs BEFORE a 2-second settle window. After the settle, snapshot again. Assert no new tab URLs containing 'src/welcome/welcome.html' appeared. Combined with A15's flag-is-set assertion: A15 proves install handler ran AT LEAST ONCE; A16 proves the handler isn't spuriously re-firing.
A17 — design-swap-readiness invariant: Fetch chrome-extension:///src/welcome/welcome.html via fetch(chrome.runtime.getURL('src/welcome/welcome.html')). Same for src/welcome/welcome.css and src/welcome/copy.ts (the bundled chunk path; locate via parsing the <script src> attribute in welcome.html — Vite's hashed chunk filename). Assertions: 1. welcome.html parses; document.querySelector('.welcome-hero') !== null. 2. welcome.css text content: NO match for /#[0-9a-fA-F]{3,8}/. 3. welcome.css text content: AT LEAST 5 occurrences of 'var(--mks-'. 4. welcome.html text: AT LEAST 7 occurrences of 'data-mokosh-key='. 5. The bundled welcome.js chunk text content: contains 'COPY[' substring (proves COPY-map indexing pattern survives the build).
No new forbidden-strings entries — A15-A17 use existing chrome.tabs.query
- chrome.storage.local.get + fetch on public surfaces (web_accessible_resources). The Tier-1 grep gate inventory stays at 10.
- Centralized Logger pattern ===============================
src/shared/logger.ts exports Logger class. Welcome page uses new Logger('Welcome'). If the executor prefers, they MAY add a WelcomeLogger class mirroring OffscreenLogger / ContentLogger pattern.
- Centralized chrome stub pattern for unit tests ===================================================
tests/background/onstartup-notification.test.ts exemplifies the chrome stub used across Plan 01-09 unit tests. Copy the buildBgStub factory + extend with:
- chrome.tabs.create: vi.fn().mockResolvedValue({id:99, url:'x'})
- chrome.storage.local.{get,set} mocks
- chrome.runtime.onInstalled rigged with _callbacks array (same idiom as onStartup at line 24).
Per 01-09 SUMMARY deviation 4: every new listener registration in SW MUST be in try/catch. The onInstalled handler is ALREADY registered (line 724); this plan ONLY extends its body. The new openWelcomeIfFirstInstall invocation gets its own try/catch via fire-and-forget .catch(...) chain.
Task 1: RED unit tests — onInstalled install creates welcome tab; update does NOT; already-completed flag suppresses; storage key contract. - tests/background/onstartup-notification.test.ts lines 1-235 (chrome stub scaffold + reset+inject pattern) - src/background/index.ts lines 724-737 (existing onInstalled handler) - manifest.json (confirms storage permission already at line 11) tests/background/onboarding.test.ts Three RED tests pinning truths 1 + 2 + 8 from must_haves:Test A — first install + empty storage:
chrome.storage.local.get.mockResolvedValue({});
Fire onInstalled with {reason: 'install'}.
Assert:
1. chrome.tabs.create called exactly once with object whose
url contains 'src/welcome/welcome.html'.
2. chrome.storage.local.set called with object containing
{'onboarding-completed': true} AND {'installed-at': <number>}.
3. chrome.storage.local.get called with EXACT key
'onboarding-completed' (pins schema for cross-version compat;
preserves the I-02 lesson from prior plan draft).
Test B — subsequent install (reason='update'):
chrome.storage.local.get.mockResolvedValue({});
Fire onInstalled with {reason: 'update'}.
Assert chrome.tabs.create NOT called AND chrome.storage.local.set
NOT called.
Test C — install but flag already set:
chrome.storage.local.get.mockResolvedValue(
{'onboarding-completed': true});
Fire onInstalled with {reason: 'install'}.
Assert chrome.tabs.create NOT called.
1. Create tests/background/onboarding.test.ts.
2. Copy the buildBgStub factory + types from
tests/background/onstartup-notification.test.ts lines 59-102.
Extend the stub with:
- chrome.tabs.create: vi.fn().mockResolvedValue({id:99, url:'x'})
- chrome.storage = { local: { get: vi.fn(), set: vi.fn() } }
- chrome.runtime.onInstalled = { addListener: vi.fn(),
_callbacks: [] }, with addListener.mockImplementation
pushing into _callbacks
3. Three tests inside describe('Plan 01-10 onboarding contract', ...)
— beforeEach { vi.resetModules() }; pattern:
const stub = buildBgStub();
stub.storage.local.get.mockResolvedValue();
(globalThis as ...).chrome = stub;
(globalThis as ...).indexedDB = { deleteDatabase: vi.fn() };
await import('../../src/background/index');
const cb = stub.runtime.onInstalled._callbacks[0];
expect(cb).toBeTypeOf('function');
cb({ reason: '' });
for (let i = 0; i < 16; i++) { await Promise.resolve(); }
4. Run npx vitest run tests/background/onboarding.test.ts — all 3
MUST be RED (openWelcomeIfFirstInstall helper does not exist;
chrome.tabs.create never called).
5. DO NOT modify src/background/index.ts. RED only.
Style/naming: full-word variable names; no `as any` (use the typed
extension); no `continue` in test loops (drain via for-i).
Commit (using Mark's em-dash + Co-Authored-By style):
test(01-10): wave-0 task-1 — RED onboarding tests (3 failing, pins install/update/flag + storage-key)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
npx vitest run tests/background/onboarding.test.ts 2>&1 | grep -E "3 failed" || (echo "expected 3 failed"; exit 1)
- tests/background/onboarding.test.ts exists with exactly 3 tests.
- All 3 tests RED.
- npx tsc --noEmit exit 0.
- Baseline (98 passing after 01-13) preserved; new state 98 + 3 RED = 101 total.
- No changes to src/background/index.ts.
3 RED tests pin onInstalled welcome-tab + storage-flag contract; baseline preserved; commit landed.
Task 2: Welcome page bundle (welcome.html + welcome.ts + welcome.css + welcome-tokens.css + copy.ts) + Vite entries in both bundles + manifest web_accessible_resources.
- src/popup/index.html (style analog — lang=ru + module script pattern)
- src/popup/style.css (sizing reference — DO NOT inherit hex literals)
- vite.config.ts lines 52-55 (rollupOptions.input current shape)
- vite.test.config.ts lines 82-92 (mirrored input shape)
- manifest.json (insertion point for web_accessible_resources)
- .planning/intel/brand-decisions-v1.md §D-02 (welcome layout: Hero + Loom dial)
- .planning/intel/brand-decisions-v1.md §D-08 (tagline RU+EN)
- .planning/intel/brand-decisions-v1.md §D-03 (Sober voice register)
- .planning/intel/design-incoming/system/bundle/mokosh-handoff/tokens.css (semantic-name reference)
- src/shared/logger.ts (Logger class shape)
src/welcome/welcome.html, src/welcome/welcome.ts, src/welcome/welcome.css,
src/welcome/welcome-tokens.css, src/welcome/copy.ts,
vite.config.ts, vite.test.config.ts, manifest.json
1. Create src/welcome/copy.ts per interfaces §7. Placeholder Russian
strings per D-03 Sober (short, declarative, period-terminated, no
superlatives). D-08 tagline strings LOCKED verbatim. Keys STABLE.
2. Create src/welcome/welcome-tokens.css per interfaces §8. Comment
header cites Plan 01-12 as the replacement moment + brand-decisions-v1.md
→ tokens.css as canonical source. Use EXACT --mks-* names from
tokens.css §Surface / §Foreground / §Accents / §Type / §Layout.
3. Create src/welcome/welcome.html per interfaces §6 — semantic
sections (welcome-hero / welcome-body / welcome-footer); every
textContent target carries data-mokosh-key=stable-key; mark slot
is data-mokosh-slot=mark (Plan 01-12 swap target); <title> also
has data-mokosh-key='welcome.page.title'. Two <link> stylesheet
tags: welcome-tokens.css BEFORE welcome.css (tokens cascade first).
4. Create src/welcome/welcome.ts per interfaces §9. Use the
filter-pipeline form (no `continue`). Import Logger from
'../shared/logger' + COPY from './copy'. document.readyState
guard: init immediately if already loaded.
5. Create src/welcome/welcome.css per interfaces §10. KEY INVARIANT:
grep -E '#[0-9a-fA-F]{3,8}' src/welcome/welcome.css MUST exit 1
(no matches). Every color = var(--mks-*). BEM-ish class names
matching the HTML.
6. Update vite.config.ts lines 52-55 — add welcome entry alongside
offscreen:
rollupOptions: {
input: {
offscreen: 'src/offscreen/index.html',
welcome: 'src/welcome/welcome.html',
},
},
7. Update vite.test.config.ts lines 82-92 — mirror the welcome entry
alongside extension_page_harness:
rollupOptions: {
input: {
extension_page_harness: 'tests/uat/extension-page-harness.html',
welcome: 'src/welcome/welcome.html',
},
},
8. Update manifest.json — insert web_accessible_resources AFTER
host_permissions block (lines 15-17) BEFORE background block
(lines 18-21). Verify storage still in permissions array line 11.
The block to insert (proper JSON, comma after host_permissions,
comma after new block):
"web_accessible_resources": [
{
"resources": ["src/welcome/welcome.html"],
"matches": ["<all_urls>"]
}
],
9. Run npm run build — exit 0; verify:
- dist/src/welcome/welcome.html exists
- dist/manifest.json carries the web_accessible_resources block
- grep -E '#[0-9a-fA-F]{3,8}' dist/src/welcome/welcome.css exits 1
10. Run npm run build:test — exit 0; verify dist-test/src/welcome/welcome.html
exists.
11. Run npx tsc --noEmit — exit 0.
12. Run npx vitest run — baseline preserved (3 RED from Task 1 stay
RED; 98 others stay GREEN).
Style/naming: full-word names; no `as any`; no `continue` in welcome.ts.
welcome.html: lang="ru" matches popup precedent. Russian strings live
ONLY in copy.ts.
Commit:
feat(01-10): wave-1 task-2 — welcome page bundle + Vite entries + web_accessible_resources — design-swap-ready (D-02 + D-08 + D-03; placeholders for D-04 + D-05 per Plan 01-12)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
npm run build && npx tsc --noEmit && test -f dist/src/welcome/welcome.html && grep -q web_accessible_resources dist/manifest.json && ! grep -E '#[0-9a-fA-F]{3,8}' dist/src/welcome/welcome.css && npm run build:test && test -f dist-test/src/welcome/welcome.html
- 5 new files exist under src/welcome/.
- welcome.css ZERO #hex literals (grep -E '#[0-9a-fA-F]{3,8}' exits 1).
- welcome.html ≥ 7 data-mokosh-key attributes (grep -o count).
- welcome.ts imports Logger + COPY; NO `as any`; NO `continue`.
- copy.ts exports frozen Record with stable keys; D-08 tagline verbatim.
- vite.config.ts + vite.test.config.ts both have welcome rollup entry.
- manifest.json has web_accessible_resources block; storage permission at line 11.
- npm run build + npm run build:test clean.
- npx tsc --noEmit exit 0.
- Vitest baseline: 3 RED from Task 1 still RED; 98 others GREEN.
Welcome page bundle staged + Vite picks both bundles + manifest declares page accessible + design-swap-readiness invariants hold in BUILT artifact.
Task 3: GREEN — openWelcomeIfFirstInstall helper + onInstalled wiring in src/background/index.ts; flips Task 1 tests GREEN.
- tests/background/onboarding.test.ts (contracts from Task 1)
- src/background/index.ts lines 70-90 (existing top-level constants area)
- src/background/index.ts lines 159-186 (ensureOffscreen pattern)
- src/background/index.ts lines 724-737 (existing onInstalled handler)
- .planning/phases/01-stabilize-video-pipeline/01-09-SUMMARY.md "Defense-in-depth chrome.* try/catch" note
src/background/index.ts
1. Add top-level constants near badge/notification block at line 73-88:
// Plan 01-10 onboarding constants (D-17-onboarding).
const ONBOARDING_FLAG = 'onboarding-completed';
const ONBOARDING_INSTALLED_AT = 'installed-at';
const WELCOME_PATH = 'src/welcome/welcome.html';
SCREAMING_SNAKE per project naming rule for true constants.
2. Add openWelcomeIfFirstInstall helper just below ensureOffscreen
(line ~186). Full JSDoc citing Plan 01-10 D-17-onboarding (CONTEXT.md
line 537+; the SUFFIX disambiguates from D-17-port-lifecycle per
CONTEXT.md lines 540-545). Defense-in-depth try/catch wraps the body:
/**
* Open the welcome page on first install (Plan 01-10 D-17-onboarding).
*
* Trigger conditions (all must hold):
* - details.reason === 'install' (NOT 'update' / 'chrome_update' /
* 'shared_module_update');
* - chrome.storage.local key 'onboarding-completed' NOT === true.
*
* Side effects:
* - chrome.tabs.create({url: chrome.runtime.getURL('src/welcome/welcome.html')})
* - chrome.storage.local.set({'onboarding-completed': true,
* 'installed-at': Date.now()})
*
* Failure mode: any thrown chrome.* call caught and logged via
* logger.warn. Welcome tab failing is NOT fatal — toolbar onClicked
* (D-16-toolbar) remains the operator's start path.
*/
async function openWelcomeIfFirstInstall(
details: chrome.runtime.InstalledDetails,
): Promise<void> {
if (details.reason !== 'install') {
return;
}
try {
const stored = await chrome.storage.local.get(ONBOARDING_FLAG);
if (stored[ONBOARDING_FLAG] === true) {
logger.log('Onboarding already completed; skipping welcome tab.');
return;
}
const url = chrome.runtime.getURL(WELCOME_PATH);
await chrome.tabs.create({ url });
await chrome.storage.local.set({
[ONBOARDING_FLAG]: true,
[ONBOARDING_INSTALLED_AT]: Date.now(),
});
logger.log(
'Welcome tab opened (D-17-onboarding); onboarding flag set.',
);
} catch (err) {
logger.warn('openWelcomeIfFirstInstall failed:', err);
}
}
3. Extend the onInstalled handler at line 724. Fire-and-forget call
AFTER initialize(); .catch() so rejected Promise cannot escape
the synchronous listener boundary:
chrome.runtime.onInstalled.addListener((details) => {
logger.log('Extension installed/updated:', details.reason);
try {
indexedDB.deleteDatabase('VideoRecorderDB');
logger.log('Cleaned up orphaned VideoRecorderDB (if present)');
} catch (e) {
logger.warn('IDB cleanup failed:', e);
}
initialize();
// Plan 01-10 D-17-onboarding: open welcome tab on first install.
// Fire-and-forget — helper logs its own errors.
openWelcomeIfFirstInstall(details).catch((err) => {
logger.warn('openWelcomeIfFirstInstall threw:', err);
});
});
4. Run npx vitest run tests/background/onboarding.test.ts — all 3 RED
MUST flip GREEN.
5. Run full vitest suite — baseline 98 + 3 new = 101 GREEN; zero RED.
6. Run npx tsc --noEmit — exit 0. If type complaint on
chrome.runtime.InstalledDetails, narrow parameter to { reason: string }.
7. Run npm run build + npm run build:test — both exit 0.
Style/naming pre-commit:
- No `as any` (chrome.runtime.InstalledDetails type or { reason: string } narrow)
- No `continue` (if-else early-return only)
- No new dependencies
- No edits outside src/background/index.ts
Commit:
feat(01-10): wave-2 task-3 — openWelcomeIfFirstInstall helper + onInstalled wiring (D-17-onboarding) — 3 RED → GREEN
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
npx vitest run tests/background/onboarding.test.ts 2>&1 | grep -E "3 passed" && npx tsc --noEmit && npm run build && npm run build:test
- openWelcomeIfFirstInstall helper exists with documented behavior.
- onInstalled handler invokes helper after initialize().
- All 3 onboarding tests GREEN.
- Full suite 98 + 3 = 101 GREEN.
- npx tsc --noEmit exit 0.
- npm run build + build:test exit 0.
- Tier-1 grep gate (tests/background/no-test-hooks-in-prod-bundle.test.ts)
stays GREEN — no new forbidden strings introduced.
onInstalled extended; SW handler tests GREEN; welcome flow wired end-to-end at the SW layer.
Task 4: Harness A15+A16+A17 extension — page-side asserts in extension-page-harness.ts, host-side drivers in harness-page-driver.ts, registration in harness.test.ts. npm run test:uat 18/18 GREEN.
- tests/uat/extension-page-harness.ts lines 1820-1966 (assertA14 + window.__mokoshHarness install pattern)
- tests/uat/lib/harness-page-driver.ts lines 971-1000 (driveA14 standard wrapper)
- tests/uat/harness.test.ts lines 49-77 + 252-313 (driver imports + drivers array)
- tests/background/no-test-hooks-in-prod-bundle.test.ts lines 105-116 (FORBIDDEN_HOOK_STRINGS — DO NOT extend; A15-A17 use public APIs)
- src/welcome/welcome.html (the artifact A17 fetches + parses)
- dist-test/src/welcome/welcome.html (the BUILT artifact A15/A16/A17 actually navigate against)
tests/uat/extension-page-harness.ts,
tests/uat/lib/harness-page-driver.ts,
tests/uat/harness.test.ts
1. **Page-side (extension-page-harness.ts)**: append three new assertion
methods AFTER assertA14 (~line 1905, before the window.__mokoshHarness
installation block at line 1920).
assertA15 — onboarding flag observability:
async function assertA15(): Promise<AssertionResult> {
const result: AssertionResult = {
passed: false,
name: "A15 — onboarding flag set on first install (chrome.storage.local 'onboarding-completed' === true; 'installed-at' is number)",
checks: [],
diagnostics: [],
};
try {
const stored = await chrome.storage.local.get([
'onboarding-completed', 'installed-at',
]);
diag(result, 'storage.local read: ' + JSON.stringify(stored));
result.checks.push({
name: 'A15.1: onboarding-completed === true',
expected: true,
actual: stored['onboarding-completed'],
passed: stored['onboarding-completed'] === true,
});
result.checks.push({
name: 'A15.2: installed-at is a number',
expected: 'number',
actual: typeof stored['installed-at'],
passed: typeof stored['installed-at'] === 'number',
});
result.passed = result.checks.every((c) => c.passed);
} catch (err) {
result.error = err instanceof Error ? err.message : String(err);
diag(result, 'THREW: ' + result.error);
}
return result;
}
assertA16 — subsequent-install does NOT re-open:
async function assertA16(): Promise<AssertionResult> {
const result: AssertionResult = {
passed: false,
name: 'A16 — subsequent install does NOT re-open welcome tab (2s settle: no new welcome.html tabs appear)',
checks: [],
diagnostics: [],
};
try {
const welcomeUrlSuffix = 'src/welcome/welcome.html';
const tabsBefore = await chrome.tabs.query({});
const beforeCount = tabsBefore.filter(
(t) => typeof t.url === 'string' &&
t.url.endsWith(welcomeUrlSuffix),
).length;
diag(result, 'A16: welcome tabs before settle: ' + String(beforeCount));
await new Promise((r) => setTimeout(r, 2000));
const tabsAfter = await chrome.tabs.query({});
const afterCount = tabsAfter.filter(
(t) => typeof t.url === 'string' &&
t.url.endsWith(welcomeUrlSuffix),
).length;
diag(result, 'A16: welcome tabs after settle: ' + String(afterCount));
const delta = afterCount - beforeCount;
result.checks.push({
name: 'A16.1: welcome-tab delta over 2s settle is 0 (onInstalled flag-gating works)',
expected: 0,
actual: delta,
passed: delta === 0,
});
result.passed = result.checks.every((c) => c.passed);
} catch (err) {
result.error = err instanceof Error ? err.message : String(err);
diag(result, 'THREW: ' + result.error);
}
return result;
}
assertA17 — design-swap-readiness invariant:
async function assertA17(): Promise<AssertionResult> {
const result: AssertionResult = {
passed: false,
name: 'A17 — design-swap-readiness: welcome.html parses; .welcome-hero exists; welcome.css zero hex literals; ≥7 data-mokosh-key; bundled JS contains COPY[',
checks: [],
diagnostics: [],
};
try {
const welcomeUrl = chrome.runtime.getURL('src/welcome/welcome.html');
const htmlRes = await fetch(welcomeUrl);
const htmlText = await htmlRes.text();
const parsed = new DOMParser().parseFromString(htmlText, 'text/html');
const hero = parsed.querySelector('.welcome-hero');
result.checks.push({
name: 'A17.1: welcome.html parses + .welcome-hero exists',
expected: 'truthy',
actual: hero === null ? 'null' : 'element',
passed: hero !== null,
});
const keyMatches = htmlText.match(/data-mokosh-key=/g) ?? [];
result.checks.push({
name: 'A17.2: welcome.html has ≥ 7 data-mokosh-key attributes',
expected: '≥ 7',
actual: String(keyMatches.length),
passed: keyMatches.length >= 7,
});
const cssUrl = chrome.runtime.getURL('src/welcome/welcome.css');
const cssRes = await fetch(cssUrl);
const cssText = await cssRes.text();
const hexMatches = cssText.match(/#[0-9a-fA-F]{3,8}/g) ?? [];
result.checks.push({
name: 'A17.3: welcome.css contains ZERO hex literals (var(--mks-*) only)',
expected: 0,
actual: hexMatches.length,
passed: hexMatches.length === 0,
});
const varMatches = cssText.match(/var\(--mks-/g) ?? [];
result.checks.push({
name: 'A17.4: welcome.css contains ≥ 5 var(--mks- references',
expected: '≥ 5',
actual: String(varMatches.length),
passed: varMatches.length >= 5,
});
// Locate the bundled welcome JS chunk via <script src> in the
// parsed HTML. Vite emits a hashed chunk name; we fetch the
// referenced path relative to welcome.html.
const scriptEl = parsed.querySelector(
'script[type="module"][src]',
);
const scriptSrc = scriptEl?.getAttribute('src') ?? '';
const baseUrl = new URL(welcomeUrl);
const jsUrl = new URL(scriptSrc, baseUrl).href;
const jsRes = await fetch(jsUrl);
const jsText = await jsRes.text();
// The bundled chunk minifies but the COPY[ access pattern
// survives (subscript access is preserved by Vite's minifier
// because copy.ts uses a Readonly<Record> indexed by string
// keys).
result.checks.push({
name: 'A17.5: bundled welcome JS chunk contains COPY[ subscript pattern',
expected: 'contains "COPY["',
actual: jsText.includes('COPY[') ? 'present' : 'absent',
passed: jsText.includes('COPY['),
});
result.passed = result.checks.every((c) => c.passed);
diag(result, 'A17: ' + result.checks.filter((c) => c.passed).length + '/' + result.checks.length + ' subchecks passed');
} catch (err) {
result.error = err instanceof Error ? err.message : String(err);
diag(result, 'THREW: ' + result.error);
}
return result;
}
NOTE A17.5 caveat: if the executor finds that Vite's minifier
transforms `COPY[` (e.g., inlines the Object.freeze entries),
relax to `var(--mks-` substring in welcome.css OR change A17.5
to assert that the bundled JS contains AT LEAST ONE of the
D-08 tagline strings verbatim — that string is uniquely identifiable
and survives minification. The check is "COPY data flows through
the bundle"; the literal-string-check fallback proves it equally well.
2. Update the global type declaration at line 1921+:
declare global {
interface Window {
__mokoshHarness: {
// ... existing assertA1..A14, getManifestVersion ...
assertA15: () => Promise<AssertionResult>;
assertA16: () => Promise<AssertionResult>;
assertA17: () => Promise<AssertionResult>;
};
}
}
window.__mokoshHarness = {
// ... existing ...
assertA15, assertA16, assertA17,
};
3. Update the page-ready log line at line 1966 to mention A17.
4. **Host-side (lib/harness-page-driver.ts)**: append three new
drivers AFTER driveA14 (~line 991):
export async function driveA15(page: Page): Promise<AssertionRecord> {
return await page.evaluate(async () => {
const harness = (window as any).__mokoshHarness;
const r: AssertionRecord = await harness.assertA15();
return r;
}) as AssertionRecord;
}
export async function driveA16(page: Page): Promise<AssertionRecord> {
return await page.evaluate(async () => {
const harness = (window as any).__mokoshHarness;
const r: AssertionRecord = await harness.assertA16();
return r;
}) as AssertionRecord;
}
export async function driveA17(page: Page): Promise<AssertionRecord> {
return await page.evaluate(async () => {
const harness = (window as any).__mokoshHarness;
const r: AssertionRecord = await harness.assertA17();
return r;
}) as AssertionRecord;
}
Re-add the same eslint-disable comment on the `as any` line that
driveA14 carries (line 985). Standard wrapper — no host-side fs
polling needed (these are read-only assertions on the SW state +
fetched static assets).
5. **Orchestrator (harness.test.ts)**: update the imports at line 55+:
import {
driveA1, ..., driveA14, driveA15, driveA16, driveA17, getManifestVersion,
} from './lib/harness-page-driver';
Update the drivers array at line 289+ to append A15+A16+A17:
{ name: 'A14', drive: driveA14 },
{ name: 'A15', drive: driveA15 },
{ name: 'A16', drive: driveA16 },
{ name: 'A17', drive: driveA17 },
6. Run npm run build:test — exit 0 (rebuilds dist-test with the new
page-side asserts).
7. Run npm run test:uat — bail-on-first-failure orchestrator now
executes 18 assertions. Expected output: "UAT harness: 18/18
assertions passed". If any A15/A16/A17 fails, surface the
diagnostic; iterate within this task.
8. Run full vitest suite — 101 GREEN preserved (no source under test
changed; the harness-page changes are dist-test/ only).
9. Run npx tsc --noEmit — exit 0.
10. Verify Tier-1 grep gate stays at 10 strings:
npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts
— all 11 sub-tests GREEN (1 build + 10 string assertions). No
new forbidden strings; the inventory is unchanged.
Style/naming pre-commit:
- No new `__mokoshTest`/`__MOKOSH_UAT__` test-hook surface
(A15-A17 use only chrome.tabs.query / chrome.storage.local.get /
fetch on public web_accessible_resources surfaces).
- No new entries in FORBIDDEN_HOOK_STRINGS.
- `as any` only inside the page.evaluate wrapper (eslint-disable
line comment matches driveA14 idiom).
Commit:
test(01-10): wave-3 task-4 — harness A15+A16+A17 (onboarding flag observability + no-re-open settle check + design-swap-readiness invariant); 18/18 GREEN
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
npm run build:test && npm run test:uat 2>&1 | grep -E "UAT harness: 18/18" && npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts && npx tsc --noEmit
- assertA15, assertA16, assertA17 exist in extension-page-harness.ts.
- window.__mokoshHarness exposes all three new methods.
- driveA15, driveA16, driveA17 exist in lib/harness-page-driver.ts.
- harness.test.ts drivers array includes A15, A16, A17 entries.
- npm run test:uat reports 18/18 assertions passed.
- Tier-1 forbidden-strings inventory unchanged at 10 (no new test-hook surface).
- npm test (vitest) baseline 101 GREEN preserved.
- npx tsc --noEmit clean.
Harness covers welcome-tab activation + design-swap-readiness; Plan 01-12 design swap is now testable as a one-commit delta.
Task 5: Operator empirical UAT — fresh-profile install opens welcome tab; placeholders render coherently; reload does NOT re-open; harness 18/18 GREEN.
(operator-driven; no specific source file modified)
See <how-to-verify> below — operator-driven empirical check.
echo "checkpoint:human-verify — see how-to-verify; resume signal is the gate"
Operator types "approved" after running the how-to-verify steps. See <resume-signal> for the gate.
Tasks 1-4 landed:
- src/welcome/* page bundle (5 files: welcome.html, welcome.ts, welcome.css, welcome-tokens.css, copy.ts).
- manifest.json web_accessible_resources for welcome.html.
- vite.config.ts + vite.test.config.ts rollupOptions.input welcome entry in BOTH bundles.
- src/background/index.ts openWelcomeIfFirstInstall helper + onInstalled wiring.
- tests/background/onboarding.test.ts 3 GREEN unit tests.
- tests/uat/* harness A15+A16+A17 extensions; 18/18 GREEN.
- Tier-1 forbidden-strings inventory unchanged at 10.
- vitest baseline 101 GREEN (98 from 01-13 + 3 new from this plan).
This checkpoint validates real Chrome behavior + brand placeholder
coherence (the design-swap-in-ready architecture should render
presentably even with engineering-grade placeholders; Plan 01-12
will swap to the canonical D-04 Loom palette + D-05 Newsreader +
IBM Plex Sans WOFF2 hosting).
Pre-checkpoint bundle gates (operator OR executor verifies before
surfacing this checkpoint — per the orchestrator-loaded
feedback-pre-checkpoint-bundle-gates.md memory):
1. npm run build — exit 0; dist/src/welcome/welcome.html exists.
2. npm run build:test — exit 0.
3. grep -E '#[0-9a-fA-F]{3,8}' dist/src/welcome/welcome.css — exit 1
(no matches — design-swap-readiness invariant holds in BUILT artifact).
4. grep -q web_accessible_resources dist/manifest.json — exit 0.
5. npx vitest run — 101 passed.
6. npx vitest run tests/background/no-test-hooks-in-prod-bundle.test.ts —
all GREEN; 10 forbidden strings absent from dist/.
7. npm run test:uat — 18/18 GREEN.
Operator empirical steps:
1. Verify dist/ is fresh: npm run build (exit 0).
2. Wipe smoke profile: rm -rf /tmp/mokosh-smoke-profile (or
KEEP_PROFILE=0 ./smoke.sh which does the wipe).
3. Launch fresh-profile Chrome: KEEP_PROFILE=0 ./smoke.sh.
4. chrome://extensions → Load Unpacked → select dist/. THE WELCOME
TAB SHOULD AUTOMATICALLY OPEN within ~1 second. URL should be
chrome-extension://<id>/src/welcome/welcome.html.
5. Verify the welcome page renders coherently:
- Centered card, max-width ~720px.
- Hero block at top: 'Mokosh' placeholder mark + 'Mokosh' H1 + RU tagline
'«Тридцать секунд назад, всегда под рукой.»' + EN tagline
'Thirty seconds ago, always at hand.' in italic serif.
- Body: 2 explainer lines about 30s screen + 10min logs +
local-only data + how to save archive.
- CTA box: 'Чтобы начать запись, нажмите иконку AI Call Recorder
на панели инструментов браузера (правый верхний угол).'
- Footer: privacy/safety note about no-server-uploads.
- Typography: serif for hero (system fallback today; Plan 01-12
swaps to Newsreader/PT-Serif WOFF2); sans for body.
- NO console errors in the welcome tab's DevTools console.
- NO inline "Start" button (D-16-toolbar charter — the welcome
tab is informational; toolbar owns start).
6. Close the welcome tab.
7. Click the AI Call Recorder toolbar icon (right-side toolbar).
Chrome's screen-share picker should appear (monitor-only per
01-09). Pick 'Entire screen' + accept. Badge transitions to
REC. (This validates that the welcome tab CTA accurately
describes the toolbar start path.)
8. Click the toolbar again (now in REC state). The popup opens
with the SAVE-only UI per 01-09 SAVE-only charter; a zip is
downloaded. Badge returns to OFF (per Plan 01-13 save-stops-
recording charter).
9. Reload the extension at chrome://extensions (toggle off + on).
Observe: the welcome tab does NOT re-open. This validates
A16's flag-gating contract.
10. Re-validate the install branch: Cmd+Q Chrome →
rm -rf /tmp/mokosh-smoke-profile → re-launch smoke.sh →
Load Unpacked again. Welcome tab opens again (chrome.storage.local
wiped with profile → onboarding-completed absent → first-install
path fires).
If step 4 (welcome tab opens), step 5 (page renders coherently),
step 7 (toolbar CTA validates), or step 9 (no re-open on reload)
fails: document the failure mode + Chrome version + SW console
errors. Iterate on Task 2 (assets) or Task 3 (SW handler) or
Task 4 (harness) accordingly.
Design-coherence note for operator:
The welcome page renders with engineering-grade placeholders TODAY
— system serif/sans, neutral grays. This is INTENDED. Plan 01-12
swaps welcome-tokens.css for an @import of tokens.css + bundles
Newsreader (or designer-picked Cyrillic-capable alternative per
brand-decisions-v1-followup-display-font.md) and the welcome page
transitions to the canonical D-04 Loom palette + D-05 type pairing
without any other code change. The harness A17 contract pins the
invariants that make this swap mechanical.
Type 'approved' after pre-checkpoint gates 1-7 ALL pass AND operator
steps 4, 5, 7, 9 ALL pass. If any step fails, paste the failure
diagnostic. The harness 18/18 GREEN is the canonical functional gate;
operator empirical is the brand-coherence + Chrome-empirical gate.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| welcome page (extension-page context) ↔ SW | Welcome page is a same-origin extension page (chrome-extension://<id>/src/welcome/welcome.html). It does NOT call chrome.runtime.sendMessage in this plan — it is informational + read-only. No new IPC boundary introduced. |
| chrome.storage.local ↔ SW | Storage flag is non-secret (boolean + timestamp); the worst case if leaked or tampered is suppressing/re-firing the welcome tab. No PII; no sensitive content. |
| chrome.runtime.onInstalled ↔ SW | Existing Chrome MV3 boundary; this plan EXTENDS the handler body but does not introduce a new listener registration. |
| web_accessible_resources ↔ external pages | welcome.html is enumerable by any page that probes chrome-extension:// URLs in web_accessible_resources. Existing extension-fingerprinting concern; matches=['<all_urls>'] is the standard pattern for welcome flows. |
STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|---|---|---|---|---|
| T-1-10-01 | Tampering | Adversary clears chrome.storage.local 'onboarding-completed' flag to spam welcome tabs across SW respawns | accept | Welcome tab is non-destructive; worst case is an extra tab on each install (NOT each SW respawn — onInstalled only fires on install/update/chrome_update). chrome.storage.local clearing requires either operator action via Chrome DevTools OR an adversarial extension installed alongside — both out-of-band. No data exfiltration path. |
| T-1-10-02 | Information Disclosure | welcome.html fingerprinting via web_accessible_resources enumeration | accept | Extension identifier is already discoverable through chrome.runtime.getURL exposure on any extension page (popup + offscreen + harness page); matches=['<all_urls>'] is the standard MV3 welcome-flow pattern. Phase 5 hardening could narrow matches to a specific install-confirmation domain; out of scope. |
| T-1-10-03 | Denial of Service | Adversary controls chrome.storage.local quota | mitigate | Two values stored (boolean true + Date.now() number) ~80 bytes; storage.local quota is 10 MB; not exploitable. |
| T-1-10-04 | Elevation of Privilege | Welcome page tricks SW into bypassing checkpoints | accept | Welcome page sends NO messages to the SW in this plan (informational only). No new EoP path. Plan 01-12 may add an i18n probe via chrome.i18n.getMessage (synchronous, no IPC) — also no EoP path. |
| T-1-10-05 | Information Disclosure | Russian copy strings leak via web_accessible_resources | accept | Copy strings are operator-facing public marketing content (D-08 tagline; D-03 voice register); no PII. |
| T-1-10-06 | Tampering | Adversary modifies copy.ts contents via supply-chain attack on the source bundle | accept | Generic supply-chain risk applying to every TS file in the project; outside this plan's scope. The Tier-1 grep gate prevents test-hook leaks; brand-copy integrity is a Phase 5 / brand-team concern. |
| T-1-10-07 | Tampering | Adversary modifies welcome-tokens.css to inject malicious styles (CSP-bypass via background-image: url(data:...)) | mitigate | The token CSS contains ONLY color/font/spacing values — no url() expressions, no @import (except to tokens.css path post Plan 01-12, which is same-origin extension URL). MV3 CSP forbids inline scripts; CSS-only injection cannot escalate. Plan 01-12 swap is from canonical tokens.css delivered by the brand team — supply-chain risk shifts to brand-team handoff verification. |
| </threat_model> |
<success_criteria> Plan 01-10 is complete when:
- The 3 onboarding unit tests are GREEN; full vitest suite 101/101 GREEN.
- UAT harness reports 18/18 GREEN (existing A0..A14 + new A15+A16+A17).
- Tier-1 forbidden-strings inventory stays at 10 (no new test-hook surface).
- Welcome page renders coherently in fresh-profile Chrome with engineering-grade placeholders (Plan 01-12 swap target).
- Design-swap-readiness invariants hold in the BUILT artifact (welcome.css zero hex literals; ≥ 7 data-mokosh-key attributes; bundled JS uses COPY[ subscript pattern).
- Operator confirms via how-to-verify steps 4 / 5 / 7 / 9 + the harness 18/18 GREEN gate.
- tsc + build + build:test all clean; manifest + vite + welcome assets consistent across both bundles.
- No references to REQUEST_PERMISSIONS in src/welcome/ (start path remains toolbar-onClicked-owned per D-16-toolbar).
- No
await import(...)introduced into src/background/index.ts (MV3 SW dynamic-import blocker per 01-11-SUMMARY). </success_criteria>