/*! * Copyright (c) 2026 Interop Alliance. All rights reserved. */ /** * The session auth store (zustand): a four-state machine * (`boot` | `local` | `connected` | `reconnect`) that owns the whole session * lifecycle -- boot (hot restore or fall to an anonymous local replica), Login * With Wallet, the non-CHAPI `connectWithGrants` path, logout, clear-data, and * the expired-access reconnect -- plus the open/hydrate/sync ordering. * * `boot` attempts a zero-popup session restore. A restore hit opens the * encrypted {@link LocalStore} under the session seed, hydrates the entity * stores, starts WAS replication, and lands `connected`. A restore miss (or any * error) opens the store under a persisted ANONYMOUS seed instead and lands * `local`: a fully usable, encrypted, local-only replica with no remote. Both * successors finish open+hydrate before leaving `boot`, so "app ready" is simply * `status !== 'boot'`. * * Login (or `connectWithGrants`) tears the anonymous replica down and opens the * connected replica under the wallet-derived seed. By default it ADOPTS the * anonymous replica's data first (`adopt: 'merge'`): the replica is detached, * its decrypted payloads are collected through a fresh handle re-derived from * the persisted seed, LWW-merged into the connected replica before its * first hydrate/sync (so they reach the server as ordinary creates), and the * anonymous seed + database are deleted once the activation lands. `adopt: * 'leave'` sets the anonymous replica aside untouched instead (it returns after * a logout). Logout returns to a fresh `local` (optionally wiping the connected * replica); `clearLocalData` mints a brand-new anonymous seed and replica and * drops any persisted connected session. * * The library cannot bind a module-level store to app config, so this is a * FACTORY: {@link createAuthStore} captures the app's {@link WasAppConfig} and * {@link StoreRegistry} once (the React provider calls it once) and returns a * vanilla zustand store the hooks consume through context. */ import { type StoreApi } from 'zustand/vanilla'; import type { RxStorage } from 'rxdb/plugins/core'; import type { IZcap } from '@interop/data-integrity-core'; import { type StoreRegistry, type WasAppConfig } from '../config.js'; import type { SharedCollectionReader } from '../storage/sharedCollectionReader.js'; import { type SeedStore } from '../identity/seedStore.js'; import { type LoginPhase } from '../auth/loginFlow.js'; /** * The four session states: * - `boot`: attempting hot restore; both successors open + hydrate before this * status is left. * - `local`: an encrypted anonymous-seed replica, no remote (the pre-connection * product state; tier-1 apps may stay here indefinitely). * - `connected`: a wallet-derived identity, parsed grants, replication. * - `reconnect`: connected but access expired/revoked; the replica stays usable, * remote invocations paused until re-login. */ export type SessionStatus = 'boot' | 'local' | 'connected' | 'reconnect'; export interface AuthState { status: SessionStatus; /** * The app's onboarding mode. Read by `ProtectedRoute` to decide whether to * render the app or redirect to login while in `local`; never affects the * store's own transitions. */ onboarding: 'local-first' | 'login-gated'; /** * The per-install LWW attribution id, resolved once at store creation from * `WasAppConfig.storageKeyPrefix` and exposed for display and debugging. The * entity-store write verbs and the adoption repair stamp it automatically, so * an app never has to: one install writes under one identity either way. It * is an attribution label, never an identity. */ writerId: string; /** * The current login phase, for the login page's progress line, and the single * source of truth for "a Login With Wallet flow is in flight" (non-`null` * throughout the flow; the `status` stays `local` meanwhile). `useSession` / * `useLogin` expose the boolean as `authenticating`, computed from this. */ phase: LoginPhase | null; error: string | null; controllerDid: string | null; /** * ISO expiry of the current grant set (earliest zcap expiry); `null` in * `local` (no grants). */ expires: string | null; reconnecting: boolean; /** * The read-only readers over the wallet-owned collections the wallet shared * with this app, keyed by the `sharedCollections` config `key`. Populated once * replication bootstraps; empty in `local`, and cleared on logout / teardown. * A shared collection the wallet declined to grant (or that this app is not a * recipient of) is simply absent -- never a session failure. */ sharedCollections: Record; /** * Attempt a zero-popup hot restore; a restore hit lands `connected`, any * miss/error falls to `local` (a fresh anonymous replica), never a dead login * screen. No-op once the status has left `boot`. */ boot: () => Promise; /** * Full Login With Wallet (first-run or returning). On success tears down the * anonymous replica and opens the connected one. `adopt` decides what happens * to data created in `local` before this login: `'merge'` (the default) * LWW-merges it into the connected replica and then deletes the anonymous * seed + database; `'leave'` sets the anonymous replica aside untouched (it * returns after a logout). A cancel or failure leaves `local` intact either * way. * * Resolves with `{ firstRun }` on success (`firstRun` is true when this login * created a brand-new app key, so the app can show a "connected for the first * time" confirmation). Resolves with `null` when the user cancels a wallet * popup (not an error). REJECTS on any genuine failure, after recording the * message in `error` so the UI state still reflects it. * * @param [options] {object} * @param [options.adopt] {'merge' | 'leave'} * @returns {Promise<{ firstRun: boolean } | null>} */ login: (options?: { adopt?: 'merge' | 'leave'; }) => Promise<{ firstRun: boolean; } | null>; /** * Non-CHAPI connect from an explicit seed + grants (dev/test and provisioned * grants). Tears down the current replica and opens the connected one, with * the same `adopt` choice (and `'merge'` default) as `login`, so this path * exercises adoption exactly as a wallet login does. * * @param options {object} * @param options.seed {Uint8Array} * @param options.grants {IZcap[]} * @param [options.adopt] {'merge' | 'leave'} * @returns {Promise} */ connectWithGrants: (options: { seed: Uint8Array; grants: IZcap[]; adopt?: 'merge' | 'leave'; }) => Promise; /** * Re-run the grants flow with the existing seed (expired access). */ reconnect: () => Promise; /** * Detach the wallet and return to a fresh `local` replica. `wipe` deletes the * connected replica's database; otherwise it is kept on the device. * * @param [options] {object} * @param [options.wipe] {boolean} * @returns {Promise} */ logout: (options?: { wipe?: boolean; }) => Promise; /** * Delete the local replica, discard the anonymous seed, and mint a fresh * anonymous seed/DID + replica. Also clears any persisted connected session, * so clearing while connected fully disconnects (a later boot lands `local` * instead of silently reconnecting and syncing the data back down). The * reset primitive behind the "Clear data" button. */ clearLocalData: () => Promise; /** * Whether the anonymous `local` replica currently holds any documents -- the * check a login screen runs to decide whether to offer the adoption choice * before `login()`. Always false outside `local`. */ hasLocalData: () => Promise; notifyAccessExpired: () => void; /** * Tears down the live replica (expiry watch, controller, local store) WITHOUT * wiping the persisted session record, and returns to `boot` so a fresh * `boot()` can re-open it. Called from the provider's unmount cleanup so an * unmount (or a React dev-mode remount) never orphans the replication loop or * the expiry-watch interval. Serialized with `boot` so a destroy fired while a * boot is still in flight tears down only once that boot has fully settled, * and a boot queued after it re-opens cleanly. */ destroy: () => Promise; } /** * The vanilla zustand store returned by {@link createAuthStore}. */ export type WasAuthStore = StoreApi; /** * Builds the session auth store bound to an app's config and store registry. * Call once (the React provider does) and share the returned store through * context; the hooks read it via `useStore`. * * @param options {object} * @param options.config {WasAppConfig} the app-wide configuration * @param options.registry {StoreRegistry} the per-collection hydrate/patch * handlers the rehydrate mechanism drives * @param [options.seedStore] {SeedStore} the session IndexedDB persistence * (defaults to one bound to `-session`; inject a fake for tests) * @param [options.storage] {RxStorage} the RxDB storage for the local store * (defaults to Dexie/IndexedDB; inject a fake for tests) * @returns {WasAuthStore} */ export declare function createAuthStore({ config, registry, seedStore, storage }: { config: WasAppConfig; registry: StoreRegistry; seedStore?: SeedStore; storage?: RxStorage; }): WasAuthStore; //# sourceMappingURL=authStore.d.ts.map