Skip to content

feat(auth): derive anonymous backend auth keypair (#864)#920

Open
piyalbasu wants to merge 12 commits into
mainfrom
feat/864-derive-auth-keypair
Open

feat(auth): derive anonymous backend auth keypair (#864)#920
piyalbasu wants to merge 12 commits into
mainfrom
feat/864-derive-auth-keypair

Conversation

@piyalbasu

@piyalbasu piyalbasu commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

TL;DR

Closes #864. Derives an anonymous, per-user auth keypair from the wallet seed so Freighter Mobile can (in a follow-up) authenticate its calls to freighter-backend-v2. The derived key is cryptographically independent from the wallet signing key, never triggers a wallet-signing prompt, and produces the same user id as the Freighter extension for the same mnemonic (locked by shared cross-platform test vectors). The keypair is memoized in memory for the unlocked session and dropped on every session-ending path — logout, lock, soft-lock, hash-key hard-expiry, and wallet import — never persisted.

This is PR 1 of 2. PR 2 (#865) adds the per-request JWT and wires it into the backend-v2 axios client.

Implementation details (for agents)

What changed:

  • src/services/auth/deriveAuthKeypair.ts (new) — pure, async (bip39's mnemonicToSeed keeps the 2048-round PBKDF2 off the JS thread). HMAC-SHA256(key = bip39 64-byte seed, msg = "freighter-auth-v1")Keypair.fromRawEd25519Seed → lowercase-hex userId. Uses bip39 (direct dep on purpose — extension parity + its async seed/validate APIs) + the crypto import (react-native-crypto on device, Node's built-in under jest) + @stellar/stellar-sdk Keypair (signs via @noble/curves, unaffected by the tweetnacl jest mock). validateMnemonic first; rejects on invalid input.
  • src/services/auth/authKeypairVectors.ts (new) — the same mnemonic → authSeedHex → userId vectors the extension committed, asserted in tests to guarantee cross-platform parity.
  • src/ducks/auth.ts — exports getActiveMnemonicPhrase(): Promise<string | null> (thin wrapper over the existing private getTemporaryStore, gated on AUTHENTICATED with a re-check after the decrypt await); calls clearAuthKeypairCache() on both logout paths, in clearAllData(), in softLock(), and on the HASH_KEY_EXPIRED transitions in getAuthStatus() — so no session-ending path leaves key material resident.
  • src/services/auth/getAuthKeypair.ts (new) — getAuthKeypair(): Promise<Keypair | null>, total (never rejects: errors are logged and surfaced as null), with an in-memory, module-scoped cache (never persisted) plus in-flight-promise memoization so the unlock request burst runs one derivation; clearAuthKeypairCache(): void. selectAccount intentionally does not clear — sub-accounts share one mnemonic and derivation is mnemonic-keyed, so the cached keypair stays correct across sub-account switches; only session-ending paths (logout/lock/soft-lock/hard-expiry/import) clear it.
  • package.json — adds bip39 pinned exact (3.1.0).

Why this mirrors the extension: HMAC-SHA256 and Ed25519 (RFC 8032) are standardized, so the RN crypto libraries produce byte-identical output for the same mnemonic. The shared vectors pin that output under jest (Node's crypto); the on-device react-native-crypto path is asserted by PR #865's gated E2E.

Verification (post-review): deriveAuthKeypair 7/7, getAuthKeypair 7/7, ducks/auth 137/137; tsc --noEmit clean; madge no circular deps; full pre-commit suite green on commit. Reviewed task-by-task (spec + quality) including the import-cycle, cache-eviction, and async-derivation paths.

Follow-ups / out of scope: PR 2 (#865) — buildAuthJwt + axios interceptor + gated E2E. Contacts UI and the backend permissive→required flip are separate.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

iOS Simulator preview build is ready: https://github.com/stellar/freighter-mobile/releases/tag/untagged-ebeba4f380a074ccc5f7 (SDF collaborators only — install instructions in the release description)

@piyalbasu

Copy link
Copy Markdown
Contributor Author

Extension parity: this is the mobile port of stellar/freighter#2876derive backend auth keypair from seed (issue stellar/freighter#2769).

Same derivation (HMAC-SHA256(bip39 seed, "freighter-auth-v1") → Ed25519 Keypair) and the same committed cross-platform test vectors — the mobile-derived userId must match the extension's for the same mnemonic (asserted in authKeypairVectors).

@piyalbasu piyalbasu marked this pull request as ready for review July 8, 2026 18:16
Copilot AI review requested due to automatic review settings July 8, 2026 18:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a deterministic, per-user “anonymous backend auth” Ed25519 keypair derived from the wallet mnemonic, intended to authenticate future requests to freighter-backend-v2 without triggering wallet-signing prompts and with cross-platform parity to the Freighter extension.

Changes:

  • Added pure synchronous derivation utilities (HMAC-SHA256(BIP39-seed, "freighter-auth-v1") → Ed25519 keypair) plus shared test vectors.
  • Added getAuthKeypair() with an in-memory (non-persisted) cache and explicit cache clearing hooks on logout/lock/import paths.
  • Added Jest coverage for derivation vectors, determinism, auth-status gating, and cache behavior; added bip39 dependency.

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
package.json Adds bip39 dependency for mnemonic → seed derivation.
yarn.lock Locks bip39@3.1.0.
src/services/auth/deriveAuthKeypair.ts Implements auth-seed + auth-keypair derivation and hex userId formatting.
src/services/auth/authKeypairVectors.ts Adds cross-platform mnemonic → seed/userId vectors for parity checks.
src/services/auth/getAuthKeypair.ts Adds authenticated-session-only accessor with module-scoped in-memory caching.
src/ducks/auth.ts Clears auth-keypair cache on logout/lock/wipe and exports getActiveMnemonicPhrase().
__tests__/services/auth/deriveAuthKeypair.test.ts Tests vectors, determinism, formatting, mnemonic validation, and wallet-key independence.
__tests__/services/auth/getAuthKeypair.test.ts Tests auth-status gating, memoization, and cache invalidation behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/services/auth/getAuthKeypair.ts Outdated
Comment thread src/ducks/auth.ts
Comment thread src/ducks/auth.ts
Comment thread __tests__/services/auth/getAuthKeypair.test.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 342d60a3f7

ℹ️ 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".

Comment thread src/services/auth/getAuthKeypair.ts Outdated
Comment thread src/ducks/auth.ts
piyalbasu and others added 7 commits July 8, 2026 14:27
…tors (#864)

Implements HMAC-SHA256(key=bip39-64-byte-seed, msg="freighter-auth-v1") ->
Keypair.fromRawEd25519Seed -> lowercase-hex userId, matching the extension's
committed vectors exactly. Adds bip39@3.1.0 as a direct dep (exact pin).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…864)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…expired (#864)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@piyalbasu piyalbasu force-pushed the feat/864-derive-auth-keypair branch from 342d60a to 9f4ec75 Compare July 8, 2026 19:20

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9f4ec75f6e

ℹ️ 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".

Comment thread src/ducks/auth.ts
…live session validity, clear cache on soft-lock (#920)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4d8e481011

ℹ️ 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".

Comment thread src/ducks/auth.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 994bb73589

ℹ️ 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".

Comment thread src/ducks/auth.ts Outdated
Comment thread src/services/auth/getAuthKeypair.ts
Comment thread src/services/auth/deriveAuthKeypair.ts Outdated

import { Keypair } from "@stellar/stellar-sdk";
import { mnemonicToSeedSync, validateMnemonic } from "bip39";
import { createHmac } from "crypto";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The crypto import resolves to react-native-crypto only via package.json's browser/react-native field maps, which metro honors but Jest doesn't — jest.config.js has no moduleNameMapper entry for crypto, so under Jest this is Node's built-in crypto. The cross-platform vectors therefore validate Node's createHmac, not the RN runtime path: if react-native-crypto ever diverges on device (e.g. Buffer-key handling under buffer@4.9.2), users derive wrong userIds/keypairs while CI stays green. The line-1 comment ("works in jest & RN") overstates what the tests prove. Suggest a Jest mapping of ^crypto$react-native-crypto, or a device-side/E2E assertion of one vector.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected the overstated comment in 02693b1 — it now states plainly that crypto resolves to Node's built-in under Jest (no moduleNameMapper override) and to react-native-crypto only on device, so the vectors pin the HMAC-SHA256 algorithm, not the RN code path.

I did not add the ^crypto$→react-native-crypto Jest mapping: it's a global remap that reroutes every crypto consumer in the suite (SDK internals included), and react-native-crypto pulls in the react-native-randombytes native module, which won't load under Jest. The honest coverage for the on-device library is a device-side vector assertion, which belongs in #865's gated E2E — I'll add it there. Leaving this open until that lands; say the word if you'd rather gate it differently.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up on this one: I over-credited the E2E earlier. On closer look the gated authJwt.integration E2E also runs under Node crypto (jest environment), so it doesn't exercise react-native-crypto either — the on-device derivation/signing path is genuinely unasserted, exactly as you flagged here.

Filed #931 to track adding a real device-runtime assertion covering both the #864 derivation vector and #865 JWT signing (the two paths that share this crypto-fidelity gap). The RN-URL half of the fidelity concern was separately fixed in #925 (signed path no longer depends on any environment-specific primitive). Leaving this thread to #931.

Comment thread src/services/auth/getAuthKeypair.ts Outdated
Comment thread src/services/auth/getAuthKeypair.ts Outdated
Comment thread __tests__/services/auth/getAuthKeypair.test.ts Outdated
Comment thread package.json
Comment thread src/services/auth/authKeypairVectors.ts
Comment thread src/ducks/auth.ts Outdated
@aristidesstaffieri

Copy link
Copy Markdown
Contributor

One eviction gap in the new keypair cache: it's cleared on clearAllData, both logout paths, and lock — but not on the AUTHENTICATED → HASH_KEY_EXPIRED transition in getAuthStatus (src/ducks/auth.ts:2827, set({ authStatus }) then navigate to lock). A session that hard-expires while the app is in memory leaves the derived Ed25519 private key in the JS heap indefinitely while the device sits on the re-auth screen — the isSessionAuthValid gate blocks use of the cache but not retention, which is the same rationale the softLock clear cites. Suggest clearing the cache where HASH_KEY_EXPIRED is set. (Top-level comment because that line predates this PR and isn't in the diff — the gap is in the PR's eviction coverage.)

- getAuthKeypair: wrap body in try/catch so the accessor is total — log and
  return null on keychain / corrupt-hash / invalid-mnemonic errors instead of
  rejecting the Promise<Keypair | null> contract
- getAuthKeypair: memoize the in-flight derivation so the unlock request burst
  (balances/prices/history) runs one PBKDF2, not one per caller
- deriveAuth{Seed,Keypair}: use async mnemonicToSeed so the 2048-round PBKDF2
  doesn't block the JS thread; document the crypto jest-vs-RN test gap and the
  deliberate direct bip39 dependency
- getAuthStatus: evict the auth-keypair cache on HASH_KEY_EXPIRED transitions
  (retention, not just use, must end at hard-expiry — matches softLock)
- getActiveMnemonicPhrase: log in the previously-silent catch
- drop the clearAuthKeypairCache re-export (one canonical import path)
- tests: collapse three identical gate tests into one, add concurrency and
  expiry-eviction regressions, update for the async API

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@piyalbasu

Copy link
Copy Markdown
Contributor Author

Addressed in 02693b1: getAuthStatus now calls clearAuthKeypairCache() on both HASH_KEY_EXPIRED transitions (the soft-locked branch at 2820 and the general set at 2827), so a hard-expiry evicts the derived Ed25519 key rather than leaving it resident on the re-auth screen — closing the retention gap alongside the existing softLock/logout/wipe eviction. Added a duck regression test asserting eviction fires on the expiry transition.

…920)

isSessionAuthValid re-implemented a subset of the lock rules (in-memory
authStatus + hash-key TTL) and omitted the auto-lock timer, so in the window
after foreground but before the getAuthStatus() funnel runs it could report a
session valid after auto-lock should have engaged — letting getAuthKeypair hand
out the backend signing key.

Delegate to the authoritative getAuthStatus() funnel instead, so the check
evaluates account existence, persisted LOCKED, hash-key hard-expiry AND the
auto-lock timer from one source of truth, removing the drift that caused the
gap. Reworked the isSessionAuthValid tests to drive the real funnel via storage
mocks and added an auto-lock-timer regression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
piyalbasu added a commit that referenced this pull request Jul 9, 2026
…865)

isSessionAuthValid delegates to the getAuthStatus() funnel (~5 keychain reads
per run), and the JWT interceptor calls it on every backend request. Add an
in-flight promise to collapse the near-simultaneous unlock burst (balances +
prices + history) into one run, and a short-TTL (1s) value memo for sequential
calls.

Tradeoff documented inline: a funnel-only transition (auto-lock timer elapse,
hash hard-expiry) is reflected at the gate up to the TTL late — bounded and
immaterial (auto-lock windows are minutes+), and it cannot leak key material
because explicit lock/logout/wipe paths clear the keypair cache and flip
in-memory authStatus. Exposes clearSessionAuthValidMemo() for eviction/tests.

Also await the now-async deriveAuthKeypair in the gated JWT e2e test (#920
made it async).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(auth): per-request EdDSA JWT builder for mobile (#865)

* feat(auth): attach per-request JWT + retry-once-on-401 to freighterBackendV2 (#865)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(auth): register JWT interceptors before apiFactory normalizer so 401 retry fires (#865)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(auth): gated E2E round-trip vs staging whoami (#865)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(auth): migrate v2 POST call sites to string body + query-in-url for correct JWT signing (#865)

* test(auth): read whoami { data } envelope; E2E verified green vs staging (#865)

* fix(auth): idempotent error normalizer (preserve 401 on retry) + skip retry for anonymous 401s (#865)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(auth): enforce string-only V2 request bodies at compile time so JWT bodyHash always matches (#865)

* fix(auth): fold config.params into the signed JWT path; refresh body/path contract doc (#865)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(auth): strip stale Authorization header on locked/anonymous requests (#865)

* fix(auth): handle URLSearchParams when folding params into signed path (#865)

* perf(auth): short-TTL memo + in-flight dedup for isSessionAuthValid (#865)

isSessionAuthValid delegates to the getAuthStatus() funnel (~5 keychain reads
per run), and the JWT interceptor calls it on every backend request. Add an
in-flight promise to collapse the near-simultaneous unlock burst (balances +
prices + history) into one run, and a short-TTL (1s) value memo for sequential
calls.

Tradeoff documented inline: a funnel-only transition (auto-lock timer elapse,
hash hard-expiry) is reflected at the gate up to the TTL late — bounded and
immaterial (auto-lock windows are minutes+), and it cannot leak key material
because explicit lock/logout/wipe paths clear the keypair cache and flip
in-memory authStatus. Exposes clearSessionAuthValidMemo() for eviction/tests.

Also await the now-async deriveAuthKeypair in the gated JWT e2e test (#920
made it async).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(auth): address review — central body serialization, clock-skew fallback (#865)

Body handling (aristides): serialize object bodies centrally in the request
interceptor before hashing, so bodyHash always matches the wire bytes. Fixes the
prod empty-bodyHash bug (non-string body silently signed an empty hash, warned
only in __DEV__). Removes the string-only contract: drop the createApiService
<string> constraint, the backendV2TypeContract test, the dev warning, and the
per-call JSON.stringify + redundant Content-Type overrides (createApiService
already defaults application/json).

Clock skew (aristides/codex): if the signed 401-retry also 401s (likely device
clock skew — re-signing from the same bad clock fails identically), fall back to
one anonymous request so these endpoints (anonymous pre-PR) keep working, and
log it. The fallback rate is the signal for widening the backend clock-skew
leeway; tracked as a follow-up.

Also: hoist the constant JWT header segment to module scope (compute once);
skip null/undefined params in mergeParamsIntoUrl to match axios; collapse
duplicated call-shape assertions in backend.test.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(api): drop the now-inert TWriteBody generic from createApiService (#865)

The TWriteBody generic existed only to support the string-only body contract
(V2 was createApiService<string>). Central body serialization removed that
contract, leaving the generic always defaulting to unknown. Remove it and type
post/put bodies as `unknown` directly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(auth): derive signed JWT path from axios getUri, not hand-rolled helpers (#865)

Replace mergeParamsIntoUrl + deriveServerPath (and the fold-then-delete of
config.params) with instance.getUri(config): axios's own baseURL-join + param
serializer. The signed methodAndPath is now pathname+search of exactly what
axios will put on the wire, so signed path == wire path by construction — no
manual param serialization that can drift from axios (the state-dependent
divergence aristides flagged), and config.params is left untouched.

Tests: fake instance gains a getUri stand-in; unit param tests assert the
getUri-derived path + params-preserved; the real-serialization specifics
(&-join, encoding) are covered by the integration tests against real axios.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* revert(auth): drop the anonymous 401 fallback; match the extension's retry-once (#865)

The extension's authedFetch retries once on 401 and returns the second 401 (no
anonymous fallback). The mobile fallback was a divergence added for clock-skew
resilience; on review it breaks parity, masks genuine auth failures behind a
permissive-backend success, and only helps during the permissive window.

Remove the __authFellBackAnon path (forced-anon request branch + response-side
fallback + the logger.warn) and return the second 401 to the caller. Clock-skew
handling is deferred entirely to the backend leeway (#930), same as the
extension. Retry-once-then-fail is preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(auth): sign the path without RN's non-spec URL; clear memo on unlock (#865)

Fable review (device-vs-jest) findings:

BLOCKER — deriveServerPath used `new URL(getUri).pathname`. React Native's
built-in URL (0.81, Libraries/Blob/URL.js) is not WHATWG-compliant: it appends
a trailing "/" to any URL with no query/hash. So on device a query-less GET
(e.g. /protocols) signed "/api/v1/protocols/" while the wire target was
"/api/v1/protocols" → guaranteed 401 (no fallback). jest uses Node's compliant
URL, so the suite was blind to it. Replace with an origin-strip on getUri's
output — environment-independent, preserves the wire target verbatim. Added a
query-less regression test.

- signIn clears the isSessionAuthValid memo on unlock, so the post-unlock
  request burst isn't served a stale `false` (would go anonymous for ≤1s).
- Central body serialization narrowed to plain objects/arrays, so a future
  FormData/Blob body isn't corrupted to "{}" on the wire.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(api): redact Authorization in the logRequests debug logger (#865)

The logRequests request/response debug logger JSON.stringify'd the full config;
on an instance carrying per-request JWT auth (via configureInstance) that would
log the Bearer token (request interceptors run LIFO, so the token is attached
before the logger runs). Mask any Authorization header at any depth via a
stringify replacer. Not reachable today (V2 doesn't set logRequests) — closes
the secret-leak foot-gun. Added a redaction test through the real chain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(backend): pass v2 network via { params } now that getUri signs them (#865)

The two v2 POST call sites embedded the network query directly in the URL string
because the pre-getUri interceptor signed the path before axios serialized
config.params — so { params } would have signed a query-less path (≠ wire → 401).
The getUri switch removed that constraint: the signed path is derived from
instance.getUri(config), which folds params in via axios's own serializer. So
migrate both to the idiomatic { params: { network } }; wire + signed output are
identical. Tests updated to lock the { params } call shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(api): explain the `unknown` body type on post/put (#865)

Document why the body is `unknown` (the TWriteBody string-only contract was
removed) and that an auth-wired instance serializes object bodies centrally so
the signed bodyHash matches the wire.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

[Mobile] Derive auth keypair from seed for backend authentication

3 participants