Skip to content

feat(extension): Web Store packaging, per-site permissions, and a wal… - #119

Merged
stormer78 merged 1 commit into
mainfrom
feat/store-packaging-permissions-ui
Aug 17, 2026
Merged

feat(extension): Web Store packaging, per-site permissions, and a wal…#119
stormer78 merged 1 commit into
mainfrom
feat/store-packaging-permissions-ui

Conversation

@stormer78

@stormer78 stormer78 commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Prepares the extension for a Chrome Web Store submission, closes the two permission problems that would have dominated its review, and rebuilds the UI around what the wallet actually does.

Adds 140 tests (215 → 355 workspace-wide). Lint clean; the MV3 single-bundle invariant holds.


Packaging

  • manifest.json is now a template. The version comes from package.json — three copies previously disagreed (0.1.0 / 0.1.2 / 0.2.0), and the Store rejects a version that is not strictly greater than the last accepted upload.
  • key is injected for local installs only. A "+ New item" upload is rejected outright if the manifest carries one, while unpacked installs need it to hold the extension ID still — and that ID is load-bearing: src/holder.ts uses chrome.runtime.id as the WebAuthn PRF rpId, so a moving ID orphans every passkey-wrapped secret.
  • npm run package produces the upload zip. CI builds and validates it on every run, so a submission is never the first time that path executes.

Permissions

  • host_permissions: <all_urls>optional_host_permissions. Granted one origin at a time, from a user gesture. This is only safe because DID resolution needs no grant — the webvh hosting service serves Access-Control-Allow-Origin: * — while the VTA does, since vta-service applies an origin allowlist. Had resolution needed a grant, this would have silently degraded verifyDid, which backs the consent prompt.
  • No static content_scripts. The page provider is registered at runtime for granted origins only (src/content-registration.ts), so a site the user has not approved cannot see the wallet at all. CI asserts the packaged manifest declares none.
  • Cookie injection is scoped and enforced, not merely intended. HTTPS-only bar loopback, RFC 6265 domain-matching against the bound origin, refusals surfaced rather than swallowed, user-initiated rather than automatic. The previous check (c.domain !== baseUrl.host) would have let a session bound to evilexample.com claim Domain=example.com; Chrome would have refused it, but inside a swallowing catch where it was indistinguishable from a transient failure.
  • docs/web-store-review.md carries the single-purpose statement and per-permission justifications, including why cookies exists only for legacy relying parties.

Agent names

Implemented against design-docs/agent-names-design-guide.html and the Rust reference, not inferred.

  • A name is an alsoKnownAs claim, never derived from DID structure. An earlier iteration of this branch derived a handle from the DID's trailing path segment; the reference implementation deliberately ships no such helper, because it "would manufacture unverified guesses behind an authoritative-looking API".
  • Canonicalisation is ported verbatim. Implementations that normalise differently disagree about whether a name verifies against a document — a security disagreement, not a cosmetic one.
  • Resolution is three-stage with a mandatory alsoKnownAs check and no permissive fallback: a redirect alone proves nothing, since whoever controls a domain can point a name at somebody else's DID.
  • Onboarding accepts either a name or a DID in one field.

UI

  • One token set across popup, options and consent; ~50 accumulated hex literals removed. Options becomes an app shell: Setup, Network, Vault, Sites, Advanced.
  • Onboarding moved to a full tab, where Chrome cannot tear it down mid-flow when a native dialog takes focus (crbug 40721470).
  • Sites merges consent trust with host grants — two halves of "what can this site do?" that previously lived in two places, or nowhere.
  • Network adds a live trust graph plus sequence diagrams for SIOPv2 sign-in and delegated trust-task execution.
  • Consent prompt gets DID rendering that never elides the host. The previous head-and-tail truncation could hide it entirely for a path-qualified DID — on the one screen whose whole job is verifying it.

Cross-repo notes

  • packages/core: verifyDid now carries the resolved document's alsoKnownAs. Additive; no behaviour change for existing callers.
  • Onboarding can now accept an operator-supplied mediator when a VTA publishes none. Previously doOnboardConnect threw, making a bare did:peer agent impossible to onboard.

Pre-merge checklist

  • No new reqwest::Client::new() / bare fetch(); all clients have finite timeouts (R1.2) — the agent-name redirect fetch carries AbortSignal.timeout(15s); proxyFetch unchanged at 20s
  • No lock held across a network await (R1.3) — n/a, single-threaded JS
  • No local state committed before its remote effect, or the flow is resumable with an idempotency key (R2.1) — host grants are Chrome-owned; the onboarding breadcrumb in chrome.storage.session is a UI resume hint that nothing reads for correctness
  • Every retry is bounded + backed off; non-idempotent ops are not blind-retried (R1.4) — no new retry loops
  • Accept/poll/listen loops survive transient errors (R1.5) — no new reconnect paths
  • Acks/deletes happen only after durable handoff (R1.6) — untouched; the known persist-before-ack defect remains open and is not addressed here
  • New/changed wire types: camelCase, deny_unknown_fields where security-relevant, schema registered, all consumers (incl. JS) updated (R3.*) — new types are extension-internal bridge messages, not VTA wire types
  • Config absence = most restrictive; fail-closed if enforcement can't start (R5.*) — no grants means the provider registers nowhere and network calls fail closed; an unverifiable agent name fails resolution rather than trusting the redirect
  • Logs/status claim only what was verified; background-job failures are surfaced (R6.*) — approver "running" and wallet lock state are re-read rather than latched, because MV3 can evict the session that backs them
  • "Process dies on the next line" answered for every mutation touched (R2.1)
  • Deviations from this guide flagged explicitly with rule numbers — none

Not verified

Every UI change here was exercised in a browser by the author during review, but the automated tests cover logic only — there is no DOM or browser-integration test in this repo. The agent-name resolution network path is covered with stubs, not against a live host.

…let UI

Prepares the extension for a Chrome Web Store submission and closes the two
permission problems that would have dominated its review, then rebuilds the UI
around what the wallet actually does.

Packaging
- `manifest.json` becomes a template: the version comes from package.json
  (three copies previously disagreed, and the Store rejects a non-increasing
  version), and `key` is injected for local installs only — a new-item upload
  is rejected outright if the manifest carries one.
- `npm run package` produces the upload zip; CI builds and validates it every
  run, so a submission is never the first exercise of that path.

Permissions
- `host_permissions: <all_urls>` becomes `optional_host_permissions`, granted
  one origin at a time from a user gesture. Safe because DID resolution needs
  no grant (the webvh host serves `Access-Control-Allow-Origin: *`) while the
  VTA does, since vta-service applies an origin allowlist.
- No static `content_scripts`. The page provider is registered at runtime for
  granted origins only, so a site the user has not approved cannot see the
  wallet. CI asserts the packaged manifest declares none.
- Cookie injection is scoped and enforced, not merely intended: https-only bar
  loopback, RFC 6265 domain-matching against the bound origin, refusals
  surfaced rather than swallowed, and user-initiated instead of automatic. The
  previous check would have let a session bound to `evilexample.com` claim
  `Domain=example.com`.
- docs/web-store-review.md carries the single-purpose statement and the
  per-permission justifications, including why `cookies` exists only for
  legacy relying parties.

Agent names
- Implemented against the specification rather than inferred: a name is an
  `alsoKnownAs` claim, never derived from DID structure. Canonicalisation is
  ported verbatim from the Rust reference, because implementations that
  normalise differently disagree about whether a name verifies. Resolution is
  three-stage with a mandatory `alsoKnownAs` check and no permissive fallback.
- Onboarding accepts either an agent name or a DID in one field.

UI
- One token set across popup, options and consent; ~50 accumulated hex
  literals removed. Options becomes an app shell: Setup, Network, Vault,
  Sites, Advanced.
- Onboarding moves to a full tab, where Chrome cannot tear it down mid-flow
  when a native dialog takes focus (crbug 40721470).
- New Sites view merges consent trust with host grants — two halves of "what
  can this site do?" that previously lived in two places, or nowhere.
- New Network view: a live trust graph plus sequence diagrams for SIOPv2
  sign-in and delegated trust-task execution.
- Consent prompt: DID rendering that never elides the host. The previous
  head-and-tail truncation could hide it entirely for a path-qualified DID,
  on the one screen whose whole job is verifying it.

Adds 140 tests. Lint clean; MV3 single-bundle invariant holds.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78
stormer78 force-pushed the feat/store-packaging-permissions-ui branch from 15730dc to 04ca660 Compare August 17, 2026 10:05
@stormer78
stormer78 merged commit 89d70c4 into main Aug 17, 2026
3 checks passed
@stormer78
stormer78 deleted the feat/store-packaging-permissions-ui branch August 17, 2026 10:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant