Skip to content
This repository was archived by the owner on Jul 31, 2026. It is now read-only.

v0.13.0 — passkey-kit v1 ground-up overhaul (contracts, SDK, indexing, demo, services) - #33

Closed
kalepail wants to merge 30 commits into
nextfrom
overhaul/ground-up
Closed

kalepail wants to merge 30 commits into
nextfrom
overhaul/ground-up

Conversation

@kalepail

Copy link
Copy Markdown
Owner

Draft — the code is complete, audited, and verified; this PR is open for review while a few outward-facing decisions are settled (see Remaining decisions). Do not merge yet.

A ground-up, forward-only overhaul of passkey-kit — contracts, SDK, bindings, indexing, demo, and services — released as v0.13.0 (tag v0.13.0). No backwards-compatibility layer with 0.12.x; every line was re-examined against the current stack.

Scope: 30 commits, ~177 files, +23.3k / −5.5k. Canonical v1 contract WASM hash: 84924c53a413318df2ce753e30de53ec651404c916d30e861718ad155c94b319 (see docs/deployments-testnet-2026-07-11.md).

Contract (smart-wallet, soroban-sdk 27)

Rebuilt on soroban-sdk 27 / wasm32v1-none, Protocol 27 ("Zipper").

  • Constructor-only init (__constructor(signer), CAP-0058); the legacy init flag and the un-authenticated first-add_signer window are gone.
  • update_contract_code → upgrade(new_wasm_hash) (emits an Upgraded event, caches the hash); new get_signer(signer_key) -> Option<SignerVal> view.
  • Timestamp expirations — SignerExpiration is a u64 UNIX timestamp (inclusive), no longer a ledger sequence (survives ledger-close-time drift).
  • Fail-closed limits — SignerLimits::Some(empty) now means no permissions; None is the one unlimited encoding. CreateContract* requires an unlimited signer (deploy permission is no longer implied by a wallet-address limits entry).
  • __check_auth correctness — pass 1 (coverage) is purely boolean and can't panic-poison a valid auth; pass 2 verifies every signatures-map entry with a single-point expiration check (fixes an order-dependent, skippable-expiration bug).
  • WebAuthn hardening — keeps the load-bearing challenge-equality binding; adds type == "webauthn.get" + UP-flag checks, bounds clientDataJSON, returns typed errors instead of panicking.
  • Policy lifecycle — install(wallet) / permissionless uninstall(wallet); no policy code runs on the remove_signer path, so a broken/malicious policy can never block its own removal. sample-policy now demonstrates a cumulative rolling-window spending allowance.
  • Typed #[contractevent] events (SignerAdded / SignerUpdated / SignerRemoved / Upgraded, SEP-48 schema in the WASM) replacing the schemaless ("sw_v1", …) tuples.
  • Errors renumbered 100–129 (disjoint from legacy 1–9); single spec-type source in smart-wallet-interface.

SDK

  • Typed errors (PasskeyKitError hierarchy + a contract-error registry decoded from diagnostics, sync-tested against the bindings) and a discriminated TransactionResult — submission returns, never throws.
  • Unified signing pipeline — signer abstractions (passkey / ed25519 / policy) over the SDK's buildAuthorizationEntryPreimage; host-order compareScVal ScMap sort replacing the old localeCompare (fixes same-type/differing-length key mis-ordering); V2 address credentials (SOROBAN_CREDENTIALS_ADDRESS_V2).
  • DI-managed decomposition (kit/*, managers/*) that's unit-testable with fakes; storage adapters (memory / localStorage / IndexedDB, passkey-kit/storage subpath); typed event emitter.
  • connectWallet ownership verification — a derived address is confirmed to actually hold the passkey signer before it's trusted.
  • Real dist packaging — compiled ESM + .d.ts with an exports map (. / ./storage / ./server), replacing raw-TS main; @stellar/stellar-sdk moved to a >=16 peer; verify:bindings drift guard + verify-esm build gate.
  • vitest suite replacing the ad-hoc bun_tests scripts (141 tests).

Indexing

One SignerIndexer abstraction with two interchangeable backends. See Remaining decisions for live-coverage status — the backends are code-complete and unit-tested; live discovery depends on external-service decisions.

  • Stellar Indexer (/v1/contract-data) — decodes the live JSON-ScVal wire format; mainnet-only (network-aware factory returns null off mainnet rather than masquerading an empty result).
  • Mercury — the client no longer points at the retired /zephyr/execute route; the integration approach is pending (see below).

Demo & services

  • Demo rebuilt on Svelte 5, componentized, zero secrets in the bundle — submission via a keyless relayer-proxy worker; both indexer backends toggleable; exercises create / reconnect / fund / transfer / add-update-remove across all three signer kinds.
  • relayer-proxy/ — a Cloudflare Worker doing keyless per-IP OZ-Relayer key minting (Durable Object custody), @openzeppelin/relayer-plugin-channels ^0.20. Purges the last Launchtube references.
  • agent-browser WebAuthn e2e harness under scripts/e2e/.

Assurance

  • Independent multi-model contract audit — three reviewers (Fable, gpt-5.6-sol, gpt-5.6-terra) over the frozen contract; core auth cleared by all; 7 findings fixed and each re-verified by its original finder (incl. a subtle budget-exhaustion revocation-block the adversarial pass caught that the correctness passes missed).
  • High-effort code review over the full diff — security surfaces clean; 2 HIGH + 5 MED + LOWs all fixed (incl. a relayer-envelope mismatch and a wrong-ledger-key eviction probe).
  • Live verification — static gate green (141 SDK + 21 worker + 65 contract tests, verify:bindings, verify-esm); browser passkey e2e on testnet with real contract IDs + tx hashes; Relayer verified on testnet and mainnet (mainnet fee-bump 97e6fcea…, pubnet ledger 63438464). Caught and fixed a deploy fee-bump bug (RelayerError 7002) that only surfaces against a live relayer.

Migration

Breaking, forward-only. Full guide with Before/After, a removed-exports list, and an A/B gap analysis: docs/migration-v1.md. Pre-1.0 wallets stay live on both networks (same address-derivation tuple) and can be upgraded in place to a v1 WASM via their own auth.

Remaining decisions

Deliberately unresolved, flagged rather than guessed:

  1. Mercury indexing approach. Mercury retired the self-serve Zephyr flow the original program targeted (/zephyr/execute is a dead route). Preferred path: have Mercury add passkey-kit's v1 WASM (84924c53…) to their existing hosted Smart Account Indexer (the same product smart-account-kit uses), so the SDK just queries /api/lookup/* with no self-deploy. Alternative: keep the self-deployed program on Mercury's current tooling.
  2. Stellar Indexer live validation is mainnet-only, so it needs a mainnet v1 wallet to confirm end-to-end (the decoder is unit-tested against the documented shape today).
  3. Publish + branch strategy. next carries two P27-prep commits (Oz #30, [codex] Prepare auth signing for Protocol 27 #31) that this overhaul supersedes; how it lands on the default branch (merge vs. reset) is a maintainer call. npm publish is pending the version go-ahead.

🤖 Generated with Claude Code

kalepail and others added 30 commits June 5, 2026 13:20
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ground-up SDK foundations (todo 948, overhaul/ground-up):
- errors.ts: PasskeyKitError + ranged code enum; ContractError registry
  (contract-errors.ts, current 9 codes) + diagnostic decode + bindings-sync test
- types.ts: discriminated TransactionResult, CreateWalletResult/
  ConnectWalletResult, StoredPasskey/StorageAdapter; rename Mercury-row
  Signer -> IndexedSigner (kills bindings Signer collision)
- utils.ts: compactSignature, extractPublicKeyFromAttestation (SPKI path),
  generateChallenge (random 32B), deriveContractAddress (+ golden vectors)
- validation.ts, base64url.ts shim, events.ts typed emitter, constants.ts,
  version.ts (+ scripts/sync-version.js)
- storage/: Memory/LocalStorage/IndexedDB adapters (passkey-kit/storage subpath)
- kit/auth-payload.ts: SDK-backed P27 preimage helpers (ports bun_tests/9)
- Packaging: compile to dist/ (ESM+d.ts), exports map (. /storage /server),
  files whitelist, sideEffects false, NodeNext; stellar-sdk 16.0.1 dep + peer
  >=16.0.0; relayer-plugin-channels ^0.20; verify-esm.mjs gate; bindings ship
  compiled JS dist
- Vitest (70 tests) replaces bun_tests/; delete bun_tests/, clone-js-sdk.sh

pnpm build (+ verify-esm) and vitest green. Old kit.ts/server.ts kept compiling
under NodeNext; full rewrites land in B2/B3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…an-sdk 27)

- single types source: interface crate exports spec types/events/errors;
  duplicated smart-wallet/src/types.rs deleted
- timestamp-based SignerExpiration (u64 unix seconds, inclusive)
- fail-closed SignerLimits (Some(empty) = no permissions); deploy requires
  unlimited signer; limited signers may always self-remove (typed, boolean)
- __check_auth: pass-1 context coverage purely boolean; pass-2 single-point
  expiration + crypto/policy verification of every signatures entry
- WebAuthn: type==webauthn.get + UP flag + authData length checks, typed
  oversized-clientDataJSON error; challenge equality unchanged
- policy install/uninstall lifecycle (hard install, best-effort uninstall);
  sample-policy demonstrates installed-wallet gating; recursion depth guard
- upgrade(new_wasm_hash) + Upgraded event + contractmeta binver; constructor-
  only init (init flag deleted); get_signer view; #[contractevent] schema
  (signer_added/signer_updated/signer_removed/upgraded)
- errors renumbered to 100+ ranges (legacy 1-9 disjoint for SDK detection)
- Makefile: fix wrong-dir wipe/double build, add test target, CLI 27 optimize

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…F7 ownership)

Ground-up rewrite of the signing pipeline into DI managers (todo 949):
- signers.ts: PasskeySigner/Ed25519Signer/PolicySigner replace the
  keyId|keypair|policy option trio with explicit signer objects.
- kit/auth-payload.ts: host-order compareScVal + Signatures map read/upsert/sort
  (replaces the localeCompare bug) + SignerKey/Signature encoding via bindings Spec.
- kit/webauthn-ops.ts: registration/authentication ceremonies with a RANDOM
  challenge (was the static "stellaristhebetterblockchain") and rpId from the
  constructor; SPKI public-key extraction via utils.
- kit/deploy-ops.ts: configurable deploySource (default preserves the canonical
  "kalepail" deterministic deployer); deterministic address derivation.
- kit/wallet-ops.ts: add/update/remove signer builders + SignerKey/SignerLimits
  encoding + getSigner via RPC ledger read (temporary-before-persistent).
- kit/tx-ops.ts: single explicit AssembledTransaction input (kills the lossy
  tri-input coercion), calculateExpiration, restoreFootprint helper.
- managers/{credential,signer,submission}-manager.ts: vi.fn-testable DI managers.
- kit.ts: full facade rewrite wiring the managers with late-bound closures;
  connectWallet ownership verification (#601 F7) — a keyId MUST resolve to a live
  signer on the resolved wallet (closes #598 F3 at the SDK layer), plus opt-in
  wasm-hash check; typed events (walletCreated/walletConnected/...). Deleted every
  P27 probe shim, dead commented experiment, and the legacy factory-address
  fallback.

pnpm build (+ verify-esm) and vitest (90 tests) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…admin/events (62 tests)

- test_webauthn: deterministic RFC6979 secp256r1 assertions shaped like real
  authenticator output; happy path, UP/UV flags, truncated authData, wrong
  type, challenge mismatch (payload binding), oversized/malformed cdj,
  corrupted signature, key confusion, fixed challenge vector
- test_auth: limits branches (unlimited/empty/granted/co-signer present,
  missing, expired), expiration boundary (inclusive), signatures-map hygiene
  (unknown/expired extras fail deterministically), self-remove rule + F5
  foreign-contract non-interference, deploy-requires-unlimited, wallet-self
  admin grant, policy-as-signature vs policy-as-limit granularity (counting
  policy), cyclic policy limits fail closed via depth guard
- test_admin: constructor/add/update/remove/upgrade + full event assertions,
  durability-flip opposing-entry removal, get_signer raw view, policy
  install/uninstall lifecycle (rejecting install blocks add; panicking
  uninstall cannot block removal)
- test_base64_url: RFC 4648 vectors, url-safe alphabet, reference cross-check
- test_integration: full set_auths flows for ed25519+policy AND the
  previously-untested secp256r1 passkey path (F1); budget regression print
- build.rs builds wasm fixtures (sample-policy + smart-wallet) into OUT_DIR
  and copies to gitignored fixtures/ — cargo test self-sufficient on clean
  checkout; wasm contract builds skip the script entirely

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Builds all three contracts from ff7bd22, uploads to testnet
(passkey-kit-deployer, friendbot-funded), verifies fetch-back hashes, and
live-smokes a deployed wallet instance (constructor + get_signer).

Canonical smart-wallet wasm hash:
9e7fad441d6560b31eafbf3b627dbc196cf19df4dcdb91e0aededaf6590d6fbe

Manifest records toolchain/build provenance, per-component hashes + upload
txs, the NORMATIVE derivation tuple (salt=sha256(keyId), canonical kalepail
deployer, F7 connect-verification requirement), and the legacy e45c42b9 hash.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
todo 950. Relayer client + true client/server separation:
- relayer.ts: RelayerClient wraps ChannelsClient ^0.20 — submitSorobanTransaction
  ({func,auth}) preferred + submitTransaction({xdr}) for envelopes + getTransaction
  polling; skipWait/fundRelayerId passthrough; PluginClientError family mapped to
  typed RelayerError (or decoded ContractError); typed never-throw TransactionResult.
- server.ts: PasskeyServer rewritten server-only — validated config at construction,
  drops PasskeyBase for plain NetworkContext (rpc + passphrase), RelayerClient
  integration (smart func/auth vs xdr routing via hasSourceAccountAuth), hardened
  Mercury methods (AbortController timeout, typed IndexerError instead of raw JSON
  throws). Superseded by the SignerIndexer abstraction in B5.
- Server-only entrypoint: PasskeyServer + RelayerClient removed from the main
  `passkey-kit` export; reachable ONLY via `passkey-kit/server` so the relayer
  secret can never be bundled into browser code. verify-esm asserts the split.
- sac decision: KEEP the generated sac-sdk client (demo needs full SEP-41 reads:
  balance, metadata, transfer) rather than collapsing to a helper; SACClient made
  standalone (base.ts deleted). Added buildTokenTransferHostFunction for the
  low-level relayer {func,auth} path. (sac-sdk regen from canonical source = B4.)
- constants: DEFAULT_RELAYER_TIMEOUT_MS (6min testnet-retry window),
  CHANNELS_MAINNET/TESTNET_URL, DEFAULT_INDEXER_TIMEOUT_MS; TransactionSuccess
  gains optional transactionId (skipWait polling).

pnpm build (+ verify-esm) and vitest (96 tests) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
todo 952. One SignerIndexer interface, two interchangeable backends (src/indexer/**):
- types.ts: SignerIndexer { getSigners, findWallets, health }; WalletSigner
  { key, publicKey?, expiration?, limits, storage, status: live|expired|evicted|
  removed }; IndexerHealth; FindWalletsHardeningDeps.
- codec.ts: shared SignerKey/SignerVal codec — signerKeyToContractScVal,
  signerKeyToIndexerJson (predicate), scValToSignerKey, decodeSignerVal (via
  bindings Spec, memoized), deriveStatus (inclusive timestamp expiration).
- mercury.ts: MercuryIndexer — get_signers_by_address / get_addresses_by_signer
  via /zephyr/execute; temporary-eviction RPC probe (#598 F6); findWallets
  derive-then-confirm hardening (#598 F3) reusing kit/wallet-ops.getSigner.
- stellar-indexer.ts: StellarIndexerBackend over POST /v1/contract-data/ —
  contract-scoped enumeration + client-side SignerVal decode; tombstone grouping
  (removed = tombstoned with no live counterpart in either durability);
  findWallets = derive candidate (Secp256r1) then confirm (<=25/query).
- index.ts: indexerForConfig (null-tolerant seam) + lookupWithRetry (20x2s poll).
- Exports: browser-safe types + lookupWithRetry from `passkey-kit`; token-holding
  backends from `passkey-kit/server`.

WIRE NOTE (flagged in-code): exact Mercury row/arg shapes (C1) and Stellar
Indexer request/response encoding (hex vs base64 bytes, deleted_at) are confirmed
live in F2. Unit tests use mocked fixtures; cross-backend equality is an F2 e2e.

pnpm build (+ verify-esm) and vitest (111 tests) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
todo 951 (ungated). Regenerated from the canonical v1 smart-wallet WASM
(9e7fad44…d6fbe, docs/deployments-testnet-2026-07-11.md; Stellar CLI 27.0.0):
- packages/passkey-kit-sdk/src/index.ts regenerated (post-gen: copy generated
  spec + README, preserve B1 packaging). New surface: error codes 100-129
  (renumbered, disjoint from legacy 1-9); SignerExpiration = Option<u64>
  (UNIX-timestamp, was ledger-seq u32); upgrade (renamed from
  update_contract_code); get_signer view (-> Option<SignerVal>); add/update/
  remove/upgrade now return Result<void>; #[contractevent] structs in the spec
  (Upgraded/SignerAdded/SignerRemoved/SignerUpdated).
- SDK integration: kit/wallet-ops WalletTx = AssembledTransaction<Result<void>>;
  toSignerExpiration encodes Option<u64> (bigint); new upgrade builder threaded
  through SignerManager + PasskeyKit facade; getSigner ledger-read unchanged
  (get_signer view now exists but the RPC read is equivalent + wallet-auth-free).
- contract-errors: registry resynced to v1 codes 100-125 (family SmartWallet) +
  kept legacy 1-9 (family SmartWalletLegacy) so errors from pre-1.0 deployed
  wallets still decode. bindings-sync test asserts the registry matches the
  regenerated Errors map.
- scripts/bindings/{build.sh,verify.sh}: build regenerates from the canonical
  hash into the package; verify (drift guard) fetches by hash, regenerates to
  mktemp (trap EXIT cleanup), and diffs the ContractSpec base64 array vs the
  committed bindings. Wired verify:bindings into prepublishOnly (network gate);
  verify:esm stays in build (offline).
- sac-sdk: KEPT as-is (B3 decision) — standard network-independent SEP-41 token
  interface, packaging already fixed in B1; no regen/drift-guard warranted.

pnpm build (+ verify-esm) green; verify:bindings green (committed bindings match
canonical WASM); vitest 112 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…coverage

Ground-up rebuild of demo/ on Svelte 5 + Vite 6 against the frozen B1-B3 SDK
client API. Kills the 427-line App.svelte and the secrets-in-bundle pattern.

- Zero secrets: purge the committed Stellar seed + relayer key / Mercury JWT
  VITE_ inlining. Submission routes through a relayer-proxy worker
  ({func,auth}|{xdr}); discovery through an indexer proxy with a
  Mercury <-> Stellar-Indexer toggle. passkey-kit/server is never imported; the
  bundle carries no relayer/Channels client (verified via grep).
- Storage via passkey-kit/storage LocalStorageAdapter (not hand-rolled localStorage).
- Full feature coverage: passkey create / reconnect, add / update / remove for all
  three signer kinds, Persistent + Temporary, SignerLimits builder
  (unlimited / none / restricted), admin rotation + active-signer selection,
  multisig + per-signer transfers, non-native SAC token picker, typed-error
  surfacing (PasskeyKitError / ContractError with decoded contract codes).
- Componentized (Header / Wallet / Transfer / Signers / Discovery / ActivityLog +
  reusable LimitsBuilder / SignerRow); Svelte 5 runes store + an actions layer so
  components stay thin.
- Drop vite-plugin-filter-replace + the VITE_mercuryKey dead type; new public-only
  .env.example (rpc, passphrase, wasm hash pinned from the deployments manifest,
  native SAC, proxy URLs); fix index.html title + favicon.
- data-testid selectors throughout for the D2 e2e harness.

pnpm build:demo green (svelte-check 0 errors / 0 warnings + vite build).
Refs #602, #597 s1.7, #600 s6; browser<->worker wire shape coordinated on todo 954.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rdening)

Addresses the 3-way audit gate (todo 958). Core auth/WebAuthn/storage/
upgrade/derivation were cleared; findings concentrated in the policy
lifecycle. 64 tests green.

- FIX-1 [HIGH]: remove untrusted uninstall from remove_signer critical path.
  Removal is now pure wallet state + SignerRemoved event — no policy code
  runs. try_uninstall could not catch non-recoverable Budget/Storage
  ExceededLimit, so a budget-exhausting policy could unwind the atomic tx and
  roll back its own removal. Policies self-clean via the now-permissionless
  uninstall entrypoint, which verifies get_signer(Policy(self))==None first.
  New test malicious_uninstall_cannot_block_removal proves removal survives a
  budget-exhausting policy.
- FIX-2 [MED]: policy__ caller authentication. Interface documents the
  public-callability contract (impls MUST be read-only OR source.require_auth).
  Confirmed require_auth works inside __check_auth via invoker auth
  (ed25519_with_policy_limits_full_stack); sample-policy uses it and
  sample_policy_rejects_spoofed_caller proves external callers are blocked.
- FIX-3 [MED]: sample-policy is now deny-by-default — fails closed on arg
  shape/type, gates only transfer within cap, rejects the wallet's own admin
  surface and all non-contract contexts.
- FIX-4 [MED]: sample-policy renews its instance/code TTL and the
  Installed(wallet) key on install and every successful policy__.
- FIX-5 [MED]: required co-signers are scope-independent approvers. Dropped
  the recursive enforcement of stored policy-limit-key limits (and the depth
  guard — no recursion, no cycle). Symmetric across key kinds; Some(empty)
  disables INDEPENDENT coverage only. Documented in types.rs; tests
  nonpolicy/policy_co_signer_own_limits_not_enforced.
- FIX-6 [LOW]: storage.rs uses saturating_sub for max_ttl - WEEK_OF_LEDGERS.
- FIX-7 [LOW]: policy__ documented side-effect-free (or require_auth); UP-only
  default and UV rationale documented on UserPresenceRequired.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ban-sdk)

Full rewrite of the Mercury/Zephyr indexer against the v1 smart wallet and
the current (P27) zephyr-sdk. Kills audit findings F1/F3/F4/F5/F7.

- F1 (build): drop the smart-wallet-interface dependency (soroban-sdk 27) that
  created a dual-soroban-sdk tree; decode events via local #[contracttype]
  mirror types on a SINGLE soroban-sdk, pinned =22.0.7 to match zephyr-sdk and
  to keep its wasm panic-handler gated off (22.0.8+ duplicates std's
  panic_impl and fails to build). zephyr-sdk pinned to stellar-main-2 rev
  3b83284; rustc pinned 1.93.0. Builds green for wasm32-unknown-unknown, no
  warnings.
- New event schema: consumes signer_added/signer_updated/signer_removed/
  upgraded #[contractevent] structs (map data_format) instead of sw_v1 tuples.
- F3 (security): WASM-hash allowlist on ingestion. Only wallets whose on-chain
  instance executable is a known passkey-kit hash (from
  docs/deployments-testnet-2026-07-11.md) are trusted; a `wallets` table holds
  the trusted set and latest hash, also refreshed from `upgraded` events.
- F4/F5 (expiration): UNIX-timestamp inclusive semantics; both read functions
  return signers with an `expired` flag instead of filtering inconsistently.
- F7: native `bool` active column; explicit val nullability (Void vs Bytes(65));
  documented base64-XDR limits encoding.
- Drops the unauthenticated debug_signers dump.
- README documents the toolchain/F1 story, schemas, function contracts, and the
  Mercury deploy-tooling status: published mercury-cli 0.2.1 targets removed
  endpoints (404); current backend moved under /rest (audit F2). Testnet deploy
  is pending correct tooling and is an orchestrator-gated step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Port the smart-account-kit 4-layer e2e harness to passkey-kit, under scripts/e2e/,
driving the real Svelte demo with data-testid selectors (no React native-setter
shims). Wired as pnpm scripts in demo/package.json (root package.json is off-limits).

Layers:
- agent-browser-webauthn-helper.mjs: attaches a CDP virtual authenticator
  (ctap2 / internal / residentKey / userVerified / automaticPresenceSimulation)
  to a live agent-browser session and tears it down in a finally. Generic port.
- webauthn-browser-probe.sh: isolated create -> get -> crypto.subtle.verify P-256
  sanity, asserts verified:true. Generic port.
- testnet-passkey-smoke.sh: orchestration. Idempotent Vite start (lsof guard),
  EXIT trap that kills only the server it started and closes only its own
  agent-browser session (no orphaned Chrome-for-Testing). MODE=probe|audit.
- browser-full-e2e-audit.sh: the passkey-kit flow — create -> reconnect
  (discoverable passkey, ownership verified) -> fund -> transfer -> add secp256r1
  / Ed25519 / policy signers -> update -> per-signer transfer -> remove ->
  discovery via BOTH indexer backends (lookupWithRetry poll) -> reverse lookup.
  Asserts REAL outputs (C... contract id, 64-hex tx hashes), an explicit failure
  pattern on every wait, and a DISTINCT exit code per layer (10/20/21/30/31/
  40..45/50/51/52). Auto-gates: skips on-chain layers when the demo header shows
  "no relayer proxy" and discovery when "no indexer proxy" (explicit log, never a
  silent pass) so it is meaningful before C2/B5 land; full live run is F2.

Demo selector hooks (D2 scope: "demo selectors as needed"): scoped log-box
testid + full data-hash on tx links; signer-update / signer-update-save /
signer-set-active / use-fund-addr testids.

Static-validated: node --check (mjs) + bash -n (all sh) clean; pnpm build:demo
green (svelte-check 0/0 + vite build). Live browser execution is F2 (orchestrator-
gated) per the master plan; the harness is isolation-safe (unique session, lsof
guard, session-scoped close) for that run. Refs #600 s6/s14, TOPROMPT verify rules.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e-policy

sol re-verify (comment 2484) confirmed FIX-3's fail-open/admin gaps closed but
kept it STILL-OPEN: Signature::Policy is secretless, so a per-transfer cap is
trivially bypassed by repeating capped transfers to drain the wallet, and
policy__ can't see co-signers — the only in-policy close is a cumulative budget.

- sample-policy upgraded from a 10M per-transfer cap to a genuine cumulative
  rolling-window spending allowance: tracks Spend(wallet)={window_start, spent}
  in policy storage, resets after WINDOW_SECONDS (24h tumbling window), sums
  transfer amounts across the invocation's contexts and rejects once the
  window total would exceed WINDOW_ALLOWANCE (100M). checked_add on both the
  per-call total and the cumulative to fail closed on overflow.
- Spend(wallet) TTL renewed alongside instance + Installed(wallet) so the
  counter can't archive-grief; uninstall also clears Spend.
- Loud docs: interface policy__ gains a 'Value transfers with a SECRETLESS
  policy' section — value-moving policies are only safe as a cumulative/rate-
  limited allowance and/or paired via SignerLimits with an authenticated
  co-signer, never an unbounded per-transfer cap. Sample module doc rewritten.
- New test sample_policy_cumulative_allowance: repeated sub-cap transfers are
  rejected once cumulative hits the cap, and the window reset restores spend.

smart-wallet wasm unchanged (84924c53…); FIX-3b is sample-policy behavior +
interface docs only. sample-policy wasm now e74af535….

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…emo Pages config

New top-level relayer-proxy/ Cloudflare Worker, adapted from the smart-account-kit
reference (audit #598/#600 §ADAPT). Fixes passkey-kit's secrets-in-bundle defect.

- Keyless per-IP Relayer API-key minting: GET {RELAYER_BASE_URL}/gen, cached in
  KV under api-key:<ip> (CF-Connecting-IP -> X-Forwarded-For -> X-Real-IP), so
  the browser submits with ZERO secrets.
- POST / accepts {func,auth} -> submitSorobanTransaction OR {xdr} ->
  submitTransaction (plugin ^0.20); exactly one required.
- Error mapping: PluginExecutionError -> 400 (code/details), PluginTransportError
  -> its status, malformed JSON -> 400, else -> 500. GET /status, GET / health.
- Testnet Friendbot retry loop for missing channel accounts (5 min); mainnet
  never retries. [env.production] mainnet block. Real wrangler.toml (placeholder
  KV ids to provision at deploy). NO deploys from this todo.
- vitest suite: 20 tests, all green (IP extraction, mode validation, both submit
  paths, 3 error mappings, testnet/mainnet retry, per-IP key lifecycle incl.
  legacy-format migration). tsc typecheck clean.

Demo Pages deploy gap (audit #597: "broken-as-scripted, no wrangler.toml"):
- Root wrangler.toml with name=passkey-kit-demo + pages_build_output_dir=demo/dist
  so the existing root `deploy:demo` (wrangler pages deploy, run from repo root)
  resolves project + assets with no flags. deploy:demo:prod's `--env production`
  is invalid for `pages deploy` (no such flag; Pages uses --branch) — that's a
  root package.json fix owned by the demo agent; documented in-config + on todo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audit gate cleared (all 7 findings confirmed-closed: terra FIX-1/4, sol
FIX-2/3/5/6/7). Re-pins the manifest to the final post-remediation build
(commit range 067f807..0563520).

Final canonical hashes (supersede interim 9e7fad44…/e6d00383…):
- smart-wallet:     84924c53a413318df2ce753e30de53ec651404c916d30e861718ad155c94b319 (29,275 B)
- sample-policy:    e74af5355f933f2c3421845178ca789c9bcf3ea7612a7c9966b9b57f26e59aed (13,064 B)
- example-contract: 47a3326360bdce2ca360ed1b226ad636e14ac698f63efc11ab28e0d41f1f76aa (unchanged)

Rebuilt clean from 0563520 (Rust 1.94.0 / soroban-sdk 27.0.0 / CLI 27.0.0),
uploaded to testnet (smart-wallet tx ceb4ed26…, sample-policy tx 2f36077b…),
all three fetched back by hash byte-identical, and live-smoked a fresh wallet
instance (CCK7VZBY…, deploy tx 4a9ff09b…, get_signer verified). Legacy hash +
normative derivation-tuple sections unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The contracts audit gate + FIX-1..7/FIX-3b re-pinned the canonical smart-wallet
WASM hash (docs/deployments-testnet-2026-07-11.md now FINAL post-audit):
9e7fad44… (interim pre-audit) -> 84924c53a413318df2ce753e30de53ec651404c916d30e861718ad155c94b319.
Update the F3 ingestion allowlist + README to consume the manifest's final hash.
Legacy hash e45c42b9… unchanged. Builds green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Re-pin canonical smart-wallet WASM
9e7fad44… -> 84924c53a413318df2ce753e30de53ec651404c916d30e861718ad155c94b319
(A4 audit-gate remediation; manifest docs/deployments-testnet-2026-07-11.md,
ff06059) in scripts/bindings/{build,verify}.sh.

Regenerated passkey-kit-sdk bindings from the new WASM via the pinned
build.sh (stellar contract fetch --wasm-hash -> bindings typescript, CLI
27.0.0). The wallet interface is semantically UNCHANGED: every error code
(100-125), error name, function signature, and type struct is byte-identical.
The only spec delta is two embedded Rust doc-comment strings the audit
reworded -- UserPresenceRequired (FIX-7) and SignerLimits (FIX-5) -- which
live inside the ScSpec and so shift the base64 spec literals.

- src/contract-errors.ts: no resync needed (codes/names identical);
  contract-errors.test.ts bindings-sync guard green.
- build.sh/verify.sh: bindings output dir gen -> pks-gen so the generated
  README's package name stays stable across regens (name is spec-independent;
  verify compares only the base64 spec, so the guard is unaffected).
- verify:bindings passes against 84924c53…; pnpm build + npx vitest run
  (132 tests, 17 files) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes stale-hash drift in demo/.env.example (was pinned to the pre-audit
9e7fad44… despite the re-pin comment). Now matches the canonical
smart-wallet hash 84924c53… from docs/deployments-testnet-2026-07-11.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…to-fund

Code-review gate (todo 966, comment 2496) findings in demo/** scope.

H1 [HIGH] — demo submission happy-path was fully broken: the relayer-proxy worker
returns { success, data: { transactionId, hash, status } } but the client read
payload.hash/ledger/transactionId at the TOP level, so payload.hash was always
undefined → the success guard fired on EVERY 200 → every submission (deploy /
transfer / add-update-remove-signer / fund) reported failure. Fix: unwrap the
`data` envelope (payload?.data), treat an explicit success:false as failure, and
drop `ledger` (the worker has none). Error path: the worker returns `error` as a
STRING + optional data:{code,details}; toError now reads the string message +
data.code (numeric → contractErrorFromCode) and scans message/raw for
Error(Contract,#N) so contract-error codes are no longer dropped.

M3 [MED] — register() called fundWallet(100) nested inside run("Create wallet");
fundWallet wraps its body in run(), which early-returns when app.busy is set, so
the post-deploy auto-fund silently no-oped. Factored an unwrapped fundWalletInner()
(fundWallet is now a thin run() wrapper) and register calls it directly, guarded so
a fund failure never fails wallet creation.

LOW — fundWalletInner uses BigInt(Math.round(amountXlm * 10_000_000)) so a
fractional fund input (<input type=number> allows 0.5) no longer throws RangeError.
format.ts toStroops/fromStroops now use exact decimal↔BigInt math (no JS Number
rounding / precision loss); number inputs in scientific notation fall back safely.

pnpm build:demo green (svelte-check 0/0 + vite build). Worker unchanged (client
aligned to worker shape per the finding). Refs todo 966 / comment 2496.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
zephyr (M4 + LOW):
- M4: fold same-ledger signer events per (address,key) into one pending state,
  then flush with a single existence-read + single write. Host db writes commit
  only after on_close, so per-event read+write couldn't see this close's own
  writes — add+remove left a signer active forever, two updates duplicated rows.
  Now correct (mirrors the wallets `trusted` in-memory cache).
- LOW: read handlers parse caller-supplied address/key fallibly (validate strkey
  / match base64 Result) and conclude empty instead of panicking (panic=abort).

relayer-proxy (M5 + LOWs):
- M5: move per-IP key custody from KV (no CAS → N concurrent first-requests mint
  N keys) into a per-IP Durable Object (`ApiKeyStore`); blockConcurrencyWhile
  serializes get-or-create so at most ONE /gen key is minted per IP. Drops the
  KV binding; wrangler.toml gains the DO binding + migration (both envs).
- LOW: never log /gen body or candidate key material — log only status/length.
- LOW: getClientIP uses CF-Connecting-IP ONLY (unspoofable at the edge); dropped
  the client-controlled X-Forwarded-For/X-Real-IP fallback. CORS left open with
  an explicit endgame/mainnet decision note.
- Tests rewritten for the DO model (21 pass): worker DO integration + direct
  ApiKeyStore get-or-create/peek/mint-fail. tsc clean.

package.json (LOW): deploy:demo:prod uses `--branch production` (Pages selects
prod by branch, not `--env`).

Not fixed here (other owners): H1 (demo client envelope unwrap), H2/M1/M2 (SDK),
M3 + demo LOWs (demo). No Mercury redeploy (F2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes the SDK findings from the F1b /code-review gate (todo 966, comment 2496).
No contract wasm change; no hash re-pin.

H2 [HIGH] — the eviction probe queried the WRONG ledger key. mercury.ts and
server.ts probed getContractData(wallet, scvBytes(keyId), Temporary), but the
contract keys signer entries by the SignerKey ScVal vec (signer.rs
set::<SignerKey, SignerVal>), so every probe missed -> catch -> every live
temporary signer was reported "evicted". Now probe by
signerKeyToContractScVal(...) — the exact key getSigner uses. getContractData
also collapses not-found AND transport errors into one {code:404}, so eviction
could not be told from a flaky RPC: added src/rpc-data.ts, which reads via
getLedgerEntries (empty entries = genuine not-found; a throw = transport) and
concludes eviction ONLY on an authoritative not-found, never on 429/5xx/timeout.

M1 [MED] — codec limitsOf encoded a limits-nested Ed25519 key as HEX, diverging
from scValToSignerKey / types.ts (a G-address); the hex value threw in
Keypair.fromPublicKey on re-encode. Now StrKey.encodeEd25519PublicKey. Fixing
this surfaced an adjacent bug: spec.scValToNative decodes an ScMap to an ARRAY
of [key,value] pairs (not a JS Map), so limitsOf's `map.entries()` mis-keyed
every entry under a numeric index — limits decoding was broken outright.
Switched to direct `for..of map` (correct for both an array-of-pairs and a real
Map).

M2 [MED] — relayer toResult used a failure DENYLIST, so a non-terminal "pending"
status mapped to success:true and a poll loop would treat an unconfirmed (or
later-reverted) tx as confirmed. Now a terminal-success ALLOWLIST
(/confirm|success/i); a pending status is a distinct non-success result carrying
the new RELAYER_PENDING code + `pending:true` context. The binary
TransactionResult union is preserved deliberately (a structural third arm would
ripple into the out-of-scope demo consumers).

LOW:
- stellar-indexer extractEntries: throw IndexerError on an unrecognized non-empty
  envelope instead of degrading to [] (empty-vs-error conflation burned the retry
  budget then reported "no signers"). null / {} still map to [].
- wallet-ops getSigner: transport errors propagate instead of being swallowed to
  null, so a transient RPC failure during connectWallet's ownership check no
  longer throws a false WalletOwnershipError for a valid passkey; connectWallet
  clears the half-connected state and rethrows the truthful error.
- tx-ops: `expiration == null` instead of `!expiration` (an explicit 0 is honored).

Tests: +9 (mercury probe-key/evict/transport, relayer pending, codec limits
round-trip, stellar-indexer response shapes, getSigner transport-propagation).
pnpm build + npx vitest run = 141 tests / 17 files green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ning

F2 live testnet verification surfaced two issues:

- SDK product bug (src/managers/submission-manager.ts): signDeploy left the
  assembled deploy fee at inclusion+resource. Deploys go through the relayer's
  fee-bump ({xdr}) path, and a fee-bumped Soroban inner tx must have
  fee == resourceFee (the fee-bump supplies inclusion) — so every
  relayer-sponsored wallet deploy failed with RelayerError 7002 "Transaction
  fee must be equal to the resource fee". Pin the inner fee to the resource fee
  before the deployer signs (surgical envelope edit; TransactionBuilder.cloneFrom
  drops SorobanTransactionData → txMalformed). Verified live: testnet deploys
  now confirm on-chain (e.g. 3d168e24…, 069c23df…); 141 SDK/worker tests green.

- e2e harness (scripts/e2e/browser-full-e2e-audit.sh): action buttons are
  disabled while app.busy, and flows hold busy past their success log
  (createWallet through its trailing auto-fund; write layers through a trailing
  balance refresh), so the next layer clicked a still-disabled button (silent
  no-op → timeout). Add wait_enabled/wait_idle settle gates before the reconnect
  and signer-row clicks and inside expect_after.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Standard self-serve Zephyr flow is retired on Mercury's current backend:
POST /zephyr/execute 404s even with a valid JWT (retroshade/query SQL works with
the same token); /zephyr_upload + /zephyr_table_new gone; mercury-cli 0.2.1 dead;
the --base CLI is source-built/non-public. Live self-serve model is Retroshades
(/rest/retroshade/*). Mercury now hosts a Smart Account Indexer
(/rest/smart-account-indexer, no auth) but it indexes OZ smart-account-kit, NOT
passkey-kit (real wallet/signer → count:0). Documented the decision tree to get
passkey-kit indexed (ask Mercury to add wasm 84924c53… to the hosted indexer, or
self-host via the current CLI/dashboard) — all USER/Mercury-gated. Full probe
evidence in Solo todo 967.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nly; gate the dead Mercury endpoint

todo 967 (F2b indexer reconciliation). src/indexer/** only; no contract/hash change.

STELLAR INDEXER (src/indexer/stellar-indexer.ts) — the live POST /v1/contract-data
returns each entry's key/val as JSON ScVal (`{"vec":[{"symbol":..},{"bytes":..}]}`),
NOT the base64-XDR the backend decoded (F2, todo 959 c2502).
- Added jsonScValToXdr: JSON ScVal -> xdr.ScVal using the standard Stellar
  convention (symbol / bytes=hex / address=strkey / vec / map=[{key,val}] /
  u64,i64=string / u32,i32=number / bool / void), then reuse the contract-spec
  decoders (scValToSignerKey / decodeSignerVal). Unknown/malformed variants
  THROW — never a silent misdecode.
- StellarIndexerEntry updated to the live field set (key/val JSON ScVal;
  id/contract_id/last_modified_ledger_seq/tx_meta_version/timestamp/created_at/
  updated_at optional); entriesToSigners groups by the raw JSON key.
- Mainnet-only made honest (the indexer returns 0 entries on testnet): added
  STELLAR_INDEXER_NETWORK, a networkPassphrase config, a forNetwork() factory
  returning null off mainnet (discovery disabled, not "no signers"), and a
  constructor guard that throws on a non-mainnet passphrase — testnet no longer
  silently returns []. extractEntries still throws on unknown non-empty shapes.
- Live validation needs a mainnet v1 wallet (endgame-gated); unit-tested against
  the documented shape now.

MERCURY (src/indexer/mercury.ts) — the assumed POST /zephyr/execute route is
RETIRED (404 even with a valid JWT; F2b todo 959 c2502 + services c2504). The
replacement is a pending USER decision, NOT the SDK's to make:
  A. Mercury's hosted Smart Account Indexer (/rest/smart-account-indexer) — but
     it currently indexes OZ smart-account-kit events, not passkey-kit (real
     passkey wallets return count:0), so it needs Mercury to add passkey-kit's
     wasm coverage.
  B. Self-deployed zephyr program + Mercury's current (non-public) tooling.
So the client is NOT re-pointed at either surface. Instead the backend is GATED:
unless the new `zephyrExecuteConfirmed` opt-in is set, every query throws a clear
"pending (todo 967)" IndexerError instead of silently 404-ing, and health()
reports { ok: false }. Both options are documented in the module.

Green: pnpm build (bindings + tsc + verify-esm) + npx vitest run = 159 tests /
17 files (+18: JSON-ScVal decode, mainnet gate, Mercury pending-gate).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… releasing

Rewrite the docs suite against the FINAL verified v1 API (every table/sample
checked against src/ and the contract interface — no aspirational docs).

- README.md: rewritten. Config tables (Option|Type|Required|Description) for
  PasskeyKit/PasskeyServer; method tables inlining the wrapped contract fn;
  Quick Start before the API reference; client (PasskeyKit) vs server
  (PasskeyServer) split; dist/exports packaging + three entrypoints; typed
  Signer pipeline; typed errors + discriminated TransactionResult + renumbered
  contract error table (100-129); storage adapters; relayer-proxy usage;
  indexer backends with the pending-decision framing; caveats/footguns
  (min-signer brick, temp sole-signer eviction, deploy front-running, WebAuthn
  UP); deterministic derivation. Killed the passkey-factory-sdk ghost, the
  Protocol-21 framing, the make deploy/init + SMART_WALLET_FACTORY note, the
  1.3.x relayer link, and the raw-TS shipping note.
- CHANGELOG.md (new): SAK discipline (per-component versions, Compatibility
  callouts). Version is a placeholder (0.13.0 vs 1.0.0, decided at publish).
- docs/migration-v1.md (new): Before/After, Removed exports, Behavior-changes
  table, and an explicit A/B GAP ANALYSIS (incl. the deferred live Mercury
  discovery regression).
- docs/releasing.md (new): dependency-ordered publish (bindings -> SDK),
  verify:bindings gate, authed --dry-run, OTP step; folds the load-bearing
  cheatsheet commands.
- Cruft: fold + delete cheatsheet.txt; move PROPOSAL.md -> docs/history/ with a
  historical banner; regenerate .cursorrules against the final stack.

Docs only — no source/test changes; green baseline (tsc --noEmit) unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Minor bump 0.12.1 → 0.13.0 for the ground-up v1 overhaul (soroban-sdk 27
contract, reworked SDK, regenerated bindings, indexer abstraction, keyless
relayer-proxy). Canonical v1 contract WASM hash:
84924c53a413318df2ce753e30de53ec651404c916d30e861718ad155c94b319

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kalepail

Copy link
Copy Markdown
Owner Author

Superseded by direct promotion: rather than merge, overhaul/ground-up was promoted to be the new default branch main (the two P27-prep commits on the old next were fully superseded by this rework). The v0.13.0 code is now the mainline at main; tag v0.13.0. Closing this review PR as redundant.

@kalepail kalepail closed this Jul 12, 2026
@kalepail
kalepail deleted the overhaul/ground-up branch July 12, 2026 21:13
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant