The uSwap browser extension — one source tree, two MV3 build targets: Chrome and Firefox. The toolbar icon opens a panel hosting the real uSwap web app (Chrome side panel / Firefox sidebar) — users execute full uSwap flows inside the panel. Feature modules add browser-integration superpowers on top.
Build target is selected at build time by
EXT_TARGET(chromedefault,firefoxopt-in) — there is no separate Firefox fork; ~95% of the code is shared, and the few platform differences are branched behind a build flag (src/core/target.ts) so the unused path is dead-code-eliminated.
Chrome (dist/) |
Firefox (dist/firefox/) |
|
|---|---|---|
| Panel surface | side_panel (chrome.sidePanel) |
sidebar_action (browser.sidebarAction) |
| Background | MV3 service worker | MV3 event page (background.scripts) |
| @zkp2p crypto + DOM parsing | offscreen document (chrome.offscreen) |
runs directly in the background event page |
| Store / distribution | Chrome Web Store (.zip) |
AMO (web-ext → signed XPI) |
| Min version | Chrome 114+ | Firefox 142+ (declared data permissions) |
Everything else (manifest, permissions, the bus, modules, content scripts) is
identical. chrome.* aliases to browser on Firefox 109+, so no polyfill is
needed for the extension to function.
| Module | Status | What it does |
|---|---|---|
peer-capture |
active | Whitelabeled Peer metadata bridge: implements window.peer for @zkp2p/sdk's createPeerExtensionSdk(), drives fiat payment capture (buyer TEE + seller credential flows) for the uSwap onramp. |
checkout-pay |
planned | Bitwarden-style overlay on checkout pages (Stripe, Cryptomus, …): one click creates a uSwap pay-mode intent (/pay/:id) to buy a virtual card for the cart amount, then autofills card details. |
context-bridge |
planned | Right-click a selected crypto address → "Swap via uSwap" → opens bridge creation with that destination. |
- A module may only import from
src/core/*. Modules never import each other. - Every manifest permission is annotated with its owning module in
manifest.config.ts. - New modules: add an entry under
src/modules/<id>/, export anExtensionModule, register it insrc/background/index.ts. Content scripts and permissions are added per-module inmanifest.config.ts(preferoptional_host_permissionsgranted at first use).
window.peer reaches the background two ways depending on where the app runs
(see AGENTS.md for the full contract + the cross-browser parity rules):
TAB: page (app-owned window.peer) supplied by the web app
⇅ window.postMessage (BusRequest/BusResponse/BusEvent)
relay (ISOLATED world) src/content/peer-relay.content.ts
⇅ chrome.runtime sendMessage + long-lived Port (per-connection event push)
PANEL: app (window.peer parent bridge) defined by the uSwap web app
⇅ window.postMessage to parent panel
panel relay src/sidepanel/peer-bridge.ts
⇅ chrome.runtime sendMessage + long-lived Port
(identical on Chrome + Firefox — no content-script injection in the panel)
background (SW on Chrome / event page on Firefox) src/background/index.ts
├─ bus router → module handlers src/core/bus/router.ts
├─ origin grants src/core/storage/origin-grants.ts (storage.local)
├─ capture sessions storage.session (TRUSTED_CONTEXTS, never disk)
└─ @zkp2p crypto + DOM parsing src/offscreen/handlers.ts
├─ Chrome → offscreen document (src/offscreen/offscreen.ts, via RPC)
└─ Firefox → run in-process in the background event page
extension pages
├─ panel src/sidepanel/ — iframes the uSwap app
├─ consent prompts src/prompt/ — connect / inline-template approvals
└─ options src/options/ — connected-sites management
The background treats sender.origin / sender.tab as authoritative; nothing
security-relevant is taken from page-supplied payloads.
- Plaintext payment session material (cookies, auth headers, request bodies) never leaves the extension — only TEE-encrypted blobs are posted to pages.
- Inline provider templates require explicit approval before account access.
- Capture is gated behind per-origin connection approval.
storage.localholds only origin grants. Capture state lives instorage.session(memory-backed) and is wiped on completion/expiry.- The extension holds no API keys; curator/maker logic stays in page/backend code.
bun install
# Chrome (default target)
bun run dev # vite dev server with CRXJS HMR
bun run build # production build → dist/
bun run package # dist/ → uswap-extension-<version>.zip
# Firefox
bun run dev:firefox # vite dev server, Firefox manifest
bun run build:firefox # production build → dist/firefox/
bun run lint:firefox # web-ext lint (AMO validation)
bun run run:firefox # launch Firefox with the extension loaded (temp add-on)
bun run package:firefox # dist/firefox/ → signed-XPI artifact (web-ext build)
bun run build:all # both targets
bun test # unit tests (template engine, selectors, redaction)- Chrome: load
dist/viachrome://extensions→ "Load unpacked". - Firefox:
bun run run:firefox, orabout:debugging→ "Load Temporary Add-on" →dist/firefox/manifest.json.
The panel embeds VITE_USWAP_APP_URL (default https://app.uswap.net, dev
default http://localhost:5173). The app must allow framing by the extension:
Content-Security-Policy: frame-ancestors 'self' chrome-extension: moz-extension:.
Compared on 2026-10-11 with the official Chrome Web Store Peer 0.7.3
package, the public branded-extension example,
and the current hosted provider templates.
The SDK is pinned to 0.14.8. The reference templates in test/fixtures/
contain public configuration only; account data in tests is invented.
| Supported method | Peer template / proof action | Metadata | Public proof selection |
|---|---|---|---|
| Venmo | venmo / transfer_venmo | JSON history | SENDER_ID + selected index |
| Cash App | cashapp / transfer_cashapp | JSON history | SENDER_ID + selected index |
| Revolut | revolut / transfer_revolut | In-page JSON history | selected index |
| Wise | wise / transfer_wise | POST activity history; one in-page retry | PROFILE_ID + TRANSACTION_ID |
| PayPal | paypal / transfer_paypal | HTML-wrapped JSON from metadata URL | PAYMENT_ID |
| Mercado Pago | mercadopago / transfer_mercadopago | embedded JSON in account HTML | PAYMENT_ID + URL_PARAMS_FROM |
| Monzo | monzo / transfer_monzo | JSON history from metadata URL | TX_ID |
| N26 | n26 / transfer_n26 | In-page GraphQL transfer detail | request body stays encrypted |
| Alipay | alipay / transfer_alipay | HTML / XPath history | TRADE_NO |
| Chime | chime / transfer_chime | In-page GraphQL Pay Anyone detail | request body stays encrypted |
| X Money (USD) | xmoney / transfer_xmoney | GraphQL transfer detail; raw micro-dollar amount | TRANSACTION_ID |
| Luxon | luxon / transfer_luxon | JSON history | TRANSFER_ID |
| Zelle Citi | zelle / transfer_zelle_citi | JSON history | selected index |
| Zelle Chase | zelle / transfer_zelle_chase | JSON history | selected index |
| Zelle Bank of America | zelle / transfer_zelle_bofa | In-page JSON history | selected index |
The Mercado Pago JSON template labels itself transfer_mercado_pago; the public
SDK's attestation contract uses transfer_mercadopago. We keep the SDK contract
and the published fetch path. No action is inferred from that inconsistent label.
X Money access is optional and limited to https://x.com/*. Amount metadata is
reported in micro-dollars (1 USD = 1,000,000 units); consuming apps must convert
that unit explicitly. A captured detail is not proof of a completed payment;
the attestation service remains authoritative. Quote availability depends on
Peer liquidity and the user having access to X Money.
- The three legacy Zelle fetch paths return 404. Both legacy bank aliases and canonical Peer inputs now resolve to the current bank-specific templates. Capture events include the canonical proof target; callers must preserve it.
- Wise history entries can lack a display date. A row remains selectable only when its amount, recipient, currency, transfer ID, and profile ID are present. Dates are not invented; the TEE verifies the actual transfer separately. Empty/incomplete history produces distinct, field-name-only errors. At most one history retry uses the authenticated provider tab; there is no loop.
- Both
errorand Peer'serrorMessageare emitted for compatibility. - Late extraction, approval, or encryption cannot publish a cancelled, expired, superseded, or revoked capture. Status changes cannot revive deleted sessions.
- The website relays expose only Peer calls, rebuild request envelopes, and cannot forward extension-only settings or forged offscreen messages.
- Attestation key services are restricted to the public Peer service and uSwap domains. Loopback services require a loopback caller. Provider navigation and replay targets must belong to the selected payment platform.
- Custom templates require approval before account access or result delivery. Closing a consent popup denies the request, and fast decisions do not race popup creation.
- Header/body selectors remain private. Nested metadata and per-row proof parameters are checked for credential leaks. JSONPath expression evaluation is disabled; all supported templates use ordinary path selectors.
- Header replay rules match an exact URL and the extension initiator, reject redirects, and are removed after use or background startup. Network reads time out, request caches are bounded, and concurrent setup per document is rejected.
- Production archives omit the development harness. The Chrome archive excludes the nested Firefox build; neither archive includes source maps or repository source files.
Peer 0.7.3 also accepts caller-supplied capture plugins in a sandbox, with plugin hash verification and capability consent. This release retains its existing JSON-template engine, which Peer still supports. It does not execute downloaded plugin programs, inject a MAIN-world script, or claim plugin-runtime parity.
Buyer capture was reviewed for all 15 methods: successful HTTP gating, method / URL / body matching, metadata replay, preprocessing, JSONPath / XPath extraction, private parameters, row selection, encryption, consent, cancellation, and delivery. The existing Venmo and Cash App seller-credential code was also reviewed; Peer 0.7.3's current seller-credential entry point supports Cash App and UPI instead. UPI is outside this extension's supported payment-method list. Identity/register capture and live seller credential uploads are not newly advertised by this release.
Validation includes the automated capture suite, TypeScript, both production builds, Firefox lint (zero errors), Chrome and Firefox runtime checks of the panel bridge and Alipay XPath path, all 15 live template fetches, and encryption of synthetic session material using the live public attestation endpoint. These last checks read public key material; they do not submit payment proofs, create orders, or exercise real payment accounts. Successful capture and proof verification in each logged-in payment account still need user testing.
The dependency audit reduced 55 advisories to one unresolved build-tool advisory
in web-ext → @devicefarmer/adbkit → node-forge (upstream advisory).
The registry's latest node-forge is still affected. This Android tooling dependency
is absent from both browser archives and is not used by desktop payment capture.
Firefox lint also reports static warnings in bundled React/JSONPath library code;
no eval permission or remote executable code is enabled.
Disable the installed uSwap extension first so only one relay is active. Extract
uswap-extension-0.6.13.zip, open chrome://extensions, turn on Developer mode,
choose Load unpacked, and select the extracted folder containing manifest.json.
Reload the uSwap tab and retry verification in the account that sent the payment.
For an already paid order, do not send another payment. Report the exact new error
if verification still fails. GitHub releases do not update the Chrome Web Store
installation automatically.
The 0.6.10 defense-in-depth cookie comparison also compared Wise's numeric PROFILE_ID / TRANSACTION_ID proof parameters with cookie context values. A public response ID repeated in a context cookie could block an otherwise valid capture. Only those two numeric fields from the hosted Wise template are now excluded from the cookie-atom comparison. Full cookie headers, authorization and secret headers, private selectors, inline templates, and every other metadata field retain strict checks. No credential value is returned to the app.
Failed verification now returns to the requesting tab and closes the temporary provider tab too, unless the provider template explicitly requests keeping it open. Real-account acceptance testing is still required.
Wise activity feeds may omit currency as well as the display date. The hosted Wise single-transfer proof now permits those missing display fields when the response contains amount, recipient, and matching numeric transfer/profile IDs. Absent fields remain absent: no currency or date is inferred from the order. The actual transfer attestation must still validate the payment before release. Inline templates and other proof targets retain strict missing-field handling. No additional provider requests are introduced. Real-account acceptance testing is still required; this release addresses the reported metadata-selection blocker.
Firefox's built-in consent declares financial/payment data, identifying payment metadata, authentication information, and website content. Verification handles selected payment-history responses and session headers/cookies, encrypting session material to the attestation service before delivery to the requesting app. These declarations describe existing behavior; capture logic is unchanged. Firefox requires acceptance of these data permissions when installing/updating. Matching readable source and reproducible build instructions must accompany the minified Firefox package submitted to AMO.