feat(extension): browser extension package, demo harness, and CI (CEP-02) - #2244
feat(extension): browser extension package, demo harness, and CI (CEP-02)#2244remicolin wants to merge 36 commits into
Conversation
…-02, SELF-3691) Ports the validated spike prototype onto dev as the production baseline (spike branch feat/chrome-extension-spike remains reference-only): - packages/chrome-extension: MV3 shell hosting the webview-app build (bridge-host NativeTransport, vault custody with passkey-first PRF + password fallback + reset, link/unlock pages, SAS emoji verification, site shim + content-script routing) - packages/extension-demo: internal RP test harness for the postMessage contract (real SelfBackendVerifier backend, MOCK_DOCS registry flag) - mobile-sdk-alpha: shared SAS module (utils/sas) + export - webview-app: home honors selectedDocumentId; Manage Documents is tap-to-select for the active proving document - app: dev-gated "Link browser extension" screen (production flow is CEP-08; this keeps the flow demoable from dev) - CI: chrome-extension-ci.yml builds the chain and gates on the boot-check + import-check puppeteer harnesses (Chrome for Testing, staging relayer end-to-end); store zip retained as artifact - boot-check now asserts the fresh-install custody gate (link page + QR) instead of the pre-custody app boot - gitleaks allowlist: MV3 manifest "key" is the extension's public key Spec: specs/projects/sdk/workstreams/chrome-extension/ (PR #2243). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe change adds a Manifest V3 Chrome extension with encrypted account linking, password and passkey vaults, popup verification bridging, a browser demo, mobile development tooling, persisted document selection, end-to-end harnesses, and GitHub Actions CI. ChangesChrome extension transfer and verification
Mobile linking and demo integration
Build and validation
Estimated code review effort: 5 (Critical) | ~120 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 13
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 9fa866c9-a7b9-48c7-97af-889bcce91bc5
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (44)
.github/workflows/chrome-extension-ci.ymlapp/src/navigation/devTools.tsxapp/src/navigation/types.tsapp/src/screens/dev/LinkBrowserExtensionScreen.tsxapp/src/screens/dev/sections/DebugShortcutsSection.tsxgitleaks-override.tomlpackages/chrome-extension/.gitignorepackages/chrome-extension/harness/boot-check.mjspackages/chrome-extension/harness/demo-check.mjspackages/chrome-extension/harness/disclose-check.mjspackages/chrome-extension/harness/import-check.mjspackages/chrome-extension/harness/relayer-transfer.mjspackages/chrome-extension/manifest.jsonpackages/chrome-extension/package.jsonpackages/chrome-extension/pages/link.htmlpackages/chrome-extension/pages/unlock.htmlpackages/chrome-extension/scripts/build.mjspackages/chrome-extension/sdk/shim.tspackages/chrome-extension/src/background.tspackages/chrome-extension/src/bridge-host.tspackages/chrome-extension/src/content-script.tspackages/chrome-extension/src/crypto.tspackages/chrome-extension/src/link.tspackages/chrome-extension/src/passkey.tspackages/chrome-extension/src/relayer-session.tspackages/chrome-extension/src/unlock.tspackages/chrome-extension/src/vault.tspackages/chrome-extension/src/verification-url.tspackages/chrome-extension/tsconfig.jsonpackages/extension-demo/README.mdpackages/extension-demo/backend/server.mjspackages/extension-demo/index.htmlpackages/extension-demo/package.jsonpackages/extension-demo/src/App.tsxpackages/extension-demo/src/main.tsxpackages/extension-demo/tsconfig.jsonpackages/extension-demo/vite.config.tspackages/mobile-sdk-alpha/package.jsonpackages/mobile-sdk-alpha/src/utils/sas.tspackages/mobile-sdk-alpha/tests/sas.test.tspackages/mobile-sdk-alpha/tsup.config.tspackages/webview-app/src/screens/home/HomeScreen.tsxpackages/webview-app/src/screens/home/ManageDocumentsScreen.tsxpnpm-workspace.yaml
| if (!existsSync(join(webviewDist, 'index.html'))) { | ||
| console.error('webview-app dist not found. Build it first:'); | ||
| console.error( | ||
| ' pnpm --filter @selfxyz/common build && pnpm --filter @selfxyz/mobile-sdk-alpha build:ts-only && pnpm --filter @selfxyz/webview-bridge build && pnpm --filter @selfxyz/webview-app build', | ||
| ); | ||
| process.exit(1); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
Breaks Workspace CI: pnpm run build exits 1 when webview-app/dist is absent.
The workspace-type-check job runs this package's build without the manual chain, and the logs show exactly this branch firing ("webview-app dist not found") followed by exit code 1. The prerequisite needs to be enforced by the dependency graph (e.g. a @selfxyz/webview-app workspace dependency plus its build in dependsOn/pnpm --filter ... build prior to this script) or built here, rather than left as a documented manual step.
Source: Pipeline failures
| let lastVerification = null; | ||
|
|
||
| function json(res, status, body) { | ||
| res.writeHead(status, { 'content-type': 'application/json', 'access-control-allow-origin': '*' }); | ||
| res.end(JSON.stringify(body)); | ||
| } | ||
|
|
||
| const server = createServer((req, res) => { | ||
| if (req.method === 'GET' && req.url === '/api/last-verification') { | ||
| return json(res, 200, lastVerification); | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
Do not publish a cross-user verification record.
This global record is returned with Access-Control-Allow-Origin: *, so any origin can read a prior verification's timestamp and potentially nationality. It also allows a later flow to display stale confirmation. Key results by a per-session nonce, authorize the matching read, and omit unneeded disclosure fields.
Also applies to: 62-67
| if (req.method === 'POST' && req.url === '/api/verify') { | ||
| let raw = ''; | ||
| req.on('data', chunk => (raw += chunk)); | ||
| req.on('end', async () => { |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Bound the incoming proof body.
This public endpoint concatenates unlimited request chunks. A malicious chunked POST can exhaust Node process memory; reject oversized bodies before appending and return 413.
| @@ -0,0 +1,155 @@ | |||
| import { useCallback, useEffect, useMemo, useState } from 'react'; | |||
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
In TypeScript, can a module that imports only named exports from reactsafely referenceReact.CSSPropertieswithout importingReactorCSSProperties? Cite official TypeScript or React type documentation.
💡 Result:
Yes, a module that imports only named exports from 'react' can safely reference CSSProperties without importing the full React namespace as a default or namespace import. The type CSSProperties is exported by the @types/react package. You can access it directly by importing it as a named export from 'react' [1][2][3]: import { CSSProperties } from 'react'; // You can now use CSSProperties directly const style: CSSProperties = { color: 'red', }; The requirement to reference it via the React namespace (e.g., React.CSSProperties) is a legacy pattern from older versions of React and TypeScript (pre-React 17 and pre-JSX transform) when the React namespace needed to be in scope for JSX or global type resolution [4][5]. In modern TypeScript/React projects, named imports are the recommended and standard approach [5]. Official React documentation on TypeScript confirms that CSSProperties is used to describe style objects [6], and it is available as a named export just like other React hooks and types (e.g., useState, FC) [7][5]. Top results: [6][4][7][5]
Citations:
- 1: https://stackoverflow.com/questions/52005083/how-to-define-css-variables-in-style-attribute-in-react-and-typescript
- 2: https://reacthustle.com/blog/how-to-set-inline-styles-in-react-typescript
- 3: https://www.curiouslychase.com/engineering/fix-css-variable-keys-in-style-attributes-in-react-and-typescript
- 4: https://dev.to/cristiansifuentes/do-you-still-need-to-import-react-in-tsx-files-5fm5
- 5: Recommended way of importing React typescript-cheatsheets/react#518
- 6: https://react.dev/learn/typescript
- 7: https://github.com/typescript-cheatsheets/react/blob/main/docs/advanced/types-react-ap.md
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo "== repo files matching App.tsx =="
fd -a 'App\.tsx$' . | sed 's#^\./##'
echo "== target App.tsx imports and style usage =="
if [ -f packages/extension-demo/src/App.tsx ]; then
sed -n '1,170p' packages/extension-demo/src/App.tsx | cat -n
else
echo "packages/extension-demo/src/App.tsx not found"
fi
echo "== tsconfig relevant settings =="
fd -a 'tsconfig.*json$' packages/extension-demo . | sed 's#^\./##' | while read -r f; do
echo "-- $f --"
sed -n '1,220p' "$f"
done
echo "== react type package version references =="
fd -a 'package.json$' packages/extension-demo . | xargs -r -I{} sh -c 'echo "-- {} --"; sed -n "1,220p" "{}" | grep -Ei "react|`@types/react`|typescript|compilerOptions" || true'Repository: selfxyz/self
Length of output: 38394
Define CSSProperties directly instead of relying on React types.
packages/extension-demo/tsconfig.json sets "types": ["vite/client"], so the React ambient namespace is not guaranteed to be available and React.CSSProperties may fail tsc --noEmit.
Proposed fix
-import { useCallback, useEffect, useMemo, useState } from 'react';
+import { type CSSProperties, useCallback, useEffect, useMemo, useState } from 'react';
...
-const styles: Record<string, React.CSSProperties> = {
+const styles: Record<string, CSSProperties> = {| it('is deterministic for a fixed shared secret', () => { | ||
| const secret = new Uint8Array(32).fill(7); | ||
| const result = sasEmojis(secret); | ||
| expect(result).toEqual([SAS_EMOJIS[1], SAS_EMOJIS[63], SAS_EMOJIS[17], SAS_EMOJIS[62]]); | ||
| expect(sasEmojis(secret)).toEqual(result); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Make the protocol vector independent of the lookup table.
Line 21 reads its expected values from SAS_EMOJIS, so reordering that table still passes despite changing the cross-client protocol output. Assert the literal vector instead.
Proposed fix
- expect(result).toEqual([SAS_EMOJIS[1], SAS_EMOJIS[63], SAS_EMOJIS[17], SAS_EMOJIS[62]]);
+ expect(result).toEqual(['🐱', '🗺️', '🌲', '🔔']);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it('is deterministic for a fixed shared secret', () => { | |
| const secret = new Uint8Array(32).fill(7); | |
| const result = sasEmojis(secret); | |
| expect(result).toEqual([SAS_EMOJIS[1], SAS_EMOJIS[63], SAS_EMOJIS[17], SAS_EMOJIS[62]]); | |
| expect(sasEmojis(secret)).toEqual(result); | |
| it('is deterministic for a fixed shared secret', () => { | |
| const secret = new Uint8Array(32).fill(7); | |
| const result = sasEmojis(secret); | |
| expect(result).toEqual(['🐱', '🗺️', '🌲', '🔔']); | |
| expect(sasEmojis(secret)).toEqual(result); |
| const selectDocument = useCallback( | ||
| (id: string) => { | ||
| haptic.trigger('selection'); | ||
| analytics.trackEvent('manage_docs_document_selected'); | ||
| const updated = { ...catalog, selectedDocumentId: id }; | ||
| setCatalog(updated); | ||
| void documents.saveDocumentCatalog(updated as Parameters<typeof documents.saveDocumentCatalog>[0]); | ||
| }, |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Do not discard selection persistence failures.
If saveDocumentCatalog() rejects, the UI retains an unsaved selection and the promise is unhandled. Await it, update local state only after success, and show the existing dialogue on failure.
Proposed fix
- (id: string) => {
+ async (id: string) => {
haptic.trigger('selection');
analytics.trackEvent('manage_docs_document_selected');
const updated = { ...catalog, selectedDocumentId: id };
- setCatalog(updated);
- void documents.saveDocumentCatalog(updated as Parameters<typeof documents.saveDocumentCatalog>[0]);
+ try {
+ await documents.saveDocumentCatalog(updated as Parameters<typeof documents.saveDocumentCatalog>[0]);
+ setCatalog(updated);
+ } catch {
+ setDialogue({
+ title: 'Could not select document',
+ description: 'Your selection was not saved. Please try again.',
+ });
+ }As per coding guidelines, “Always use try-catch for async operations with graceful degradation when native modules fail and comprehensive error boundaries.”
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const selectDocument = useCallback( | |
| (id: string) => { | |
| haptic.trigger('selection'); | |
| analytics.trackEvent('manage_docs_document_selected'); | |
| const updated = { ...catalog, selectedDocumentId: id }; | |
| setCatalog(updated); | |
| void documents.saveDocumentCatalog(updated as Parameters<typeof documents.saveDocumentCatalog>[0]); | |
| }, | |
| const selectDocument = useCallback( | |
| async (id: string) => { | |
| haptic.trigger('selection'); | |
| analytics.trackEvent('manage_docs_document_selected'); | |
| const updated = { ...catalog, selectedDocumentId: id }; | |
| try { | |
| await documents.saveDocumentCatalog(updated as Parameters<typeof documents.saveDocumentCatalog>[0]); | |
| setCatalog(updated); | |
| } catch { | |
| setDialogue({ | |
| title: 'Could not select document', | |
| description: 'Your selection was not saved. Please try again.', | |
| }); | |
| } | |
| }, |
Source: Coding guidelines
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3dfe32757e
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| function nextUrl(): string { | ||
| const next = new URLSearchParams(window.location.search).get('next'); | ||
| // Only ever continue to our own pages. | ||
| if (next && /^[a-z0-9./?=&_-]+$/i.test(next) && !next.startsWith('//')) { |
There was a problem hiding this comment.
Preserve encoded next targets after unlock
When a locked vault receives a site verification request, background.gatedUrl stores next=encodeURIComponent('index.html?...'); after URLSearchParams decodes it, embedded query values such as appEndpoint still contain %3A%2F%2F, so this whitelist rejects the valid internal target and falls back to index.html. That means the first verification after a browser restart unlocks into the extension home screen instead of the disclosure request, leaving the RP session pending or cancelled. Allow percent-encoded query strings or validate with URL parsing before calling chrome.runtime.getURL.
Useful? React with 👍 / 👎.
| <ShortcutRow | ||
| label="Link Browser Extension" | ||
| onPress={() => navigation.navigate('LinkBrowserExtension')} |
There was a problem hiding this comment.
Hide account-export shortcut in production
In production builds, a user can enable the hidden Debug menu and this row remains visible because it is outside the neighboring IS_DEV_MODE guards; tapping it opens the dev-only flow that transfers the recovery secret and every document to the browser extension. Gate this shortcut behind IS_DEV_MODE (or otherwise remove it from release builds) so the account-export path fails closed outside development.
AGENTS.md reference: AGENTS.md:L36-L36
Useful? React with 👍 / 👎.
| if (pending && !pending.resolved) { | ||
| return { accepted: false, result: failureResult('BUSY', 'Another verification is in progress') }; | ||
| } |
There was a problem hiding this comment.
Reserve pending state before opening popup
If a page issues two requestVerification calls back-to-back, both invocations can pass this check before either awaits vaultState()/chrome.windows.create() and assigns pending, so the BUSY guard is bypassed and two approval popups can open with only the later one tracked; the earlier result can then be lost or misrouted. Set a pending/reserved session before the first await, and clear it on failure, to default-deny overlapping lifecycle requests.
AGENTS.md reference: AGENTS.md:L36-L36
Useful? React with 👍 / 👎.
| - name: Install Chrome for Testing | ||
| id: cft | ||
| run: | | ||
| out=$(npx --yes @puppeteer/browsers install chrome@stable --path "$PWD/packages/chrome-extension/chrome") |
There was a problem hiding this comment.
Use pnpm to fetch Chrome for Testing
This CI step invokes npx --yes @puppeteer/browsers, and npx --help confirms it runs commands from local or remote npm packages, so after the pnpm install the workflow can still fetch an unpinned npm CLI outside the lockfile. Use a pnpm-invoked, lockfile-controlled command for the already-declared browser installer instead.
AGENTS.md reference: AGENTS.md:L15-L15
Useful? React with 👍 / 👎.
| const messageRef = useRef<Record<string, unknown> | null>(null); | ||
| const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); | ||
|
|
||
| const cleanup = useCallback(() => { |
There was a problem hiding this comment.
Cancel the transfer when leaving the screen
This cleanup is only called from explicit cancel/fail/success paths; if the user backs out or the screen unmounts while the account transfer is connected or sending, the sockets, timeout, and messageRef stay alive until ack/timeout, so the encrypted account can continue being delivered after the user has left the flow. Return cleanup from an unmount effect so leaving the screen cancels the security-sensitive session.
AGENTS.md reference: AGENTS.md:L36-L36
Useful? React with 👍 / 👎.
| id: doc.id, | ||
| label: getIdCardProps(doc.documentCategory, doc.mock).title ?? 'Document', | ||
| description: selected ? `In use for proofs - ${registration}` : `${registration} - tap to use for proofs`, | ||
| onPress: () => selectDocument(doc.id), |
There was a problem hiding this comment.
Keep document details reachable from Manage Documents
Changing the row action to only selectDocument removes the existing path from Settings → Manage Documents → document details; the only remaining non-test call to navigate('/docs/current') is now behind the never-set dialogue callbacks, while the existing support-screen test still expects tapping the Passport row to open /docs/current. Keep selection separate from the row/details action (or open the details/dialog after selecting) so users can still view/manage the document.
Useful? React with 👍 / 👎.
- declare webview-app + webview-bridge as chrome-extension devDependencies so turbo orders the build chain (workspace build/types jobs no longer hit a missing webview-app dist) - prettier pass on the ported app dev screen and webview-app screens - Manage Documents: tapping a document now selects it for proofs AND opens the dialogue (path to ID details preserved); WV-14 test updated to the new behavior and the async catalog load
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…l + OS-lock, fail-closed mid-session) Session record in chrome.storage.session carries expiresAt + lastActivityAt; every vault access validates both and self-locks on violation. Manual lock via the action context menu closes open extension windows and fails any pending verification; OS session lock triggers the same. A VAULT_LOCKED error mid-use redirects the app page to unlock with the return path. Harness covers the idle-expired path. Spec: SPEC-PRODUCTION.html session & lock policy section. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… at-rest keystore, egress CSP Security review 2026-07-29. Highest-severity finding: the transfer handler derived its key from whatever senderPublicKey arrived, independent of the hello key whose SAS the user verified, so anyone who could reach the transfer room and read the QR could substitute their own keypair and account. - link.ts pins the SAS-verified hello key: an envelope from a different sender key, a transfer with no hello, or conflicting hellos are refused. `handled` now latches only after decrypt + validation, so a junk first message no longer burns the session. - transfer protocol v2 (shared module, both ends + harnesses): HKDF-SHA256 over the ECDH secret salted by session id and bound to a transcript of both public keys, with the transcript as GCM additional data, replacing the bare x-coordinate as the AES key. SAS widened 4 -> 6 emojis (24 -> 36 bits) and derived from the transcript, so grinding a preimage against the emojis the user compares is not feasible. - bridge-host crypto keystore writes private JWKs through the vault instead of plaintext chrome.storage.local, inheriting at-rest encryption and the session gate; reset() now also sweeps cryptoKey:* and passkeyMeta. - vault: refuse to re-initialize over an existing vault, clamp PBKDF2 iterations read back from storage, reject password unlock on passkey-mode vaults instead of throwing. - manifest CSP gains default-src 'self' and connect-src limited to Self hosts (was unrestricted egress); relay URLs allowlisted on both ends; storage.session access level pinned to trusted contexts; background vault state now uses the TTL-aware check. - harness: substituted-sender envelope must be refused (new regression), plus v2 crypto throughout. Spec: SPEC-PRODUCTION.html threat model section. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…k codes The link screen could sit on "Waiting for your phone" forever: a relayer drop after connect, a relayer-side rejection, and code expiry had no UI at all, and a failed reset left a dead button. - link codes expire after 5 minutes (spec'd, never implemented): the QR dims, a "Get a new code" button appears, and late transfers are refused with an explanation - relayer connect_error, disconnect, and the relayer's own error event each get an actionable line plus the regenerate affordance; connect confirms the waiting state rather than leaving an ambiguous spinner - phone side surfaces the same three cases, including a mid-send drop - unlock: a failed reset reports why and re-enables the button - harness asserts the scan step always carries status text Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…link channel The QR is a trusted out-of-band channel we were under-using: v2 carried the receiver public key (confidentiality) but nothing proving the sender had scanned the code, so sender authentication rested on a human comparing 6 emojis. Both session ids reach the relayer, so they authenticate nothing. v3 adds a 32-byte linkSecret to the QR, never transmitted through the relayer, mixed in as the HKDF salt for the envelope key and the SAS. Only a device that physically scanned the code can derive a key that authenticates: an attacker in relayer position fails the AEAD tag regardless of which keys they inject, with no user action involved. This is the pattern mainstream device-linking flows use. The emoji step is demoted, not removed: it still covers the case linkSecret cannot (a QR observed via shoulder-surf, screen share, or remote support) and remains where the user authorizes the send. The phone refuses a pre-v3 link code with copy telling the user to update the extension, rather than silently falling back to weaker authentication. Harness now proves both attacks dead: substituted sender key (hello pin) and off-path envelope from an attacker who never saw the QR (linkSecret). Spec: SPEC-PRODUCTION.html link channel authentication.
- P1 security: the "Link Browser Extension" debug row sat outside the IS_DEV_MODE guards, so a production user who enabled the hidden debug menu could reach the account-export flow. Now dev-gated like its neighbours. - P1 correctness: the unlock `next=` charset regex rejected percent-encoded embed queries, so the first verification after a browser restart unlocked into the home screen instead of the disclosure request and left the RP session hanging. Replaced with a path allowlist plus query re-serialization. - P1 tooling: CI fetched Chrome for Testing through `npx --yes`, outside the lockfile. Now invoked via pnpm against the declared puppeteer dependency. - P2: two back-to-back verification requests could both pass the BUSY check before either awaited, opening two popups with only the later tracked. The pending slot is now reserved synchronously and released on failure. - P2: the phone transfer survived screen unmount (sockets, ack timer, and the pending payload stayed alive), so the account could still be delivered after the user backed out. Cleanup now runs on unmount. Codex's ManageDocuments finding was already resolved: selection opens the dialogue that carries the details path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dency) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…F-3706) Chrome terminates an idle MV3 worker after ~30s, which is guaranteed to happen while a user reads the consent screen or waits 10-30s on proving. Pending session state lived in module globals with a setTimeout, so after that termination the popup's result was silently dropped (the RP page hung until the shim's own timeout) and the BUSY, TIMEOUT and window-closed guards stopped working, allowing concurrent sessions. - pending session and the home window id move to chrome.storage.session; the 5-minute timeout becomes a chrome.alarms alarm. Both survive restarts. - startup reconciliation settles a session whose window closed while the worker was asleep. - lock closes extension windows by enumeration rather than cached ids, and pages now self-evict: bridge-host watches the session key and navigates to unlock, because a page that already read the vault holds decrypted documents in memory and clearing the key alone does not remove them. - results carry the requesting origin and are dropped if the tab navigated away, so one origin cannot receive another's result. - new permissions: alarms, tabs. Harness: lock now evicts the open page (asserted), and a pending session survives a forced worker restart (asserted). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…unlock throttle (CEP-04) Security review: 600k PBKDF2 with an 8-character minimum is GPU-feasible offline against a stolen storage dump, and nothing throttled online guessing against a live browser. - password floor raised 8 -> 12 characters, enforced in the vault (not just the form) so no caller can create a weaker vault - live strength readout on the custody step (length + character classes) - unlock throttles after 3 failures with exponential backoff capped at 5 minutes, held in chrome.storage.session and cleared on success; the unlock screen tells the user how long to wait instead of silently rejecting - harness asserts the throttle engages Remaining in CEP-04: Argon2id via wasm and scoping the passkey rpId to a dedicated host (both need decisions outside the code). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… (CEP-14, CEP-05) - the phone refuses to link an account with no registered document: a vault that cannot prove anything would leave the extension in an empty state the user cannot resolve from the browser - the payload carries linkedAt, persisted as self_linked_at, so the browser can show how old its copy is (transfer is a copy; the phone can add documents afterwards) - unknown envelope versions are refused rather than best-effort parsed: a partially understood account is worse than none The version refusal is not yet asserted in the harness. It needs a second concurrent session, and the deployed relayer drops a message that arrives before its peer joins without buffering it, so the test races the extension's own join. self-infra PR #166 adds that buffering and makes the case deterministic; the harness carries a note where the test belongs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…de (CEP-13) The manifest had no icons at all, which the store hard-rejects, and still called itself "Self (Spike)". - icons at 16/32/48/128 rasterized from the Self mark by scripts/make-icons.mjs (headless Chrome, no new dependency) onto the brand ground with the store's padding ratio; committed because the store cannot accept a build without them - name becomes "Self: Verify Your Identity" and the manifest description is the 132-char store summary, carrying the search terms "Self" alone will never rank for - `pnpm build --store` strips the pinned dev `key` (which fixes the extension id for local loads and the harnesses, and must not ship), stamps a monotonic YYYY.MM.DD.N version, and produces the upload zip Still needs design and legal for CEP-13: screenshots, the promo tile, the detailed description, and the privacy policy URL plus practices disclosures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…traps (CEP-11) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…legible Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…l check parses it Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…fer protocol v3 Both senders still spoke the v1 protocol (no hello room, no linkSecret binding, no AAD) and were refused by the extension since 22c6bb0. They are not CI gates, so the rot was silent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…app settings (CEP-14) New custody bridge domain (lock/reset) advertised via a custodyControls capability that only the extension host sets; the capability defaults false everywhere else so the phone app never renders the section. Reset keeps the unlock-page two-press confirm; lock reuses the background lockNow eviction. Import harness drives both from a real popup window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…QA round) Phone (LinkBrowserExtensionScreen): design-system Title/Description/ PrimaryButton/SecondaryButton, emoji row that cannot wrap, actions pinned to the bottom safe area, copy cut to one line per step, and a dev-menu demo shortcut that stages the confirm step with fixed emojis and no live channel. Extension: toolbar click now opens an anchored action popup (default_popup) instead of a detached window; dead home-window machinery removed; popup size floored at 400x600 across index, link, and unlock pages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The row stays for every other host; gating is by the getConfig platform string, now exposed as hostPlatform on the operating-mode context. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… of hardcoded mock data Loads the selected document from the catalog, extracts fields via the SDK's getDocumentAttributes (now exported from the browser entry - the root '.' export resolves to browser.js under the browser condition, so it was unreachable from webview-app), formats MRZ names/dates, and falls back to blank fields instead of fabricated ones. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…isibility) Derives the points address from the transferred mnemonic exactly like the phone (account index 1 of m/44'/60'/0'/0) and reads the points API, so a linked browser shows the same balance. Known-vector unit test pins the derivation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same mark on both surfaces; small sizes zoom into the center since the app icon's built-in margins make the 16px toolbar mark illegible. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Label-level marker until euclid's DocumentItem selected state ships (storybook PR #192). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SAS_LENGTH is 6; the four-emoji fixture came from stale spike notes and under-tested the row width the layout must fit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
28pt glyphs with 12px gaps keep the row under 300px, inside a 375pt screen's content width. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The popup sizes itself to the document, so wide content blew it up to the 800px cap with dead space around the app column. A ctx=popup marker on default_popup rides every custody redirect (gate, eviction, unlock next, reset) and pins the frame; verification windows and tabs are untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ators, tap-to-select) Mirrors the phone app: tapping a row selects it in place with the green radio indicator (activeDocumentId, shipped in euclid slice 2), nothing pops on select, unregistered documents explain themselves, and details/remove moved behind a per-row chevron. Removes the interim label check mark. euclid PRs #191/#194 merged upstream, published as 1.4.7 (slice 2's original PR #192 had merged into its stacked branch instead of main and was re-landed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…parses Popup opens painted nothing until the 7.6MB bundle finished (no FCP at all); locked and fresh installs paid the full parse just to bounce to unlock/link. The build now strips the module tag (bridge-host injects it after the gate passes, ~10ms, with modulepreload keeping the fetch warm), paints a pulsing-mark splash at ~32ms, and drops sourcemaps from dist (59MB -> 37MB). New harness/perf-check.mjs benchmarks popup cold loads (median warm render 178ms, unchanged; FCP none -> 32ms). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dev mode, disclosure demo, tunnel flow, and the coming-soon support and share entries are gated off the extension host (audit P0). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…EP-07) UI-free link-session engine extracted from link.html (sockets, SAS handshake, envelope decrypt, vault persistence); bridge custody domain gains state, createLinkSession + link events, completeLink, unlock/unlockPasskey with cooldowns, enablePasskey, passwordStrength. Groundwork for euclid-rendered link/unlock screens in webview-app. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pnpm recorded the first-party exception locally at install time; without it committed, Vercel's frozen install fails the supply-chain check on the freshly published packages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ke pages deleted (CEP-07) The dark hand-rolled link.html/unlock.html pages are gone: custody gating now routes into /ext/link and /ext/unlock inside webview-app, rendered by euclid 1.4.8's ExtensionLinkScreen and ExtensionUnlockScreen (light theme, library tokens, Touch ID auto-prompt, continue-in-a-window escape hatch for the popover). Two loops found and fixed while wiring it: the app's locked-vault storage reads bounced off the gate screens forever (nesting ext_route/next on every hop), and after an in-app unlock the boot-time gate marker went stale so lock events were ignored - the check is live now. Harnesses rewritten against the screens with shared drivers in harness/ext-ui.mjs; boot, import, disclose, demo and perf all green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rd (euclid 1.5.0) The steps were stacked: the QR stayed while the SAS emojis appeared, and the emojis persisted into the password step. Now each step does one job - QR until the phone scans, emojis alone while waiting for the phone's Send, password choice alone once the account arrives - plus explicit success and failure states with a drawn status mark and a retry path. Copy is overridable through euclid's copy prop, so strings change without a library release; secure fields no longer show the 'Text...' placeholder. Harnesses assert the sequencing (QR gone on verify, SAS gone on custody) and the rogue-sender attacks now fire against the custody step where the session is live. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Linear: SELF-3691 (CEP-02). Specs: PR #2243 (
specs/projects/sdk/workstreams/chrome-extension/).Ports the validated spike prototype onto dev as the production baseline. The spike branch (
feat/chrome-extension-spike) stays reference-only and never merges; this PR is the reviewed port.What's in
packages/chrome-extension(new, ~1.5k LOC): MV3 shell hosting the webview-app build unmodified. Bridge-host implements theNativeTransporthandler set; vault custody is passkey-first (WebAuthn PRF wraps a random key, Touch ID unlock) with password fallback and a lost-credential reset; account link via QR handshake + E2E-encrypted transfer with SAS emoji verification (hello handshake shows the 4 emojis on both devices BEFORE the phone's send button fires anything); site shim + content-script routing for the hosted-page trigger.packages/extension-demo(new): internal test harness for the postMessage contract - realSelfBackendVerifierbackend with aMOCK_DOCSregistry flag. Not partner-facing (RP integration is Enterprise-only via the hosted verification page, per spec).mobile-sdk-alpha: shared SAS module (utils/sas, 64-emoji table + sha256 derivation) with a known-vector test pinning the protocol; new export entry.webview-app: home screen honorsselectedDocumentId; Manage Documents lists the real catalog with tap-to-select for the active proving document.app: dev-gated "Link browser extension" screen (settings dev menu). The production link flow is CEP-08; this keeps the flow demoable from dev.chrome-extension-ci.ymlbuilds the full chain (common -> sdk -> bridge -> webview-app -> extension) and gates on two puppeteer harnesses against Chrome for Testing:boot-check(fresh install gates to the link page + QR renders) andimport-check(full transfer over the real staging relayer: envelope relay, SAS match on both sides, custody, lock/unlock roundtrip, wrong-password rejection, reset). Store zip retained as a workflow artifact.Validation (local, this branch)
boot-checkandimport-checkharnesses green end-to-end against staging.Notes for review
*.pngis now gitignored in the package.keyfield: it is the extension's PUBLIC key (pins the dev extension id), not a secret. Store builds will strip it (CEP-03).🤖 Generated with Claude Code
Summary by CodeRabbit