feat(wallet-sdk): accounts slice (step 6)#1167
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hUser, user params) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ssword generator Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…chinery Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ken getter Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…h; add createUserApi Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nts namespaces Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ret config into it Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…codable refresh tokens A restore whose fetchUser resolved after a concurrent auth verb (or session end) could clobber the newer session with the stale result; a session generation counter now gates the apply. A present-but-undecodable refresh token now restores anonymous instead of establishing a session the expiry machinery can neither time out nor refresh. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dd a reload fallback The SDK arms its expiry timer during init(), so an expiry can fire before Wallet's event subscription mounts and emit to no subscribers; the hook now detects the already-dead SDK session on mount and runs the expiry handling. The handler also gets a hard window.location.reload() fallback so an unexpected failure mid-reset can't strand the user on a blank page. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each migration slice now settles its types in its own sdk/<namespace>.ts instead of growing one contract file (19 placeholders across steps 6-16 remain to land); cross-cutting pieces (SdkConfig, Sdk, Logger) stay in sdk/index.ts. Pure move — the exported surface is unchanged, and existing './sdk' import specifiers resolve to the directory index as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ick<Sdk> The compiler now checks the implemented subset (auth, user, events, init, dispose) against the Sdk contract; each slice adds its namespace to the Pick until it collapses to the full Sdk. Unimplemented namespaces stay absent rather than stubbed, so consuming them early remains a compile error instead of a runtime crash. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A host that wants no diagnostics now passes the exported no-op nullLogger instead of omitting the field, so silence is an explicit choice. Internal components take a required Logger too — forgetting to forward one is now a compile error rather than a silently diagnostics-less component. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…chinery A fetchUser continuation resolving after teardown (restore or verb) could re-arm the expiry timer on a disposed instance, resurrecting the zombie timer the HMR dispose hook exists to prevent. A disposed flag now blocks re-arming and a late-firing timer callback; verbs and the session snapshot are unaffected — disposal is not logout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Vite awaits a promise returned from hot.dispose, so the old SDK's teardown is guaranteed to complete before the replacement module constructs the new instance — which matters once dispose() gains async teardown work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nOut and the expiry handler Both ran the same order-sensitive sequence (flags reset, auth invalidation, navigate/revalidate, Sentry clear, queryClient.clear) with the ordering rationale documented on only one of them; useSessionEndCleanup now owns the sequence and its invariants, while each caller keeps its own edges (the sdk.auth.signOut() call and redirect target vs the toast and reload fallback). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fail-soft decode idiom (a corrupt STORED token must degrade to null, never throw) was duplicated in the SDK's auth service and the web's auth glue, and missing from shared/auth.ts's isLoggedIn — where a corrupt refresh token would throw through the DB client's token getter into every query; it now reads as logged out. Tokens freshly minted by a server keep decoding with jwtDecode directly so a malformed one fails loudly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ry leak Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drops the rejection sampling added in 7d3bc1c (review LOW finding), returning the generator to master's algorithm verbatim. The modulo skew is ~1 in 5.7e7 per character over a 32-character password — cryptographically negligible — and master parity is preferred for the slice. The redraw test goes with it; the length sanity test stays. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> revert(wallet-sdk): unfence the expiry handler, park its races as a known issue Reverts the generation fencing of handleSessionExpiry from 1e6a627 (review finding 1). The per-await fence pattern is too subtle to maintain — the plan is to fix the race class structurally by serializing all session transitions through a single command lane (single-writer) instead. The handler returns to the shape inherited from master's expiry hook, with a KNOWN ISSUE note: a sign-out landing while the guest re-sign-in is in flight is silently undone, and an in-flight handler survives teardown() (HMR). The two race regression tests stay as it.todo — they are the spec the command-lane refactor must satisfy. Kept from the reverted commit: the signInGuestAccount extraction (the guest-persist undo lives there) and the test fake's real-SDK sign-out fidelity (clears token keys), which later tests rely on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> fix(wallet-sdk): tighten three lib-level invariants from the review - generateRandomPassword now rejection-samples instead of a bare modulo, so every charset character is exactly equally likely — this string is the sole credential of funded guest accounts. - WalletEventEmitter.emit dispatches over a snapshot, so a handler that (un)subscribes mid-emit can't alter the current dispatch. - AgicashSdk.create() now throws while an undisposed instance exists, making the one-instance-per-process constraint (module-global Open Secret state) self-enforcing; dispose() releases the slot, so the HMR dispose-then-recreate cycle still works. LOW findings of the cross-model review on PR #1166; each regression test fails against the previous code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> fix(web-wallet): make the session-end web cleanup unconditional and close two expiry-path gaps Three findings from the PR #1166 cross-model review: - A throwing sdk.auth.signOut() skipped the whole web cleanup while the SDK had already ended the local session (its own finally), leaving query caches, the Sentry identity, and the hint cookie serving a dead session — with the sign-out button wedged in its loading state. The cleanup and the loading reset now run in finally. - A guest auto-extension during init() fires auth.session-refreshed before the events hook subscribes, so the auth query and session-hint cookie stayed pinned to the old expiry. The hook now re-syncs on mount when the stored refresh-token expiry moved past what the query captured. - The expiry hard-fallback reload had no guard; it is now throttled through a sessionStorage timestamp so a persistently failing cleanup can't reload-loop the tab. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> fix(wallet-sdk): read the session once per setDefaultAccount verb The verb derived the user from the live session twice — once inside the account-ownership read and again, after that await, for the row update. A session switch between the two wrote the previous user's account id onto the next user's row (the schema has no same-user constraint on the default account FKs, so the broken pointer persists; RLS still hides the other user's data). The session is now captured once at verb entry and threaded through, so the validating user and the written user cannot diverge. Finding 3 of the cross-model review on PR #1166. The regression test fails against the previous code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> fix(wallet-sdk): stop the public receive projections leaking proofs and collapsing variants The receive domain entities are intersections over variant unions, and the contract projected them with a bare `Omit`. TypeScript collapses each union to its shared keys under `keyof`, so the projections simultaneously leaked the swap's top-level `tokenProofs` (spendable Cashu proof material, also via the receive events' payload types) and silently dropped every variant-only field (`tokenReceiveData`, `failureReason`, keyset fields) along with discriminant narrowing. The domain schemas now name their variant unions once (feeding both the schema and an exported variant type), and the projections omit base keys only, re-apply the variant unions, and strip proof material at both levels — top-level `tokenProofs` and the melt data's nested `tokenProofs`. The implementing slices (steps 9/11/12) must strip the same fields at runtime. Inherited from #1164 (the projection sweep missed the receive entities); finding 2 of the cross-model review on PR #1166. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> fix(wallet-sdk): harden the restore memo and guest sign-up edge paths Three LOW findings from the PR #1166 cross-model review: - A slow-failing restore cleared the restore memo unconditionally and its endSession bumped the generation, fencing out a newer restore's apply — an anonymous boot despite valid tokens. The rejection now un-memoizes only its own memo, and the failure path leaves the session alone when another transition owns it. - A restore after a same-lifetime sign-out/sign-in repeated the verb's user fetch; doRestore now returns early when a verb already established the session. - A guest sign-up whose credential persist failed left a live session whose account would strand (with any funds) at its first expiry. The sign-up is now undone and the verb rejects, so the retry lands on a recoverable account. All three regression tests fail against the previous code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> fix(wallet-sdk): fence the expiry handler against concurrent session transitions A fired expiry timer can't be cancelled, so its handler raced every other session transition. Worst case: a sign-out landing while the guest auto-extension's re-sign-in was in flight got silently undone — the re-sign-in wrote fresh tokens over the sign-out's clear and the unfenced snapshot apply flipped the session back to logged-in (money access after a believed sign-out on a shared device). An in-flight handler also survived teardown(), letting a disposed instance clear tokens its successor had just restored. The handler now captures the session generation at entry and re-checks it plus the disposed flag after every await: a raced re-sign-in's tokens are cleared again instead of resurrecting the session, the extension's apply is generation-fenced, and the death path (sign-out, end, expired event) only runs while the handler still owns the session it started from. Finding 1 (HIGH) of the cross-model review on PR #1166. Both regression tests fail against the previous code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
- useHandleSessionEvents now takes an options object ({ onSessionExpired })
so the call site names the event the toast fires on (Petar agreed).
- Delete the expiry hard-fallback reload, its 30s lastReloadAt guard, and
the sessionStorage key: the SDK has already ended the session by the time
cleanup can throw, so nothing sensitive survives; the catch now just logs
to console + Sentry. This drops the risk of a deterministic cleanup
failure reload-looping the tab against the server.
- Keep reading Open Secret's token keys from local storage (the web app owns
the storage it hands the SDK); drop the "temporary leak / step 18" framing.
- Explain why entry.client's sdk.client side-effect import must precede the
body (SDK construction configures Open Secret, which loadFeatureFlags
needs for a returning user's token) and that the arrangement is temporary.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#1166) Cover the charset invariants: throws when no set is selected, and the output alphabet is restricted to the enabled sets (letters-only, digits-only, special-only, and the letters+digits mix). Add a note on lib/password.ts explaining why the Web Crypto global is used directly — there is no isomorphic import (node:crypto is Node-only and breaks browser bundling). Threads: [15] [17] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…1166) `background` did not convey what the namespace does. Rename to `task-processor` across the contract: the `taskProcessor` namespace, the `TaskProcessorApi` / `TaskProcessorState` types, the `task-processor.state-changed` event, the `task-processor.ts` file, and the related doc comments. This matches the term already used in the web app (the TaskProcessor component). Threads: [8] [13] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#1166) The receive and send namespaces re-export the domain entity types directly instead of projecting them. Consumers of this SDK only read these shapes, so exposing the domain fields (proofs, userId) is acceptable for now and keeps a single source of truth; narrowing can come later. This removes the projection type aliases in sdk/receive.ts and sdk/send.ts and the variant-union type exports in the receive domain files that existed only to support them. Threads: [11] [12] [14] (accounts [0] flagged separately) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…er note (#1166) - supabase-session: `generateToken` is now required and injected from the composition root, so wallet-sdk/db no longer imports @agicash/opensecret. agicash-sdk passes openSecret.generateThirdPartyToken. - Define the Supabase url + anon key in sdk.client.ts and import them into database.client.ts, which is the file being retired. - Add a note on the console logger explaining why the meta === undefined check is shaped that way (it avoids logging a trailing "undefined"). Threads: [3] [24] [26] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Update-only consumers (UserService, user-api) do not need AccountRepository, so the write repository is split: UpdateUserRepository takes only `db`, and UpsertUserRepository takes `db` + `accountRepository` in its constructor rather than as a per-call parameter — consistent with how other services receive their dependencies. The web _protected upsert glue constructs UpsertUserRepository with the account repository. Threads: [22] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rd disposal (#1166) auth-service: - The session is scoped by an AbortController (`sessionScope`): every session transition aborts the current scope and installs a fresh one, and an in-flight restore or apply bails when its scope is aborted. This fences a stale restore out when a sign-out or a newer login wins the race, and clears a raced guest re-sign-in's fresh tokens when a sign-out wins. - Invoking an action after teardown throws DisposedError (new error type). - The expiry timer helpers are named setExpiryTimer / clearExpiryTimer, and the discarded-promise call site no longer needs the `void` operator. - Comments read in plain language: an auth action (sign-in, sign-up, …), with each explaining its own reason directly. - events: auth.session-refreshed and the event-naming grammar read plainly. - Tests cover the abort paths (a sign-out fences an in-flight restore) and that actions throw once the instance is disposed. Threads: [6] [27] [29] [30] [31] [32] [34] [36] [37] [38] [39] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
From the self-review pass (Opus adversarial + codex-review) on the r3 diff: - teardown's doc states that actions throw DisposedError after disposal and that stored tokens + the session snapshot are left intact for a successor instance. - receive.ts / send.ts note that the public types are the domain entities for now (the apps only read them; proofs/userId ride along until a later slice narrows the surface, #1164). - an auth-service test comment describes the fencing as aborting the session scope, matching the code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…actory to lib (#1166) - setDefaultAccount takes `userId: string` (the SDK path has only the session user id; a full domain User would reintroduce the account read step 6 removed) and the full domain `account`. - The guest account storage factory lives in lib/ (a runtime adapter, not domain wiring) and stays exported so its unit tests and the auth-service tests keep using it. AuthService builds its own guest storage from the storage + logger it already receives, so the composition root no longer wires it in. Threads: [21] [2] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
On Why it currently is the way it is. A cashu account's balance is derived state — What changes under the expose-full-domain-types direction (the direction #1166 has now taken for the receive/send equivalents): the projection rationale collapses, and accounts can align cleanly — declare The call is yours + petar's, not mine — it reopens B1, which petar ruled explicitly for this slice. Two coherent end-states:
Sized and ready to execute either way on your word. |
…ltime + init doc (#1166) - applySessionFromServer wipes the previous user's per-session caches when a different user's session starts over a live one (a login without a sign-out in between), keyed off the live session. Each memo cleared by onSessionEnded already fences its own in-flight writes, so a direct cross-user login is the only case that needs this check; agicash-sdk documents that contract at the onSessionEnded wiring. Tests cover the cross-user wipe, no double-wipe after sign-out, and same-user warmth. - events: the connection.changed doc says it is the Supabase realtime data channel (the per-user row-change subscription), not the auth/network connection. - web: the sdk.init() failure log reads "Failed to initialize sdk", tracking the method name as init grows. Threads: [33] [7] [44] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… mid-extension sign-out (#1166) Two session-scope gaps from the cross-model review: - teardown() aborts the session scope and applySessionFromServer skips its writes when disposed, so an in-flight restore that resolves after dispose no longer writes a session or runs onSessionEnded — onSessionEnded clears process-wide caches (spark wallets, mint CAT) that a hot-reload successor instance may already own. - The guest auto-extension threads its session scope through the post-re-sign-in apply. applySessionFromServer reports whether it applied, so a sign-out that wins while that apply is in flight skips it (no resurrection of the ended session) and the handler bails instead of running its death path over the transition that already owns the state. Tests: dispose-mid-restore is fenced (no session write, no onSessionEnded); sign-out during the post-extension apply does not resurrect the session. Threads: round-3c H1, H3 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dk/ into domain/ (#1166) - AgicashSdk implements the full Sdk; the eight not-yet-migrated namespaces are getters that throw NotImplementedError (auth/user/events + init/dispose stay live) - move sdk/ to domain/sdk/ and rename agicash-sdk.ts to domain/sdk/sdk.ts (and its test), with imports updated - generateRandomPassword is synchronous (its body has no awaits), fixing the missing-await at the call site and the redundant awaits in its test - drop web-app references from the events and config contract docs Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ontract (#1166) Drop the projected CashuAccount/SparkAccount (the omit + computed balance) and re-export the domain Account/CashuAccount/SparkAccount directly, matching the receive/send contracts (#1164). Cashu accounts carry no balance field — consumers sum the now-exposed proofs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move packages/wallet-sdk/supabase to packages/wallet-sdk/db/supabase and retarget every reference: the Supabase CLI --workdir becomes packages/wallet-sdk/db, and the generated-types output, biome ignore, both tsconfig path-alias targets, the CI migration/types paths, and the live docs (README + supabase skill) follow. The `supabase/database.types` import alias is unchanged. The self-signed TLS cert paths gain one `../` (ci.yml + web-wallet-e2e/.env.test) because Supabase resolves config.toml paths relative to the now-deeper project folder; the web app is unaffected (it loads its cert via a hardcoded path). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…drop receive schema churn (#1166) - move WalletEventEmitter from lib/events.ts into domain/sdk/events.ts, next to the WalletEventMap/WalletEvents types it implements, and move its test with it - revert the schema-extraction refactor in the cashu-receive-quote, cashu-receive-swap, and spark-receive-quote domain files (unnecessary here; back to master) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Rebased onto the domain-types contract (d5d2f18 landing in #1166: domain What changed vs the previous head:
Verified at the keeper gate on the rebuilt head: fix:all clean, all packages typecheck, tests exit 0 (wallet-sdk 105, web 36); repo-wide grep confirms zero projection references remain. Independent delta review running. |
b5fb64f to
4bef09b
Compare
Add createSessionKeys: per-session memoized getters for the encryption keypair, cashu seed, spark mnemonic, cashu locking xpub, and spark identity public key, derived from Open Secret. Each memo is generation-fenced so a derivation started before reset() cannot repopulate the cache for the next session, and rejections are not cached so a retry can recover. Wire keys.reset() into the AgicashSdk onSessionEnded teardown alongside the existing session-token, spark-wallet, and mint-auth-token clears, so a signed-in user's key material never survives into the next login. The accounts and user namespaces consume these getters in later commits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implement createAccountsApi: get(id), list() (session-gated for the userId), and cashu.add(params) over an internal AccountRepository built from the db, session keys, and spark config. The namespace returns the domain account types directly per the accounts contract (#1166) — no projection mapping. cashu.add re-injects type:'cashu' and the session userId before the service call, and list()/add gate on the session while get() relies on RLS (repository posture). getRepository is exposed alongside the api so the /temporary bridge and the user namespace's ensure() build the repository through one path. Pin AddCashuAccountParams to { name, mintUrl, currency, purpose } and wire the accounts field into AgicashSdk, replacing the not-implemented getter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Expose getInternalAccountRepository through '@agicash/wallet-sdk/temporary': a module-scoped accessor that hands unmigrated receive/send flows and realtime row mapping the live instance's internal domain accounts repository, built through the same path as sdk.accounts.*. It throws 'No live AgicashSdk instance' before create and after dispose (the module reference is cleared on dispose alongside liveInstance), so the domain repository never leaks onto the public AgicashSdk surface. Removed at step 18 when those flows read from the SDK. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The SDK's user.ensure() needs the retry helper the web owned, so lift withRetry and delay from apps/web-wallet/app/lib into @agicash/utils (named exports, barrel re-exported) and delete the web copies. Flip the remaining consumers — the cashu receive-quote hook, the protected-route bootstrap, and the e2e Open Secret fixture — onto @agicash/utils, and add the workspace dependency to the e2e package. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add user.ensure(params) to the contract and implement it on the user namespace:
it derives the encryption public key, cashu locking xpub, and spark identity
public key, upserts the user row (creating the default accounts and persisting
the keys on first sign-in) through the base UpsertUserRepository, and returns
{ user, accounts } domain-typed for the host to seed its caches. The upsert
returns domain accounts already, so ensure returns them as-is — no mapping.
The key-derivation batch and the upsert each run under withRetry (matching the
resilience the host's query layer gave master); the upsert retry is Zod-aware so
a validation error fails fast. EnsureUserParams carries the replayed pending-terms
acceptance timestamps, distinct from acceptTerms' in-session boolean stamps.
ensure builds its repository through the accounts namespace's getRepository, so
the whole instance shares one construction path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Flip the accounts data path onto the SDK namespace: accountsQueryOptions' queryFn calls sdk.accounts.list(), useAccountOrNull's lazy fetch calls sdk.accounts.get(id), useAddCashuAccount calls sdk.accounts.cashu.add (callers drop the now-implicit type: 'cashu'), and the realtime ACCOUNT_CREATED/UPDATED handlers map rows through getInternalAccountRepository().toAccount. The cache still holds domain accounts, so every consumer (getAccountBalance, wallet/proofs readers) is unchanged. account-service-hooks is deleted (the service lives behind sdk.accounts.cashu.add) and account-repository-hooks moves to features/receive, its only remaining consumers being the unmigrated cashu receive-quote and receive-swap repos. Extract getExtendedAccounts and isDefaultAccount from UserService into standalone pure functions exported from the package root, and flip their consumers (the accounts hooks, the claim route, and the SDK-internal claim service). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Route _protected's bootstrap through sdk.user.ensure(): it replaces the inline key-derivation + upsert, seeds the user and accounts caches from the public return, and keeps master's structure (cache short-circuit, session-token warm, ensureBreezWasm placement, conditional seed on the upsert branch). Source the web-side key queries from the live instance's session keys via a new getInternalSessionKeys /temporary accessor: useEncryption collapses to a single ['encryption'] query over getEncryption(), and the cashu-seed and spark-mnemonic query fns delegate to the same getters. The per-key encryption query options and their hooks are deleted (only encryption-hooks and the two route warms read them), sparkIdentityPublicKeyQueryOptions is dropped (its only reader was the ensure warm), and xpubQueryOptions is untouched. The bootstrap warms encryption, seed, and mnemonic concurrently with ensure() so the unmigrated receive/send/claim repos keep master's warm-cache and fail-in-the-middleware behavior. The web defaultAccounts copy is removed now that ensure() owns it SDK-side. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bring the step-6 decision record onto the branch and append a supersession entry: the maintainer's contract-level domain-types ruling (d5d2f18, #1166) superseded B1's projection apparatus after the slice was first built, so sdk.accounts.* returns domain types directly (no mapper, no checked cast), B5 balance reads revert to getAccountBalance, and the runtime-fat reality-class record and the step-18 physical strip retire as moot. The record's prior entries stand as the decision trail. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4bef09b to
d9a1fa2
Compare
Step 6 of the wallet-SDK migration (
docs/superpowers/specs/2026-06-24-wallet-sdk-no-cache-production-design.md): wrap the accounts domain inAccountsApiand flip the web's accounts data layer ontosdk.accounts.*. Plan with the full Decision Record (B1–B7, maintainer-resolved) and gate record:docs/superpowers/plans/2026-07-13-wallet-sdk-accounts-slice.md.Stacked on
sdk/auth-slice(#1166). After #1166 merges: rebase onto master, retarget, re-verify (two-green-PRs rule).What lands
session-keys.ts): memoized, generation-fenced getters over Open Secret (encryption keypair, cashu seed, spark mnemonic, locking xpub, spark identity pubkey), cleared inonSessionEnded— a different user can never be served the previous user's keys.sdk.accounts(get/list/cashu.add) over one shared domain→projection mapper;AddCashuAccountParamspinned ({ name, mintUrl, currency, purpose });AgicashSdkPickgrows'accounts'.['accounts']cache holds projection-typed objects fetched viasdk.accounts.list(); hidden domain fields ride along at runtime until step 18 (type-level strip now, physical then). The shared mapper is the only cache entry point (queryFn, realtime, add onSuccess, ensure seed, claim upserts). Reality-class:sdk.accounts.*is TYPE-honest / RUNTIME-fat-until-18, intended and time-boxed; assumes web-internal consumers only during the window./temporarybridge v2: internal-repo accessor +toDomainAccount()checked cast (throwsMissingDomainFieldsErrornaming missing fields — never a bare cast) + mapper re-export. All carry step-18 removal notes.sdk.user.ensure(): the_protected.tsxbootstrap ported verbatim (key derivation, default accounts, Zod-aware retry, session-fenced memo); returns{ user, accounts }projection-typed. Timestamp params are replayed acceptance times from pending-terms storage (maintainer-confirmed intent, JSDoc'd). Web glue keeps master's structure exactly: short-circuit, prefetch warm-up, conditional seeding.Account/CashuAccount/SparkAccount/ExtendedAccountare now the projections (domain types import from/temporary); display reads.balanceoff the cache; money flows stay domain end-to-end through sanctioned unwrap seams.Unwrap seams (every hidden-field read passes
toDomainAccount())account-hooks: the four getter hooks unwrap internally;useDefaultAccount/useAccountOrDefaultreturn domain.buy-input/send-input/receive-input: unwrap at the selector option build;account-selectorrenders domain viagetAccountBalance(master verbatim).send-providergetAccounts; routes_protected.buy.checkout/receive.cashu/receive.spark/transfer.$destinationAccountId/send; claim route.account-proofs,transaction-additional-details,use-track-spark-account-balances(spark listener) at their seam.receive-cashu-token-hooks: unwraps forgetSourceAndDestinationAccounts; maps the domain source account back through the shared mapper for display items.Declared (questions and transitional items, not silent changes)
accounts.get(id)is not session-gated (master's repository posture — RLS scopes it);list/addgate on the session foruserId,user.*gates every verb. Parity kept; consistency call open to review.withRetry/delayported into the SDK lib; web copies remain for unmigrated consumers (dies with their slices).account-repository-hooksrelocated tofeatures/receive/(not deleted): its only consumers are unmigrated receive repos constructingAccountRepositorysynchronously; reshaping them is step 8–16 scope.account-service-hooksdeleted.session-keys.tsports master's hardcoded MAINNET for the spark identity pubkey (master's own TODO).sparkDebugLogstays a named/temporaryexception (maintainer ruling); dies at 18 with its call site.Verification
fix:allclean · workspace typecheck green (all 9 packages incl. web) · tests exit 0: wallet-sdk 96 (25 new: projection completeness type+runtime, mapper single-entry, checked-cast thin-object throw, ensure memo/retry, key fencing across session end), web 36, money 14, bolt11 9, ecies 18, cashu 35. Chain re-run independently at the keeper gate on the frozen tree.🤖 Generated with Claude Code