feat(auth): route all backend-v2 calls through the authed fetch chokepoint (#2879)#2880
Draft
piyalbasu wants to merge 21 commits into
Draft
feat(auth): route all backend-v2 calls through the authed fetch chokepoint (#2879)#2880piyalbasu wants to merge 21 commits into
piyalbasu wants to merge 21 commits into
Conversation
Contributor
|
PR Preview build is ready: https://github.com/stellar/freighter/releases/tag/untagged-3f739afa89f5c31ad987 (SDF collaborators only — install instructions in the release description) |
65143fd to
9594e94
Compare
00c2a9e to
4e522b0
Compare
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…2770) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n script (#2770) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ace-script commit) (#2770)
…ndPath buildAuthJwt bakes method.toUpperCase() into the methodAndPath claim, but authedFetch sent the raw-case method on the wire. fetch only auto-uppercases the standard verbs (GET/POST/...), not PATCH or custom methods — so a lower-case non-standard method would leave the server's r.Method mismatching the signed claim and yield a silent 401. Normalize the method once and use it for both the JWT and the request. Adds a regression test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…path The backend verifies methodAndPath against r.URL.RequestURI() (the full path+query). authedFetch signed the caller's `path` fragment alone, but the backend base URL (INDEXER_V2_URL) carries an "/api/v1" prefix and helpers append the endpoint suffix — so base "<host>/api/v1" + path "/contacts" fetched "/api/v1/contacts" while signing "/contacts", a guaranteed 401 once wired into the real path. Derive the signed target from the final URL's pathname+search so it always matches the wire request regardless of where the prefix lives. Adds prefix + query-string regression tests. Addresses Codex review (P2) on PR #2877. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9594e94 to
ac942ce
Compare
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…KEND_V2 (#2879) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ccounts
Resolves TS2339 errors from the webpack pre-commit hook — the generic
defaults to the web Response type; pass { status: number; body: unknown }
so TypeScript resolves the destructure correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
#2879) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…#2879) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Thread sessionStore into getIsRpcHealthy / loadBackendSettings and swap the direct fetch for callBackendV2 so the JWT chokepoint covers /rpc-health. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…est types (#2879) Remove the top-level `import { callBackendV2 }` from account.ts and replace it with a deferred dynamic import() inside getIsRpcHealthy. This breaks the module-eval-time cycle: ducks/session → account → callBackendV2 → session.ts → store → ducks/session, which caused sessionSlice to resolve as undefined in ~11 background test suites. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4e522b0 to
88c629a
Compare
Base automatically changed from
feat/2770-per-request-backend-jwt
to
feat/2769-derive-auth-keypair
June 30, 2026 20:26
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR
Routes every Freighter-extension call to
freighter-backend-v2through a single background "chokepoint" that attaches the user's per-request auth token when the wallet is unlocked, and sends anonymously otherwise (#2879). This is the client groundwork so the backend can attribute requests to a user and, eventually, require authentication.It's non-breaking: the backend stays permissive, so old clients (and locked/onboarding states) keep working — updated clients just start including the token, which shows up in the backend's auth metrics as adoption signal. No user-facing change.
Implementation details (for agents/reviewers)
The chokepoint —
extension/src/background/helpers/callBackendV2.ts: derives the auth keypair on-demand from the unlocked session mnemonic (elsenull→ anonymous), and either delegates toauthedFetch(#2770, JWT + retry-once-on-401) or does a plainfetch. Returns{ status, body }.INDEXER_V2_URLalready includes/api/v1, so the JWT'smethodAndPathis derived from the resolved URL'spathname + search(full server request-target incl. query, e.g.POST /api/v1/ledger-key/accounts?network=PUBLIC) — matching the server'sr.URL.RequestURI(). Unit-tested by decoding the JWT claim.Wiring (option A — route through the background): popup callers reach the chokepoint via a new
FETCH_BACKEND_V2message (SERVICE_TYPES+message-request.tsunion + apopupMessageListenercase gated byisFromExtensionPageso content scripts can't trigger authed calls). The seed never leaves the background; only{ status, body }returns to the popup; the base URL is server-fixed (not an open proxy).Migrated call sites:
getDiscoverData→GET /protocols(popup → message)getLedgerKeyAccounts→POST /ledger-key/accounts?network=(popup → message)fetchCollectibles→POST /collectibles?network=(popup → message)account.ts) →GET /rpc-health?network=(background → directcallBackendV2)Notable: routing rpc-health introduced a circular import (
ducks/session → account.ts → callBackendV2 → helpers/session → store → ducks/session); fixed by lazy-importingcallBackendV2insidegetIsRpcHealthy.npx madge --circularconfirms zero cycles remain.Verification:
yarn jest @shared/api76/76;yarn jest extension/src/background33 suites / 0 failed. New unit tests cover the chokepoint (unlocked/locked, POST body+Content-Type, full signed query path, non-2xx) and each migrated wrapper's message shape + mapping. Reviewed per-task + a whole-branch review (verdict: ready to merge).Scope / follow-ups (out of scope here):
freighter-backend-v2#114), gated on extension+mobile adoption metrics + a pre-flip audit that none of these endpoints is hit by a locked/watch-only account.freighter-mobile#865).translation.jsonkeys in the diff are the huskyi18next-scannerhook backfilling a pre-existingmastergap — unrelated.