@drakkar.software/starfish-client 3.0.0-alpha.29 → 3.0.0-alpha.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/index.ts", "../src/client.ts", "../src/types.ts", "../src/fetch.ts", "../src/sync.ts", "../src/validate.ts", "../src/append-log.ts", "../src/logger.ts", "../src/migrate.ts", "../src/resolvers.ts", "../src/history.ts", "../src/polling.ts", "../src/dedup.ts", "../src/mutate.ts", "../src/config.ts", "../src/storage/indexeddb.ts", "../src/export.ts", "../src/background-sync.ts", "../src/service-worker.ts", "../src/bindings/suspense.ts", "../src/debounced-sync.ts", "../src/mobile-lifecycle.ts", "../src/multi-store.ts"],
4
- "sourcesContent": ["export { configurePlatform } from \"@drakkar.software/starfish-protocol\"\nexport type { CryptoProvider, Base64Provider, PlatformConfig } from \"@drakkar.software/starfish-protocol\"\nexport { stableStringify, computeHash } from \"@drakkar.software/starfish-protocol\"\nexport { buildRevocationList, revocationListCanonicalSigningInput } from \"@drakkar.software/starfish-protocol\"\nexport type {\n RevocationList,\n RevocationEntry,\n RevokedSubject,\n BuildRevocationListOpts,\n} from \"@drakkar.software/starfish-protocol\"\nexport type { PullResult, PushSuccess, PullKeyringProjection } from \"@drakkar.software/starfish-protocol\"\n\nexport { StarfishClient, pullWasFromCache } from \"./client.js\"\nexport type {\n BlobPullResult,\n BlobPushResult,\n AppendPullOptions,\n PullOptions,\n BatchPullOptions,\n BatchPullResult,\n BatchPullEntry,\n} from \"./client.js\"\nexport { SyncManager, AbortError } from \"./sync.js\"\nexport type { SyncManagerOptions, SyncSigner } from \"./sync.js\"\nexport { AppendLogCursor, AppendAuthorError, checkpointOf } from \"./append-log.js\"\nexport type { AppendLogCursorOptions, AppendElement, AuthorVerifier, ElementErrorPolicy } from \"./append-log.js\"\nexport { ENCRYPTED_KEY } from \"@drakkar.software/starfish-protocol\"\nexport type { Encryptor } from \"@drakkar.software/starfish-protocol\"\nexport {\n ConflictError,\n StarfishHttpError,\n} from \"./types.js\"\nexport type {\n StarfishClientOptions,\n StarfishCapProvider,\n PullCache,\n ConflictResolver,\n ClientPlugin,\n} from \"./types.js\"\nexport { consoleSyncLogger, noopSyncLogger, createMetricsCollector } from \"./logger.js\"\nexport type { SyncLogger, SyncMetrics, MetricsCollector } from \"./logger.js\"\nexport { createMigrator } from \"./migrate.js\"\nexport type { MigrationFn, MigrationConfig } from \"./migrate.js\"\nexport { ValidationError, createSchemaValidator } from \"./validate.js\"\nexport type { Validator, ValidationResult } from \"./validate.js\"\nexport { classifyError, parseRetryAfterMs } from \"./fetch.js\"\nexport type { ErrorCategory } from \"./fetch.js\"\nexport {\n createUnionMerge,\n createSoftDeleteResolver,\n timestampWinner,\n pruneTombstones,\n withConflictMeta,\n} from \"./resolvers.js\"\nexport type { ConflictMeta, ConflictResolverWithMeta } from \"./resolvers.js\"\nexport { SnapshotHistory } from \"./history.js\"\nexport type { Snapshot, SnapshotHistoryOptions } from \"./history.js\"\nexport { startPolling, startAdaptivePolling } from \"./polling.js\"\nexport type { PollableState, AdaptivePollingOptions, AdaptivePollingControls } from \"./polling.js\"\nexport { createDedupFetch } from \"./dedup.js\"\nexport { mutateDoc } from \"./mutate.js\"\nexport type { DocState, DocMutator, MutateDocOptions } from \"./mutate.js\"\nexport { fetchServerConfig } from \"./config.js\"\nexport type { EncryptionMode, CollectionClientInfo, ConfigResponse } from \"./config.js\"\nexport { createIndexedDBStorage } from \"./storage/indexeddb.js\"\nexport type { IndexedDBStorageOptions, AsyncStateStorage } from \"./storage/indexeddb.js\"\nexport { exportData, importData, exportToBlob } from \"./export.js\"\nexport type { ExportOptions } from \"./export.js\"\nexport { isBackgroundSyncSupported, registerBackgroundSync } from \"./background-sync.js\"\nexport type { BackgroundSyncOptions } from \"./background-sync.js\"\nexport { isServiceWorkerSupported, registerServiceWorker, unregisterServiceWorkers } from \"./service-worker.js\"\nexport type { ServiceWorkerOptions } from \"./service-worker.js\"\nexport { createSuspenseResource } from \"./bindings/suspense.js\"\nexport { createDebouncedSync, createDebouncedPush } from \"./debounced-sync.js\"\nexport type { DebouncedSyncOptions, DebouncedSync, DebouncedPushOptions, DebouncedPush } from \"./debounced-sync.js\"\nexport { createMobileLifecycle, createAppendLogMobileLifecycle } from \"./mobile-lifecycle.js\"\nexport type { AppStateModule, NetInfoModule, MobileLifecycleDeps, MobileLifecycleOptions, AppendLogLifecycleOptions } from \"./mobile-lifecycle.js\"\nexport { createMultiStoreSync } from \"./multi-store.js\"\nexport type {\n StoreSlice,\n BackupDocument,\n MultiStoreMigrationFn,\n MultiStoreSyncOptions,\n MultiStoreSync,\n} from \"./multi-store.js\"\nexport type { AppendOnlyClientInfo } from \"./config.js\"\n", "import type { PullResult, PushSuccess } from \"@drakkar.software/starfish-protocol\"\nimport {\n AUTHOR_PUBKEY_FIELD,\n AUTHOR_SIGNATURE_FIELD,\n DATA_FIELD,\n TS_FIELD,\n BASE_HASH_FIELD,\n PUSH_PATH_PREFIX,\n HEADER_AUTHORIZATION,\n HEADER_SIG,\n HEADER_TS,\n HEADER_NONCE,\n HEADER_PUB,\n HEADER_CONTENT_TYPE,\n HEADER_ACCEPT,\n signAppendAuthor,\n signRequest,\n stableStringify,\n type AppendAuthor,\n type SignableMethod,\n type SignableRequest,\n} from \"@drakkar.software/starfish-protocol\"\nimport type {\n StarfishClientOptions,\n StarfishCapProvider,\n PullCache,\n} from \"./types.js\"\nimport { ConflictError, StarfishHttpError } from \"./types.js\"\nimport { parseRetryAfterMs } from \"./fetch.js\"\n\nconst APPEND_DEFAULT_FIELD = \"items\"\nconst MAX_REVALIDATE_ATTEMPTS = 5\nconst REVALIDATE_INITIAL_DELAY_MS = 1_000\nconst REVALIDATE_MAX_DELAY_MS = 30_000\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * Shape persisted in a {@link PullCache} for one document: the raw server\n * `PullResult` fields. For E2E collections `data` is the sealed ciphertext.\n */\ninterface CachedPull {\n data: Record<string, unknown>\n hash: string\n timestamp: number\n /** Wall-clock ms when this snapshot was written \u2014 for {@link StarfishClientOptions.cacheMaxAgeMs} expiry. */\n cachedAt: number\n}\n\n/**\n * The cache key for a pull `pathAndQuery`: the path with any query string\n * dropped, so a checkpoint'd or `withKeyring` pull and a plain pull of the same\n * document share one stable key (the document identity, not the request shape).\n */\nfunction pullCacheKey(pathAndQuery: string): string {\n const q = pathAndQuery.indexOf(\"?\")\n return q === -1 ? pathAndQuery : pathAndQuery.slice(0, q)\n}\n\n/**\n * Whether a {@link PullResult} was served from the offline read-through cache\n * (the transport was unreachable) rather than a live server response. Used by\n * {@link SyncManager} to surface a `stale` flag to the UI without treating a\n * cache hit as proof the server is reachable.\n */\nexport function pullWasFromCache(result: PullResult): boolean {\n return (result as { fromCache?: boolean }).fromCache === true\n}\n\n/** The storage `documentKey` for a push `path`: the path with the `/push/`\n * action prefix stripped (the namespace lives only in the URL). The author\n * signature binds to this key. */\nexport function stripPushPrefix(path: string): string {\n return path.startsWith(PUSH_PATH_PREFIX) ? path.slice(PUSH_PATH_PREFIX.length) : path\n}\n\n/** Result of pulling a binary blob from the server. */\nexport interface BlobPullResult {\n data: ArrayBuffer\n /** Content hash from the ETag header. Null if the server didn't include an ETag. */\n hash: string | null\n contentType: string\n}\n\n/** Result of pushing a binary blob to the server. */\nexport interface BlobPushResult {\n hash: string\n}\n\n/** Options for append-only pull \u2014 extracts a single array field from the response. */\nexport interface AppendPullOptions {\n /** Array field name in `data`. Defaults to `\"items\"`. */\n appendField?: string\n /** Only return items appended after this timestamp (ms). Sent as `?checkpoint=`. */\n since?: number\n /** Return only the last K items (applied after `since` filter). Sent as `?last=`. */\n last?: number\n /** Return only the last K items. Alias of `last`; sent as `?limit=`. When both\n * are given, `limit` wins. */\n limit?: number\n /** Explicitly fetch the whole collection (sent as `?full=true`). Mutually\n * exclusive with `since`/`limit`/`last` \u2014 the server requires a pull to declare\n * exactly one of {checkpoint, limit/last, full}. */\n full?: boolean\n}\n\n/**\n * Options for a structured (non-append) pull.\n *\n * `withKeyring: true` appends `?withKeyring=1` so the server includes the\n * collection's sibling `<collection>/_keyring` document in the response,\n * saving a cold-start round-trip. The cap-cert scope MUST cover BOTH the\n * data path and `<collection>/_keyring` \u2014 `scopes.writer(collection)` denies\n * the keyring path and will produce a 403; use `scopes.readWrite()` or grant\n * the keyring path explicitly when opting in.\n */\nexport interface PullOptions {\n /** Server timestamp of the last successful pull (ms). Sent as `?checkpoint=`. */\n checkpoint?: number\n /** Include the sibling `_keyring` document in the response. Defaults to false. */\n withKeyring?: boolean\n}\n\n/** Per-collection result in a {@link BatchPullResult}: either the pulled\n * document (`data`/`hash`/`timestamp`) or a per-collection `error` string. */\nexport interface BatchPullEntry {\n data?: unknown\n hash?: string\n timestamp?: number\n error?: string\n}\n\n/** Response of {@link StarfishClient.batchPull}: a map of requested collection\n * name \u2192 an ARRAY of {@link BatchPullEntry}, one per requested param-set, in\n * request order. A collection read with no params yields a one-element array. */\nexport interface BatchPullResult {\n collections: Record<string, BatchPullEntry[]>\n}\n\n/** Options for {@link StarfishClient.batchPull}. */\nexport interface BatchPullOptions {\n /** Per-collection path params: collection name \u2192 an ARRAY of param-sets, one\n * per document to read from that collection, e.g.\n * `{ profile: [{ identity: \"a\" }, { identity: \"b\" }] }` reads two profiles in\n * one round-trip. Serialized to a URL-encoded JSON `params` query. The\n * `{identity}` param is auto-filled by the server from the authenticated\n * caller when a set omits it, so a single self-doc read can pass `[{}]` \u2014 or\n * omit the collection from `params` entirely (an unlisted collection reads one\n * auto-filled doc). Results come back under the same name in request order. */\n params?: Record<string, Record<string, string>[]>\n}\n\n/**\n * Base64-encode the canonical stable-stringification of a cap-cert.\n *\n * Used as the value of the `Authorization: Cap <\u2026>` header in v3.0. We rely\n * on the host's `btoa` for browsers and fall back to `Buffer` in Node so the\n * client stays free of native dependencies.\n */\nfunction encodeCapAuth(cap: unknown): string {\n const json = stableStringify(cap as Record<string, unknown>)\n if (typeof btoa === \"function\") {\n return btoa(json)\n }\n const bufCtor = (globalThis as { Buffer?: { from: (s: string, enc: string) => { toString: (enc: string) => string } } }).Buffer\n if (bufCtor) return bufCtor.from(json, \"utf-8\").toString(\"base64\")\n throw new Error(\"No base64 encoder available\")\n}\n\n/**\n * Low-level HTTP client for the Starfish sync protocol.\n * Handles auth headers and response parsing.\n */\nexport class StarfishClient {\n private readonly baseUrl: string\n private readonly namespace?: string\n private readonly capProvider?: StarfishCapProvider\n private readonly fetch: typeof globalThis.fetch\n private readonly cache?: PullCache\n private readonly cacheMaxAgeMs?: number\n private readonly cacheFallbackStatuses?: ReadonlyArray<number>\n private readonly onRevalidated?: (path: string, result: PullResult) => void\n private readonly revalidating = new Set<string>()\n /**\n * Installed client-side plugins. Currently stored as inert data; no\n * hooks fire yet. Extensions can inspect this list if needed.\n */\n public readonly plugins: ReadonlyArray<import(\"./types.js\").ClientPlugin>\n\n constructor(options: StarfishClientOptions) {\n this.baseUrl = options.baseUrl.replace(/\\/$/, \"\")\n // Empty string \u21D2 no namespace (treat like unset), so a falsy env value\n // doesn't produce a malformed `/v1//\u2026` path.\n this.namespace = options.namespace || undefined\n this.capProvider = options.capProvider\n this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis)\n this.cache = options.cache\n this.cacheMaxAgeMs = options.cacheMaxAgeMs\n this.cacheFallbackStatuses = options.cacheFallbackStatuses\n this.onRevalidated = options.onRevalidated\n this.plugins = options.plugins ? [...options.plugins] : []\n }\n\n /**\n * Mark a `PullResult` as having been served from the offline read-through\n * cache (transport was unreachable). Non-enumerable so it doesn't leak into\n * JSON / equality / re-caching; read via {@link pullWasFromCache}.\n */\n private tagFromCache(result: PullResult): PullResult {\n Object.defineProperty(result, \"fromCache\", { value: true, enumerable: false })\n return result\n }\n\n /**\n * Resolve the host portion of the URL the client will send to. The host\n * is folded into the signed canonical input as the `h` field so the\n * server can refuse a signature that was minted against a different\n * Starfish host (replay-across-servers defence).\n *\n * When `baseUrl` is relative \u2014 e.g. the consumer passed a custom `fetch`\n * that resolves relative URLs in its own context \u2014 there is no parseable\n * host; we return `\"\"` so signing still proceeds. The server-side\n * verifier will also reconstruct host from its inbound URL, so the\n * empty-host case still verifies symmetrically when both sides agree.\n */\n private signingHost(): string {\n try {\n return new URL(this.baseUrl).host\n } catch {\n return \"\"\n }\n }\n\n /**\n * Rewrite a request path for the configured namespace. A no-op when no\n * namespace is set; otherwise `/{action}/\u2026` becomes `/v1/{namespace}/{action}/\u2026`\n * (the `/v1` protocol-version segment is part of the namespaced route, matching\n * the Python client and the server's namespace mount).\n *\n * Applied to the path used for BOTH the signature and the URL so the canonical\n * path the client signs equals the path the server reconstructs from the URL.\n * Covers SDK-helper-built paths too \u2014 that's the point: a namespace-unaware\n * helper passing `/push/spaces/x/_keyring` reaches `/v1/{ns}/push/spaces/x/_keyring`.\n */\n private applyNamespace(path: string): string {\n return this.namespace ? `/v1/${this.namespace}${path}` : path\n }\n\n /**\n * Build auth headers for a request. When a `capProvider` is set, signs the\n * request with the device's Ed25519 private key and returns the v3 header\n * set (`Authorization: Cap \u2026`, `X-Starfish-Sig`, `X-Starfish-Ts`,\n * `X-Starfish-Nonce`). Empty when no provider is configured (public reads).\n *\n * Body bytes signed MUST equal the bytes sent on the wire \u2014 callers pass\n * the already-serialized body string here so signing and transmission agree.\n * The host bound into the signature is derived from `baseUrl` once per call.\n */\n private async buildAuthHeaders(\n method: SignableMethod,\n pathAndQuery: string,\n body: string | undefined,\n ): Promise<Record<string, string>> {\n if (!this.capProvider) return {}\n const capCtx = await this.capProvider.getCap()\n return this.capRequestHeaders(capCtx, method, pathAndQuery, body)\n }\n\n /**\n * Build the request-signing headers from an ALREADY-fetched cap context. Split\n * out of {@link buildAuthHeaders} so {@link append} can fetch the cap once and\n * reuse it for BOTH the author signature (over the element data) and the\n * request signature (over the body), without redeeming the cap twice \u2014 a\n * second `getCap()` could rotate keys and break the `authorPubkey ===\n * presenter` bind the server checks.\n */\n private async capRequestHeaders(\n capCtx: Awaited<ReturnType<StarfishCapProvider[\"getCap\"]>>,\n method: SignableMethod,\n pathAndQuery: string,\n body: string | undefined,\n ): Promise<Record<string, string>> {\n const { cap, devEdPrivHex, pubHex } = capCtx\n const req: SignableRequest = {\n method,\n pathAndQuery,\n body,\n host: this.signingHost(),\n }\n const { sig, ts, nonce } = await signRequest(req, devEdPrivHex)\n const headers: Record<string, string> = {\n [HEADER_AUTHORIZATION]: `Cap ${encodeCapAuth(cap)}`,\n [HEADER_SIG]: sig,\n [HEADER_TS]: String(ts),\n [HEADER_NONCE]: nonce,\n }\n // Audience (public-link) caps bind no single subject, so the server needs\n // the presenter's pubkey to verify the signature and check the allow-list.\n if (pubHex !== undefined) headers[HEADER_PUB] = pubHex\n return headers\n }\n\n /**\n * Resolve the author public key to attach to a signed append: the redeemer's\n * `pubHex` for an audience cap, else the cert subject `cap.sub` for a\n * device/member cap. This is the SAME key that signs the request, so a server\n * enforcing author proof can bind the stored element to its writer. Returns\n * undefined only for a (malformed) cap with neither \u2014 the append then goes\n * unsigned and a server requiring signatures rejects it.\n */\n private appendAuthorKey(\n capCtx: Awaited<ReturnType<StarfishCapProvider[\"getCap\"]>>,\n ): { authorPubHex: string } | null {\n const { cap, pubHex } = capCtx\n const authorPubHex = pubHex ?? cap.sub\n if (authorPubHex === undefined) return null\n return { authorPubHex }\n }\n\n /** Pull synced data from the server. Returns the raw `PullResult`. */\n async pull(path: string, checkpoint?: number): Promise<PullResult>\n /** Pull synced data with structured options (e.g. `{withKeyring: true}`). */\n async pull(path: string, options: PullOptions): Promise<PullResult>\n /** Pull an append-only collection. Extracts and returns `data[appendField]` as `T[]`. */\n async pull<T = unknown>(path: string, options: AppendPullOptions): Promise<T[]>\n async pull<T = unknown>(\n path: string,\n checkpointOrOptions?: number | AppendPullOptions | PullOptions,\n ): Promise<PullResult | T[]> {\n let pathAndQuery = this.applyNamespace(path)\n let appendField: string | undefined\n\n if (typeof checkpointOrOptions === \"number\") {\n if (checkpointOrOptions) pathAndQuery += `?checkpoint=${checkpointOrOptions}`\n } else if (checkpointOrOptions != null) {\n // Disambiguate AppendPullOptions vs PullOptions.\n //\n // PullOptions are identified by the presence of `withKeyring` or\n // `checkpoint` keys (which AppendPullOptions does not have \u2014 append\n // uses `since`, not `checkpoint`). Anything else, including an empty\n // `{}` object, retains the historical behavior of AppendPullOptions\n // (extracts `data.items` with `?` query).\n const opts = checkpointOrOptions as AppendPullOptions & PullOptions\n const isPullOptions =\n opts.withKeyring !== undefined || opts.checkpoint !== undefined\n const params = new URLSearchParams()\n\n if (isPullOptions) {\n if (opts.checkpoint != null && opts.checkpoint > 0) {\n params.set(\"checkpoint\", String(opts.checkpoint))\n }\n if (opts.withKeyring) {\n params.set(\"withKeyring\", \"1\")\n }\n } else {\n appendField = opts.appendField ?? APPEND_DEFAULT_FIELD\n // `full` means \"the whole collection\" \u2014 it cannot be combined with a bound.\n if (opts.full && (opts.since != null || opts.limit != null || opts.last != null)) {\n throw new Error(\"full cannot be combined with since, limit, or last\")\n }\n if (opts.since != null) {\n if (opts.since < 0) throw new Error(\"since must be non-negative\")\n params.set(\"checkpoint\", String(opts.since))\n }\n if (opts.limit != null) {\n if (opts.limit < 0) throw new Error(\"limit must be non-negative\")\n params.set(\"limit\", String(opts.limit))\n }\n if (opts.last != null) {\n if (opts.last < 0) throw new Error(\"last must be non-negative\")\n params.set(\"last\", String(opts.last))\n }\n if (opts.full) {\n params.set(\"full\", \"true\")\n }\n }\n if (params.size > 0) pathAndQuery += `?${params.toString()}`\n }\n\n const url = `${this.baseUrl}${pathAndQuery}`\n const authHeaders = await this.buildAuthHeaders(\"GET\", pathAndQuery, undefined)\n\n // Read-through cache: only for structured (non-append) pulls. Append\n // collections own their own warm-start persistence via AppendLogCursor.\n const cacheKey =\n this.cache && appendField === undefined ? pullCacheKey(pathAndQuery) : undefined\n\n let res: Response\n try {\n res = await this.fetch(url, {\n method: \"GET\",\n headers: { [HEADER_ACCEPT]: \"application/json\", ...authHeaders },\n })\n } catch (err) {\n // The TRANSPORT failed (offline / DNS / timeout) \u2014 fall back to the last\n // cached snapshot for this document if we have one, tagged so callers can\n // tell it's stale. A real HTTP error (below) is a genuine server answer\n // and never gets here; 403 and 404 always propagate. 429 and 5xx\n // propagate by default too, but can fall back to cache when\n // `cacheFallbackStatuses` is set \u2014 see the stale-while-revalidate branch.\n if (cacheKey) {\n const cached = await this.readCache(cacheKey)\n if (cached) return cached\n }\n throw err\n }\n if (!res.ok) {\n const status = res.status\n if (cacheKey && this.cacheFallbackStatuses?.includes(status)) {\n // Stale-while-revalidate: serve the last-synced snapshot immediately and\n // retry in the background. 403/404 are not in the configured set so they\n // still propagate as genuine answers.\n const retryAfterHeader = res.headers.get(\"Retry-After\")\n this.scheduleRevalidate(cacheKey, pathAndQuery, retryAfterHeader)\n const cached = await this.readCache(cacheKey)\n if (cached) {\n // Discard the response body so the underlying connection can be reused.\n void res.body?.cancel()\n return cached\n }\n }\n throw new StarfishHttpError(status, await res.text())\n }\n\n const result = await res.json() as PullResult\n if (appendField !== undefined) {\n const list = (result.data as Record<string, unknown> | null)?.[appendField]\n return (Array.isArray(list) ? list : []) as T[]\n }\n if (cacheKey) {\n const snapshot: CachedPull = {\n data: result.data,\n hash: result.hash,\n timestamp: result.timestamp,\n cachedAt: Date.now(),\n }\n void this.cache!.set(cacheKey, JSON.stringify(snapshot)).catch(() => {})\n }\n return result\n }\n\n /** Deduplicated fire-and-forget: starts one revalidation loop per cacheKey. */\n private scheduleRevalidate(\n cacheKey: string,\n pathAndQuery: string,\n retryAfterHeader: string | null,\n ): void {\n if (this.revalidating.has(cacheKey)) return\n this.revalidating.add(cacheKey)\n void this.revalidateLoop(cacheKey, pathAndQuery, retryAfterHeader).finally(() => {\n this.revalidating.delete(cacheKey)\n })\n }\n\n /**\n * Background revalidation loop for a {@link cacheFallbackStatuses} hit.\n * Retries the pull (honoring `Retry-After`) up to {@link MAX_REVALIDATE_ATTEMPTS}\n * times. On a live 2xx response the fresh snapshot is written through to the\n * cache and {@link onRevalidated} fires. Stops early on a non-fallback HTTP\n * status (e.g. 404/403 \u2014 the server gave a genuine answer).\n */\n private async revalidateLoop(\n cacheKey: string,\n pathAndQuery: string,\n firstRetryAfter: string | null,\n ): Promise<void> {\n let retryAfterHeader = firstRetryAfter\n for (let attempt = 0; attempt < MAX_REVALIDATE_ATTEMPTS; attempt++) {\n const delay = parseRetryAfterMs(retryAfterHeader, {\n fallbackMs: Math.min(\n REVALIDATE_INITIAL_DELAY_MS * Math.pow(2, attempt),\n REVALIDATE_MAX_DELAY_MS,\n ),\n maxMs: REVALIDATE_MAX_DELAY_MS,\n })\n await sleep(delay)\n\n try {\n const url = `${this.baseUrl}${pathAndQuery}`\n const authHeaders = await this.buildAuthHeaders(\"GET\", pathAndQuery, undefined)\n const res = await this.fetch(url, {\n method: \"GET\",\n headers: { [HEADER_ACCEPT]: \"application/json\", ...authHeaders },\n })\n\n if (res.ok) {\n const result = (await res.json()) as PullResult\n if (this.cache) {\n const snapshot: CachedPull = {\n data: result.data,\n hash: result.hash,\n timestamp: result.timestamp,\n cachedAt: Date.now(),\n }\n void this.cache.set(cacheKey, JSON.stringify(snapshot)).catch(() => {})\n }\n this.onRevalidated?.(pathAndQuery, result)\n return\n }\n\n if (!this.cacheFallbackStatuses?.includes(res.status)) {\n // Genuine server answer (e.g. 403 or 404) \u2014 stop retrying.\n return\n }\n\n retryAfterHeader = res.headers.get(\"Retry-After\")\n } catch {\n // Transport failure \u2014 keep retrying with exponential backoff.\n retryAfterHeader = null\n }\n }\n }\n\n /**\n * Read the cached snapshot for a document `path` WITHOUT hitting the network \u2014\n * the basis for cache-first paint (seed the UI from the last-synced snapshot,\n * then revalidate with a live {@link pull}). Returns the tagged `PullResult`,\n * or null when no cache is configured / there's no entry. Namespacing matches\n * {@link pull}, so the key lines up with whatever `pull` wrote.\n */\n async peekCache(path: string): Promise<PullResult | null> {\n if (!this.cache) return null\n return this.readCache(pullCacheKey(this.applyNamespace(path)))\n }\n\n /** Read + parse a cached pull snapshot, tagged {@link tagFromCache}. Returns\n * null on a miss or an unparseable blob (never throws \u2014 a corrupt cache entry\n * must not break a pull, just miss). */\n private async readCache(cacheKey: string): Promise<PullResult | null> {\n try {\n const raw = await this.cache!.get(cacheKey)\n if (!raw) return null\n const parsed = JSON.parse(raw) as CachedPull\n if (!parsed || typeof parsed.hash !== \"string\") return null\n // Expiry: a snapshot older than the configured max age is a miss. Entries\n // written before this field existed (cachedAt missing) count as age 0 \u21D2\n // expired under any TTL, forcing a fresh pull once.\n if (this.cacheMaxAgeMs != null && Date.now() - (parsed.cachedAt ?? 0) > this.cacheMaxAgeMs) {\n return null\n }\n return this.tagFromCache({ data: parsed.data ?? {}, hash: parsed.hash, timestamp: parsed.timestamp ?? 0 })\n } catch {\n return null\n }\n }\n\n /**\n * Pull several documents in one round-trip via `/batch/pull`. `collections` is\n * the list of distinct collection names; `opts.params` supplies, per collection,\n * an ARRAY of path-param sets \u2014 one per document to read \u2014 so the SAME collection\n * can fan in many documents (e.g. many users' `profile`) in a single request.\n * The server auto-fills the `{identity}` param from the authenticated caller for\n * any set that omits it, so a self-doc collection needs no params. Returns a map\n * of collection name \u2192 an ARRAY of pulled documents (or per-document `{ error }`),\n * in request order. Honors the configured namespace.\n *\n * For the common \"many docs of one collection\" case prefer {@link batchPullMany}.\n *\n * Note: not append/checkpoint-aware \u2014 for incremental append-only reads use\n * `pull(path, { since })` (or `AppendLogCursor`) per collection.\n */\n async batchPull(\n collections: string[],\n opts: BatchPullOptions = {},\n ): Promise<BatchPullResult> {\n const search = new URLSearchParams()\n search.set(\"collections\", collections.join(\",\"))\n if (opts.params && Object.keys(opts.params).length > 0) {\n search.set(\"params\", JSON.stringify(opts.params))\n }\n const pathAndQuery = `${this.applyNamespace(\"/batch/pull\")}?${search.toString()}`\n const url = `${this.baseUrl}${pathAndQuery}`\n const authHeaders = await this.buildAuthHeaders(\"GET\", pathAndQuery, undefined)\n\n const res = await this.fetch(url, {\n method: \"GET\",\n headers: { [HEADER_ACCEPT]: \"application/json\", ...authHeaders },\n })\n if (!res.ok) {\n throw new StarfishHttpError(res.status, await res.text())\n }\n return await res.json() as BatchPullResult\n }\n\n /**\n * Convenience over {@link batchPull} for reading MANY documents of ONE\n * collection in a single round-trip: pass the per-document param-sets and get\n * back the {@link BatchPullEntry} array aligned to `paramsList` by index (each\n * entry is `{ data, hash, timestamp }` or `{ error }`). An empty `paramsList`\n * issues no request and returns `[]`.\n */\n async batchPullMany(\n collection: string,\n paramsList: Record<string, string>[],\n ): Promise<BatchPullEntry[]> {\n if (paramsList.length === 0) return []\n const res = await this.batchPull([collection], { params: { [collection]: paramsList } })\n return res.collections[collection] ?? []\n }\n\n /**\n * Push synced data to the server.\n * @param path - The push endpoint path (e.g. \"/push/users/abc/settings\")\n * @param data - The full document data to push\n * @param baseHash - Hash of the document this push is based on (null for first push)\n *\n * v3 author proof (`authorPubkey` + `authorSignature`) is passed via `author`\n * (produced by `SyncManager` when a `signer` is configured) and sent as\n * top-level body siblings of `data`, where the server verifies it.\n * @throws {ConflictError} if the server detects a hash mismatch (409)\n */\n async push(\n path: string,\n data: Record<string, unknown>,\n baseHash: string | null,\n author?: AppendAuthor,\n ): Promise<PushSuccess> {\n const body = JSON.stringify({\n [DATA_FIELD]: data,\n [BASE_HASH_FIELD]: baseHash,\n ...(author && {\n [AUTHOR_PUBKEY_FIELD]: author.authorPubkey,\n [AUTHOR_SIGNATURE_FIELD]: author.authorSignature,\n }),\n })\n\n const sendPath = this.applyNamespace(path)\n const authHeaders = await this.buildAuthHeaders(\"POST\", sendPath, body)\n\n const res = await this.fetch(`${this.baseUrl}${sendPath}`, {\n method: \"POST\",\n headers: {\n [HEADER_CONTENT_TYPE]: \"application/json\",\n [HEADER_ACCEPT]: \"application/json\",\n ...authHeaders,\n },\n body,\n })\n\n if (res.status === 409) {\n throw new ConflictError()\n }\n if (!res.ok) {\n throw new StarfishHttpError(res.status, await res.text())\n }\n const result = (await res.json()) as PushSuccess\n // Write-through: update the pull cache with the pushed data so an offline\n // restart reads the just-written state rather than the pre-push snapshot.\n // The push path is /push/X; the corresponding pull cache key is /pull/X.\n if (this.cache) {\n const pullPath = sendPath.replace(\"/push/\", \"/pull/\")\n const cacheKey = pullCacheKey(pullPath)\n const snapshot: CachedPull = {\n data,\n hash: result.hash,\n timestamp: result.timestamp,\n cachedAt: Date.now(),\n }\n void this.cache.set(cacheKey, JSON.stringify(snapshot)).catch(() => {})\n }\n return result\n }\n\n /**\n * Append an element to an appendOnly (`by_timestamp`) collection.\n *\n * Unlike {@link push}, appendOnly writes carry no hash/conflict check \u2014 an\n * authorized append is always accepted. Each element is stored server-side as\n * `{ts, data}` and pulls can filter by `ts` via `since`/`checkpoint`.\n *\n * @param path - the push endpoint (e.g. \"/push/events\")\n * @param data - the element payload. For a `delegated` collection, encrypt it\n * first (e.g. `createKeyringEncryptor(keyring, kem).encrypt(data)`); the\n * server stores it opaquely and never reads it.\n * @param opts.ts - optional client-supplied element timestamp (ms). Must be a\n * non-negative integer strictly greater than the latest stored element's ts\n * (else the server responds 409). Omit to let the server assign one.\n * @throws {StarfishHttpError} on a non-2xx response \u2014 e.g. 409\n * `{ error: \"non_monotonic_timestamp\" }` for a non-monotonic timestamp, or\n * `{ error: \"append_limit_exceeded\", limit }` if the collection's `maxItems`\n * cap is reached (partition by a path parameter for higher volume).\n */\n async append(\n path: string,\n data: Record<string, unknown>,\n opts: { ts?: number } = {},\n ): Promise<PushSuccess> {\n const sendPath = this.applyNamespace(path)\n const bodyObj: Record<string, unknown> = { [DATA_FIELD]: data }\n if (opts.ts !== undefined) bodyObj[TS_FIELD] = opts.ts\n\n // Author proof. Fetch the cap ONCE and reuse it for both the author\n // signature (over the element `data`) and the request signature (over the\n // final body) \u2014 see {@link capRequestHeaders}. The author fields are signed\n // with the same key that authenticates the request, so a collection with\n // `requireAuthorSignature` (the default) binds the stored element to its\n // writer. Without a cap provider the append is sent unsigned and such a\n // collection rejects it.\n const capCtx = this.capProvider ? await this.capProvider.getCap() : null\n if (capCtx) {\n const authorKey = this.appendAuthorKey(capCtx)\n if (authorKey) {\n // The signature binds the author to BOTH the element data AND the\n // document it is written to (the storage path = `path` minus the\n // `/push/` action prefix; the namespace lives only in the URL).\n const documentKey = stripPushPrefix(path)\n const { authorPubkey, authorSignature } = signAppendAuthor(\n documentKey,\n data,\n authorKey.authorPubHex,\n capCtx.devEdPrivHex,\n )\n bodyObj[AUTHOR_PUBKEY_FIELD] = authorPubkey\n bodyObj[AUTHOR_SIGNATURE_FIELD] = authorSignature\n }\n }\n\n const body = JSON.stringify(bodyObj)\n const authHeaders = capCtx\n ? await this.capRequestHeaders(capCtx, \"POST\", sendPath, body)\n : {}\n\n const res = await this.fetch(`${this.baseUrl}${sendPath}`, {\n method: \"POST\",\n headers: {\n [HEADER_CONTENT_TYPE]: \"application/json\",\n [HEADER_ACCEPT]: \"application/json\",\n ...authHeaders,\n },\n body,\n })\n\n if (!res.ok) {\n throw new StarfishHttpError(res.status, await res.text())\n }\n return res.json() as Promise<PushSuccess>\n }\n\n /**\n * Pull binary data from a blob collection.\n * Returns raw bytes with the content hash from the ETag header.\n */\n async pullBlob(path: string): Promise<BlobPullResult> {\n const sendPath = this.applyNamespace(path)\n const authHeaders = await this.buildAuthHeaders(\"GET\", sendPath, undefined)\n\n const res = await this.fetch(`${this.baseUrl}${sendPath}`, {\n method: \"GET\",\n headers: { [HEADER_ACCEPT]: \"*/*\", ...authHeaders },\n })\n if (!res.ok) {\n throw new StarfishHttpError(res.status, await res.text())\n }\n\n const etag = res.headers.get(\"ETag\")?.replace(/\"/g, \"\") ?? null\n const contentType = res.headers.get(HEADER_CONTENT_TYPE) ?? \"application/octet-stream\"\n const data = await res.arrayBuffer()\n\n return { data, hash: etag, contentType }\n }\n\n /**\n * Push binary data to a blob collection.\n * Binary collections use last-write-wins (no conflict detection).\n */\n async pushBlob(\n path: string,\n data: ArrayBuffer | Uint8Array | Blob,\n contentType: string,\n ): Promise<BlobPushResult> {\n // Blobs are not JSON; we leave body undefined when signing \u2014 server-side\n // verification is expected to use a separate path for blob uploads.\n const sendPath = this.applyNamespace(path)\n const authHeaders = await this.buildAuthHeaders(\"POST\", sendPath, undefined)\n\n const res = await this.fetch(`${this.baseUrl}${sendPath}`, {\n method: \"POST\",\n headers: {\n [HEADER_CONTENT_TYPE]: contentType,\n [HEADER_ACCEPT]: \"application/json\",\n ...authHeaders,\n },\n body: data as BodyInit,\n })\n if (!res.ok) {\n throw new StarfishHttpError(res.status, await res.text())\n }\n return res.json() as Promise<BlobPushResult>\n }\n}\n", "import type { CapCert, PullResult } from \"@drakkar.software/starfish-protocol\"\n\n/** Push conflict error (HTTP 409). */\nexport class ConflictError extends Error {\n constructor() {\n super(\"hash_mismatch\")\n this.name = \"ConflictError\"\n }\n}\n\n/** HTTP error from the Starfish server. */\nexport class StarfishHttpError extends Error {\n constructor(\n public readonly status: number,\n public readonly body: string\n ) {\n super(`HTTP ${status}: ${body}`)\n this.name = \"StarfishHttpError\"\n }\n}\n\n/**\n * v3.0 cap-cert provider for `StarfishClient`. Returns the device's cap-cert and\n * the matching Ed25519 private key (hex). The client calls `getCap()` once per\n * outgoing request; implementations are expected to cache so this is cheap.\n *\n * When set, the client signs every outgoing request: each call carries\n * `Authorization: Cap <base64(stableStringify(cap))>` plus `X-Starfish-Sig`,\n * `X-Starfish-Ts`, `X-Starfish-Nonce`.\n */\nexport interface StarfishCapProvider {\n /**\n * Returns the device's cap-cert and its Ed25519 private key (hex).\n * Implementations are expected to cache; the client may call this once per\n * authenticated request.\n *\n * For an `audience` (public-link) cap, which binds no single subject, also\n * return `pubHex` \u2014 the redeemer's own Ed25519 pubkey matching `devEdPrivHex`.\n * The client then sends it as `X-Starfish-Pub` so the server can verify the\n * request signature against it and check the cap's `aud` allow-list. Omit\n * `pubHex` for device/member caps (the server uses `cap.sub`).\n */\n getCap(): Promise<{\n cap: CapCert\n devEdPrivHex: string\n pubHex?: string\n }>\n}\n\n/**\n * A minimal async key-value store the client uses as a read-through cache for\n * {@link StarfishClient.pull} (offline-first reads). Host-provided so the SDK\n * stays storage-agnostic \u2014 back it by `localStorage`, `AsyncStorage`, a file,\n * etc. Shaped like a subset of zustand's `StateStorage` so an existing adapter\n * fits.\n *\n * IMPORTANT \u2014 what gets stored: the client caches the RAW server response only\n * (`data`/`hash`/`timestamp`). For E2E (`delegated`) collections that payload is\n * the SEALED ciphertext the server holds \u2014 never the decrypted form \u2014 so this\n * cache is ciphertext-at-rest by construction. Decryption always happens in\n * memory on read (see {@link SyncManager}). Public/plaintext collections cache\n * their plaintext, exactly as the server stores it.\n */\nexport interface PullCache {\n /** Return the previously-stored string for `key`, or null if absent. Must not throw. */\n get(key: string): Promise<string | null>\n /** Store `value` under `key`. Must not throw (failures are swallowed by the client). */\n set(key: string, value: string): Promise<void>\n}\n\n/** Options for creating a StarfishClient. */\nexport interface StarfishClientOptions {\n /** Base URL of the Starfish server (e.g. \"https://api.example.com/v1\"). */\n baseUrl: string\n /**\n * Optional namespace for a namespace-mounted server. When set, every request\n * path `/{action}/\u2026` is rewritten to `/v1/{namespace}/{action}/\u2026` for BOTH the\n * URL the client hits AND the canonical path it signs, so the signature the\n * server reconstructs from the namespaced URL verifies (no rewrite layer\n * needed). Mirrors the Python client's `namespace` parameter.\n *\n * Crucially this also rewrites the paths that namespace-unaware SDK helpers\n * build internally (e.g. `starfish-keyring`'s `addCollectionRecipient`, blob\n * uploads), so consumers no longer hand-prefix paths or wrap the client to\n * reach a namespaced deployment. Leave unset (default) for a root-mounted\n * server \u2014 paths pass through unchanged, byte-identical to before.\n *\n * Pass the bare namespace name (e.g. `\"octochat\"`); `baseUrl` then carries only\n * the origin (and any reverse-proxy mount the proxy strips), not the `/v1`\n * version segment. Must match `[A-Za-z0-9_-]+` and not be a reserved route name\n * (`pull`, `push`, `health`, `batch`).\n */\n namespace?: string\n /**\n * Cap-cert provider. When set, requests are signed with Ed25519 and carry\n * `Authorization: Cap <\u2026>`. Omit for unauthenticated public-read collections.\n */\n capProvider?: StarfishCapProvider\n /** Optional fetch implementation (defaults to global fetch). */\n fetch?: typeof fetch\n /**\n * Optional read-through cache for {@link StarfishClient.pull} \u2014 the basis for\n * offline-first reads. When set, every successful non-append pull is written\n * through to the cache (keyed by document path), and a pull that fails because\n * the TRANSPORT is unreachable (offline / DNS / timeout \u2014 `fetch` rejects)\n * falls back to the cached response, tagged so callers can tell it's stale.\n *\n * A real HTTP error (404/403/5xx) is a genuine server answer and always\n * propagates \u2014 the cache is NOT consulted \u2014 so \"no document yet\" and\n * \"access denied\" keep their meaning. Caches ciphertext for E2E collections\n * (the server only ever holds sealed payloads); never decrypted data.\n */\n cache?: PullCache\n /**\n * Optional max age (ms) for {@link cache} entries. An entry older than this is\n * treated as a cache MISS on every read \u2014 both cache-first paint and the\n * offline fallback \u2014 so a stale-beyond-policy snapshot is never served (the\n * pull then goes to the network, or rethrows the transport error offline).\n * Each cached snapshot records its write time; expiry is `now - cachedAt >\n * cacheMaxAgeMs`. Omit (default) for entries that never expire \u2014 recommended\n * for an offline-first app where any last-synced data beats none.\n */\n cacheMaxAgeMs?: number\n /**\n * HTTP status codes for which a structured `pull()` falls back to the\n * last-synced cached snapshot rather than throwing `StarfishHttpError` \u2014\n * a **stale-while-revalidate** strategy for transient server failures.\n *\n * When a pull returns one of these statuses AND a {@link cache} is configured\n * AND a cached snapshot exists for the document, `pull()` returns the cached\n * result immediately (tagged stale via `pullWasFromCache`) and spawns a\n * background revalidation loop that retries until it gets a live response.\n * On success the fresh snapshot is written through and {@link onRevalidated}\n * fires. When no cached snapshot exists the error propagates as usual.\n *\n * Applies only to structured (non-append) pulls. Do NOT include `403` or `404`\n * \u2014 they are genuine server answers (access denied / no document yet).\n *\n * Default `undefined` \u2014 every non-2xx status throws as before.\n *\n * Recommended set for offline-first apps: `[429, 500, 502, 503, 504]`.\n */\n cacheFallbackStatuses?: number[]\n /**\n * Called after a background revalidation succeeds following a\n * {@link cacheFallbackStatuses} hit: the server returned a live response and\n * the fresh snapshot has been written through to the cache.\n *\n * Use this to signal to the host app that the server is reachable again (e.g.\n * call `reportReachability(true)`) so any stale views can re-pull and recover.\n *\n * `path` is the namespaced document path sent to the server (namespace prefix\n * + path + query string, matching the key under which the snapshot was cached).\n */\n onRevalidated?: (path: string, result: PullResult) => void\n /**\n * Optional list of client-side plugins. The list is stored on the client\n * instance but does not fire any hooks yet \u2014 the contract is plumbed so\n * extension packages (`starfish-identities`, `starfish-keyring`,\n * `starfish-sharing`, \u2026) can register against it later without a breaking\n * API change.\n *\n * The current set of hooks is purposely empty; extensions that need to\n * react to mint events or transport actions today can wrap the client\n * directly. Future hook additions will be additive.\n */\n plugins?: ClientPlugin[]\n}\n\n/**\n * Client-side plugin contract.\n *\n * A placeholder shape: the interface intentionally has no required hooks\n * yet; extensions declare a plugin object with `name` and opt into\n * specific lifecycle hooks once those exist. Apps wire plugins via\n * `new StarfishClient({ plugins: [...] })`.\n */\nexport interface ClientPlugin {\n /** Human-readable name. Used in error messages and audit output. */\n name: string\n /**\n * Reserved for future hook fields. Plugins typically declare only\n * `name`. Hook additions are additive \u2014 extensions implementing a\n * future hook will populate the relevant optional property without\n * affecting existing zero-hook plugins.\n */\n}\n\n/** Conflict resolver: given local and remote data, return merged result. */\nexport type ConflictResolver = (\n local: Record<string, unknown>,\n remote: Record<string, unknown>\n) => Record<string, unknown>\n", "/**\n * Parse a `Retry-After` header value into milliseconds.\n *\n * - Numeric string (`\"30\"`) \u2014 treated as seconds \u00D7 1000.\n * - HTTP-date string \u2014 delta from now in ms (floored to 0).\n * - `null`, empty, or unparseable \u2014 returns `opts.fallbackMs`.\n *\n * All results are clamped to `[0, opts.maxMs]`.\n */\nexport function parseRetryAfterMs(\n header: string | null | undefined,\n opts: { fallbackMs: number; maxMs: number },\n): number {\n const { fallbackMs, maxMs } = opts\n const trimmed = header?.trim()\n if (trimmed) {\n const seconds = Number(trimmed)\n if (!isNaN(seconds)) return Math.min(seconds * 1000, maxMs)\n const date = Date.parse(trimmed)\n if (!isNaN(date)) return Math.min(Math.max(date - Date.now(), 0), maxMs)\n }\n return Math.min(fallbackMs, maxMs)\n}\n\n/** Error category returned by classifyError. */\nexport type ErrorCategory =\n | \"network\"\n | \"auth\"\n | \"conflict\"\n | \"rate-limited\"\n | \"server\"\n | \"client\"\n | \"unknown\"\n\n/** Classify an error from a fetch response or network failure. */\nexport function classifyError(err: unknown): ErrorCategory {\n if (err instanceof Response || (err && typeof err === \"object\" && \"status\" in err)) {\n const status = (err as { status: unknown }).status\n if (typeof status !== \"number\" || isNaN(status)) return \"unknown\"\n if (status === 0) return \"network\"\n if (status === 401 || status === 403) return \"auth\"\n if (status === 409) return \"conflict\"\n if (status === 429) return \"rate-limited\"\n if (status >= 500) return \"server\"\n if (status >= 400) return \"client\"\n }\n if (err instanceof Error && /failed to fetch|fetch failed|network|load failed|ECONNREFUSED|ENOTFOUND/i.test(err.message)) return \"network\"\n return \"unknown\"\n}\n\nexport interface RetryOptions {\n /** Max number of retries (default: 3). */\n maxRetries?: number\n /** Initial delay in ms before first retry (default: 500). */\n initialDelayMs?: number\n /** Maximum delay in ms (default: 10000). */\n maxDelayMs?: number\n}\n\n/**\n * Wraps a fetch function with automatic retry for retriable errors\n * (network failures, 429, 5xx). Respects Retry-After headers.\n */\nexport function createRetryFetch(options?: RetryOptions): typeof globalThis.fetch {\n const maxRetries = Math.max(0, options?.maxRetries ?? 3)\n const initialDelay = options?.initialDelayMs ?? 500\n const maxDelay = options?.maxDelayMs ?? 10_000\n\n return async (input, init?) => {\n let attempt = 0\n while (true) {\n try {\n const res = await globalThis.fetch(input, init)\n if (res.ok || attempt >= maxRetries) return res\n\n const category = classifyError(res)\n if (category !== \"rate-limited\" && category !== \"server\") return res\n\n const retryAfterHeader = res.headers.get(\"Retry-After\")\n const exponentialDelay = Math.min(initialDelay * Math.pow(2, attempt), maxDelay)\n // When the header is present but unparseable, original falls back to\n // initialDelay (not exponential). Preserve that by checking presence first.\n const delay = parseRetryAfterMs(retryAfterHeader, {\n fallbackMs: retryAfterHeader?.trim() ? initialDelay : exponentialDelay,\n maxMs: maxDelay,\n })\n\n await new Promise<void>((r) => setTimeout(r, delay))\n attempt++\n } catch (err) {\n if (attempt >= maxRetries) throw err\n const category = classifyError(err)\n if (category !== \"network\") throw err\n\n const delay = Math.min(initialDelay * Math.pow(2, attempt), maxDelay)\n await new Promise<void>((r) => setTimeout(r, delay))\n attempt++\n }\n }\n }\n}\n\ntype BreakerState = \"closed\" | \"open\" | \"half-open\"\n\nexport interface CircuitBreakerOptions {\n /** Number of consecutive failures to open the circuit (default: 5). */\n threshold?: number\n /** Cooldown in ms before transitioning from open to half-open (default: 30000). */\n cooldownMs?: number\n}\n\n/** Circuit breaker that prevents requests when the backend is unavailable. */\nexport class CircuitBreaker {\n private state: BreakerState = \"closed\"\n private failures = 0\n private openedAt = 0\n private readonly threshold: number\n private readonly cooldownMs: number\n\n constructor(options?: CircuitBreakerOptions) {\n this.threshold = options?.threshold ?? 5\n this.cooldownMs = options?.cooldownMs ?? 30_000\n }\n\n getState(): BreakerState {\n this.maybeTransition()\n return this.state\n }\n\n isOpen(): boolean {\n return this.getState() === \"open\"\n }\n\n recordSuccess(): void {\n this.failures = 0\n this.state = \"closed\"\n }\n\n recordFailure(): void {\n this.failures++\n if (this.state === \"half-open\" || this.failures >= this.threshold) {\n this.state = \"open\"\n this.openedAt = Date.now()\n }\n }\n\n private maybeTransition(): void {\n if (this.state === \"open\" && Date.now() - this.openedAt >= this.cooldownMs) {\n this.state = \"half-open\"\n }\n }\n}\n\n/**\n * Wraps fetch to gzip-compress string request bodies using the CompressionStream API.\n * Adds Content-Encoding: gzip header. Non-string bodies (ArrayBuffer, Blob, etc.)\n * are passed through uncompressed. Requires CompressionStream (browsers, Node.js 18+, Deno).\n */\nexport function createCompressedFetch(inner?: typeof globalThis.fetch): typeof globalThis.fetch {\n const baseFetch = inner ?? globalThis.fetch.bind(globalThis)\n return async (input, init?) => {\n if (!init?.body || typeof CompressionStream === \"undefined\") {\n return baseFetch(input, init)\n }\n\n const bodyText = typeof init.body === \"string\" ? init.body : null\n if (!bodyText) return baseFetch(input, init)\n\n try {\n const stream = new Blob([bodyText]).stream().pipeThrough(new CompressionStream(\"gzip\"))\n const compressed = await new Response(stream).arrayBuffer()\n\n const normalized = Object.fromEntries(new Headers(init.headers as HeadersInit).entries())\n normalized[\"content-encoding\"] = \"gzip\"\n\n return baseFetch(input, {\n ...init,\n body: compressed,\n headers: normalized,\n })\n } catch {\n return baseFetch(input, init)\n }\n }\n}\n\n/**\n * Combines retry and circuit breaker into a single resilient fetch wrapper.\n * Rejects immediately when the circuit is open.\n */\nexport function createResilientFetch(\n retryOptions?: RetryOptions,\n breakerOptions?: CircuitBreakerOptions,\n): { fetch: typeof globalThis.fetch; breaker: CircuitBreaker } {\n const breaker = new CircuitBreaker(breakerOptions)\n const retryFetch = createRetryFetch(retryOptions)\n\n const resilientFetch: typeof globalThis.fetch = async (input, init?) => {\n if (breaker.isOpen()) {\n const cooldown = Math.ceil((breakerOptions?.cooldownMs ?? 30_000) / 1000)\n throw new Error(`Request blocked: too many consecutive failures. Retry in ${cooldown}s.`)\n }\n\n try {\n const res = await retryFetch(input, init)\n if (res.status >= 500) {\n breaker.recordFailure()\n } else {\n breaker.recordSuccess()\n }\n return res\n } catch (err) {\n breaker.recordFailure()\n throw err\n }\n }\n\n return { fetch: resilientFetch, breaker }\n}\n", "import type { PullResult } from \"@drakkar.software/starfish-protocol\"\nimport {\n AUTHOR_PUBKEY_FIELD,\n AUTHOR_SIGNATURE_FIELD,\n PUSH_PATH_PREFIX,\n deepMerge,\n docAuthorCanonicalInput,\n getBase64,\n type AppendAuthor,\n} from \"@drakkar.software/starfish-protocol\"\nimport type { ConflictResolver } from \"./types.js\"\nimport { ConflictError } from \"./types.js\"\nimport type { Encryptor } from \"@drakkar.software/starfish-protocol\"\nimport { StarfishClient, stripPushPrefix, pullWasFromCache } from \"./client.js\"\nimport type { SyncLogger } from \"./logger.js\"\nimport type { Validator } from \"./validate.js\"\nimport { ValidationError } from \"./validate.js\"\n\nexport class AbortError extends Error {\n constructor() {\n super(\"SyncManager was aborted\")\n this.name = \"AbortError\"\n }\n}\n\n/**\n * v3.0 author-signature plumbing for `SyncManager`.\n *\n * Returns the device's Ed25519 public key (hex) and a function that signs\n * arbitrary payload bytes. `SyncManager` calls `getSigner()` once per push\n * and uses the returned `sign` to produce a base64-encoded signature over\n * the canonical stringification of the encrypted payload (sans author fields).\n *\n * Implementations typically wrap the same Ed25519 private key used by\n * `StarfishCapProvider` so that `cap.sub === devEdPubHex`.\n */\nexport interface SyncSigner {\n /**\n * Returns the device's `cap.sub` (Ed25519 pubkey, hex) and a payload signer.\n * The `sign` function receives the canonical signing input bytes and must\n * return the raw 64-byte Ed25519 signature.\n */\n getSigner(): Promise<{ devEdPubHex: string; sign(payload: Uint8Array): Promise<Uint8Array> }>\n}\n\n\nexport interface SyncManagerOptions {\n client: StarfishClient\n pullPath: string\n pushPath: string\n /** Custom conflict resolver. Defaults to remote-wins deep merge. Arrays are atomic. */\n onConflict?: ConflictResolver\n /** Max conflict retry attempts (default: 3). */\n maxRetries?: number\n /**\n * Encryptor for client-side E2E encryption. For v3 `delegated` collections,\n * build it via `createKeyringEncryptor(keyring, deviceKemKeys)`.\n */\n encryptor?: Encryptor\n /**\n * v3 author-signature plumbing. When set, every push attaches\n * `authorPubkey` (= `cap.sub`) and `authorSignature` (= base64 Ed25519 over\n * stable-stringify of the encrypted payload minus author fields).\n */\n signer?: SyncSigner\n /** Structured logger for sync events. */\n logger?: SyncLogger\n /** Name passed to logger methods (default: derived from pullPath). */\n loggerName?: string\n /** Validate data before push. Throws ValidationError on failure. */\n validate?: Validator\n}\n\nexport class SyncManager {\n private readonly client: StarfishClient\n private readonly pullPath: string\n private readonly pushPath: string\n private readonly onConflict: ConflictResolver\n private readonly maxRetries: number\n private readonly encryptor: Encryptor | null\n private readonly signer?: SyncSigner\n private readonly logger?: SyncLogger\n private readonly loggerName: string\n private readonly validate?: Validator\n\n private lastHash: string | null = null\n private lastCheckpoint: number = 0\n private localData: Record<string, unknown> = {}\n private aborted: boolean = false\n private lastFromCache: boolean = false\n\n constructor(options: SyncManagerOptions) {\n this.client = options.client\n this.pullPath = options.pullPath\n this.pushPath = options.pushPath\n this.onConflict = options.onConflict ?? deepMerge\n this.maxRetries = options.maxRetries ?? 3\n this.signer = options.signer\n this.logger = options.logger\n this.loggerName = options.loggerName ?? options.pullPath.split(\"/\").filter(Boolean).pop() ?? options.pullPath\n this.validate = options.validate\n this.encryptor = options.encryptor ?? null\n }\n\n abort(): void {\n this.aborted = true\n }\n\n get isAborted(): boolean {\n return this.aborted\n }\n\n getData(): Record<string, unknown> {\n return { ...this.localData }\n }\n\n /**\n * Merge a remote snapshot with local (optimistic) data using this manager's\n * conflict resolver \u2014 the same resolver the push-conflict path uses. A plain\n * {@link pull} overwrites the store's data with the server snapshot, which\n * would drop un-pushed local writes (they live only in the store, never in\n * `localData` until a push succeeds). The zustand binding calls this on pull\n * while the store is dirty so those writes survive. `local` wins by the same\n * rules as a push conflict.\n */\n resolve(\n local: Record<string, unknown>,\n remote: Record<string, unknown>,\n ): Record<string, unknown> {\n return this.onConflict(local, remote)\n }\n\n getHash(): string | null {\n return this.lastHash\n }\n\n /** Set the last-known server hash. Used by persistence layers to restore state across restarts. */\n setHash(hash: string | null): void {\n this.lastHash = hash\n }\n\n /**\n * Whether the most recent {@link pull} (or {@link seedFromCache}) was served\n * from the client's offline read-through cache rather than a live server\n * response. The binding surfaces this as a `stale` flag so the UI can show an\n * offline indicator without treating a cache hit as \"reachable\". Reset to\n * false by the next successful network pull.\n */\n getLastPullFromCache(): boolean {\n return this.lastFromCache\n }\n\n /**\n * Cache-first paint: seed `localData` from the client's read-through cache\n * WITHOUT touching the network, decrypting in memory for E2E collections.\n * Returns whether anything was seeded (false on a miss, an expired entry, or\n * a decrypt failure \u2014 e.g. keyring skew). Call once on store creation before\n * the initial live {@link pull}, which then supersedes the seeded snapshot.\n * Requires the client to have been built with a `cache`.\n */\n async seedFromCache(): Promise<boolean> {\n if (this.aborted) return false\n const cached = await this.client.peekCache(this.pullPath)\n if (!cached) return false\n let data: Record<string, unknown>\n try {\n data = this.encryptor ? await this.encryptor.decrypt(cached.data) : cached.data\n } catch {\n return false // undecryptable (keyring skew / foreign epoch) \u2014 seed nothing\n }\n if (this.aborted) return false\n this.localData = data\n this.lastHash = cached.hash\n // Leave lastCheckpoint at 0 so the first live pull is a full (re)sync, not a\n // delta against a stale cached checkpoint.\n this.lastFromCache = true\n return true\n }\n\n getCheckpoint(): number {\n return this.lastCheckpoint\n }\n\n async pull(): Promise<PullResult> {\n if (this.aborted) throw new AbortError()\n this.logger?.pullStart(this.loggerName)\n const start = performance.now()\n try {\n // NOTE: `SyncManager.pull` does NOT auto-enable `withKeyring`. Clients\n // that drive the keyring helpers from `recipients.ts` and want to save\n // the cold-start round-trip should call `client.pull(path, {withKeyring: true})`\n // directly. We keep `SyncManager` keyring-agnostic so it stays usable\n // for collections that don't use delegated encryption.\n const result = await this.client.pull(this.pullPath, this.lastCheckpoint)\n if (this.aborted) throw new AbortError()\n // True when the client served this from its offline cache (transport was\n // unreachable); a live response clears it. Surfaced as `stale` by the binding.\n this.lastFromCache = pullWasFromCache(result)\n\n if (this.encryptor) {\n const decrypted = await this.encryptor.decrypt(result.data)\n if (this.aborted) throw new AbortError()\n this.localData = decrypted\n result.data = decrypted\n } else if (this.lastCheckpoint > 0) {\n this.localData = deepMerge(this.localData, result.data)\n result.data = this.localData\n } else {\n this.localData = result.data\n }\n\n this.lastHash = result.hash\n this.lastCheckpoint = result.timestamp\n this.logger?.pullSuccess(this.loggerName, Math.round(performance.now() - start))\n return result\n } catch (err) {\n this.logger?.pullError(this.loggerName, err instanceof Error ? err.message : String(err))\n throw err\n }\n }\n\n async push(data: Record<string, unknown>): Promise<{ hash: string; timestamp: number }> {\n if (this.aborted) throw new AbortError()\n if (this.validate) {\n const result = this.validate(data)\n if (result !== true) throw new ValidationError(result)\n }\n this.logger?.pushStart(this.loggerName)\n const start = performance.now()\n let attempt = 0\n let pendingData = data\n\n while (attempt <= this.maxRetries) {\n try {\n const sealed = this.encryptor\n ? await this.encryptor.encrypt(pendingData)\n : pendingData\n if (this.aborted) throw new AbortError()\n\n // v3.0 signer path: sign the document author proof over the doc-author\n // canonical input (domain-tagged, bound to documentKey) and pass it as\n // top-level body siblings of `data` (NOT inside `data`), where the server\n // verifies it and stores the raw author pubkey.\n let author: AppendAuthor | undefined\n if (this.signer) {\n const { devEdPubHex, sign } = await this.signer.getSigner()\n if (this.aborted) throw new AbortError()\n const documentKey = stripPushPrefix(this.pushPath)\n const canonical = docAuthorCanonicalInput(documentKey, sealed as Record<string, unknown>)\n const sigBytes = await sign(new TextEncoder().encode(canonical))\n if (this.aborted) throw new AbortError()\n author = {\n [AUTHOR_PUBKEY_FIELD]: devEdPubHex,\n [AUTHOR_SIGNATURE_FIELD]: getBase64().encode(sigBytes),\n }\n }\n\n const result = await this.client.push(\n this.pushPath,\n sealed as Record<string, unknown>,\n this.lastHash,\n author,\n )\n if (this.aborted) throw new AbortError()\n this.lastHash = result.hash\n this.lastCheckpoint = result.timestamp\n this.localData = pendingData\n this.logger?.pushSuccess(this.loggerName, Math.round(performance.now() - start))\n return result\n } catch (err) {\n if (err instanceof AbortError) throw err\n if (!(err instanceof ConflictError) || attempt >= this.maxRetries) {\n this.logger?.pushError(this.loggerName, err instanceof Error ? err.message : String(err))\n throw err\n }\n this.logger?.conflict(this.loggerName, attempt + 1)\n try {\n const remote = await this.client.pull(this.pullPath)\n if (this.aborted) throw new AbortError()\n const remoteData = this.encryptor\n ? await this.encryptor.decrypt(remote.data)\n : remote.data\n if (this.aborted) throw new AbortError()\n this.lastHash = remote.hash\n this.lastCheckpoint = remote.timestamp\n pendingData = this.onConflict(pendingData, remoteData)\n } catch (resolveErr) {\n if (resolveErr instanceof AbortError) throw resolveErr\n const msg = resolveErr instanceof Error ? resolveErr.message : String(resolveErr)\n this.logger?.pushError(this.loggerName, `Conflict resolution failed (attempt ${attempt + 1}): ${msg}`)\n throw resolveErr\n }\n await new Promise<void>(resolve => setTimeout(resolve, Math.min(100 * Math.pow(2, attempt), 2000) + Math.random() * 100))\n attempt++\n }\n }\n throw new ConflictError()\n }\n\n async update(\n modifier: (current: Record<string, unknown>) => Record<string, unknown>\n ): Promise<{ hash: string; timestamp: number }> {\n await this.pull()\n const updated = modifier(this.localData)\n return this.push(updated)\n }\n}\n", "/** Validation result: true if valid, or an array of error messages. */\nexport type ValidationResult = true | string[]\n\n/** A function that validates data before push. */\nexport type Validator = (data: Record<string, unknown>) => ValidationResult\n\n/** Error thrown when pre-push validation fails. */\nexport class ValidationError extends Error {\n constructor(public readonly errors: string[]) {\n super(`Validation failed: ${errors.join(\"; \")}`)\n this.name = \"ValidationError\"\n }\n}\n\n/**\n * Creates a validator from a JSON Schema object.\n * Requires an Ajv-compatible validate function.\n *\n * @example\n * ```ts\n * import Ajv from \"ajv\"\n * const ajv = new Ajv()\n * const validator = createSchemaValidator(ajv, mySchema)\n * ```\n */\nexport function createSchemaValidator(\n ajv: { compile: (schema: object) => { (data: unknown): boolean; errors?: unknown }; errorsText: (errors?: unknown) => string },\n schema: object,\n): Validator {\n const validate = ajv.compile(schema)\n return (data) => {\n if (validate(data)) return true\n return [ajv.errorsText(validate.errors)]\n }\n}\n", "import {\n verifyAppendAuthor,\n type Encryptor,\n} from \"@drakkar.software/starfish-protocol\"\nimport type { StarfishClient, AppendPullOptions } from \"./client.js\"\nimport type { SyncLogger } from \"./logger.js\"\n\n/** The `/pull/` action prefix; mirrors `PUSH_PATH_PREFIX` for the read side. */\nconst PULL_PATH_PREFIX = \"/pull/\"\n\n/** The storage `documentKey` for a pull `path`: the path with the `/pull/`\n * action prefix stripped (the namespace lives only in the URL). The author\n * signature binds to this key, so a reader re-derives it the same way the\n * writer did from `/push/\u2026`. */\nfunction stripPullPrefix(path: string): string {\n return path.startsWith(PULL_PATH_PREFIX) ? path.slice(PULL_PATH_PREFIX.length) : path\n}\n\n/**\n * A single stored element of an append-only collection, exactly as returned by\n * `client.pull(path, { appendField })`. `ts` is the server-assigned, strictly\n * increasing element timestamp; `data` is the payload (plaintext under the\n * `\"none\"` encryption mode, the opaque encryptor wrapper under `\"delegated\"`).\n *\n * When an {@link AppendLogCursor} is given an `encryptor`, the elements it\n * stores and returns carry the **decrypted** `data` while preserving `ts` and\n * the author fields \u2014 so the shape is uniform and re-seedable. The exception is\n * `persistEncrypted` mode (see {@link AppendLogCursorOptions.persistEncrypted}),\n * where the stored elements keep their **ciphertext** `data` (E2EE-safe to\n * persist) and decryption happens only on read via {@link AppendLogCursor.pull}\n * and {@link AppendLogCursor.getDecryptedItems}.\n */\nexport interface AppendElement {\n ts: number\n data: Record<string, unknown>\n authorPubkey?: string\n authorSignature?: string\n}\n\n/** Per-element author-signature verification policy for {@link AppendLogCursor}. */\nexport interface AuthorVerifier {\n /** If set, every element's `authorPubkey` MUST equal this key (compared as\n * case-insensitive hex), else the pull fails. Omit to accept any signing key\n * (verify only that the signature is valid for the element's self-declared\n * `authorPubkey` \u2014 see the `verifyAuthor` note on restricting authors). */\n expectedAuthorPubkey?: string\n}\n\n/**\n * What to do when a single element fails verification or decryption during a\n * {@link AppendLogCursor.pull} (or {@link AppendLogCursor.getDecryptedItems}).\n *\n * - `\"throw\"` (default): the pull is **atomic** \u2014 the first bad element throws\n * and NO state is mutated, so the checkpoint never advances past an element\n * that could not be re-fetched. Use when every element must be readable.\n * - `\"skip\"`: a bad element is dropped from the returned/decrypted batch and the\n * checkpoint still **advances past it** (so it is never re-fetched), letting one\n * poison element fail without blanking the whole log. Intended for tolerating\n * **decrypt** failures in a multi-writer / E2EE log (keyring skew, a foreign or\n * corrupt element). SECURITY NOTE: `\"skip\"` ALSO silently drops elements that\n * fail *author* verification rather than failing loudly \u2014 so if you also need\n * strict authorship, set `verifyAuthor.expectedAuthorPubkey` (single author) or\n * check each element's `authorPubkey` against your authorized set after pull.\n */\nexport type ElementErrorPolicy = \"throw\" | \"skip\"\n\nexport interface AppendLogCursorOptions {\n client: StarfishClient\n /** Pull endpoint path, e.g. `\"/pull/events\"`. */\n pullPath: string\n /** Array field name in the pulled document. Defaults to `\"items\"`. */\n appendField?: string\n /**\n * Warm-start seed: raw envelopes the caller persisted last session. The\n * cursor adopts them verbatim and derives its initial checkpoint from their\n * max `ts`. Under the default mode they are taken as already-decrypted (and\n * never re-decrypted/re-verified); under `persistEncrypted` they are the\n * persisted **ciphertext** and are decrypted on read (see `persistEncrypted`).\n */\n initialItems?: AppendElement[]\n /**\n * Explicit checkpoint-only seed (ms). Resume incrementally without\n * rehydrating history. When given together with `initialItems`, it must be\n * `>= max(ts of initialItems)` (a lower value would re-fetch held items).\n */\n since?: number\n /**\n * When set, each freshly-pulled element's `.data` is decrypted via this\n * encryptor (the `ts`/author fields are preserved). Author verification, when\n * enabled, runs over the original (pre-decryption) `data`.\n *\n * Caveat (default mode): a returned / `getItems()` element then holds DECRYPTED\n * `data` but an `authorSignature` computed over the stored CIPHERTEXT \u2014 they no\n * longer match, so do NOT re-verify a decrypted element with `verifyAppendAuthor`.\n * The cursor already verified it (over the ciphertext) at pull time when\n * `verifyAuthor` is on; `authorPubkey` is retained for identity. (Under\n * `persistEncrypted` the stored elements keep their ciphertext, so this caveat\n * does not apply to `getItems()`.)\n */\n encryptor?: Encryptor\n /**\n * Per-element failure policy for verification/decryption. Defaults to\n * `\"throw\"` (atomic pull \u2014 preserves the pre-existing behavior). See\n * {@link ElementErrorPolicy}.\n */\n onElementError?: ElementErrorPolicy\n /**\n * Keep the **ciphertext** form of each element in the cursor's accumulated log\n * instead of the decrypted form (requires `encryptor`; a no-op without one,\n * since plaintext is already its own stored form). This makes\n * {@link AppendLogCursor.getItems} return the persistable ciphertext \u2014 safe to\n * write to disk for an end-to-end-encrypted log without leaking plaintext at\n * rest \u2014 while {@link AppendLogCursor.pull} still returns the freshly-decrypted\n * batch and {@link AppendLogCursor.getDecryptedItems} returns the full log\n * decrypted (for warm-start rendering). Defaults to `false` (store decrypted).\n */\n persistEncrypted?: boolean\n /**\n * `true` to verify every element's author signature, or a policy object.\n *\n * This verifies the signature is valid for the element's self-declared\n * `authorPubkey` \u2014 it does NOT by itself restrict WHICH authors are accepted.\n * To restrict authorship, set `expectedAuthorPubkey` (single author), or check\n * each `el.authorPubkey` against your own authorization source (keyring /\n * member list / cap allow-list) after pull \u2014 for a multi-writer log, the\n * authorized set lives there and changes over time, not here.\n *\n * The signature covers `data` + the document key, but NOT `ts`: a malicious\n * server cannot forge content, but can reorder or re-timestamp authentic\n * elements, so trust `ts` only as far as you trust the server.\n */\n verifyAuthor?: boolean | AuthorVerifier\n /** Structured logger for pull events. */\n logger?: SyncLogger\n /** Name passed to logger methods (default: derived from `pullPath`). */\n loggerName?: string\n}\n\n/** Thrown when an append element's author signature fails verification. */\nexport class AppendAuthorError extends Error {\n constructor(public readonly ts: number) {\n super(`append element author verification failed (ts=${ts})`)\n this.name = \"AppendAuthorError\"\n }\n}\n\n/** Largest `ts` among `items`, or `0` when empty. The checkpoint for an\n * append-only log is exactly this \u2014 the server returns elements with\n * `ts > checkpoint`, and element timestamps are strictly increasing. */\nexport function checkpointOf(items: readonly { ts: number }[]): number {\n let max = 0\n for (const it of items) if (it.ts > max) max = it.ts\n return max\n}\n\n/** Copy the optional author fields from `src` onto a fresh element with `data`. */\nfunction withAuthor(ts: number, data: Record<string, unknown>, src: AppendElement): AppendElement {\n const out: AppendElement = { ts, data }\n if (src.authorPubkey !== undefined) out.authorPubkey = src.authorPubkey\n if (src.authorSignature !== undefined) out.authorSignature = src.authorSignature\n return out\n}\n\n/**\n * A stateful cursor over an append-only collection. It owns the accumulated\n * array of elements and pulls only what is new: each {@link pull} derives the\n * checkpoint from the last element it holds and asks the server for elements\n * with a greater `ts`.\n *\n * This is the incremental, stateful counterpart to the deliberately stateless\n * `client.pull(path, { appendField, since })`, and the sibling of\n * {@link SyncManager} for append-only logs (no merge / push-conflict\n * machinery \u2014 a log only grows).\n *\n * The cursor accumulates every pulled element in memory; for an unboundedly\n * large log, pull a bounded window with raw `client.pull(path, { last })` instead.\n *\n * Cold start (nothing persisted) \u2014 first `pull()` fetches the whole collection:\n * ```ts\n * const log = new AppendLogCursor({ client, pullPath: \"/pull/events\" })\n * const all = await log.pull()\n * ```\n * Warm start (resume from persisted data) \u2014 first `pull()` fetches only newer\n * elements; persistence is a round-trip of `getItems()`:\n * ```ts\n * const log = new AppendLogCursor({ client, pullPath: \"/pull/events\",\n * initialItems: await store.load() })\n * const fresh = await log.pull()\n * await store.save(log.getItems())\n * ```\n * Warm start for an **E2EE** log \u2014 persist ciphertext, render decrypted:\n * ```ts\n * const log = new AppendLogCursor({ client, pullPath: \"/pull/streamchat\",\n * encryptor, persistEncrypted: true, onElementError: \"skip\",\n * initialItems: await store.load() }) // ciphertext from disk\n * const history = await log.getDecryptedItems() // render persisted history\n * const fresh = await log.pull() // decrypted delta\n * await store.save(log.getItems()) // ciphertext back to disk\n * ```\n */\nexport class AppendLogCursor {\n private readonly client: StarfishClient\n private readonly pullPath: string\n private readonly appendField: string\n private readonly encryptor?: Encryptor\n private readonly verifyAuthor?: boolean | AuthorVerifier\n private readonly onElementError: ElementErrorPolicy\n private readonly persistEncrypted: boolean\n private readonly documentKey: string\n private readonly logger?: SyncLogger\n private readonly loggerName: string\n\n private readonly items: AppendElement[]\n private lastCheckpoint: number\n\n /** Tail of the serialized pull chain. Concurrent `pull()` calls queue behind\n * it so each runs against the checkpoint the previous one advanced \u2014 no two\n * overlapping fetches read the same checkpoint and double-append a window. */\n private pullChain: Promise<unknown> = Promise.resolve()\n\n constructor(options: AppendLogCursorOptions) {\n this.client = options.client\n this.pullPath = options.pullPath\n this.appendField = options.appendField ?? \"items\"\n this.encryptor = options.encryptor\n this.verifyAuthor = options.verifyAuthor\n this.onElementError = options.onElementError ?? \"throw\"\n this.persistEncrypted = options.persistEncrypted ?? false\n this.documentKey = stripPullPrefix(options.pullPath)\n this.logger = options.logger\n this.loggerName =\n options.loggerName ?? options.pullPath.split(\"/\").filter(Boolean).pop() ?? options.pullPath\n\n const seed = options.initialItems ?? []\n const seedCheckpoint = checkpointOf(seed)\n if (options.since != null) {\n if (options.since < 0) throw new Error(\"since must be non-negative\")\n if (options.since < seedCheckpoint) {\n throw new Error(\"since must be >= the max ts of initialItems\")\n }\n this.lastCheckpoint = options.since\n } else {\n this.lastCheckpoint = seedCheckpoint\n }\n this.items = [...seed]\n }\n\n /**\n * Fetch elements newer than the current checkpoint, verify + decrypt them,\n * append them to the local log, and return ONLY the newly-fetched batch\n * (decrypted when an `encryptor` is set).\n *\n * Atomic under `onElementError: \"throw\"` (the default): the batch is fully\n * verified and decrypted into a local before any state mutation, so a\n * verify/decrypt failure throws without advancing the checkpoint past elements\n * that could never be re-fetched. Under `\"skip\"`, a failing element is dropped\n * from the returned batch but the checkpoint still advances past it.\n *\n * Safe to call concurrently: overlapping calls are serialized internally, so\n * each runs against the checkpoint the previous one advanced (no double-fetch\n * of the same window). The next pull after one completes will pick up anything\n * that arrived in between.\n */\n async pull(): Promise<AppendElement[]> {\n // Chain onto the previous pull (whether it resolved or rejected) so calls\n // run one-at-a-time against the latest checkpoint. `pullChain` swallows\n // outcomes to stay alive; the caller still sees this call's real result.\n const run = this.pullChain.then(\n () => this.doPull(),\n () => this.doPull(),\n )\n this.pullChain = run.then(\n () => undefined,\n () => undefined,\n )\n return run\n }\n\n private async doPull(): Promise<AppendElement[]> {\n this.logger?.pullStart(this.loggerName)\n const start = performance.now()\n try {\n const since = this.lastCheckpoint\n // Cold start (`since === 0`) wants the whole log: send an explicit\n // `?full=true` since the server now rejects an unbounded append-only pull.\n // Warm resume carries `?checkpoint=since`.\n const opts: AppendPullOptions =\n since > 0 ? { appendField: this.appendField, since } : { appendField: this.appendField, full: true }\n const raw = await this.client.pull<AppendElement>(this.pullPath, opts)\n\n const batch: AppendElement[] = [] // decrypted, returned to the caller\n const stored: AppendElement[] = [] // what we keep in `items` (cipher- or plaintext)\n let maxTs = since\n let skipped = 0\n for (const el of raw) {\n // Defensive: guard a misbehaving/mocked server from making us\n // double-append a held element. Gated on `since > 0` to mirror the\n // server (which only filters when checkpoint > 0): on a cold start\n // `since` is 0 and we must NOT drop a legitimate `ts: 0` first element.\n if (since > 0 && el.ts <= since) continue\n // Advance past every windowed element BEFORE verify/decrypt so a skipped\n // element still moves the checkpoint and is never re-fetched.\n if (el.ts > maxTs) maxTs = el.ts\n\n let decrypted: AppendElement | null = null\n try {\n this.verifyOne(el)\n const data = this.encryptor ? await this.encryptor.decrypt(el.data) : el.data\n decrypted = withAuthor(el.ts, data, el)\n } catch (err) {\n // \"throw\" rethrows here, before any state mutation below \u2014 atomic.\n if (this.onElementError !== \"skip\") throw err\n skipped++\n }\n\n if (this.persistEncrypted) {\n // Keep the original ciphertext envelope (even for a skipped element:\n // it is valid data we simply cannot read now \u2014 a later key might).\n stored.push(withAuthor(el.ts, el.data, el))\n } else if (decrypted) {\n stored.push(decrypted)\n }\n if (decrypted) batch.push(decrypted)\n }\n\n this.items.push(...stored)\n this.lastCheckpoint = maxTs\n this.logger?.pullSuccess(\n this.loggerName,\n Math.round(performance.now() - start),\n skipped > 0 ? { skippedCount: skipped } : undefined,\n )\n return batch\n } catch (err) {\n this.logger?.pullError(this.loggerName, err instanceof Error ? err.message : String(err))\n throw err\n }\n }\n\n /** Verify a single element's author signature over its RAW (pre-decryption)\n * `data`. Throws {@link AppendAuthorError} on any failure. No-op when\n * verification is disabled. */\n private verifyOne(el: AppendElement): void {\n if (!this.verifyAuthor) return\n const policy: AuthorVerifier = typeof this.verifyAuthor === \"object\" ? this.verifyAuthor : {}\n const { authorPubkey, authorSignature } = el\n if (!authorPubkey || !authorSignature) throw new AppendAuthorError(el.ts)\n // Public keys are hex, which is case-insensitive \u2014 compare normalized so a\n // caller passing a differently-cased `expectedAuthorPubkey` isn't falsely rejected.\n if (\n policy.expectedAuthorPubkey &&\n authorPubkey.toLowerCase() !== policy.expectedAuthorPubkey.toLowerCase()\n ) {\n throw new AppendAuthorError(el.ts)\n }\n void policy\n const ok = verifyAppendAuthor(\n this.documentKey,\n el.data,\n authorPubkey,\n authorSignature,\n )\n if (!ok) throw new AppendAuthorError(el.ts)\n }\n\n /** The full accumulated log (a shallow copy), in `ts` order. Under\n * `persistEncrypted` these carry CIPHERTEXT `data` (persist them as-is, then\n * re-seed via `initialItems`); otherwise they carry decrypted/plaintext data. */\n getItems(): AppendElement[] {\n return [...this.items]\n }\n\n /**\n * The full accumulated log, DECRYPTED \u2014 for rendering warm-started history in\n * `persistEncrypted` mode (where {@link getItems} holds ciphertext). Honors\n * `onElementError` (a `\"skip\"` cursor drops elements it cannot read). When the\n * cursor has no `encryptor`, or is not in `persistEncrypted` mode, the held\n * elements are already plaintext/decrypted and are returned as-is.\n */\n async getDecryptedItems(): Promise<AppendElement[]> {\n const snapshot = [...this.items]\n if (!this.encryptor || !this.persistEncrypted) return snapshot\n const out: AppendElement[] = []\n for (const el of snapshot) {\n try {\n this.verifyOne(el)\n const data = await this.encryptor.decrypt(el.data)\n out.push(withAuthor(el.ts, data, el))\n } catch (err) {\n if (this.onElementError !== \"skip\") throw err\n }\n }\n return out\n }\n\n /** The current checkpoint: the max `ts` held (the next pull's `since`). `0`\n * when nothing has been pulled or seeded. */\n getCheckpoint(): number {\n return this.lastCheckpoint\n }\n\n /** Restore the checkpoint without seeding items \u2014 for persistence layers that\n * store only the checkpoint. Used to resume incrementally across restarts.\n * Rejects a value below the max `ts` already held: rewinding would make the\n * next pull re-deliver, and duplicate, elements the cursor already has. */\n setCheckpoint(ts: number): void {\n if (ts < checkpointOf(this.items)) {\n throw new Error(\"checkpoint must be >= the max ts already held\")\n }\n this.lastCheckpoint = ts\n }\n}\n", "/** Extended metrics for sync operations. */\nexport interface SyncMetrics {\n bytesTransferred?: number\n compressedSize?: number\n conflictCount?: number\n retryCount?: number\n cacheHit?: boolean\n /** Elements an append-log pull dropped under `onElementError: \"skip\"`\n * (failed verification/decryption). Omitted when none were skipped. */\n skippedCount?: number\n}\n\n/** Structured logger for sync operations. */\nexport interface SyncLogger {\n pullStart(store: string): void\n pullSuccess(store: string, durationMs: number, metrics?: SyncMetrics): void\n pullError(store: string, error: string): void\n pushStart(store: string): void\n pushSuccess(store: string, durationMs: number, metrics?: SyncMetrics): void\n pushError(store: string, error: string): void\n conflict(store: string, attempt: number): void\n}\n\n/** Console-based sync logger with structured output. */\nexport const consoleSyncLogger: SyncLogger = {\n pullStart: (s) => console.log(`[starfish:${s}] pull started`),\n pullSuccess: (s, ms, m) => {\n let msg = `[starfish:${s}] pull OK (${ms}ms)`\n if (m?.bytesTransferred) msg += ` ${m.bytesTransferred}B`\n if (m?.cacheHit) msg += ` (cache hit)`\n console.log(msg)\n },\n pullError: (s, err) => console.error(`[starfish:${s}] pull failed: ${err}`),\n pushStart: (s) => console.log(`[starfish:${s}] push started`),\n pushSuccess: (s, ms, m) => {\n let msg = `[starfish:${s}] push OK (${ms}ms)`\n if (m?.bytesTransferred) msg += ` ${m.bytesTransferred}B`\n console.log(msg)\n },\n pushError: (s, err) => console.error(`[starfish:${s}] push failed: ${err}`),\n conflict: (s, n) => console.warn(`[starfish:${s}] conflict (attempt ${n})`),\n}\n\n/** Silent sync logger (no output). */\nexport const noopSyncLogger: SyncLogger = {\n pullStart: () => {},\n pullSuccess: () => {},\n pullError: () => {},\n pushStart: () => {},\n pushSuccess: () => {},\n pushError: () => {},\n conflict: () => {},\n}\n\n/** Accumulated metrics for a single store. */\ninterface StoreSummary {\n totalPulls: number\n totalPushes: number\n totalDurationMs: number\n totalBytes: number\n totalConflicts: number\n}\n\n/** Collects sync metrics over time. */\nexport interface MetricsCollector {\n recordPull(name: string, durationMs: number, metrics?: SyncMetrics): void\n recordPush(name: string, durationMs: number, metrics?: SyncMetrics): void\n recordConflict(name: string): void\n getSummary(): Record<string, { totalPulls: number; totalPushes: number; avgDurationMs: number; totalBytes: number; totalConflicts: number }>\n reset(): void\n}\n\n/** Create a metrics collector that accumulates sync statistics. */\nexport function createMetricsCollector(): MetricsCollector {\n const stores = new Map<string, StoreSummary>()\n\n function ensureStore(name: string): StoreSummary {\n let s = stores.get(name)\n if (!s) {\n s = { totalPulls: 0, totalPushes: 0, totalDurationMs: 0, totalBytes: 0, totalConflicts: 0 }\n stores.set(name, s)\n }\n return s\n }\n\n return {\n recordPull(name, durationMs, metrics) {\n const s = ensureStore(name)\n s.totalPulls++\n s.totalDurationMs += durationMs\n if (metrics?.bytesTransferred) s.totalBytes += metrics.bytesTransferred\n },\n recordPush(name, durationMs, metrics) {\n const s = ensureStore(name)\n s.totalPushes++\n s.totalDurationMs += durationMs\n if (metrics?.bytesTransferred) s.totalBytes += metrics.bytesTransferred\n },\n recordConflict(name) {\n ensureStore(name).totalConflicts++\n },\n getSummary() {\n const result: Record<string, { totalPulls: number; totalPushes: number; avgDurationMs: number; totalBytes: number; totalConflicts: number }> = {}\n for (const [name, s] of stores) {\n const totalOps = s.totalPulls + s.totalPushes\n result[name] = {\n totalPulls: s.totalPulls,\n totalPushes: s.totalPushes,\n avgDurationMs: totalOps > 0 ? Math.round(s.totalDurationMs / totalOps) : 0,\n totalBytes: s.totalBytes,\n totalConflicts: s.totalConflicts,\n }\n }\n return result\n },\n reset() {\n stores.clear()\n },\n }\n}\n", "/** A function that migrates data from one schema version to the next. */\nexport type MigrationFn = (data: Record<string, unknown>) => Record<string, unknown>\n\nexport interface MigrationConfig {\n /** The current schema version of the application. */\n currentVersion: number\n /** Map of version number to the migration that upgrades FROM that version. */\n migrations: Record<number, MigrationFn>\n}\n\n/**\n * Creates a migration runner that upgrades documents to the current schema version.\n *\n * Given a document with `_schemaVersion`, applies each migration in sequence\n * until the document reaches `currentVersion`. Throws if the document version\n * is ahead of the app (forward compatibility guard).\n */\nexport function createMigrator(\n config: MigrationConfig,\n): (data: Record<string, unknown>) => Record<string, unknown> {\n // Eagerly validate the migration chain\n for (let v = 1; v < config.currentVersion; v++) {\n if (!config.migrations[v]) {\n throw new Error(`Missing migration for version ${v} -> ${v + 1}`)\n }\n }\n\n return (data) => {\n const version = typeof data._schemaVersion === \"number\" ? data._schemaVersion : 1\n\n if (version > config.currentVersion) {\n throw new Error(\n `Document schema version ${version} is newer than app version ${config.currentVersion}. Update the app.`,\n )\n }\n\n if (version === config.currentVersion) return data\n\n let result = { ...data }\n for (let v = version; v < config.currentVersion; v++) {\n const fn = config.migrations[v]\n if (!fn) {\n throw new Error(`Missing migration for version ${v} -> ${v + 1}`)\n }\n try {\n result = fn(result)\n } catch (err) {\n throw new Error(\n `Migration from version ${v} to ${v + 1} failed: ${err instanceof Error ? err.message : String(err)}`,\n { cause: err },\n )\n }\n }\n result._schemaVersion = config.currentVersion\n return result\n }\n}\n", "import type { ConflictResolver } from \"./types.js\"\n\n/** Metadata about which fields were affected during conflict resolution. */\nexport interface ConflictMeta {\n /** Field names that differed between local and remote. */\n conflictedFields: string[]\n /** How the conflict was resolved. */\n resolvedBy: \"local\" | \"remote\" | \"merged\"\n /** Timestamp of resolution. */\n timestamp: number\n}\n\n/** Conflict resolver that also returns metadata about the resolution. */\nexport type ConflictResolverWithMeta = (\n local: Record<string, unknown>,\n remote: Record<string, unknown>,\n) => { data: Record<string, unknown>; meta: ConflictMeta }\n\n/** Shallow structural comparison of two values. Handles objects, arrays, and primitives. */\nfunction shallowEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true\n if (a == null || b == null) return a === b\n if (typeof a !== typeof b) return false\n if (typeof a !== \"object\") return false\n\n if (Array.isArray(a) !== Array.isArray(b)) return false\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false\n return a.every((v, i) => shallowEqual(v, b[i]))\n }\n\n const aObj = a as Record<string, unknown>\n const bObj = b as Record<string, unknown>\n const aKeys = Object.keys(aObj)\n const bKeys = Object.keys(bObj)\n if (aKeys.length !== bKeys.length) return false\n return aKeys.every((k) => shallowEqual(aObj[k], bObj[k]))\n}\n\n/**\n * Wrap a standard ConflictResolver to also return metadata about which fields conflicted.\n * Compares local and remote keys to detect differing fields.\n */\nexport function withConflictMeta(resolver: ConflictResolver): ConflictResolverWithMeta {\n return (local, remote) => {\n const conflictedFields: string[] = []\n const allKeys = new Set([...Object.keys(local), ...Object.keys(remote)])\n for (const key of allKeys) {\n const lv = local[key]\n const rv = remote[key]\n if (!shallowEqual(lv, rv)) {\n conflictedFields.push(key)\n }\n }\n\n const data = resolver(local, remote)\n\n // Determine how it was resolved using structural comparison\n let resolvedBy: \"local\" | \"remote\" | \"merged\" = \"merged\"\n if (shallowEqual(data, local)) resolvedBy = \"local\"\n else if (shallowEqual(data, remote)) resolvedBy = \"remote\"\n\n return {\n data,\n meta: {\n conflictedFields,\n resolvedBy,\n timestamp: Date.now(),\n },\n }\n }\n}\n\n/** Compare two timestamp values. Handles both numeric (epoch) and string (ISO-8601) timestamps. */\nfunction compareTimestamps(a: unknown, b: unknown): boolean {\n if (typeof a === \"number\" && typeof b === \"number\") return a >= b\n return String(a ?? \"\") >= String(b ?? \"\")\n}\n\n/**\n * Creates a conflict resolver that merges arrays by ID with per-item\n * timestamp comparison, and uses document-level timestamp for scalars.\n *\n * For arrays: builds a union of both sets keyed by `idKey`. When both\n * sides have the same item, the one with the newer `timestampKey` wins.\n * For scalars: the document with the newer `documentTimestampKey` wins.\n *\n * @example\n * ```ts\n * const merge = createUnionMerge()\n * const sync = new SyncManager({ ..., onConflict: merge })\n * ```\n */\nexport function createUnionMerge(options?: {\n /** Key used to identify items in arrays (default: \"id\"). */\n idKey?: string\n /** Key used for per-item timestamp comparison (default: \"updatedAt\"). */\n timestampKey?: string\n /** Key used for document-level timestamp comparison (default: \"timestamp\"). */\n documentTimestampKey?: string\n}): ConflictResolver {\n const idKey = options?.idKey ?? \"id\"\n const tsKey = options?.timestampKey ?? \"updatedAt\"\n const docTsKey = options?.documentTimestampKey ?? \"timestamp\"\n\n return (local, remote) => {\n const result: Record<string, unknown> = {}\n const localNewer = compareTimestamps(local[docTsKey], remote[docTsKey])\n const allKeys = new Set([...Object.keys(local), ...Object.keys(remote)])\n\n for (const key of allKeys) {\n const lv = local[key]\n const rv = remote[key]\n\n // Both sides have arrays \u2014 attempt ID-based union\n if (Array.isArray(lv) && Array.isArray(rv)) {\n const map = new Map<unknown, Record<string, unknown>>()\n\n // Seed with remote items\n for (const item of rv) {\n if (item && typeof item === \"object\" && idKey in item) {\n map.set((item as Record<string, unknown>)[idKey], item as Record<string, unknown>)\n } else {\n map.set(Symbol(), item as Record<string, unknown>)\n }\n }\n\n // Overlay local items (per-item timestamp wins)\n for (const item of lv) {\n if (item && typeof item === \"object\" && idKey in item) {\n const localItem = item as Record<string, unknown>\n const id = localItem[idKey]\n const remoteItem = map.get(id)\n if (!remoteItem) {\n map.set(id, localItem)\n } else {\n if (compareTimestamps(localItem[tsKey], remoteItem[tsKey])) {\n map.set(id, localItem)\n }\n }\n } else {\n map.set(Symbol(), item as Record<string, unknown>)\n }\n }\n\n result[key] = [...map.values()]\n } else if (lv !== undefined && rv !== undefined) {\n // Scalar: document-level timestamp wins\n result[key] = localNewer ? lv : rv\n } else {\n // Only one side has the key\n result[key] = lv ?? rv\n }\n }\n\n return result\n }\n}\n\n/**\n * Creates a conflict resolver that handles soft-deleted items (tombstones).\n * Extends union merge with tombstone awareness: if an item exists on one side\n * with a `deletedAtKey` set, that deletion is respected even if the other side\n * still has the item alive \u2014 as long as the deletion timestamp is newer.\n */\nexport function createSoftDeleteResolver(options?: {\n idKey?: string\n timestampKey?: string\n documentTimestampKey?: string\n /** Key marking an item as deleted (default: \"_deletedAt\"). */\n deletedAtKey?: string\n}): ConflictResolver {\n const idKey = options?.idKey ?? \"id\"\n const tsKey = options?.timestampKey ?? \"updatedAt\"\n const deletedAtKey = options?.deletedAtKey ?? \"_deletedAt\"\n const baseMerge = createUnionMerge(options)\n\n return (local, remote) => {\n const merged = baseMerge(local, remote)\n\n // Build a tombstone map from both sides: id \u2192 deletedAt timestamp\n const tombstones = new Map<unknown, unknown>()\n for (const source of [local, remote]) {\n for (const key of Object.keys(source)) {\n const arr = source[key]\n if (!Array.isArray(arr)) continue\n for (const item of arr) {\n if (item && typeof item === \"object\" && idKey in item && deletedAtKey in item) {\n const rec = item as Record<string, unknown>\n const id = rec[idKey]\n const deletedAt = rec[deletedAtKey]\n if (typeof deletedAt === \"number\" || typeof deletedAt === \"string\") {\n const existing = tombstones.get(id)\n if (existing == null || compareTimestamps(deletedAt, existing)) tombstones.set(id, deletedAt)\n }\n }\n }\n }\n }\n\n // For merged arrays, ensure tombstoned items stay deleted\n // (don't resurrect an item if its tombstone is newer than its updatedAt)\n for (const key of Object.keys(merged)) {\n const value = merged[key]\n if (!Array.isArray(value)) continue\n\n merged[key] = value.filter((item) => {\n if (!item || typeof item !== \"object\" || !(idKey in item)) return true\n const rec = item as Record<string, unknown>\n const id = rec[idKey]\n const deletedAt = tombstones.get(id)\n if (deletedAt == null) return true\n // Keep the item if it has a deletedAt (it's the tombstone itself)\n if (rec[deletedAtKey] != null) return true\n // Filter out alive items that have a newer tombstone\n return compareTimestamps(rec[tsKey], deletedAt) && rec[tsKey] !== deletedAt\n })\n }\n\n return merged\n }\n}\n\n/**\n * Simple resolver: the document with the newer timestamp wins entirely.\n * No per-field or per-item merging.\n */\nexport function timestampWinner(\n timestampKey = \"timestamp\",\n): ConflictResolver {\n return (local, remote) => {\n return compareTimestamps(local[timestampKey], remote[timestampKey])\n ? local\n : remote\n }\n}\n\n/**\n * Remove expired tombstones from an array of items.\n * Items with a `deletedAtKey` older than `ttlMs` are pruned.\n *\n * @param items - Array of items, some with a deletedAt timestamp\n * @param ttlMs - Time-to-live in ms for tombstones (default: 30 days)\n * @param deletedAtKey - Key marking deletion timestamp (default: \"_deletedAt\")\n */\nexport function pruneTombstones<T extends Record<string, unknown>>(\n items: T[],\n ttlMs = 30 * 24 * 60 * 60 * 1000,\n deletedAtKey = \"_deletedAt\",\n): T[] {\n const cutoff = Date.now() - ttlMs\n return items.filter((item) => {\n const deletedAt = item[deletedAtKey]\n if (deletedAt == null) return true\n if (typeof deletedAt === \"number\") return deletedAt > cutoff\n if (typeof deletedAt === \"string\") return new Date(deletedAt).getTime() > cutoff\n return false\n })\n}\n", "export interface Snapshot {\n timestamp: number\n label: string\n data: string\n}\n\nexport interface SnapshotHistoryOptions {\n /** Maximum number of snapshots to retain. Oldest are trimmed first. Default: 20. */\n maxSnapshots?: number\n /** localStorage key for persistence. Pass to enable auto-save/load. */\n storageKey?: string\n}\n\nexport class SnapshotHistory {\n private snapshots: Snapshot[] = []\n private readonly maxSnapshots: number\n private readonly storageKey: string | undefined\n\n constructor(options?: SnapshotHistoryOptions) {\n this.maxSnapshots = options?.maxSnapshots ?? 20\n this.storageKey = options?.storageKey\n\n if (this.storageKey) {\n try {\n const raw = localStorage.getItem(this.storageKey)\n if (raw) {\n const parsed = JSON.parse(raw)\n if (Array.isArray(parsed)) this.snapshots = parsed\n }\n } catch { /* corrupted or unavailable \u2014 start fresh */ }\n }\n }\n\n /** Take a labeled snapshot of the given data. */\n take(label: string, data: Record<string, unknown>): void {\n this.snapshots.push({\n timestamp: Date.now(),\n label,\n data: JSON.stringify(data),\n })\n if (this.snapshots.length > this.maxSnapshots) {\n this.snapshots = this.snapshots.slice(-this.maxSnapshots)\n }\n this.persist()\n }\n\n /** Restore data from a snapshot at the given index. Returns undefined if index is invalid or data is corrupt. */\n restore(index: number): Record<string, unknown> | undefined {\n const snapshot = this.snapshots[index]\n if (!snapshot) return undefined\n try {\n return JSON.parse(snapshot.data)\n } catch {\n return undefined\n }\n }\n\n /** List available snapshots (metadata only, no data payload). */\n list(): Array<{ timestamp: number; label: string }> {\n return this.snapshots.map(({ timestamp, label }) => ({ timestamp, label }))\n }\n\n /** Clear all snapshots. */\n clear(): void {\n this.snapshots = []\n this.persist()\n }\n\n private persist(): void {\n if (!this.storageKey) return\n try {\n localStorage.setItem(this.storageKey, JSON.stringify(this.snapshots))\n } catch { /* quota exceeded \u2014 skip silently */ }\n }\n}\n", "/** Minimal state needed by polling utilities. */\nexport interface PollableState {\n online: boolean\n syncing: boolean\n}\n\nconst DEFAULT_INTERVALS: Record<string, number> = {\n \"slow-2g\": 120_000,\n \"2g\": 60_000,\n \"3g\": 30_000,\n \"4g\": 10_000,\n}\n\nconst DEFAULT_FALLBACK_MS = 15_000\n\n/**\n * Start periodic pulling at a fixed interval.\n * Skips pulls when offline or already syncing.\n * Returns a cleanup function that stops polling.\n */\nexport function startPolling(\n pullFn: () => Promise<void>,\n getState: () => PollableState,\n intervalMs = 30_000,\n): () => void {\n const timer = setInterval(() => {\n const { online, syncing } = getState()\n if (online && !syncing) pullFn().catch((err) => { console.error(\"[Starfish] poll failed:\", err) })\n }, intervalMs)\n\n return () => clearInterval(timer)\n}\n\nexport interface AdaptivePollingOptions {\n /** Override the base interval in ms. If set, skips network quality detection. */\n intervalMs?: number\n /** Custom mapping from effectiveType to interval in ms. */\n intervals?: Record<string, number>\n}\n\nexport interface AdaptivePollingControls {\n pause: () => void\n resume: () => void\n stop: () => void\n}\n\n/**\n * Start polling with adaptive intervals based on network quality.\n * Uses the Network Information API (`navigator.connection.effectiveType`) when available.\n * Returns controls to pause, resume, or stop polling.\n */\nexport function startAdaptivePolling(\n pullFn: () => Promise<void>,\n getState: () => PollableState,\n options?: AdaptivePollingOptions,\n): AdaptivePollingControls {\n let intervalMs: number\n\n if (options?.intervalMs != null) {\n intervalMs = options.intervalMs\n } else {\n const intervals = options?.intervals ?? DEFAULT_INTERVALS\n let effectiveType: string | undefined\n if (typeof navigator !== \"undefined\" && \"connection\" in navigator) {\n effectiveType = (navigator as unknown as { connection: { effectiveType?: string } }).connection.effectiveType\n }\n intervalMs = (effectiveType != null ? intervals[effectiveType] : undefined) ?? DEFAULT_FALLBACK_MS\n }\n\n let paused = false\n\n const timer = setInterval(() => {\n if (paused) return\n const { online, syncing } = getState()\n if (online && !syncing) pullFn().catch((err) => { console.error(\"[Starfish] adaptive poll failed:\", err) })\n }, intervalMs)\n\n return {\n pause: () => { paused = true },\n resume: () => { paused = false },\n stop: () => clearInterval(timer),\n }\n}\n", "/**\n * Request deduplication: prevents multiple concurrent identical GET requests.\n * If a GET request is in-flight for a URL, subsequent identical GET requests\n * return the same Promise. POST/PUT/DELETE/PATCH are never deduped.\n */\nexport function createDedupFetch(\n baseFetch: typeof globalThis.fetch = globalThis.fetch.bind(globalThis),\n): typeof globalThis.fetch {\n const inflightGets = new Map<string, Promise<Response>>()\n\n return (async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {\n const method = (init?.method ?? \"GET\").toUpperCase()\n\n // Only dedup GET requests\n if (method !== \"GET\") {\n return baseFetch(input, init)\n }\n\n const url = typeof input === \"string\"\n ? input\n : input instanceof URL\n ? input.toString()\n : (input as Request).url\n\n const existing = inflightGets.get(url)\n if (existing) {\n // Return a clone \u2014 the original is reserved for cloning only\n return existing.then((res) => res.clone())\n }\n\n // Store a promise that resolves to a response we keep solely for cloning.\n // The first caller also gets a clone, ensuring the \"master\" body is never consumed.\n const promise = baseFetch(input, init)\n .then((res) => res)\n .finally(() => {\n inflightGets.delete(url)\n })\n\n inflightGets.set(url, promise)\n\n // First caller also gets a clone so the cached response body stays unconsumed\n return promise.then((res) => res.clone())\n }) as typeof globalThis.fetch\n}\n", "/**\n * Read-modify-write a document with hash-CAS conflict retry.\n *\n * The everyday way to atomically edit a synced document: pull the current\n * version, apply a pure `mutator` to its data, push the result with the read\n * hash, and retry on a {@link ConflictError} (a concurrent writer moved the hash)\n * by re-reading FRESH server state and re-applying the mutator. A missing\n * document (404) is surfaced to the mutator as `{ data: null, hash: null }` so it\n * can create the doc on first write.\n *\n * This replaces the ad-hoc `for (attempt\u2026) { pull; mutate; try push catch\n * ConflictError }` loop that applications otherwise hand-roll around every\n * editable doc. The `mutator` MUST be idempotent \u2014 it re-runs on each retry \u2014 and\n * returns `null` to signal a no-op (nothing changed; skip the write).\n */\nimport { StarfishClient } from \"./client.js\"\nimport { ConflictError, StarfishHttpError } from \"./types.js\"\n\n/** The current state handed to a {@link DocMutator}: the document data (or `null`\n * when the doc does not exist yet) and the hash to base the next push on. */\nexport interface DocState<T> {\n data: T | null\n hash: string | null\n}\n\n/**\n * Pure transform from the current document to the next. Return the full next\n * document body to write, or `null` for a no-op (the write is skipped). Runs once\n * per attempt on freshly-pulled state, so it must be idempotent.\n */\nexport type DocMutator<T> = (cur: DocState<T>) => T | null\n\nexport interface MutateDocOptions {\n /** Max push attempts before a persistent conflict propagates. Default 3. */\n maxAttempts?: number\n}\n\n/**\n * Atomically read-modify-write the document at `path`. Returns the document that\n * was written, or `null` if the mutator signalled a no-op. Throws the underlying\n * error on a non-conflict failure, or a {@link ConflictError} if every attempt\n * raced and lost.\n */\nexport async function mutateDoc<T extends Record<string, unknown> = Record<string, unknown>>(\n client: StarfishClient,\n path: string,\n mutator: DocMutator<T>,\n options: MutateDocOptions = {},\n): Promise<T | null> {\n const maxAttempts = Math.max(1, options.maxAttempts ?? 3)\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n let data: T | null = null\n let hash: string | null = null\n try {\n const res = await client.pull(path)\n data = (res.data as T) ?? null\n hash = res.hash ?? null\n } catch (err) {\n // A 404 means the doc does not exist yet \u2014 hand the mutator a null state so\n // it can create it. Any other HTTP/transport error propagates.\n if (!(err instanceof StarfishHttpError) || err.status !== 404) throw err\n }\n const next = mutator({ data, hash })\n if (next === null) return null\n try {\n await client.push(path, next, hash)\n return next\n } catch (err) {\n if (err instanceof ConflictError && attempt < maxAttempts - 1) continue\n throw err\n }\n }\n // Unreachable: the final attempt either returns or re-throws above.\n throw new ConflictError()\n}\n", "/** Encryption modes supported by the Starfish server. */\nexport type EncryptionMode = \"none\" | \"delegated\"\n\n/** Append-only configuration exposed via GET /config. */\nexport interface AppendOnlyClientInfo {\n /** Append-only strategy. Only `\"by_timestamp\"` is currently supported. */\n type: \"by_timestamp\"\n /** Array field name in the stored document. Defaults to \"items\". */\n field?: string\n /** false = no storage write (replaces queueOnly). true/absent = append to array. */\n persist?: boolean\n}\n\n/** Append-only configuration exposed via GET /config. */\nexport interface AppendOnlyClientInfo {\n /** Array field name in the stored document. Defaults to \"items\". */\n field?: string\n /** false = no storage write (replaces queueOnly). true/absent = append to array. */\n persist?: boolean\n /** When true, server validates client's baseHash against hash(lastItem). */\n checkLastItem?: boolean\n}\n\n/** Per-collection metadata returned by GET /config. */\nexport interface CollectionClientInfo {\n name: string\n maxBodyBytes: number\n encryption: EncryptionMode\n allowedMimeTypes: string[]\n pullOnly?: boolean\n pushOnly?: boolean\n appendOnly?: AppendOnlyClientInfo\n ttlMs?: number\n forceFullFetch?: boolean\n}\n\n/** Response shape of GET /config. */\nexport interface ConfigResponse {\n collections: CollectionClientInfo[]\n namespaces?: Record<string, { collections: CollectionClientInfo[] }>\n}\n\n/**\n * Fetch the server's collection manifest from GET /config.\n *\n * @param baseUrl - Base URL of the Starfish server (e.g. `\"https://api.example.com/v1\"`).\n * @param options.headers - Optional request headers (e.g. `Authorization`).\n * @throws {Error} if the server returns a non-2xx response.\n */\nexport async function fetchServerConfig(\n baseUrl: string,\n options?: { headers?: Record<string, string> },\n): Promise<ConfigResponse> {\n const url = `${baseUrl.replace(/\\/$/, \"\")}/config`\n const res = await fetch(url, {\n method: \"GET\",\n headers: options?.headers,\n })\n if (!res.ok) {\n throw new Error(`fetchServerConfig: ${res.status} ${res.statusText}`)\n }\n return res.json() as Promise<ConfigResponse>\n}\n", "/**\n * IndexedDB-based storage adapter for Zustand persistence.\n * Implements the same interface as Zustand's StateStorage (getItem/setItem/removeItem).\n * Supports larger data than localStorage (typically 50MB+).\n */\n\nexport interface IndexedDBStorageOptions {\n /** Database name. Default: \"starfish\" */\n dbName?: string\n /** Object store name. Default: \"state\" */\n storeName?: string\n}\n\nexport interface AsyncStateStorage {\n getItem: (name: string) => Promise<string | null>\n setItem: (name: string, value: string) => Promise<void>\n removeItem: (name: string) => Promise<void>\n}\n\nfunction openDB(dbName: string, storeName: string): Promise<IDBDatabase> {\n return new Promise((resolve, reject) => {\n const request = indexedDB.open(dbName, 1)\n request.onupgradeneeded = () => {\n const db = request.result\n if (!db.objectStoreNames.contains(storeName)) {\n db.createObjectStore(storeName)\n }\n }\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => reject(request.error)\n })\n}\n\nfunction idbRequest<T>(request: IDBRequest<T>): Promise<T> {\n return new Promise((resolve, reject) => {\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => reject(request.error)\n })\n}\n\nexport function createIndexedDBStorage(\n opts?: IndexedDBStorageOptions,\n): AsyncStateStorage {\n const dbName = opts?.dbName ?? \"starfish\"\n const storeName = opts?.storeName ?? \"state\"\n let dbPromise: Promise<IDBDatabase> | null = null\n\n function getDB(): Promise<IDBDatabase> {\n if (!dbPromise) {\n dbPromise = openDB(dbName, storeName).catch((err) => {\n dbPromise = null // Reset so next call retries\n throw err\n })\n }\n return dbPromise\n }\n\n return {\n async getItem(name: string): Promise<string | null> {\n const db = await getDB()\n const tx = db.transaction(storeName, \"readonly\")\n const store = tx.objectStore(storeName)\n const result = await idbRequest(store.get(name))\n return result ?? null\n },\n\n async setItem(name: string, value: string): Promise<void> {\n const db = await getDB()\n const tx = db.transaction(storeName, \"readwrite\")\n const store = tx.objectStore(storeName)\n await idbRequest(store.put(value, name))\n },\n\n async removeItem(name: string): Promise<void> {\n const db = await getDB()\n const tx = db.transaction(storeName, \"readwrite\")\n const store = tx.objectStore(storeName)\n await idbRequest(store.delete(name))\n },\n }\n}\n", "/**\n * Data export/import helpers for Starfish sync data.\n * Supports JSON and CSV formats.\n */\n\nexport interface ExportOptions {\n /** Output format. Default: \"json\" */\n format?: \"json\" | \"csv\"\n /** Pretty-print JSON output. Default: false */\n pretty?: boolean\n}\n\n/**\n * Export data to a string representation.\n * JSON: serializes the full object.\n * CSV: flattens top-level keys into columns. Array values are JSON-encoded.\n */\nexport function exportData(\n data: Record<string, unknown>,\n opts?: ExportOptions,\n): string {\n const format = opts?.format ?? \"json\"\n\n if (format === \"json\") {\n return opts?.pretty\n ? JSON.stringify(data, null, 2)\n : JSON.stringify(data)\n }\n\n // CSV export: each top-level key becomes a column\n return toCsv(data)\n}\n\n/**\n * Import data from a string representation.\n */\nexport function importData(\n raw: string,\n format: \"json\" | \"csv\" = \"json\",\n): Record<string, unknown> {\n if (format === \"json\") {\n const parsed = JSON.parse(raw)\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n throw new Error(\"Expected a JSON object\")\n }\n return parsed as Record<string, unknown>\n }\n\n return fromCsv(raw)\n}\n\n/**\n * Export data to a Blob suitable for download.\n */\nexport function exportToBlob(\n data: Record<string, unknown>,\n opts?: ExportOptions,\n): Blob {\n const format = opts?.format ?? \"json\"\n const content = exportData(data, opts)\n const mimeType = format === \"csv\" ? \"text/csv;charset=utf-8\" : \"application/json;charset=utf-8\"\n return new Blob([content], { type: mimeType })\n}\n\nfunction toCsv(data: Record<string, unknown>): string {\n const keys = Object.keys(data)\n const header = keys.map(escapeCsvField).join(\",\")\n\n const values = keys.map((k) => {\n const v = data[k]\n if (v === null || v === undefined) return \"\"\n if (typeof v === \"object\") return escapeCsvField(JSON.stringify(v))\n return escapeCsvField(String(v))\n })\n\n return `${header}\\n${values.join(\",\")}`\n}\n\nfunction fromCsv(raw: string): Record<string, unknown> {\n const lines = raw.trim().split(\"\\n\")\n if (lines.length < 2) {\n throw new Error(\"CSV must have at least a header row and a data row\")\n }\n\n const headers = parseCsvLine(lines[0]!)\n const values = parseCsvLine(lines[1]!)\n\n const result: Record<string, unknown> = {}\n for (let i = 0; i < headers.length; i++) {\n const key = headers[i]!\n const val = values[i] ?? \"\"\n // Try to parse JSON values\n try {\n result[key] = JSON.parse(val)\n } catch {\n result[key] = val\n }\n }\n return result\n}\n\nfunction escapeCsvField(field: string): string {\n if (field.includes(\",\") || field.includes('\"') || field.includes(\"\\n\")) {\n return `\"${field.replace(/\"/g, '\"\"')}\"`\n }\n return field\n}\n\nfunction parseCsvLine(line: string): string[] {\n const result: string[] = []\n let current = \"\"\n let inQuotes = false\n\n for (let i = 0; i < line.length; i++) {\n const ch = line[i]!\n if (inQuotes) {\n if (ch === '\"' && line[i + 1] === '\"') {\n current += '\"'\n i++\n } else if (ch === '\"') {\n inQuotes = false\n } else {\n current += ch\n }\n } else {\n if (ch === '\"') {\n inQuotes = true\n } else if (ch === \",\") {\n result.push(current)\n current = \"\"\n } else {\n current += ch\n }\n }\n }\n result.push(current)\n return result\n}\n", "/**\n * Background Sync API integration for pending changes.\n * Uses the Web Background Sync API to retry failed sync operations\n * when connectivity is restored, even if the app is closed.\n */\n\nexport interface BackgroundSyncOptions {\n /** Sync event tag. Default: \"starfish-sync\" */\n tag?: string\n}\n\n/** Check if the Background Sync API is supported in the current environment. */\nexport function isBackgroundSyncSupported(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n \"serviceWorker\" in navigator &&\n \"SyncManager\" in globalThis\n )\n}\n\n/**\n * Register a background sync event with the active service worker.\n * Returns true if registration succeeded, false if not supported or no active SW.\n */\nexport async function registerBackgroundSync(\n opts?: BackgroundSyncOptions,\n): Promise<boolean> {\n if (!isBackgroundSyncSupported()) return false\n\n const tag = opts?.tag ?? \"starfish-sync\"\n\n try {\n const registration = await navigator.serviceWorker.ready\n // @ts-expect-error - SyncManager types may not be available\n await registration.sync.register(tag)\n return true\n } catch {\n return false\n }\n}\n", "/**\n * Service Worker utilities for offline support and PWA functionality.\n */\n\nexport interface ServiceWorkerOptions {\n /** Scope for the service worker registration. */\n scope?: string\n /** Called when an updated service worker is available. */\n onUpdate?: (registration: ServiceWorkerRegistration) => void\n}\n\n/** Check if service workers are supported in the current environment. */\nexport function isServiceWorkerSupported(): boolean {\n return typeof navigator !== \"undefined\" && \"serviceWorker\" in navigator\n}\n\n/**\n * Register a service worker for offline support.\n * Returns the registration, or null if not supported.\n */\nexport async function registerServiceWorker(\n scriptUrl: string,\n opts?: ServiceWorkerOptions,\n): Promise<ServiceWorkerRegistration | null> {\n if (!isServiceWorkerSupported()) return null\n\n try {\n const registration = await navigator.serviceWorker.register(scriptUrl, {\n scope: opts?.scope,\n })\n\n if (opts?.onUpdate) {\n registration.onupdatefound = () => {\n const installingWorker = registration.installing\n if (installingWorker) {\n installingWorker.onstatechange = () => {\n if (\n installingWorker.state === \"installed\" &&\n navigator.serviceWorker.controller\n ) {\n opts.onUpdate!(registration)\n }\n }\n }\n }\n }\n\n return registration\n } catch {\n return null\n }\n}\n\n/** Unregister all service worker registrations. Returns true if any were unregistered. */\nexport async function unregisterServiceWorkers(): Promise<boolean> {\n if (!isServiceWorkerSupported()) return false\n\n try {\n const registrations = await navigator.serviceWorker.getRegistrations()\n let unregistered = false\n for (const registration of registrations) {\n const result = await registration.unregister()\n if (result) unregistered = true\n }\n return unregistered\n } catch {\n return false\n }\n}\n", "/**\n * React Suspense integration for Starfish sync data.\n * Creates resources that throw Promises while loading (Suspense protocol).\n */\n\ntype SuspenseStatus = \"pending\" | \"resolved\" | \"rejected\"\n\ninterface SuspenseResource<T> {\n /** Read the resource value. Throws a Promise while pending (Suspense protocol). */\n read(): T\n}\n\n/**\n * Create a Suspense-compatible resource from an async fetcher.\n * The first call to `read()` triggers the fetch. While loading, `read()` throws\n * a Promise (which React Suspense catches to show a fallback). Once resolved,\n * `read()` returns the value synchronously.\n *\n * @example\n * ```tsx\n * const resource = createSuspenseResource(() => syncManager.pull())\n * function MyComponent() {\n * const data = resource.read() // throws while loading, returns data when ready\n * return <div>{JSON.stringify(data)}</div>\n * }\n * ```\n */\nexport function createSuspenseResource<T>(\n fetcher: () => Promise<T>,\n): SuspenseResource<T> {\n let status: SuspenseStatus = \"pending\"\n let result: T\n let error: unknown\n let promise: Promise<void> | null = null\n\n function init(): Promise<void> {\n if (promise) return promise\n promise = fetcher().then(\n (value) => {\n status = \"resolved\"\n result = value\n },\n (err) => {\n status = \"rejected\"\n error = err\n },\n )\n return promise\n }\n\n return {\n read(): T {\n switch (status) {\n case \"pending\":\n throw init()\n case \"resolved\":\n return result\n case \"rejected\":\n throw error\n }\n },\n }\n}\n", "import type { StoreApi } from \"zustand/vanilla\"\nimport type { StarfishStore } from \"./bindings/zustand.js\"\nimport type { SyncManager } from \"./sync.js\"\n\n// \u2500\u2500 Shared types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface DebouncedSyncOptions {\n /**\n * How long to wait after the last `notify()` call before pushing (default: 2000 ms).\n * Shorter values reduce latency; longer values batch more edits into a single push.\n */\n delayMs?: number\n /**\n * Emit a warning when the estimated encrypted payload exceeds this byte count (default: 900 KB).\n * The estimate multiplies the JSON size by 1.34 (base64 overhead for encrypted blobs).\n * Set to `Infinity` to disable.\n */\n warnBytes?: number\n /**\n * Block the push when the estimated encrypted payload exceeds this byte count (default: 1 MB).\n * Prevents cryptic 413 errors from the server. Set to `Infinity` to disable.\n */\n maxBytes?: number\n /**\n * Serialize store data to a sync document before pushing.\n * Called inside the debounce timer, so it always captures the latest state.\n * If omitted, `store.getState().data` is used as-is.\n */\n serialize?: (currentData: Record<string, unknown>) => Record<string, unknown>\n /**\n * Called when the estimated payload size exceeds `warnBytes` but is still below `maxBytes`.\n * Use to show a warning in the UI.\n */\n onSizeWarning?: (estimatedBytes: number) => void\n /**\n * Called when the estimated payload size exceeds `maxBytes`.\n * The push is blocked. Use to alert the user that data needs to be pruned.\n * If omitted, a console error is printed.\n */\n onSizeExceeded?: (estimatedBytes: number) => void\n}\n\nexport interface DebouncedSync {\n /**\n * Schedule a push. If called again within `delayMs`, the timer resets.\n * Safe to call on every domain store mutation.\n */\n notify: () => void\n /** Cancel any pending debounced push. Does not affect an already-in-flight push. */\n cancel: () => void\n}\n\nexport interface DebouncedPushOptions {\n /**\n * How long to wait after the last `notify()` call before pushing (default: 2000 ms).\n */\n delayMs?: number\n /**\n * Required: provides the document to push when the debounce timer fires.\n * Called inside the timer so it always captures the latest state.\n */\n serialize: () => Record<string, unknown>\n /**\n * Emit a warning when the estimated encrypted payload exceeds this byte count (default: 900 KB).\n * Set to `Infinity` to disable.\n */\n warnBytes?: number\n /**\n * Block the push when the estimated encrypted payload exceeds this byte count (default: 1 MB).\n * Set to `Infinity` to disable.\n */\n maxBytes?: number\n /**\n * Called when the estimated payload size exceeds `warnBytes` but is below `maxBytes`.\n */\n onSizeWarning?: (estimatedBytes: number) => void\n /**\n * Called when the estimated payload size exceeds `maxBytes`. The push is blocked.\n * If omitted, a console error is printed.\n */\n onSizeExceeded?: (estimatedBytes: number) => void\n /**\n * Called when `syncManager.push()` throws. Default: `console.warn`.\n */\n onError?: (err: unknown) => void\n}\n\nexport interface DebouncedPush {\n /**\n * Schedule a push. If called again within `delayMs`, the timer resets.\n */\n notify: () => void\n /** Cancel any pending debounced push. Does not affect an already-in-flight push. */\n cancel: () => void\n}\n\n// \u2500\u2500 Implementation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst DEFAULT_DELAY_MS = 2000\nconst DEFAULT_WARN_BYTES = 900 * 1024 // 900 KB\nconst DEFAULT_MAX_BYTES = 1024 * 1024 // 1 MB\n\ninterface SizeGuardOptions {\n warnBytes: number\n maxBytes: number\n onSizeWarning?: (bytes: number) => void\n onSizeExceeded?: (bytes: number) => void\n}\n\n/** Returns true if the push should be blocked. */\nfunction checkPayloadSize(doc: Record<string, unknown>, opts: SizeGuardOptions): boolean {\n // Estimate encrypted payload size. AES-GCM output is similar to input size;\n // base64 encoding adds ~33% overhead, plus a small IV/tag overhead.\n const estimatedBytes = Math.ceil(JSON.stringify(doc).length * 1.34)\n\n if (estimatedBytes > opts.maxBytes) {\n if (opts.onSizeExceeded) {\n opts.onSizeExceeded(estimatedBytes)\n } else {\n console.error(\n `[starfish] Push blocked: estimated payload ${(estimatedBytes / 1024).toFixed(0)} KB ` +\n `exceeds limit of ${(opts.maxBytes / 1024).toFixed(0)} KB. Prune your data before syncing.`,\n )\n }\n return true\n }\n\n if (estimatedBytes > opts.warnBytes) {\n if (opts.onSizeWarning) {\n opts.onSizeWarning(estimatedBytes)\n } else {\n console.warn(\n `[starfish] Payload approaching limit: estimated ${(estimatedBytes / 1024).toFixed(0)} KB ` +\n `(warn threshold: ${(opts.warnBytes / 1024).toFixed(0)} KB).`,\n )\n }\n }\n\n return false\n}\n\n/**\n * Creates a debounced push helper that coalesces rapid mutations into a single sync.\n *\n * Designed to be called on every domain store mutation (e.g., every keystroke).\n * The push is delayed by `delayMs` after the **last** call, so typing quickly\n * results in one push, not one per character.\n *\n * Also estimates the encrypted payload size before pushing and warns / blocks\n * if it approaches the server's body size limit.\n *\n * ```ts\n * const { notify } = createDebouncedSync(starfishStore, {\n * serialize: () => ({ tasks: taskStore.getState().tasks }),\n * })\n *\n * // Call on every domain store mutation:\n * taskStore.subscribe(() => notify())\n * ```\n */\nexport function createDebouncedSync(\n store: StoreApi<StarfishStore>,\n options: DebouncedSyncOptions = {},\n): DebouncedSync {\n const {\n delayMs = DEFAULT_DELAY_MS,\n warnBytes = DEFAULT_WARN_BYTES,\n maxBytes = DEFAULT_MAX_BYTES,\n serialize,\n onSizeWarning,\n onSizeExceeded,\n } = options\n\n let timer: ReturnType<typeof setTimeout> | null = null\n\n function cancel(): void {\n if (timer !== null) {\n clearTimeout(timer)\n timer = null\n }\n }\n\n function notify(): void {\n cancel()\n timer = setTimeout(() => {\n timer = null\n const current = store.getState().data\n const doc = serialize ? serialize(current) : current\n\n if (checkPayloadSize(doc, { warnBytes, maxBytes, onSizeWarning, onSizeExceeded })) return\n\n store.getState().set(() => doc)\n }, delayMs)\n }\n\n return { notify, cancel }\n}\n\n/**\n * Creates a debounced push helper that calls `syncManager.push()` directly,\n * without requiring a Zustand store.\n *\n * Use this for one-way publishing workflows: public pages, derived snapshots,\n * or any case where you want to push data without a full `createStarfishStore` setup.\n *\n * ```ts\n * const syncManager = new SyncManager({ client, pullPath, pushPath })\n *\n * const { notify, cancel } = createDebouncedPush(syncManager, {\n * serialize: () => buildPublicPageDocument(),\n * })\n *\n * // Push after every relevant store mutation:\n * planningStore.subscribe(() => notify())\n *\n * // Clean up on teardown:\n * cancel()\n * ```\n */\nexport function createDebouncedPush(\n syncManager: SyncManager,\n options: DebouncedPushOptions,\n): DebouncedPush {\n const {\n delayMs = DEFAULT_DELAY_MS,\n warnBytes = DEFAULT_WARN_BYTES,\n maxBytes = DEFAULT_MAX_BYTES,\n serialize,\n onSizeWarning,\n onSizeExceeded,\n onError,\n } = options\n\n let timer: ReturnType<typeof setTimeout> | null = null\n\n function cancel(): void {\n if (timer !== null) {\n clearTimeout(timer)\n timer = null\n }\n }\n\n function notify(): void {\n cancel()\n timer = setTimeout(() => {\n timer = null\n const doc = serialize()\n\n if (checkPayloadSize(doc, { warnBytes, maxBytes, onSizeWarning, onSizeExceeded })) return\n\n syncManager.push(doc).catch((err: unknown) => {\n if (onError) {\n onError(err)\n } else {\n console.warn(\"[starfish] Push failed:\", err)\n }\n })\n }, delayMs)\n }\n\n return { notify, cancel }\n}\n", "import type { StoreApi } from \"zustand/vanilla\"\nimport type { StarfishStore, StarfishLogStore } from \"./bindings/zustand.js\"\n\n// \u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Minimal interface matching React Native's `AppState` module.\n * Pass `AppState` from `react-native` directly.\n */\nexport interface AppStateModule {\n addEventListener: (\n type: \"change\",\n listener: (state: string) => void,\n ) => { remove: () => void }\n}\n\n/**\n * Minimal interface matching `@react-native-community/netinfo`'s default export.\n * Pass `NetInfo` from `@react-native-community/netinfo` directly.\n */\nexport interface NetInfoModule {\n addEventListener: (\n listener: (state: { isConnected: boolean | null }) => void,\n ) => () => void\n}\n\nexport interface MobileLifecycleDeps {\n /** React Native `AppState` module. */\n appState: AppStateModule\n /**\n * Optional: NetInfo module from `@react-native-community/netinfo`.\n * When provided, connectivity changes are forwarded to `store.getState().setOnline()`.\n */\n netInfo?: NetInfoModule\n}\n\nexport interface MobileLifecycleOptions {\n /**\n * Pull remote changes when the app returns to the foreground.\n * Only pulls if the store is online and not already syncing.\n * Default: `true`.\n */\n pullOnForeground?: boolean\n /**\n * Flush dirty data when the app transitions to the background.\n * Only flushes if the store has unsaved changes.\n * Default: `true`.\n */\n flushOnBackground?: boolean\n}\n\n// \u2500\u2500 Implementation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Wires React Native app lifecycle events to a Starfish store.\n *\n * - **Background**: flushes pending changes before the OS suspends the app.\n * - **Foreground**: pulls remote changes when the user returns to the app.\n * - **NetInfo**: forwards connectivity changes to `store.getState().setOnline()`.\n *\n * Uses dependency injection so no `react-native` or `netinfo` imports are needed\n * in this package. Pass the modules directly:\n *\n * ```ts\n * import { AppState } from \"react-native\"\n * import NetInfo from \"@react-native-community/netinfo\"\n * import { createMobileLifecycle } from \"@drakkar.software/starfish-client\"\n *\n * // Call once, after the store is created:\n * const cleanup = createMobileLifecycle(\n * store,\n * { appState: AppState, netInfo: NetInfo },\n * )\n *\n * // In a React component (e.g. root layout):\n * useEffect(() => cleanup, [])\n * ```\n *\n * @returns A cleanup function that removes all event listeners.\n */\nexport function createMobileLifecycle(\n store: StoreApi<StarfishStore>,\n deps: MobileLifecycleDeps,\n options: MobileLifecycleOptions = {},\n): () => void {\n const { pullOnForeground = true, flushOnBackground = true } = options\n\n const appSub = deps.appState.addEventListener(\"change\", (appState) => {\n if (appState === \"background\" && flushOnBackground) {\n if (store.getState().dirty) {\n store.getState().flush().catch((err) => { console.error(\"[Starfish] background flush failed:\", err) })\n }\n } else if (appState === \"active\" && pullOnForeground) {\n const { online, syncing } = store.getState()\n if (online && !syncing) {\n store.getState().pull().catch((err) => { console.error(\"[Starfish] foreground pull failed:\", err) })\n }\n }\n // \"inactive\" (iOS transition) and other states are intentionally ignored\n })\n\n let netUnsub: (() => void) | null = null\n if (deps.netInfo) {\n netUnsub = deps.netInfo.addEventListener(({ isConnected }) => {\n store.getState().setOnline(!!isConnected)\n })\n }\n\n return () => {\n appSub.remove()\n netUnsub?.()\n }\n}\n\n// \u2500\u2500 Append-only log lifecycle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface AppendLogLifecycleOptions {\n /**\n * Pull new elements when the app returns to the foreground.\n * Only pulls if the store is online and not already loading.\n * Default: `true`.\n */\n pullOnForeground?: boolean\n}\n\n/**\n * Wires React Native app lifecycle events to an append-log store\n * (`createStarfishLog`). A log is read-only, so this only pulls on foreground\n * (there is nothing to flush on background). NetInfo connectivity changes are\n * forwarded to `store.getState().setOnline()`.\n *\n * ```ts\n * import { AppState } from \"react-native\"\n * import NetInfo from \"@react-native-community/netinfo\"\n * import { createStarfishLog, createAppendLogMobileLifecycle } from \"@drakkar.software/starfish-client\"\n *\n * const store = createStarfishLog({ cursor })\n * const cleanup = createAppendLogMobileLifecycle(store, { appState: AppState, netInfo: NetInfo })\n * useEffect(() => cleanup, [])\n * ```\n *\n * @returns A cleanup function that removes all event listeners.\n */\nexport function createAppendLogMobileLifecycle(\n store: StoreApi<StarfishLogStore>,\n deps: MobileLifecycleDeps,\n options: AppendLogLifecycleOptions = {},\n): () => void {\n const { pullOnForeground = true } = options\n\n const appSub = deps.appState.addEventListener(\"change\", (appState) => {\n if (appState === \"active\" && pullOnForeground) {\n const { online, loading } = store.getState()\n if (online && !loading) {\n store.getState().pull().catch((err) => { console.error(\"[Starfish] foreground log pull failed:\", err) })\n }\n }\n })\n\n let netUnsub: (() => void) | null = null\n if (deps.netInfo) {\n netUnsub = deps.netInfo.addEventListener(({ isConnected }) => {\n store.getState().setOnline(!!isConnected)\n })\n }\n\n return () => {\n appSub.remove()\n netUnsub?.()\n }\n}\n", "// \u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Serializer/deserializer pair for one slice of application state.\n *\n * `serialize` snapshots the current state into a plain object.\n * `restore` applies a snapshot (potentially from a different app version after migration).\n */\nexport interface StoreSlice<T = unknown> {\n /**\n * Snapshot the current state of this slice into a serializable value.\n * Called during `serialize()`.\n */\n serialize: () => T\n /**\n * Apply a snapshot to this slice.\n * Called during `restore()` \u2014 data may be from an older schema version after migration.\n */\n restore: (data: T) => void\n}\n\n/**\n * A versioned backup document produced by `MultiStoreSync.serialize()`.\n * Safe to pass to `store.set()` as the Starfish sync document.\n */\nexport interface BackupDocument<T = Record<string, unknown>> {\n /** Schema version declared in `createMultiStoreSync`. */\n version: number\n /** Unix timestamp (ms) when this backup was created. */\n timestamp: number\n /** Serialized slice data, keyed by slice name. */\n data: T\n}\n\n/**\n * A migration function that transforms data from one version to the next.\n * Receives the full `data` object and must return an updated `data` object.\n * Only the `data` field is passed; `version` and `timestamp` are managed automatically.\n */\nexport type MultiStoreMigrationFn = (data: Record<string, unknown>) => Record<string, unknown>\n\nexport interface MultiStoreSyncOptions<T extends Record<string, unknown>> {\n /**\n * Named slices to include in the backup document.\n * Each slice provides `serialize()` and `restore()` methods.\n *\n * @example\n * ```ts\n * slices: {\n * tasks: {\n * serialize: () => taskStore.getState().tasks,\n * restore: (data) => taskStore.setState({ tasks: data }),\n * },\n * settings: {\n * serialize: () => settingsStore.getState().settings,\n * restore: (data) => settingsStore.setState({ settings: data }),\n * },\n * }\n * ```\n */\n slices: { [K in keyof T]: StoreSlice<T[K]> }\n /**\n * Current schema version. Increment when slices are added, renamed, or their shape changes.\n * Used to detect forward-incompatible documents from future app versions.\n */\n version: number\n /**\n * Optional migration chain. Key is the version number that produced the data;\n * value is a function that upgrades it to the next version.\n *\n * Migrations run sequentially from the document version up to the current version.\n *\n * @example\n * ```ts\n * migrations: {\n * 1: (data) => ({ ...data, settings: { ...data.settings, theme: \"light\" } }),\n * 2: (data) => ({ ...data, tasks: data.todos, todos: undefined }),\n * }\n * ```\n */\n migrations?: Record<number, MultiStoreMigrationFn>\n}\n\n/**\n * Returned by `createMultiStoreSync`. Serialize and restore coordinated multi-store state.\n */\nexport interface MultiStoreSync<T extends Record<string, unknown>> {\n /**\n * Snapshot all slices into a `BackupDocument`.\n * Pass the result to `starfishStore.getState().set(() => multiSync.serialize())`.\n */\n serialize: () => BackupDocument<T>\n /**\n * Apply a `BackupDocument` to all slices, running migrations as needed.\n *\n * Throws if the document version is newer than the current version (forward-incompatible).\n * Silently migrates older documents.\n */\n restore: (doc: BackupDocument) => void\n /** Current schema version as declared in options. */\n readonly version: number\n}\n\n// \u2500\u2500 Implementation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Creates a multi-store sync coordinator.\n *\n * Collects multiple application stores into a single Starfish sync document,\n * with versioned schema migrations for backward compatibility.\n *\n * ```ts\n * const multiSync = createMultiStoreSync({\n * slices: {\n * tasks: {\n * serialize: () => taskStore.getState().tasks,\n * restore: (tasks) => taskStore.setState({ tasks }),\n * },\n * settings: {\n * serialize: () => settingsStore.getState().settings,\n * restore: (settings) => settingsStore.setState({ settings }),\n * },\n * },\n * version: 2,\n * migrations: {\n * // data from version 1 \u2192 upgrade to version 2\n * 1: (data) => ({ ...data, settings: { ...(data.settings as object), darkMode: false } }),\n * },\n * })\n *\n * // Push:\n * starfishStore.getState().set(() => multiSync.serialize())\n *\n * // Restore on pull (pass as onRemoteUpdate to createStarfishStore):\n * createStarfishStore({\n * name: \"app\",\n * syncManager,\n * onRemoteUpdate: (doc) => multiSync.restore(doc as BackupDocument),\n * })\n * ```\n */\nexport function createMultiStoreSync<T extends Record<string, unknown>>(\n options: MultiStoreSyncOptions<T>,\n): MultiStoreSync<T> {\n const { slices, version, migrations = {} } = options\n\n // Validate migration chain at construction time (fail fast)\n for (const fromVersion of Object.keys(migrations)) {\n const v = Number(fromVersion)\n if (isNaN(v) || v < 1) {\n throw new Error(`Migration key must be a positive integer, got: \"${fromVersion}\"`)\n }\n }\n\n function serialize(): BackupDocument<T> {\n const data = {} as T\n for (const key of Object.keys(slices) as Array<keyof T>) {\n data[key] = slices[key].serialize() as T[typeof key]\n }\n return { version, timestamp: Date.now(), data }\n }\n\n function restore(doc: BackupDocument): void {\n if (typeof doc !== \"object\" || doc === null) {\n throw new Error(\"restore: expected a BackupDocument object\")\n }\n\n const docVersion = doc.version ?? 1\n\n if (typeof docVersion !== \"number\" || !Number.isInteger(docVersion) || docVersion < 1) {\n throw new Error(`restore: invalid document version: ${String(doc.version)}`)\n }\n\n if (docVersion > version) {\n throw new Error(\n `restore: document version ${docVersion} is newer than current version ${version}. ` +\n `Update the app to restore this backup.`,\n )\n }\n\n // Run migrations sequentially from docVersion up to current version\n let data: Record<string, unknown> =\n typeof doc.data === \"object\" && doc.data !== null\n ? { ...(doc.data as Record<string, unknown>) }\n : {}\n\n for (let v = docVersion; v < version; v++) {\n const migration = migrations[v]\n if (!migration) continue\n try {\n data = migration(data)\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err)\n throw new Error(`restore: migration from version ${v} to ${v + 1} failed: ${msg}`)\n }\n }\n\n // Restore each slice\n for (const key of Object.keys(slices) as Array<keyof T>) {\n const sliceData = data[key as string]\n if (sliceData !== undefined) {\n slices[key].restore(sliceData as T[typeof key])\n }\n }\n }\n\n return { serialize, restore, version }\n}\n"],
5
- "mappings": "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",
3
+ "sources": ["../src/index.ts", "../src/client.ts", "../src/types.ts", "../src/fetch.ts", "../src/sync.ts", "../src/validate.ts", "../src/append-log.ts", "../src/logger.ts", "../src/migrate.ts", "../src/resolvers.ts", "../src/history.ts", "../src/polling.ts", "../src/dedup.ts", "../src/mutate.ts", "../src/config.ts", "../src/storage/indexeddb.ts", "../src/kv-cache.ts", "../src/export.ts", "../src/background-sync.ts", "../src/service-worker.ts", "../src/bindings/suspense.ts", "../src/debounced-sync.ts", "../src/mobile-lifecycle.ts", "../src/multi-store.ts"],
4
+ "sourcesContent": ["export { configurePlatform } from \"@drakkar.software/starfish-protocol\"\nexport type { CryptoProvider, Base64Provider, PlatformConfig } from \"@drakkar.software/starfish-protocol\"\nexport { stableStringify, computeHash } from \"@drakkar.software/starfish-protocol\"\nexport { buildRevocationList, revocationListCanonicalSigningInput } from \"@drakkar.software/starfish-protocol\"\nexport type {\n RevocationList,\n RevocationEntry,\n RevokedSubject,\n BuildRevocationListOpts,\n} from \"@drakkar.software/starfish-protocol\"\nexport type { PullResult, PushSuccess, PullKeyringProjection } from \"@drakkar.software/starfish-protocol\"\n\nexport { StarfishClient, pullWasFromCache } from \"./client.js\"\nexport type {\n BlobPullResult,\n BlobPushResult,\n AppendPullOptions,\n PullOptions,\n BatchPullOptions,\n BatchPullResult,\n BatchPullEntry,\n} from \"./client.js\"\nexport { SyncManager, AbortError } from \"./sync.js\"\nexport type { SyncManagerOptions, SyncSigner } from \"./sync.js\"\nexport { AppendLogCursor, AppendAuthorError, checkpointOf } from \"./append-log.js\"\nexport type { AppendLogCursorOptions, AppendElement, AuthorVerifier, ElementErrorPolicy } from \"./append-log.js\"\nexport { ENCRYPTED_KEY } from \"@drakkar.software/starfish-protocol\"\nexport type { Encryptor } from \"@drakkar.software/starfish-protocol\"\nexport {\n ConflictError,\n StarfishHttpError,\n AppendHttpError,\n} from \"./types.js\"\nexport type {\n StarfishClientOptions,\n StarfishCapProvider,\n PullCache,\n ConflictResolver,\n ClientPlugin,\n} from \"./types.js\"\nexport { consoleSyncLogger, noopSyncLogger, createMetricsCollector } from \"./logger.js\"\nexport type { SyncLogger, SyncMetrics, MetricsCollector } from \"./logger.js\"\nexport { createMigrator } from \"./migrate.js\"\nexport type { MigrationFn, MigrationConfig } from \"./migrate.js\"\nexport { ValidationError, createSchemaValidator } from \"./validate.js\"\nexport type { Validator, ValidationResult } from \"./validate.js\"\nexport { classifyError, parseRetryAfterMs } from \"./fetch.js\"\nexport type { ErrorCategory } from \"./fetch.js\"\nexport {\n createUnionMerge,\n createSoftDeleteResolver,\n timestampWinner,\n pruneTombstones,\n withConflictMeta,\n} from \"./resolvers.js\"\nexport type { ConflictMeta, ConflictResolverWithMeta } from \"./resolvers.js\"\nexport { SnapshotHistory } from \"./history.js\"\nexport type { Snapshot, SnapshotHistoryOptions } from \"./history.js\"\nexport { startPolling, startAdaptivePolling } from \"./polling.js\"\nexport type { PollableState, AdaptivePollingOptions, AdaptivePollingControls } from \"./polling.js\"\nexport { createDedupFetch } from \"./dedup.js\"\nexport { mutateDoc } from \"./mutate.js\"\nexport type { DocState, DocMutator, MutateDocOptions } from \"./mutate.js\"\nexport { fetchServerConfig } from \"./config.js\"\nexport type { EncryptionMode, CollectionClientInfo, ConfigResponse } from \"./config.js\"\nexport { createIndexedDBStorage } from \"./storage/indexeddb.js\"\nexport type { IndexedDBStorageOptions, AsyncStateStorage } from \"./storage/indexeddb.js\"\nexport { createKvPullCache } from \"./kv-cache.js\"\nexport type { KvStore, KvPullCacheOptions } from \"./kv-cache.js\"\nexport { exportData, importData, exportToBlob } from \"./export.js\"\nexport type { ExportOptions } from \"./export.js\"\nexport { isBackgroundSyncSupported, registerBackgroundSync } from \"./background-sync.js\"\nexport type { BackgroundSyncOptions } from \"./background-sync.js\"\nexport { isServiceWorkerSupported, registerServiceWorker, unregisterServiceWorkers } from \"./service-worker.js\"\nexport type { ServiceWorkerOptions } from \"./service-worker.js\"\nexport { createSuspenseResource } from \"./bindings/suspense.js\"\nexport { createDebouncedSync, createDebouncedPush } from \"./debounced-sync.js\"\nexport type { DebouncedSyncOptions, DebouncedSync, DebouncedPushOptions, DebouncedPush } from \"./debounced-sync.js\"\nexport { createMobileLifecycle, createAppendLogMobileLifecycle } from \"./mobile-lifecycle.js\"\nexport type { AppStateModule, NetInfoModule, MobileLifecycleDeps, MobileLifecycleOptions, AppendLogLifecycleOptions } from \"./mobile-lifecycle.js\"\nexport { createMultiStoreSync } from \"./multi-store.js\"\nexport type {\n StoreSlice,\n BackupDocument,\n MultiStoreMigrationFn,\n MultiStoreSyncOptions,\n MultiStoreSync,\n} from \"./multi-store.js\"\nexport type { AppendOnlyClientInfo } from \"./config.js\"\n", "import type { PullResult, PushSuccess } from \"@drakkar.software/starfish-protocol\"\nimport {\n AUTHOR_PUBKEY_FIELD,\n AUTHOR_SIGNATURE_FIELD,\n DATA_FIELD,\n TS_FIELD,\n BASE_HASH_FIELD,\n PUSH_PATH_PREFIX,\n HEADER_AUTHORIZATION,\n HEADER_SIG,\n HEADER_TS,\n HEADER_NONCE,\n HEADER_PUB,\n HEADER_CONTENT_TYPE,\n HEADER_ACCEPT,\n signAppendAuthor,\n signRequest,\n stableStringify,\n type AppendAuthor,\n type SignableMethod,\n type SignableRequest,\n} from \"@drakkar.software/starfish-protocol\"\nimport type {\n StarfishClientOptions,\n StarfishCapProvider,\n PullCache,\n} from \"./types.js\"\nimport { AppendHttpError, ConflictError, StarfishHttpError } from \"./types.js\"\nimport { parseRetryAfterMs } from \"./fetch.js\"\n\nconst APPEND_DEFAULT_FIELD = \"items\"\nconst MAX_REVALIDATE_ATTEMPTS = 5\nconst REVALIDATE_INITIAL_DELAY_MS = 1_000\nconst REVALIDATE_MAX_DELAY_MS = 30_000\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * Shape persisted in a {@link PullCache} for one document: the raw server\n * `PullResult` fields. For E2E collections `data` is the sealed ciphertext.\n */\ninterface CachedPull {\n data: Record<string, unknown>\n hash: string\n timestamp: number\n /** Wall-clock ms when this snapshot was written \u2014 for {@link StarfishClientOptions.cacheMaxAgeMs} expiry. */\n cachedAt: number\n}\n\n/**\n * The cache key for a pull `pathAndQuery`: the path with any query string\n * dropped, so a checkpoint'd or `withKeyring` pull and a plain pull of the same\n * document share one stable key (the document identity, not the request shape).\n */\nfunction pullCacheKey(pathAndQuery: string): string {\n const q = pathAndQuery.indexOf(\"?\")\n return q === -1 ? pathAndQuery : pathAndQuery.slice(0, q)\n}\n\n/**\n * Whether a {@link PullResult} was served from the offline read-through cache\n * (the transport was unreachable) rather than a live server response. Used by\n * {@link SyncManager} to surface a `stale` flag to the UI without treating a\n * cache hit as proof the server is reachable.\n */\nexport function pullWasFromCache(result: PullResult): boolean {\n return (result as { fromCache?: boolean }).fromCache === true\n}\n\n/** The storage `documentKey` for a push `path`: the path with the `/push/`\n * action prefix stripped (the namespace lives only in the URL). The author\n * signature binds to this key. */\nexport function stripPushPrefix(path: string): string {\n return path.startsWith(PUSH_PATH_PREFIX) ? path.slice(PUSH_PATH_PREFIX.length) : path\n}\n\n/** Result of pulling a binary blob from the server. */\nexport interface BlobPullResult {\n data: ArrayBuffer\n /** Content hash from the ETag header. Null if the server didn't include an ETag. */\n hash: string | null\n contentType: string\n}\n\n/** Result of pushing a binary blob to the server. */\nexport interface BlobPushResult {\n hash: string\n}\n\n/** Options for append-only pull \u2014 extracts a single array field from the response. */\nexport interface AppendPullOptions {\n /** Array field name in `data`. Defaults to `\"items\"`. */\n appendField?: string\n /** Only return items appended after this timestamp (ms). Sent as `?checkpoint=`. */\n since?: number\n /** Return only the last K items (applied after `since` filter). Sent as `?last=`. */\n last?: number\n /** Return only the last K items. Alias of `last`; sent as `?limit=`. When both\n * are given, `limit` wins. */\n limit?: number\n /** Explicitly fetch the whole collection (sent as `?full=true`). Mutually\n * exclusive with `since`/`limit`/`last` \u2014 the server requires a pull to declare\n * exactly one of {checkpoint, limit/last, full}. */\n full?: boolean\n}\n\n/**\n * Options for a structured (non-append) pull.\n *\n * `withKeyring: true` appends `?withKeyring=1` so the server includes the\n * collection's sibling `<collection>/_keyring` document in the response,\n * saving a cold-start round-trip. The cap-cert scope MUST cover BOTH the\n * data path and `<collection>/_keyring` \u2014 `scopes.writer(collection)` denies\n * the keyring path and will produce a 403; use `scopes.readWrite()` or grant\n * the keyring path explicitly when opting in.\n */\nexport interface PullOptions {\n /** Server timestamp of the last successful pull (ms). Sent as `?checkpoint=`. */\n checkpoint?: number\n /** Include the sibling `_keyring` document in the response. Defaults to false. */\n withKeyring?: boolean\n}\n\n/** Per-collection result in a {@link BatchPullResult}: either the pulled\n * document (`data`/`hash`/`timestamp`) or a per-collection `error` string. */\nexport interface BatchPullEntry {\n data?: unknown\n hash?: string\n timestamp?: number\n error?: string\n}\n\n/** Response of {@link StarfishClient.batchPull}: a map of requested collection\n * name \u2192 an ARRAY of {@link BatchPullEntry}, one per requested param-set, in\n * request order. A collection read with no params yields a one-element array. */\nexport interface BatchPullResult {\n collections: Record<string, BatchPullEntry[]>\n}\n\n/** Options for {@link StarfishClient.batchPull}. */\nexport interface BatchPullOptions {\n /** Per-collection path params: collection name \u2192 an ARRAY of param-sets, one\n * per document to read from that collection, e.g.\n * `{ profile: [{ identity: \"a\" }, { identity: \"b\" }] }` reads two profiles in\n * one round-trip. Serialized to a URL-encoded JSON `params` query. The\n * `{identity}` param is auto-filled by the server from the authenticated\n * caller when a set omits it, so a single self-doc read can pass `[{}]` \u2014 or\n * omit the collection from `params` entirely (an unlisted collection reads one\n * auto-filled doc). Results come back under the same name in request order. */\n params?: Record<string, Record<string, string>[]>\n}\n\n/**\n * Base64-encode the canonical stable-stringification of a cap-cert.\n *\n * Used as the value of the `Authorization: Cap <\u2026>` header in v3.0. We rely\n * on the host's `btoa` for browsers and fall back to `Buffer` in Node so the\n * client stays free of native dependencies.\n */\nfunction encodeCapAuth(cap: unknown): string {\n const json = stableStringify(cap as Record<string, unknown>)\n if (typeof btoa === \"function\") {\n return btoa(json)\n }\n const bufCtor = (globalThis as { Buffer?: { from: (s: string, enc: string) => { toString: (enc: string) => string } } }).Buffer\n if (bufCtor) return bufCtor.from(json, \"utf-8\").toString(\"base64\")\n throw new Error(\"No base64 encoder available\")\n}\n\n/**\n * Low-level HTTP client for the Starfish sync protocol.\n * Handles auth headers and response parsing.\n */\nexport class StarfishClient {\n private readonly baseUrl: string\n private readonly namespace?: string\n private readonly capProvider?: StarfishCapProvider\n private readonly fetch: typeof globalThis.fetch\n private readonly cache?: PullCache\n private readonly cacheMaxAgeMs?: number\n private readonly cacheFallbackStatuses?: ReadonlyArray<number>\n private readonly onRevalidated?: (path: string, result: PullResult) => void\n private readonly revalidating = new Set<string>()\n /**\n * Installed client-side plugins. Currently stored as inert data; no\n * hooks fire yet. Extensions can inspect this list if needed.\n */\n public readonly plugins: ReadonlyArray<import(\"./types.js\").ClientPlugin>\n\n constructor(options: StarfishClientOptions) {\n this.baseUrl = options.baseUrl.replace(/\\/$/, \"\")\n // Empty string \u21D2 no namespace (treat like unset), so a falsy env value\n // doesn't produce a malformed `/v1//\u2026` path.\n this.namespace = options.namespace || undefined\n this.capProvider = options.capProvider\n this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis)\n this.cache = options.cache\n this.cacheMaxAgeMs = options.cacheMaxAgeMs\n this.cacheFallbackStatuses = options.cacheFallbackStatuses\n this.onRevalidated = options.onRevalidated\n this.plugins = options.plugins ? [...options.plugins] : []\n }\n\n /**\n * Mark a `PullResult` as having been served from the offline read-through\n * cache (transport was unreachable). Non-enumerable so it doesn't leak into\n * JSON / equality / re-caching; read via {@link pullWasFromCache}.\n */\n private tagFromCache(result: PullResult): PullResult {\n Object.defineProperty(result, \"fromCache\", { value: true, enumerable: false })\n return result\n }\n\n /**\n * Resolve the host portion of the URL the client will send to. The host\n * is folded into the signed canonical input as the `h` field so the\n * server can refuse a signature that was minted against a different\n * Starfish host (replay-across-servers defence).\n *\n * When `baseUrl` is relative \u2014 e.g. the consumer passed a custom `fetch`\n * that resolves relative URLs in its own context \u2014 there is no parseable\n * host; we return `\"\"` so signing still proceeds. The server-side\n * verifier will also reconstruct host from its inbound URL, so the\n * empty-host case still verifies symmetrically when both sides agree.\n */\n private signingHost(): string {\n try {\n return new URL(this.baseUrl).host\n } catch {\n return \"\"\n }\n }\n\n /**\n * Rewrite a request path for the configured namespace. A no-op when no\n * namespace is set; otherwise `/{action}/\u2026` becomes `/v1/{namespace}/{action}/\u2026`\n * (the `/v1` protocol-version segment is part of the namespaced route, matching\n * the Python client and the server's namespace mount).\n *\n * Applied to the path used for BOTH the signature and the URL so the canonical\n * path the client signs equals the path the server reconstructs from the URL.\n * Covers SDK-helper-built paths too \u2014 that's the point: a namespace-unaware\n * helper passing `/push/spaces/x/_keyring` reaches `/v1/{ns}/push/spaces/x/_keyring`.\n */\n private applyNamespace(path: string): string {\n return this.namespace ? `/v1/${this.namespace}${path}` : path\n }\n\n /**\n * Build auth headers for a request. When a `capProvider` is set, signs the\n * request with the device's Ed25519 private key and returns the v3 header\n * set (`Authorization: Cap \u2026`, `X-Starfish-Sig`, `X-Starfish-Ts`,\n * `X-Starfish-Nonce`). Empty when no provider is configured (public reads).\n *\n * Body bytes signed MUST equal the bytes sent on the wire \u2014 callers pass\n * the already-serialized body string here so signing and transmission agree.\n * The host bound into the signature is derived from `baseUrl` once per call.\n */\n private async buildAuthHeaders(\n method: SignableMethod,\n pathAndQuery: string,\n body: string | undefined,\n ): Promise<Record<string, string>> {\n if (!this.capProvider) return {}\n const capCtx = await this.capProvider.getCap()\n return this.capRequestHeaders(capCtx, method, pathAndQuery, body)\n }\n\n /**\n * Build the request-signing headers from an ALREADY-fetched cap context. Split\n * out of {@link buildAuthHeaders} so {@link append} can fetch the cap once and\n * reuse it for BOTH the author signature (over the element data) and the\n * request signature (over the body), without redeeming the cap twice \u2014 a\n * second `getCap()` could rotate keys and break the `authorPubkey ===\n * presenter` bind the server checks.\n */\n private async capRequestHeaders(\n capCtx: Awaited<ReturnType<StarfishCapProvider[\"getCap\"]>>,\n method: SignableMethod,\n pathAndQuery: string,\n body: string | undefined,\n ): Promise<Record<string, string>> {\n const { cap, devEdPrivHex, pubHex } = capCtx\n const req: SignableRequest = {\n method,\n pathAndQuery,\n body,\n host: this.signingHost(),\n }\n const { sig, ts, nonce } = await signRequest(req, devEdPrivHex)\n const headers: Record<string, string> = {\n [HEADER_AUTHORIZATION]: `Cap ${encodeCapAuth(cap)}`,\n [HEADER_SIG]: sig,\n [HEADER_TS]: String(ts),\n [HEADER_NONCE]: nonce,\n }\n // Audience (public-link) caps bind no single subject, so the server needs\n // the presenter's pubkey to verify the signature and check the allow-list.\n if (pubHex !== undefined) headers[HEADER_PUB] = pubHex\n return headers\n }\n\n /**\n * Resolve the author public key to attach to a signed append: the redeemer's\n * `pubHex` for an audience cap, else the cert subject `cap.sub` for a\n * device/member cap. This is the SAME key that signs the request, so a server\n * enforcing author proof can bind the stored element to its writer. Returns\n * undefined only for a (malformed) cap with neither \u2014 the append then goes\n * unsigned and a server requiring signatures rejects it.\n */\n private appendAuthorKey(\n capCtx: Awaited<ReturnType<StarfishCapProvider[\"getCap\"]>>,\n ): { authorPubHex: string } | null {\n const { cap, pubHex } = capCtx\n const authorPubHex = pubHex ?? cap.sub\n if (authorPubHex === undefined) return null\n return { authorPubHex }\n }\n\n /** Pull synced data from the server. Returns the raw `PullResult`. */\n async pull(path: string, checkpoint?: number): Promise<PullResult>\n /** Pull synced data with structured options (e.g. `{withKeyring: true}`). */\n async pull(path: string, options: PullOptions): Promise<PullResult>\n /** Pull an append-only collection. Extracts and returns `data[appendField]` as `T[]`. */\n async pull<T = unknown>(path: string, options: AppendPullOptions): Promise<T[]>\n async pull<T = unknown>(\n path: string,\n checkpointOrOptions?: number | AppendPullOptions | PullOptions,\n ): Promise<PullResult | T[]> {\n let pathAndQuery = this.applyNamespace(path)\n let appendField: string | undefined\n\n if (typeof checkpointOrOptions === \"number\") {\n if (checkpointOrOptions) pathAndQuery += `?checkpoint=${checkpointOrOptions}`\n } else if (checkpointOrOptions != null) {\n // Disambiguate AppendPullOptions vs PullOptions.\n //\n // PullOptions are identified by the presence of `withKeyring` or\n // `checkpoint` keys (which AppendPullOptions does not have \u2014 append\n // uses `since`, not `checkpoint`). Anything else, including an empty\n // `{}` object, retains the historical behavior of AppendPullOptions\n // (extracts `data.items` with `?` query).\n const opts = checkpointOrOptions as AppendPullOptions & PullOptions\n const isPullOptions =\n opts.withKeyring !== undefined || opts.checkpoint !== undefined\n const params = new URLSearchParams()\n\n if (isPullOptions) {\n if (opts.checkpoint != null && opts.checkpoint > 0) {\n params.set(\"checkpoint\", String(opts.checkpoint))\n }\n if (opts.withKeyring) {\n params.set(\"withKeyring\", \"1\")\n }\n } else {\n appendField = opts.appendField ?? APPEND_DEFAULT_FIELD\n // `full` means \"the whole collection\" \u2014 it cannot be combined with a bound.\n if (opts.full && (opts.since != null || opts.limit != null || opts.last != null)) {\n throw new Error(\"full cannot be combined with since, limit, or last\")\n }\n if (opts.since != null) {\n if (opts.since < 0) throw new Error(\"since must be non-negative\")\n params.set(\"checkpoint\", String(opts.since))\n }\n if (opts.limit != null) {\n if (opts.limit < 0) throw new Error(\"limit must be non-negative\")\n params.set(\"limit\", String(opts.limit))\n }\n if (opts.last != null) {\n if (opts.last < 0) throw new Error(\"last must be non-negative\")\n params.set(\"last\", String(opts.last))\n }\n if (opts.full) {\n params.set(\"full\", \"true\")\n }\n }\n if (params.size > 0) pathAndQuery += `?${params.toString()}`\n }\n\n const url = `${this.baseUrl}${pathAndQuery}`\n const authHeaders = await this.buildAuthHeaders(\"GET\", pathAndQuery, undefined)\n\n // Read-through cache: only for structured (non-append) pulls. Append\n // collections own their own warm-start persistence via AppendLogCursor.\n const cacheKey =\n this.cache && appendField === undefined ? pullCacheKey(pathAndQuery) : undefined\n\n let res: Response\n try {\n res = await this.fetch(url, {\n method: \"GET\",\n headers: { [HEADER_ACCEPT]: \"application/json\", ...authHeaders },\n })\n } catch (err) {\n // The TRANSPORT failed (offline / DNS / timeout) \u2014 fall back to the last\n // cached snapshot for this document if we have one, tagged so callers can\n // tell it's stale. A real HTTP error (below) is a genuine server answer\n // and never gets here; 403 and 404 always propagate. 429 and 5xx\n // propagate by default too, but can fall back to cache when\n // `cacheFallbackStatuses` is set \u2014 see the stale-while-revalidate branch.\n if (cacheKey) {\n const cached = await this.readCache(cacheKey)\n if (cached) return cached\n }\n throw err\n }\n if (!res.ok) {\n const status = res.status\n if (cacheKey && this.cacheFallbackStatuses?.includes(status)) {\n // Stale-while-revalidate: serve the last-synced snapshot immediately and\n // retry in the background. 403/404 are not in the configured set so they\n // still propagate as genuine answers.\n const retryAfterHeader = res.headers.get(\"Retry-After\")\n this.scheduleRevalidate(cacheKey, pathAndQuery, retryAfterHeader)\n const cached = await this.readCache(cacheKey)\n if (cached) {\n // Discard the response body so the underlying connection can be reused.\n void res.body?.cancel()\n return cached\n }\n }\n throw new StarfishHttpError(status, await res.text())\n }\n\n const result = await res.json() as PullResult\n if (appendField !== undefined) {\n const list = (result.data as Record<string, unknown> | null)?.[appendField]\n return (Array.isArray(list) ? list : []) as T[]\n }\n if (cacheKey) {\n const snapshot: CachedPull = {\n data: result.data,\n hash: result.hash,\n timestamp: result.timestamp,\n cachedAt: Date.now(),\n }\n void this.cache!.set(cacheKey, JSON.stringify(snapshot)).catch(() => {})\n }\n return result\n }\n\n /** Deduplicated fire-and-forget: starts one revalidation loop per cacheKey. */\n private scheduleRevalidate(\n cacheKey: string,\n pathAndQuery: string,\n retryAfterHeader: string | null,\n ): void {\n if (this.revalidating.has(cacheKey)) return\n this.revalidating.add(cacheKey)\n void this.revalidateLoop(cacheKey, pathAndQuery, retryAfterHeader).finally(() => {\n this.revalidating.delete(cacheKey)\n })\n }\n\n /**\n * Background revalidation loop for a {@link cacheFallbackStatuses} hit.\n * Retries the pull (honoring `Retry-After`) up to {@link MAX_REVALIDATE_ATTEMPTS}\n * times. On a live 2xx response the fresh snapshot is written through to the\n * cache and {@link onRevalidated} fires. Stops early on a non-fallback HTTP\n * status (e.g. 404/403 \u2014 the server gave a genuine answer).\n */\n private async revalidateLoop(\n cacheKey: string,\n pathAndQuery: string,\n firstRetryAfter: string | null,\n ): Promise<void> {\n let retryAfterHeader = firstRetryAfter\n for (let attempt = 0; attempt < MAX_REVALIDATE_ATTEMPTS; attempt++) {\n const delay = parseRetryAfterMs(retryAfterHeader, {\n fallbackMs: Math.min(\n REVALIDATE_INITIAL_DELAY_MS * Math.pow(2, attempt),\n REVALIDATE_MAX_DELAY_MS,\n ),\n maxMs: REVALIDATE_MAX_DELAY_MS,\n })\n await sleep(delay)\n\n try {\n const url = `${this.baseUrl}${pathAndQuery}`\n const authHeaders = await this.buildAuthHeaders(\"GET\", pathAndQuery, undefined)\n const res = await this.fetch(url, {\n method: \"GET\",\n headers: { [HEADER_ACCEPT]: \"application/json\", ...authHeaders },\n })\n\n if (res.ok) {\n const result = (await res.json()) as PullResult\n if (this.cache) {\n const snapshot: CachedPull = {\n data: result.data,\n hash: result.hash,\n timestamp: result.timestamp,\n cachedAt: Date.now(),\n }\n void this.cache.set(cacheKey, JSON.stringify(snapshot)).catch(() => {})\n }\n this.onRevalidated?.(pathAndQuery, result)\n return\n }\n\n if (!this.cacheFallbackStatuses?.includes(res.status)) {\n // Genuine server answer (e.g. 403 or 404) \u2014 stop retrying.\n return\n }\n\n retryAfterHeader = res.headers.get(\"Retry-After\")\n } catch {\n // Transport failure \u2014 keep retrying with exponential backoff.\n retryAfterHeader = null\n }\n }\n }\n\n /**\n * Read the cached snapshot for a document `path` WITHOUT hitting the network \u2014\n * the basis for cache-first paint (seed the UI from the last-synced snapshot,\n * then revalidate with a live {@link pull}). Returns the tagged `PullResult`,\n * or null when no cache is configured / there's no entry. Namespacing matches\n * {@link pull}, so the key lines up with whatever `pull` wrote.\n */\n async peekCache(path: string): Promise<PullResult | null> {\n if (!this.cache) return null\n return this.readCache(pullCacheKey(this.applyNamespace(path)))\n }\n\n /** Read + parse a cached pull snapshot, tagged {@link tagFromCache}. Returns\n * null on a miss or an unparseable blob (never throws \u2014 a corrupt cache entry\n * must not break a pull, just miss). */\n private async readCache(cacheKey: string): Promise<PullResult | null> {\n try {\n const raw = await this.cache!.get(cacheKey)\n if (!raw) return null\n const parsed = JSON.parse(raw) as CachedPull\n if (!parsed || typeof parsed.hash !== \"string\") return null\n // Expiry: a snapshot older than the configured max age is a miss. Entries\n // written before this field existed (cachedAt missing) count as age 0 \u21D2\n // expired under any TTL, forcing a fresh pull once.\n if (this.cacheMaxAgeMs != null && Date.now() - (parsed.cachedAt ?? 0) > this.cacheMaxAgeMs) {\n return null\n }\n return this.tagFromCache({ data: parsed.data ?? {}, hash: parsed.hash, timestamp: parsed.timestamp ?? 0 })\n } catch {\n return null\n }\n }\n\n /**\n * Pull several documents in one round-trip via `/batch/pull`. `collections` is\n * the list of distinct collection names; `opts.params` supplies, per collection,\n * an ARRAY of path-param sets \u2014 one per document to read \u2014 so the SAME collection\n * can fan in many documents (e.g. many users' `profile`) in a single request.\n * The server auto-fills the `{identity}` param from the authenticated caller for\n * any set that omits it, so a self-doc collection needs no params. Returns a map\n * of collection name \u2192 an ARRAY of pulled documents (or per-document `{ error }`),\n * in request order. Honors the configured namespace.\n *\n * For the common \"many docs of one collection\" case prefer {@link batchPullMany}.\n *\n * Note: not append/checkpoint-aware \u2014 for incremental append-only reads use\n * `pull(path, { since })` (or `AppendLogCursor`) per collection.\n */\n async batchPull(\n collections: string[],\n opts: BatchPullOptions = {},\n ): Promise<BatchPullResult> {\n const search = new URLSearchParams()\n search.set(\"collections\", collections.join(\",\"))\n if (opts.params && Object.keys(opts.params).length > 0) {\n search.set(\"params\", JSON.stringify(opts.params))\n }\n const pathAndQuery = `${this.applyNamespace(\"/batch/pull\")}?${search.toString()}`\n const url = `${this.baseUrl}${pathAndQuery}`\n const authHeaders = await this.buildAuthHeaders(\"GET\", pathAndQuery, undefined)\n\n const res = await this.fetch(url, {\n method: \"GET\",\n headers: { [HEADER_ACCEPT]: \"application/json\", ...authHeaders },\n })\n if (!res.ok) {\n throw new StarfishHttpError(res.status, await res.text())\n }\n return await res.json() as BatchPullResult\n }\n\n /**\n * Convenience over {@link batchPull} for reading MANY documents of ONE\n * collection in a single round-trip: pass the per-document param-sets and get\n * back the {@link BatchPullEntry} array aligned to `paramsList` by index (each\n * entry is `{ data, hash, timestamp }` or `{ error }`). An empty `paramsList`\n * issues no request and returns `[]`.\n */\n async batchPullMany(\n collection: string,\n paramsList: Record<string, string>[],\n ): Promise<BatchPullEntry[]> {\n if (paramsList.length === 0) return []\n const res = await this.batchPull([collection], { params: { [collection]: paramsList } })\n return res.collections[collection] ?? []\n }\n\n /**\n * Push synced data to the server.\n * @param path - The push endpoint path (e.g. \"/push/users/abc/settings\")\n * @param data - The full document data to push\n * @param baseHash - Hash of the document this push is based on (null for first push)\n *\n * v3 author proof (`authorPubkey` + `authorSignature`) is passed via `author`\n * (produced by `SyncManager` when a `signer` is configured) and sent as\n * top-level body siblings of `data`, where the server verifies it.\n * @throws {ConflictError} if the server detects a hash mismatch (409)\n */\n async push(\n path: string,\n data: Record<string, unknown>,\n baseHash: string | null,\n author?: AppendAuthor,\n ): Promise<PushSuccess> {\n const body = JSON.stringify({\n [DATA_FIELD]: data,\n [BASE_HASH_FIELD]: baseHash,\n ...(author && {\n [AUTHOR_PUBKEY_FIELD]: author.authorPubkey,\n [AUTHOR_SIGNATURE_FIELD]: author.authorSignature,\n }),\n })\n\n const sendPath = this.applyNamespace(path)\n const authHeaders = await this.buildAuthHeaders(\"POST\", sendPath, body)\n\n const res = await this.fetch(`${this.baseUrl}${sendPath}`, {\n method: \"POST\",\n headers: {\n [HEADER_CONTENT_TYPE]: \"application/json\",\n [HEADER_ACCEPT]: \"application/json\",\n ...authHeaders,\n },\n body,\n })\n\n if (res.status === 409) {\n throw new ConflictError()\n }\n if (!res.ok) {\n throw new StarfishHttpError(res.status, await res.text())\n }\n const result = (await res.json()) as PushSuccess\n // Write-through: update the pull cache with the pushed data so an offline\n // restart reads the just-written state rather than the pre-push snapshot.\n // The push path is /push/X; the corresponding pull cache key is /pull/X.\n if (this.cache) {\n const pullPath = sendPath.replace(\"/push/\", \"/pull/\")\n const cacheKey = pullCacheKey(pullPath)\n const snapshot: CachedPull = {\n data,\n hash: result.hash,\n timestamp: result.timestamp,\n cachedAt: Date.now(),\n }\n void this.cache.set(cacheKey, JSON.stringify(snapshot)).catch(() => {})\n }\n return result\n }\n\n /**\n * Append an element to an appendOnly (`by_timestamp`) collection.\n *\n * Unlike {@link push}, appendOnly writes carry no hash/conflict check \u2014 an\n * authorized append is always accepted. Each element is stored server-side as\n * `{ts, data}` and pulls can filter by `ts` via `since`/`checkpoint`.\n *\n * @param path - the push endpoint (e.g. \"/push/events\")\n * @param data - the element payload. For a `delegated` collection, encrypt it\n * first (e.g. `createKeyringEncryptor(keyring, kem).encrypt(data)`); the\n * server stores it opaquely and never reads it.\n * @param opts.ts - optional client-supplied element timestamp (ms). Must be a\n * non-negative integer strictly greater than the latest stored element's ts\n * (else the server responds 409). Omit to let the server assign one.\n * @throws {StarfishHttpError} on a non-2xx response \u2014 e.g. 409\n * `{ error: \"non_monotonic_timestamp\" }` for a non-monotonic timestamp, or\n * `{ error: \"append_limit_exceeded\", limit }` if the collection's `maxItems`\n * cap is reached (partition by a path parameter for higher volume).\n */\n async append(\n path: string,\n data: Record<string, unknown>,\n opts: { ts?: number } = {},\n ): Promise<PushSuccess> {\n const sendPath = this.applyNamespace(path)\n const bodyObj: Record<string, unknown> = { [DATA_FIELD]: data }\n if (opts.ts !== undefined) bodyObj[TS_FIELD] = opts.ts\n\n // Author proof. Fetch the cap ONCE and reuse it for both the author\n // signature (over the element `data`) and the request signature (over the\n // final body) \u2014 see {@link capRequestHeaders}. The author fields are signed\n // with the same key that authenticates the request, so a collection with\n // `requireAuthorSignature` (the default) binds the stored element to its\n // writer. Without a cap provider the append is sent unsigned and such a\n // collection rejects it.\n const capCtx = this.capProvider ? await this.capProvider.getCap() : null\n if (capCtx) {\n const authorKey = this.appendAuthorKey(capCtx)\n if (authorKey) {\n // The signature binds the author to BOTH the element data AND the\n // document it is written to (the storage path = `path` minus the\n // `/push/` action prefix; the namespace lives only in the URL).\n const documentKey = stripPushPrefix(path)\n const { authorPubkey, authorSignature } = signAppendAuthor(\n documentKey,\n data,\n authorKey.authorPubHex,\n capCtx.devEdPrivHex,\n )\n bodyObj[AUTHOR_PUBKEY_FIELD] = authorPubkey\n bodyObj[AUTHOR_SIGNATURE_FIELD] = authorSignature\n }\n }\n\n const body = JSON.stringify(bodyObj)\n const authHeaders = capCtx\n ? await this.capRequestHeaders(capCtx, \"POST\", sendPath, body)\n : {}\n\n const res = await this.fetch(`${this.baseUrl}${sendPath}`, {\n method: \"POST\",\n headers: {\n [HEADER_CONTENT_TYPE]: \"application/json\",\n [HEADER_ACCEPT]: \"application/json\",\n ...authHeaders,\n },\n body,\n })\n\n if (!res.ok) {\n throw new StarfishHttpError(res.status, await res.text())\n }\n return res.json() as Promise<PushSuccess>\n }\n\n /**\n * Append one element to a **public-write** append-only collection with an\n * Ed25519 author proof but **no cap `Authorization` header**.\n *\n * Unlike {@link append}, which always attaches a cap-signed `Authorization`\n * header from the configured `capProvider`, this method signs only the\n * append-author proof (binding the element to the writer's Ed25519 key) and\n * sends the request without authentication headers. This is required for\n * collections with `writeRoles: [\"public\"]` \u2014 the server's cap-scope check\n * would reject a request carrying a cap whose scope does not cover the path.\n *\n * Typical use-case: writing a sealed invitation to another user's\n * public-write inbox collection without needing a cap scoped to the\n * recipient's namespace. The author proof is optional on the server side\n * (`requireAuthorSignature: false` for a public inbox), but signing anyway\n * binds the stored element to the sender's Ed25519 key for verification in\n * the receive path.\n *\n * The element is sent as `{ data, authorPubkey, authorSignature }`.\n *\n * @param path The push path, e.g. `/push/inbox/{userId}/{shard}`.\n * @param element The JSON element to append.\n * @param signer The sender's Ed25519 keypair (signs the author proof).\n *\n * @throws {AppendHttpError} on a non-2xx response.\n */\n async appendAnonymous(\n path: string,\n element: Record<string, unknown>,\n signer: { edPubHex: string; edPrivHex: string },\n ): Promise<void> {\n const sendPath = this.applyNamespace(path)\n const documentKey = stripPushPrefix(path)\n const { authorPubkey, authorSignature } = signAppendAuthor(\n documentKey,\n element,\n signer.edPubHex,\n signer.edPrivHex,\n )\n const body = JSON.stringify({\n [DATA_FIELD]: element,\n [AUTHOR_PUBKEY_FIELD]: authorPubkey,\n [AUTHOR_SIGNATURE_FIELD]: authorSignature,\n })\n const res = await this.fetch(`${this.baseUrl}${sendPath}`, {\n method: \"POST\",\n headers: {\n [HEADER_CONTENT_TYPE]: \"application/json\",\n [HEADER_ACCEPT]: \"application/json\",\n },\n body,\n })\n if (!res.ok) {\n const detail = await res.text().catch(() => \"\")\n throw new AppendHttpError(\n res.status,\n `anonymous append failed: HTTP ${res.status} ${detail}`.trim(),\n )\n }\n }\n\n /**\n * Pull binary data from a blob collection.\n * Returns raw bytes with the content hash from the ETag header.\n */\n async pullBlob(path: string): Promise<BlobPullResult> {\n const sendPath = this.applyNamespace(path)\n const authHeaders = await this.buildAuthHeaders(\"GET\", sendPath, undefined)\n\n const res = await this.fetch(`${this.baseUrl}${sendPath}`, {\n method: \"GET\",\n headers: { [HEADER_ACCEPT]: \"*/*\", ...authHeaders },\n })\n if (!res.ok) {\n throw new StarfishHttpError(res.status, await res.text())\n }\n\n const etag = res.headers.get(\"ETag\")?.replace(/\"/g, \"\") ?? null\n const contentType = res.headers.get(HEADER_CONTENT_TYPE) ?? \"application/octet-stream\"\n const data = await res.arrayBuffer()\n\n return { data, hash: etag, contentType }\n }\n\n /**\n * Push binary data to a blob collection.\n * Binary collections use last-write-wins (no conflict detection).\n */\n async pushBlob(\n path: string,\n data: ArrayBuffer | Uint8Array | Blob,\n contentType: string,\n ): Promise<BlobPushResult> {\n // Blobs are not JSON; we leave body undefined when signing \u2014 server-side\n // verification is expected to use a separate path for blob uploads.\n const sendPath = this.applyNamespace(path)\n const authHeaders = await this.buildAuthHeaders(\"POST\", sendPath, undefined)\n\n const res = await this.fetch(`${this.baseUrl}${sendPath}`, {\n method: \"POST\",\n headers: {\n [HEADER_CONTENT_TYPE]: contentType,\n [HEADER_ACCEPT]: \"application/json\",\n ...authHeaders,\n },\n body: data as BodyInit,\n })\n if (!res.ok) {\n throw new StarfishHttpError(res.status, await res.text())\n }\n return res.json() as Promise<BlobPushResult>\n }\n}\n", "import type { CapCert, PullResult } from \"@drakkar.software/starfish-protocol\"\n\n/** Push conflict error (HTTP 409). */\nexport class ConflictError extends Error {\n constructor() {\n super(\"hash_mismatch\")\n this.name = \"ConflictError\"\n }\n}\n\n/** HTTP error from the Starfish server. */\nexport class StarfishHttpError extends Error {\n constructor(\n public readonly status: number,\n public readonly body: string\n ) {\n super(`HTTP ${status}: ${body}`)\n this.name = \"StarfishHttpError\"\n }\n}\n\n/**\n * Non-2xx HTTP error from an anonymous (cap-less) append call.\n *\n * Distinct from {@link StarfishHttpError} so callers can distinguish \"the\n * anonymous write was rejected\" (e.g. auth required, payload too large) from\n * other client errors without pattern-matching on the error message.\n */\nexport class AppendHttpError extends Error {\n constructor(\n /** HTTP status returned by the server. */\n public readonly status: number,\n message: string,\n ) {\n super(message)\n this.name = \"AppendHttpError\"\n }\n}\n\n/**\n * v3.0 cap-cert provider for `StarfishClient`. Returns the device's cap-cert and\n * the matching Ed25519 private key (hex). The client calls `getCap()` once per\n * outgoing request; implementations are expected to cache so this is cheap.\n *\n * When set, the client signs every outgoing request: each call carries\n * `Authorization: Cap <base64(stableStringify(cap))>` plus `X-Starfish-Sig`,\n * `X-Starfish-Ts`, `X-Starfish-Nonce`.\n */\nexport interface StarfishCapProvider {\n /**\n * Returns the device's cap-cert and its Ed25519 private key (hex).\n * Implementations are expected to cache; the client may call this once per\n * authenticated request.\n *\n * For an `audience` (public-link) cap, which binds no single subject, also\n * return `pubHex` \u2014 the redeemer's own Ed25519 pubkey matching `devEdPrivHex`.\n * The client then sends it as `X-Starfish-Pub` so the server can verify the\n * request signature against it and check the cap's `aud` allow-list. Omit\n * `pubHex` for device/member caps (the server uses `cap.sub`).\n */\n getCap(): Promise<{\n cap: CapCert\n devEdPrivHex: string\n pubHex?: string\n }>\n}\n\n/**\n * A minimal async key-value store the client uses as a read-through cache for\n * {@link StarfishClient.pull} (offline-first reads). Host-provided so the SDK\n * stays storage-agnostic \u2014 back it by `localStorage`, `AsyncStorage`, a file,\n * etc. Shaped like a subset of zustand's `StateStorage` so an existing adapter\n * fits.\n *\n * IMPORTANT \u2014 what gets stored: the client caches the RAW server response only\n * (`data`/`hash`/`timestamp`). For E2E (`delegated`) collections that payload is\n * the SEALED ciphertext the server holds \u2014 never the decrypted form \u2014 so this\n * cache is ciphertext-at-rest by construction. Decryption always happens in\n * memory on read (see {@link SyncManager}). Public/plaintext collections cache\n * their plaintext, exactly as the server stores it.\n */\nexport interface PullCache {\n /** Return the previously-stored string for `key`, or null if absent. Must not throw. */\n get(key: string): Promise<string | null>\n /** Store `value` under `key`. Must not throw (failures are swallowed by the client). */\n set(key: string, value: string): Promise<void>\n}\n\n/** Options for creating a StarfishClient. */\nexport interface StarfishClientOptions {\n /** Base URL of the Starfish server (e.g. \"https://api.example.com/v1\"). */\n baseUrl: string\n /**\n * Optional namespace for a namespace-mounted server. When set, every request\n * path `/{action}/\u2026` is rewritten to `/v1/{namespace}/{action}/\u2026` for BOTH the\n * URL the client hits AND the canonical path it signs, so the signature the\n * server reconstructs from the namespaced URL verifies (no rewrite layer\n * needed). Mirrors the Python client's `namespace` parameter.\n *\n * Crucially this also rewrites the paths that namespace-unaware SDK helpers\n * build internally (e.g. `starfish-keyring`'s `addCollectionRecipient`, blob\n * uploads), so consumers no longer hand-prefix paths or wrap the client to\n * reach a namespaced deployment. Leave unset (default) for a root-mounted\n * server \u2014 paths pass through unchanged, byte-identical to before.\n *\n * Pass the bare namespace name (e.g. `\"octochat\"`); `baseUrl` then carries only\n * the origin (and any reverse-proxy mount the proxy strips), not the `/v1`\n * version segment. Must match `[A-Za-z0-9_-]+` and not be a reserved route name\n * (`pull`, `push`, `health`, `batch`).\n */\n namespace?: string\n /**\n * Cap-cert provider. When set, requests are signed with Ed25519 and carry\n * `Authorization: Cap <\u2026>`. Omit for unauthenticated public-read collections.\n */\n capProvider?: StarfishCapProvider\n /** Optional fetch implementation (defaults to global fetch). */\n fetch?: typeof fetch\n /**\n * Optional read-through cache for {@link StarfishClient.pull} \u2014 the basis for\n * offline-first reads. When set, every successful non-append pull is written\n * through to the cache (keyed by document path), and a pull that fails because\n * the TRANSPORT is unreachable (offline / DNS / timeout \u2014 `fetch` rejects)\n * falls back to the cached response, tagged so callers can tell it's stale.\n *\n * A real HTTP error (404/403/5xx) is a genuine server answer and always\n * propagates \u2014 the cache is NOT consulted \u2014 so \"no document yet\" and\n * \"access denied\" keep their meaning. Caches ciphertext for E2E collections\n * (the server only ever holds sealed payloads); never decrypted data.\n */\n cache?: PullCache\n /**\n * Optional max age (ms) for {@link cache} entries. An entry older than this is\n * treated as a cache MISS on every read \u2014 both cache-first paint and the\n * offline fallback \u2014 so a stale-beyond-policy snapshot is never served (the\n * pull then goes to the network, or rethrows the transport error offline).\n * Each cached snapshot records its write time; expiry is `now - cachedAt >\n * cacheMaxAgeMs`. Omit (default) for entries that never expire \u2014 recommended\n * for an offline-first app where any last-synced data beats none.\n */\n cacheMaxAgeMs?: number\n /**\n * HTTP status codes for which a structured `pull()` falls back to the\n * last-synced cached snapshot rather than throwing `StarfishHttpError` \u2014\n * a **stale-while-revalidate** strategy for transient server failures.\n *\n * When a pull returns one of these statuses AND a {@link cache} is configured\n * AND a cached snapshot exists for the document, `pull()` returns the cached\n * result immediately (tagged stale via `pullWasFromCache`) and spawns a\n * background revalidation loop that retries until it gets a live response.\n * On success the fresh snapshot is written through and {@link onRevalidated}\n * fires. When no cached snapshot exists the error propagates as usual.\n *\n * Applies only to structured (non-append) pulls. Do NOT include `403` or `404`\n * \u2014 they are genuine server answers (access denied / no document yet).\n *\n * Default `undefined` \u2014 every non-2xx status throws as before.\n *\n * Recommended set for offline-first apps: `[429, 500, 502, 503, 504]`.\n */\n cacheFallbackStatuses?: number[]\n /**\n * Called after a background revalidation succeeds following a\n * {@link cacheFallbackStatuses} hit: the server returned a live response and\n * the fresh snapshot has been written through to the cache.\n *\n * Use this to signal to the host app that the server is reachable again (e.g.\n * call `reportReachability(true)`) so any stale views can re-pull and recover.\n *\n * `path` is the namespaced document path sent to the server (namespace prefix\n * + path + query string, matching the key under which the snapshot was cached).\n */\n onRevalidated?: (path: string, result: PullResult) => void\n /**\n * Optional list of client-side plugins. The list is stored on the client\n * instance but does not fire any hooks yet \u2014 the contract is plumbed so\n * extension packages (`starfish-identities`, `starfish-keyring`,\n * `starfish-sharing`, \u2026) can register against it later without a breaking\n * API change.\n *\n * The current set of hooks is purposely empty; extensions that need to\n * react to mint events or transport actions today can wrap the client\n * directly. Future hook additions will be additive.\n */\n plugins?: ClientPlugin[]\n}\n\n/**\n * Client-side plugin contract.\n *\n * A placeholder shape: the interface intentionally has no required hooks\n * yet; extensions declare a plugin object with `name` and opt into\n * specific lifecycle hooks once those exist. Apps wire plugins via\n * `new StarfishClient({ plugins: [...] })`.\n */\nexport interface ClientPlugin {\n /** Human-readable name. Used in error messages and audit output. */\n name: string\n /**\n * Reserved for future hook fields. Plugins typically declare only\n * `name`. Hook additions are additive \u2014 extensions implementing a\n * future hook will populate the relevant optional property without\n * affecting existing zero-hook plugins.\n */\n}\n\n/** Conflict resolver: given local and remote data, return merged result. */\nexport type ConflictResolver = (\n local: Record<string, unknown>,\n remote: Record<string, unknown>\n) => Record<string, unknown>\n", "/**\n * Parse a `Retry-After` header value into milliseconds.\n *\n * - Numeric string (`\"30\"`) \u2014 treated as seconds \u00D7 1000.\n * - HTTP-date string \u2014 delta from now in ms (floored to 0).\n * - `null`, empty, or unparseable \u2014 returns `opts.fallbackMs`.\n *\n * All results are clamped to `[0, opts.maxMs]`.\n */\nexport function parseRetryAfterMs(\n header: string | null | undefined,\n opts: { fallbackMs: number; maxMs: number },\n): number {\n const { fallbackMs, maxMs } = opts\n const trimmed = header?.trim()\n if (trimmed) {\n const seconds = Number(trimmed)\n if (!isNaN(seconds)) return Math.min(seconds * 1000, maxMs)\n const date = Date.parse(trimmed)\n if (!isNaN(date)) return Math.min(Math.max(date - Date.now(), 0), maxMs)\n }\n return Math.min(fallbackMs, maxMs)\n}\n\n/** Error category returned by classifyError. */\nexport type ErrorCategory =\n | \"network\"\n | \"auth\"\n | \"conflict\"\n | \"rate-limited\"\n | \"server\"\n | \"client\"\n | \"unknown\"\n\n/** Classify an error from a fetch response or network failure. */\nexport function classifyError(err: unknown): ErrorCategory {\n if (err instanceof Response || (err && typeof err === \"object\" && \"status\" in err)) {\n const status = (err as { status: unknown }).status\n if (typeof status !== \"number\" || isNaN(status)) return \"unknown\"\n if (status === 0) return \"network\"\n if (status === 401 || status === 403) return \"auth\"\n if (status === 409) return \"conflict\"\n if (status === 429) return \"rate-limited\"\n if (status >= 500) return \"server\"\n if (status >= 400) return \"client\"\n }\n if (err instanceof Error && /failed to fetch|fetch failed|network|load failed|ECONNREFUSED|ENOTFOUND/i.test(err.message)) return \"network\"\n return \"unknown\"\n}\n\nexport interface RetryOptions {\n /** Max number of retries (default: 3). */\n maxRetries?: number\n /** Initial delay in ms before first retry (default: 500). */\n initialDelayMs?: number\n /** Maximum delay in ms (default: 10000). */\n maxDelayMs?: number\n}\n\n/**\n * Wraps a fetch function with automatic retry for retriable errors\n * (network failures, 429, 5xx). Respects Retry-After headers.\n */\nexport function createRetryFetch(options?: RetryOptions): typeof globalThis.fetch {\n const maxRetries = Math.max(0, options?.maxRetries ?? 3)\n const initialDelay = options?.initialDelayMs ?? 500\n const maxDelay = options?.maxDelayMs ?? 10_000\n\n return async (input, init?) => {\n let attempt = 0\n while (true) {\n try {\n const res = await globalThis.fetch(input, init)\n if (res.ok || attempt >= maxRetries) return res\n\n const category = classifyError(res)\n if (category !== \"rate-limited\" && category !== \"server\") return res\n\n const retryAfterHeader = res.headers.get(\"Retry-After\")\n const exponentialDelay = Math.min(initialDelay * Math.pow(2, attempt), maxDelay)\n // When the header is present but unparseable, original falls back to\n // initialDelay (not exponential). Preserve that by checking presence first.\n const delay = parseRetryAfterMs(retryAfterHeader, {\n fallbackMs: retryAfterHeader?.trim() ? initialDelay : exponentialDelay,\n maxMs: maxDelay,\n })\n\n await new Promise<void>((r) => setTimeout(r, delay))\n attempt++\n } catch (err) {\n if (attempt >= maxRetries) throw err\n const category = classifyError(err)\n if (category !== \"network\") throw err\n\n const delay = Math.min(initialDelay * Math.pow(2, attempt), maxDelay)\n await new Promise<void>((r) => setTimeout(r, delay))\n attempt++\n }\n }\n }\n}\n\ntype BreakerState = \"closed\" | \"open\" | \"half-open\"\n\nexport interface CircuitBreakerOptions {\n /** Number of consecutive failures to open the circuit (default: 5). */\n threshold?: number\n /** Cooldown in ms before transitioning from open to half-open (default: 30000). */\n cooldownMs?: number\n}\n\n/** Circuit breaker that prevents requests when the backend is unavailable. */\nexport class CircuitBreaker {\n private state: BreakerState = \"closed\"\n private failures = 0\n private openedAt = 0\n private readonly threshold: number\n private readonly cooldownMs: number\n\n constructor(options?: CircuitBreakerOptions) {\n this.threshold = options?.threshold ?? 5\n this.cooldownMs = options?.cooldownMs ?? 30_000\n }\n\n getState(): BreakerState {\n this.maybeTransition()\n return this.state\n }\n\n isOpen(): boolean {\n return this.getState() === \"open\"\n }\n\n recordSuccess(): void {\n this.failures = 0\n this.state = \"closed\"\n }\n\n recordFailure(): void {\n this.failures++\n if (this.state === \"half-open\" || this.failures >= this.threshold) {\n this.state = \"open\"\n this.openedAt = Date.now()\n }\n }\n\n private maybeTransition(): void {\n if (this.state === \"open\" && Date.now() - this.openedAt >= this.cooldownMs) {\n this.state = \"half-open\"\n }\n }\n}\n\n/**\n * Wraps fetch to gzip-compress string request bodies using the CompressionStream API.\n * Adds Content-Encoding: gzip header. Non-string bodies (ArrayBuffer, Blob, etc.)\n * are passed through uncompressed. Requires CompressionStream (browsers, Node.js 18+, Deno).\n */\nexport function createCompressedFetch(inner?: typeof globalThis.fetch): typeof globalThis.fetch {\n const baseFetch = inner ?? globalThis.fetch.bind(globalThis)\n return async (input, init?) => {\n if (!init?.body || typeof CompressionStream === \"undefined\") {\n return baseFetch(input, init)\n }\n\n const bodyText = typeof init.body === \"string\" ? init.body : null\n if (!bodyText) return baseFetch(input, init)\n\n try {\n const stream = new Blob([bodyText]).stream().pipeThrough(new CompressionStream(\"gzip\"))\n const compressed = await new Response(stream).arrayBuffer()\n\n const normalized = Object.fromEntries(new Headers(init.headers as HeadersInit).entries())\n normalized[\"content-encoding\"] = \"gzip\"\n\n return baseFetch(input, {\n ...init,\n body: compressed,\n headers: normalized,\n })\n } catch {\n return baseFetch(input, init)\n }\n }\n}\n\n/**\n * Wrap `fetch` to bound the **connect / Time-to-First-Byte** phase with a\n * timeout. The timer is cleared as soon as the response HEADERS arrive (i.e.\n * the `fetch()` promise resolves), so a slow large-body download after a fast\n * connection is not interrupted. Only the initial \"will the server even\n * respond?\" window is bounded.\n *\n * The wrapper composes with the caller's `AbortSignal`: if the caller's signal\n * fires first the request is still aborted and the timeout timer is cleaned up.\n *\n * @param timeoutMs How long (in ms) to wait for the server to start\n * responding before aborting. Default `10 000`.\n * @param inner Optional underlying `fetch` to wrap (defaults to\n * `globalThis.fetch`).\n *\n * @example\n * ```ts\n * import { createTimeoutFetch, createResilientFetch } from \"@drakkar.software/starfish-client/fetch\"\n *\n * const { fetch: resilient } = createResilientFetch()\n * const client = new StarfishClient({\n * baseUrl: \"https://api.example.com\",\n * fetch: createTimeoutFetch(8_000, resilient),\n * })\n * ```\n */\nexport function createTimeoutFetch(\n timeoutMs = 10_000,\n inner?: typeof globalThis.fetch,\n): typeof globalThis.fetch {\n const baseFetch = inner ?? globalThis.fetch.bind(globalThis)\n return async (input, init?) => {\n const timeoutCtrl = new AbortController()\n const timer = setTimeout(() => timeoutCtrl.abort(new Error(`connect timeout after ${timeoutMs}ms`)), timeoutMs)\n\n // Compose with a caller-supplied AbortSignal if present.\n const callerSignal = init?.signal as AbortSignal | null | undefined\n let combinedSignal: AbortSignal\n\n if (callerSignal) {\n if (typeof AbortSignal.any === \"function\") {\n combinedSignal = AbortSignal.any([timeoutCtrl.signal, callerSignal])\n } else {\n // Polyfill for environments without AbortSignal.any.\n const combo = new AbortController()\n const onCallerAbort = () => combo.abort(callerSignal.reason)\n const onTimeout = () => combo.abort(timeoutCtrl.signal.reason)\n callerSignal.addEventListener(\"abort\", onCallerAbort, { once: true })\n timeoutCtrl.signal.addEventListener(\"abort\", onTimeout, { once: true })\n combinedSignal = combo.signal\n }\n } else {\n combinedSignal = timeoutCtrl.signal\n }\n\n try {\n const res = await baseFetch(input, { ...init, signal: combinedSignal })\n clearTimeout(timer) // Headers arrived \u2014 clear the connect timeout.\n return res\n } catch (err) {\n clearTimeout(timer)\n throw err\n }\n }\n}\n\n/**\n * Combines retry and circuit breaker into a single resilient fetch wrapper.\n * Rejects immediately when the circuit is open.\n */\nexport function createResilientFetch(\n retryOptions?: RetryOptions,\n breakerOptions?: CircuitBreakerOptions,\n): { fetch: typeof globalThis.fetch; breaker: CircuitBreaker } {\n const breaker = new CircuitBreaker(breakerOptions)\n const retryFetch = createRetryFetch(retryOptions)\n\n const resilientFetch: typeof globalThis.fetch = async (input, init?) => {\n if (breaker.isOpen()) {\n const cooldown = Math.ceil((breakerOptions?.cooldownMs ?? 30_000) / 1000)\n throw new Error(`Request blocked: too many consecutive failures. Retry in ${cooldown}s.`)\n }\n\n try {\n const res = await retryFetch(input, init)\n if (res.status >= 500) {\n breaker.recordFailure()\n } else {\n breaker.recordSuccess()\n }\n return res\n } catch (err) {\n breaker.recordFailure()\n throw err\n }\n }\n\n return { fetch: resilientFetch, breaker }\n}\n", "import type { PullResult } from \"@drakkar.software/starfish-protocol\"\nimport {\n AUTHOR_PUBKEY_FIELD,\n AUTHOR_SIGNATURE_FIELD,\n PUSH_PATH_PREFIX,\n deepMerge,\n docAuthorCanonicalInput,\n getBase64,\n type AppendAuthor,\n} from \"@drakkar.software/starfish-protocol\"\nimport type { ConflictResolver } from \"./types.js\"\nimport { ConflictError } from \"./types.js\"\nimport type { Encryptor } from \"@drakkar.software/starfish-protocol\"\nimport { StarfishClient, stripPushPrefix, pullWasFromCache } from \"./client.js\"\nimport type { SyncLogger } from \"./logger.js\"\nimport type { Validator } from \"./validate.js\"\nimport { ValidationError } from \"./validate.js\"\n\nexport class AbortError extends Error {\n constructor() {\n super(\"SyncManager was aborted\")\n this.name = \"AbortError\"\n }\n}\n\n/**\n * v3.0 author-signature plumbing for `SyncManager`.\n *\n * Returns the device's Ed25519 public key (hex) and a function that signs\n * arbitrary payload bytes. `SyncManager` calls `getSigner()` once per push\n * and uses the returned `sign` to produce a base64-encoded signature over\n * the canonical stringification of the encrypted payload (sans author fields).\n *\n * Implementations typically wrap the same Ed25519 private key used by\n * `StarfishCapProvider` so that `cap.sub === devEdPubHex`.\n */\nexport interface SyncSigner {\n /**\n * Returns the device's `cap.sub` (Ed25519 pubkey, hex) and a payload signer.\n * The `sign` function receives the canonical signing input bytes and must\n * return the raw 64-byte Ed25519 signature.\n */\n getSigner(): Promise<{ devEdPubHex: string; sign(payload: Uint8Array): Promise<Uint8Array> }>\n}\n\n\nexport interface SyncManagerOptions {\n client: StarfishClient\n pullPath: string\n pushPath: string\n /** Custom conflict resolver. Defaults to remote-wins deep merge. Arrays are atomic. */\n onConflict?: ConflictResolver\n /** Max conflict retry attempts (default: 3). */\n maxRetries?: number\n /**\n * Encryptor for client-side E2E encryption. For v3 `delegated` collections,\n * build it via `createKeyringEncryptor(keyring, deviceKemKeys)`.\n */\n encryptor?: Encryptor\n /**\n * v3 author-signature plumbing. When set, every push attaches\n * `authorPubkey` (= `cap.sub`) and `authorSignature` (= base64 Ed25519 over\n * stable-stringify of the encrypted payload minus author fields).\n */\n signer?: SyncSigner\n /** Structured logger for sync events. */\n logger?: SyncLogger\n /** Name passed to logger methods (default: derived from pullPath). */\n loggerName?: string\n /** Validate data before push. Throws ValidationError on failure. */\n validate?: Validator\n}\n\nexport class SyncManager {\n private readonly client: StarfishClient\n private readonly pullPath: string\n private readonly pushPath: string\n private readonly onConflict: ConflictResolver\n private readonly maxRetries: number\n private readonly encryptor: Encryptor | null\n private readonly signer?: SyncSigner\n private readonly logger?: SyncLogger\n private readonly loggerName: string\n private readonly validate?: Validator\n\n private lastHash: string | null = null\n private lastCheckpoint: number = 0\n private localData: Record<string, unknown> = {}\n private aborted: boolean = false\n private lastFromCache: boolean = false\n\n constructor(options: SyncManagerOptions) {\n this.client = options.client\n this.pullPath = options.pullPath\n this.pushPath = options.pushPath\n this.onConflict = options.onConflict ?? deepMerge\n this.maxRetries = options.maxRetries ?? 3\n this.signer = options.signer\n this.logger = options.logger\n this.loggerName = options.loggerName ?? options.pullPath.split(\"/\").filter(Boolean).pop() ?? options.pullPath\n this.validate = options.validate\n this.encryptor = options.encryptor ?? null\n }\n\n abort(): void {\n this.aborted = true\n }\n\n get isAborted(): boolean {\n return this.aborted\n }\n\n getData(): Record<string, unknown> {\n return { ...this.localData }\n }\n\n /**\n * Merge a remote snapshot with local (optimistic) data using this manager's\n * conflict resolver \u2014 the same resolver the push-conflict path uses. A plain\n * {@link pull} overwrites the store's data with the server snapshot, which\n * would drop un-pushed local writes (they live only in the store, never in\n * `localData` until a push succeeds). The zustand binding calls this on pull\n * while the store is dirty so those writes survive. `local` wins by the same\n * rules as a push conflict.\n */\n resolve(\n local: Record<string, unknown>,\n remote: Record<string, unknown>,\n ): Record<string, unknown> {\n return this.onConflict(local, remote)\n }\n\n getHash(): string | null {\n return this.lastHash\n }\n\n /** Set the last-known server hash. Used by persistence layers to restore state across restarts. */\n setHash(hash: string | null): void {\n this.lastHash = hash\n }\n\n /**\n * Whether the most recent {@link pull} (or {@link seedFromCache}) was served\n * from the client's offline read-through cache rather than a live server\n * response. The binding surfaces this as a `stale` flag so the UI can show an\n * offline indicator without treating a cache hit as \"reachable\". Reset to\n * false by the next successful network pull.\n */\n getLastPullFromCache(): boolean {\n return this.lastFromCache\n }\n\n /**\n * Cache-first paint: seed `localData` from the client's read-through cache\n * WITHOUT touching the network, decrypting in memory for E2E collections.\n * Returns whether anything was seeded (false on a miss, an expired entry, or\n * a decrypt failure \u2014 e.g. keyring skew). Call once on store creation before\n * the initial live {@link pull}, which then supersedes the seeded snapshot.\n * Requires the client to have been built with a `cache`.\n */\n async seedFromCache(): Promise<boolean> {\n if (this.aborted) return false\n const cached = await this.client.peekCache(this.pullPath)\n if (!cached) return false\n let data: Record<string, unknown>\n try {\n data = this.encryptor ? await this.encryptor.decrypt(cached.data) : cached.data\n } catch {\n return false // undecryptable (keyring skew / foreign epoch) \u2014 seed nothing\n }\n if (this.aborted) return false\n this.localData = data\n this.lastHash = cached.hash\n // Leave lastCheckpoint at 0 so the first live pull is a full (re)sync, not a\n // delta against a stale cached checkpoint.\n this.lastFromCache = true\n return true\n }\n\n getCheckpoint(): number {\n return this.lastCheckpoint\n }\n\n async pull(): Promise<PullResult> {\n if (this.aborted) throw new AbortError()\n this.logger?.pullStart(this.loggerName)\n const start = performance.now()\n try {\n // NOTE: `SyncManager.pull` does NOT auto-enable `withKeyring`. Clients\n // that drive the keyring helpers from `recipients.ts` and want to save\n // the cold-start round-trip should call `client.pull(path, {withKeyring: true})`\n // directly. We keep `SyncManager` keyring-agnostic so it stays usable\n // for collections that don't use delegated encryption.\n const result = await this.client.pull(this.pullPath, this.lastCheckpoint)\n if (this.aborted) throw new AbortError()\n // True when the client served this from its offline cache (transport was\n // unreachable); a live response clears it. Surfaced as `stale` by the binding.\n this.lastFromCache = pullWasFromCache(result)\n\n if (this.encryptor) {\n const decrypted = await this.encryptor.decrypt(result.data)\n if (this.aborted) throw new AbortError()\n this.localData = decrypted\n result.data = decrypted\n } else if (this.lastCheckpoint > 0) {\n this.localData = deepMerge(this.localData, result.data)\n result.data = this.localData\n } else {\n this.localData = result.data\n }\n\n this.lastHash = result.hash\n this.lastCheckpoint = result.timestamp\n this.logger?.pullSuccess(this.loggerName, Math.round(performance.now() - start))\n return result\n } catch (err) {\n this.logger?.pullError(this.loggerName, err instanceof Error ? err.message : String(err))\n throw err\n }\n }\n\n async push(data: Record<string, unknown>): Promise<{ hash: string; timestamp: number }> {\n if (this.aborted) throw new AbortError()\n if (this.validate) {\n const result = this.validate(data)\n if (result !== true) throw new ValidationError(result)\n }\n this.logger?.pushStart(this.loggerName)\n const start = performance.now()\n let attempt = 0\n let pendingData = data\n\n while (attempt <= this.maxRetries) {\n try {\n const sealed = this.encryptor\n ? await this.encryptor.encrypt(pendingData)\n : pendingData\n if (this.aborted) throw new AbortError()\n\n // v3.0 signer path: sign the document author proof over the doc-author\n // canonical input (domain-tagged, bound to documentKey) and pass it as\n // top-level body siblings of `data` (NOT inside `data`), where the server\n // verifies it and stores the raw author pubkey.\n let author: AppendAuthor | undefined\n if (this.signer) {\n const { devEdPubHex, sign } = await this.signer.getSigner()\n if (this.aborted) throw new AbortError()\n const documentKey = stripPushPrefix(this.pushPath)\n const canonical = docAuthorCanonicalInput(documentKey, sealed as Record<string, unknown>)\n const sigBytes = await sign(new TextEncoder().encode(canonical))\n if (this.aborted) throw new AbortError()\n author = {\n [AUTHOR_PUBKEY_FIELD]: devEdPubHex,\n [AUTHOR_SIGNATURE_FIELD]: getBase64().encode(sigBytes),\n }\n }\n\n const result = await this.client.push(\n this.pushPath,\n sealed as Record<string, unknown>,\n this.lastHash,\n author,\n )\n if (this.aborted) throw new AbortError()\n this.lastHash = result.hash\n this.lastCheckpoint = result.timestamp\n this.localData = pendingData\n this.logger?.pushSuccess(this.loggerName, Math.round(performance.now() - start))\n return result\n } catch (err) {\n if (err instanceof AbortError) throw err\n if (!(err instanceof ConflictError) || attempt >= this.maxRetries) {\n this.logger?.pushError(this.loggerName, err instanceof Error ? err.message : String(err))\n throw err\n }\n this.logger?.conflict(this.loggerName, attempt + 1)\n try {\n const remote = await this.client.pull(this.pullPath)\n if (this.aborted) throw new AbortError()\n const remoteData = this.encryptor\n ? await this.encryptor.decrypt(remote.data)\n : remote.data\n if (this.aborted) throw new AbortError()\n this.lastHash = remote.hash\n this.lastCheckpoint = remote.timestamp\n pendingData = this.onConflict(pendingData, remoteData)\n } catch (resolveErr) {\n if (resolveErr instanceof AbortError) throw resolveErr\n const msg = resolveErr instanceof Error ? resolveErr.message : String(resolveErr)\n this.logger?.pushError(this.loggerName, `Conflict resolution failed (attempt ${attempt + 1}): ${msg}`)\n throw resolveErr\n }\n await new Promise<void>(resolve => setTimeout(resolve, Math.min(100 * Math.pow(2, attempt), 2000) + Math.random() * 100))\n attempt++\n }\n }\n throw new ConflictError()\n }\n\n async update(\n modifier: (current: Record<string, unknown>) => Record<string, unknown>\n ): Promise<{ hash: string; timestamp: number }> {\n await this.pull()\n const updated = modifier(this.localData)\n return this.push(updated)\n }\n}\n", "/** Validation result: true if valid, or an array of error messages. */\nexport type ValidationResult = true | string[]\n\n/** A function that validates data before push. */\nexport type Validator = (data: Record<string, unknown>) => ValidationResult\n\n/** Error thrown when pre-push validation fails. */\nexport class ValidationError extends Error {\n constructor(public readonly errors: string[]) {\n super(`Validation failed: ${errors.join(\"; \")}`)\n this.name = \"ValidationError\"\n }\n}\n\n/**\n * Creates a validator from a JSON Schema object.\n * Requires an Ajv-compatible validate function.\n *\n * @example\n * ```ts\n * import Ajv from \"ajv\"\n * const ajv = new Ajv()\n * const validator = createSchemaValidator(ajv, mySchema)\n * ```\n */\nexport function createSchemaValidator(\n ajv: { compile: (schema: object) => { (data: unknown): boolean; errors?: unknown }; errorsText: (errors?: unknown) => string },\n schema: object,\n): Validator {\n const validate = ajv.compile(schema)\n return (data) => {\n if (validate(data)) return true\n return [ajv.errorsText(validate.errors)]\n }\n}\n", "import {\n verifyAppendAuthor,\n type Encryptor,\n} from \"@drakkar.software/starfish-protocol\"\nimport type { StarfishClient, AppendPullOptions } from \"./client.js\"\nimport type { SyncLogger } from \"./logger.js\"\n\n/** The `/pull/` action prefix; mirrors `PUSH_PATH_PREFIX` for the read side. */\nconst PULL_PATH_PREFIX = \"/pull/\"\n\n/** The storage `documentKey` for a pull `path`: the path with the `/pull/`\n * action prefix stripped (the namespace lives only in the URL). The author\n * signature binds to this key, so a reader re-derives it the same way the\n * writer did from `/push/\u2026`. */\nfunction stripPullPrefix(path: string): string {\n return path.startsWith(PULL_PATH_PREFIX) ? path.slice(PULL_PATH_PREFIX.length) : path\n}\n\n/**\n * A single stored element of an append-only collection, exactly as returned by\n * `client.pull(path, { appendField })`. `ts` is the server-assigned, strictly\n * increasing element timestamp; `data` is the payload (plaintext under the\n * `\"none\"` encryption mode, the opaque encryptor wrapper under `\"delegated\"`).\n *\n * When an {@link AppendLogCursor} is given an `encryptor`, the elements it\n * stores and returns carry the **decrypted** `data` while preserving `ts` and\n * the author fields \u2014 so the shape is uniform and re-seedable. The exception is\n * `persistEncrypted` mode (see {@link AppendLogCursorOptions.persistEncrypted}),\n * where the stored elements keep their **ciphertext** `data` (E2EE-safe to\n * persist) and decryption happens only on read via {@link AppendLogCursor.pull}\n * and {@link AppendLogCursor.getDecryptedItems}.\n */\nexport interface AppendElement {\n ts: number\n data: Record<string, unknown>\n authorPubkey?: string\n authorSignature?: string\n}\n\n/** Per-element author-signature verification policy for {@link AppendLogCursor}. */\nexport interface AuthorVerifier {\n /** If set, every element's `authorPubkey` MUST equal this key (compared as\n * case-insensitive hex), else the pull fails. Omit to accept any signing key\n * (verify only that the signature is valid for the element's self-declared\n * `authorPubkey` \u2014 see the `verifyAuthor` note on restricting authors). */\n expectedAuthorPubkey?: string\n}\n\n/**\n * What to do when a single element fails verification or decryption during a\n * {@link AppendLogCursor.pull} (or {@link AppendLogCursor.getDecryptedItems}).\n *\n * - `\"throw\"` (default): the pull is **atomic** \u2014 the first bad element throws\n * and NO state is mutated, so the checkpoint never advances past an element\n * that could not be re-fetched. Use when every element must be readable.\n * - `\"skip\"`: a bad element is dropped from the returned/decrypted batch and the\n * checkpoint still **advances past it** (so it is never re-fetched), letting one\n * poison element fail without blanking the whole log. Intended for tolerating\n * **decrypt** failures in a multi-writer / E2EE log (keyring skew, a foreign or\n * corrupt element). SECURITY NOTE: `\"skip\"` ALSO silently drops elements that\n * fail *author* verification rather than failing loudly \u2014 so if you also need\n * strict authorship, set `verifyAuthor.expectedAuthorPubkey` (single author) or\n * check each element's `authorPubkey` against your authorized set after pull.\n */\nexport type ElementErrorPolicy = \"throw\" | \"skip\"\n\nexport interface AppendLogCursorOptions {\n client: StarfishClient\n /** Pull endpoint path, e.g. `\"/pull/events\"`. */\n pullPath: string\n /** Array field name in the pulled document. Defaults to `\"items\"`. */\n appendField?: string\n /**\n * Warm-start seed: raw envelopes the caller persisted last session. The\n * cursor adopts them verbatim and derives its initial checkpoint from their\n * max `ts`. Under the default mode they are taken as already-decrypted (and\n * never re-decrypted/re-verified); under `persistEncrypted` they are the\n * persisted **ciphertext** and are decrypted on read (see `persistEncrypted`).\n */\n initialItems?: AppendElement[]\n /**\n * Explicit checkpoint-only seed (ms). Resume incrementally without\n * rehydrating history. When given together with `initialItems`, it must be\n * `>= max(ts of initialItems)` (a lower value would re-fetch held items).\n */\n since?: number\n /**\n * When set, each freshly-pulled element's `.data` is decrypted via this\n * encryptor (the `ts`/author fields are preserved). Author verification, when\n * enabled, runs over the original (pre-decryption) `data`.\n *\n * Caveat (default mode): a returned / `getItems()` element then holds DECRYPTED\n * `data` but an `authorSignature` computed over the stored CIPHERTEXT \u2014 they no\n * longer match, so do NOT re-verify a decrypted element with `verifyAppendAuthor`.\n * The cursor already verified it (over the ciphertext) at pull time when\n * `verifyAuthor` is on; `authorPubkey` is retained for identity. (Under\n * `persistEncrypted` the stored elements keep their ciphertext, so this caveat\n * does not apply to `getItems()`.)\n */\n encryptor?: Encryptor\n /**\n * Per-element failure policy for verification/decryption. Defaults to\n * `\"throw\"` (atomic pull \u2014 preserves the pre-existing behavior). See\n * {@link ElementErrorPolicy}.\n */\n onElementError?: ElementErrorPolicy\n /**\n * Keep the **ciphertext** form of each element in the cursor's accumulated log\n * instead of the decrypted form (requires `encryptor`; a no-op without one,\n * since plaintext is already its own stored form). This makes\n * {@link AppendLogCursor.getItems} return the persistable ciphertext \u2014 safe to\n * write to disk for an end-to-end-encrypted log without leaking plaintext at\n * rest \u2014 while {@link AppendLogCursor.pull} still returns the freshly-decrypted\n * batch and {@link AppendLogCursor.getDecryptedItems} returns the full log\n * decrypted (for warm-start rendering). Defaults to `false` (store decrypted).\n */\n persistEncrypted?: boolean\n /**\n * `true` to verify every element's author signature, or a policy object.\n *\n * This verifies the signature is valid for the element's self-declared\n * `authorPubkey` \u2014 it does NOT by itself restrict WHICH authors are accepted.\n * To restrict authorship, set `expectedAuthorPubkey` (single author), or check\n * each `el.authorPubkey` against your own authorization source (keyring /\n * member list / cap allow-list) after pull \u2014 for a multi-writer log, the\n * authorized set lives there and changes over time, not here.\n *\n * The signature covers `data` + the document key, but NOT `ts`: a malicious\n * server cannot forge content, but can reorder or re-timestamp authentic\n * elements, so trust `ts` only as far as you trust the server.\n */\n verifyAuthor?: boolean | AuthorVerifier\n /** Structured logger for pull events. */\n logger?: SyncLogger\n /** Name passed to logger methods (default: derived from `pullPath`). */\n loggerName?: string\n}\n\n/** Thrown when an append element's author signature fails verification. */\nexport class AppendAuthorError extends Error {\n constructor(public readonly ts: number) {\n super(`append element author verification failed (ts=${ts})`)\n this.name = \"AppendAuthorError\"\n }\n}\n\n/** Largest `ts` among `items`, or `0` when empty. The checkpoint for an\n * append-only log is exactly this \u2014 the server returns elements with\n * `ts > checkpoint`, and element timestamps are strictly increasing. */\nexport function checkpointOf(items: readonly { ts: number }[]): number {\n let max = 0\n for (const it of items) if (it.ts > max) max = it.ts\n return max\n}\n\n/** Copy the optional author fields from `src` onto a fresh element with `data`. */\nfunction withAuthor(ts: number, data: Record<string, unknown>, src: AppendElement): AppendElement {\n const out: AppendElement = { ts, data }\n if (src.authorPubkey !== undefined) out.authorPubkey = src.authorPubkey\n if (src.authorSignature !== undefined) out.authorSignature = src.authorSignature\n return out\n}\n\n/**\n * A stateful cursor over an append-only collection. It owns the accumulated\n * array of elements and pulls only what is new: each {@link pull} derives the\n * checkpoint from the last element it holds and asks the server for elements\n * with a greater `ts`.\n *\n * This is the incremental, stateful counterpart to the deliberately stateless\n * `client.pull(path, { appendField, since })`, and the sibling of\n * {@link SyncManager} for append-only logs (no merge / push-conflict\n * machinery \u2014 a log only grows).\n *\n * The cursor accumulates every pulled element in memory; for an unboundedly\n * large log, pull a bounded window with raw `client.pull(path, { last })` instead.\n *\n * Cold start (nothing persisted) \u2014 first `pull()` fetches the whole collection:\n * ```ts\n * const log = new AppendLogCursor({ client, pullPath: \"/pull/events\" })\n * const all = await log.pull()\n * ```\n * Warm start (resume from persisted data) \u2014 first `pull()` fetches only newer\n * elements; persistence is a round-trip of `getItems()`:\n * ```ts\n * const log = new AppendLogCursor({ client, pullPath: \"/pull/events\",\n * initialItems: await store.load() })\n * const fresh = await log.pull()\n * await store.save(log.getItems())\n * ```\n * Warm start for an **E2EE** log \u2014 persist ciphertext, render decrypted:\n * ```ts\n * const log = new AppendLogCursor({ client, pullPath: \"/pull/streamchat\",\n * encryptor, persistEncrypted: true, onElementError: \"skip\",\n * initialItems: await store.load() }) // ciphertext from disk\n * const history = await log.getDecryptedItems() // render persisted history\n * const fresh = await log.pull() // decrypted delta\n * await store.save(log.getItems()) // ciphertext back to disk\n * ```\n */\nexport class AppendLogCursor {\n private readonly client: StarfishClient\n private readonly pullPath: string\n private readonly appendField: string\n private readonly encryptor?: Encryptor\n private readonly verifyAuthor?: boolean | AuthorVerifier\n private readonly onElementError: ElementErrorPolicy\n private readonly persistEncrypted: boolean\n private readonly documentKey: string\n private readonly logger?: SyncLogger\n private readonly loggerName: string\n\n private readonly items: AppendElement[]\n private lastCheckpoint: number\n\n /** Tail of the serialized pull chain. Concurrent `pull()` calls queue behind\n * it so each runs against the checkpoint the previous one advanced \u2014 no two\n * overlapping fetches read the same checkpoint and double-append a window. */\n private pullChain: Promise<unknown> = Promise.resolve()\n\n constructor(options: AppendLogCursorOptions) {\n this.client = options.client\n this.pullPath = options.pullPath\n this.appendField = options.appendField ?? \"items\"\n this.encryptor = options.encryptor\n this.verifyAuthor = options.verifyAuthor\n this.onElementError = options.onElementError ?? \"throw\"\n this.persistEncrypted = options.persistEncrypted ?? false\n this.documentKey = stripPullPrefix(options.pullPath)\n this.logger = options.logger\n this.loggerName =\n options.loggerName ?? options.pullPath.split(\"/\").filter(Boolean).pop() ?? options.pullPath\n\n const seed = options.initialItems ?? []\n const seedCheckpoint = checkpointOf(seed)\n if (options.since != null) {\n if (options.since < 0) throw new Error(\"since must be non-negative\")\n if (options.since < seedCheckpoint) {\n throw new Error(\"since must be >= the max ts of initialItems\")\n }\n this.lastCheckpoint = options.since\n } else {\n this.lastCheckpoint = seedCheckpoint\n }\n this.items = [...seed]\n }\n\n /**\n * Fetch elements newer than the current checkpoint, verify + decrypt them,\n * append them to the local log, and return ONLY the newly-fetched batch\n * (decrypted when an `encryptor` is set).\n *\n * Atomic under `onElementError: \"throw\"` (the default): the batch is fully\n * verified and decrypted into a local before any state mutation, so a\n * verify/decrypt failure throws without advancing the checkpoint past elements\n * that could never be re-fetched. Under `\"skip\"`, a failing element is dropped\n * from the returned batch but the checkpoint still advances past it.\n *\n * Safe to call concurrently: overlapping calls are serialized internally, so\n * each runs against the checkpoint the previous one advanced (no double-fetch\n * of the same window). The next pull after one completes will pick up anything\n * that arrived in between.\n */\n async pull(): Promise<AppendElement[]> {\n // Chain onto the previous pull (whether it resolved or rejected) so calls\n // run one-at-a-time against the latest checkpoint. `pullChain` swallows\n // outcomes to stay alive; the caller still sees this call's real result.\n const run = this.pullChain.then(\n () => this.doPull(),\n () => this.doPull(),\n )\n this.pullChain = run.then(\n () => undefined,\n () => undefined,\n )\n return run\n }\n\n private async doPull(): Promise<AppendElement[]> {\n this.logger?.pullStart(this.loggerName)\n const start = performance.now()\n try {\n const since = this.lastCheckpoint\n // Cold start (`since === 0`) wants the whole log: send an explicit\n // `?full=true` since the server now rejects an unbounded append-only pull.\n // Warm resume carries `?checkpoint=since`.\n const opts: AppendPullOptions =\n since > 0 ? { appendField: this.appendField, since } : { appendField: this.appendField, full: true }\n const raw = await this.client.pull<AppendElement>(this.pullPath, opts)\n\n const batch: AppendElement[] = [] // decrypted, returned to the caller\n const stored: AppendElement[] = [] // what we keep in `items` (cipher- or plaintext)\n let maxTs = since\n let skipped = 0\n for (const el of raw) {\n // Defensive: guard a misbehaving/mocked server from making us\n // double-append a held element. Gated on `since > 0` to mirror the\n // server (which only filters when checkpoint > 0): on a cold start\n // `since` is 0 and we must NOT drop a legitimate `ts: 0` first element.\n if (since > 0 && el.ts <= since) continue\n // Advance past every windowed element BEFORE verify/decrypt so a skipped\n // element still moves the checkpoint and is never re-fetched.\n if (el.ts > maxTs) maxTs = el.ts\n\n let decrypted: AppendElement | null = null\n try {\n this.verifyOne(el)\n const data = this.encryptor ? await this.encryptor.decrypt(el.data) : el.data\n decrypted = withAuthor(el.ts, data, el)\n } catch (err) {\n // \"throw\" rethrows here, before any state mutation below \u2014 atomic.\n if (this.onElementError !== \"skip\") throw err\n skipped++\n }\n\n if (this.persistEncrypted) {\n // Keep the original ciphertext envelope (even for a skipped element:\n // it is valid data we simply cannot read now \u2014 a later key might).\n stored.push(withAuthor(el.ts, el.data, el))\n } else if (decrypted) {\n stored.push(decrypted)\n }\n if (decrypted) batch.push(decrypted)\n }\n\n this.items.push(...stored)\n this.lastCheckpoint = maxTs\n this.logger?.pullSuccess(\n this.loggerName,\n Math.round(performance.now() - start),\n skipped > 0 ? { skippedCount: skipped } : undefined,\n )\n return batch\n } catch (err) {\n this.logger?.pullError(this.loggerName, err instanceof Error ? err.message : String(err))\n throw err\n }\n }\n\n /** Verify a single element's author signature over its RAW (pre-decryption)\n * `data`. Throws {@link AppendAuthorError} on any failure. No-op when\n * verification is disabled. */\n private verifyOne(el: AppendElement): void {\n if (!this.verifyAuthor) return\n const policy: AuthorVerifier = typeof this.verifyAuthor === \"object\" ? this.verifyAuthor : {}\n const { authorPubkey, authorSignature } = el\n if (!authorPubkey || !authorSignature) throw new AppendAuthorError(el.ts)\n // Public keys are hex, which is case-insensitive \u2014 compare normalized so a\n // caller passing a differently-cased `expectedAuthorPubkey` isn't falsely rejected.\n if (\n policy.expectedAuthorPubkey &&\n authorPubkey.toLowerCase() !== policy.expectedAuthorPubkey.toLowerCase()\n ) {\n throw new AppendAuthorError(el.ts)\n }\n void policy\n const ok = verifyAppendAuthor(\n this.documentKey,\n el.data,\n authorPubkey,\n authorSignature,\n )\n if (!ok) throw new AppendAuthorError(el.ts)\n }\n\n /** The full accumulated log (a shallow copy), in `ts` order. Under\n * `persistEncrypted` these carry CIPHERTEXT `data` (persist them as-is, then\n * re-seed via `initialItems`); otherwise they carry decrypted/plaintext data. */\n getItems(): AppendElement[] {\n return [...this.items]\n }\n\n /**\n * The full accumulated log, DECRYPTED \u2014 for rendering warm-started history in\n * `persistEncrypted` mode (where {@link getItems} holds ciphertext). Honors\n * `onElementError` (a `\"skip\"` cursor drops elements it cannot read). When the\n * cursor has no `encryptor`, or is not in `persistEncrypted` mode, the held\n * elements are already plaintext/decrypted and are returned as-is.\n */\n async getDecryptedItems(): Promise<AppendElement[]> {\n const snapshot = [...this.items]\n if (!this.encryptor || !this.persistEncrypted) return snapshot\n const out: AppendElement[] = []\n for (const el of snapshot) {\n try {\n this.verifyOne(el)\n const data = await this.encryptor.decrypt(el.data)\n out.push(withAuthor(el.ts, data, el))\n } catch (err) {\n if (this.onElementError !== \"skip\") throw err\n }\n }\n return out\n }\n\n /** The current checkpoint: the max `ts` held (the next pull's `since`). `0`\n * when nothing has been pulled or seeded. */\n getCheckpoint(): number {\n return this.lastCheckpoint\n }\n\n /** Restore the checkpoint without seeding items \u2014 for persistence layers that\n * store only the checkpoint. Used to resume incrementally across restarts.\n * Rejects a value below the max `ts` already held: rewinding would make the\n * next pull re-deliver, and duplicate, elements the cursor already has. */\n setCheckpoint(ts: number): void {\n if (ts < checkpointOf(this.items)) {\n throw new Error(\"checkpoint must be >= the max ts already held\")\n }\n this.lastCheckpoint = ts\n }\n}\n", "/** Extended metrics for sync operations. */\nexport interface SyncMetrics {\n bytesTransferred?: number\n compressedSize?: number\n conflictCount?: number\n retryCount?: number\n cacheHit?: boolean\n /** Elements an append-log pull dropped under `onElementError: \"skip\"`\n * (failed verification/decryption). Omitted when none were skipped. */\n skippedCount?: number\n}\n\n/** Structured logger for sync operations. */\nexport interface SyncLogger {\n pullStart(store: string): void\n pullSuccess(store: string, durationMs: number, metrics?: SyncMetrics): void\n pullError(store: string, error: string): void\n pushStart(store: string): void\n pushSuccess(store: string, durationMs: number, metrics?: SyncMetrics): void\n pushError(store: string, error: string): void\n conflict(store: string, attempt: number): void\n}\n\n/** Console-based sync logger with structured output. */\nexport const consoleSyncLogger: SyncLogger = {\n pullStart: (s) => console.log(`[starfish:${s}] pull started`),\n pullSuccess: (s, ms, m) => {\n let msg = `[starfish:${s}] pull OK (${ms}ms)`\n if (m?.bytesTransferred) msg += ` ${m.bytesTransferred}B`\n if (m?.cacheHit) msg += ` (cache hit)`\n console.log(msg)\n },\n pullError: (s, err) => console.error(`[starfish:${s}] pull failed: ${err}`),\n pushStart: (s) => console.log(`[starfish:${s}] push started`),\n pushSuccess: (s, ms, m) => {\n let msg = `[starfish:${s}] push OK (${ms}ms)`\n if (m?.bytesTransferred) msg += ` ${m.bytesTransferred}B`\n console.log(msg)\n },\n pushError: (s, err) => console.error(`[starfish:${s}] push failed: ${err}`),\n conflict: (s, n) => console.warn(`[starfish:${s}] conflict (attempt ${n})`),\n}\n\n/** Silent sync logger (no output). */\nexport const noopSyncLogger: SyncLogger = {\n pullStart: () => {},\n pullSuccess: () => {},\n pullError: () => {},\n pushStart: () => {},\n pushSuccess: () => {},\n pushError: () => {},\n conflict: () => {},\n}\n\n/** Accumulated metrics for a single store. */\ninterface StoreSummary {\n totalPulls: number\n totalPushes: number\n totalDurationMs: number\n totalBytes: number\n totalConflicts: number\n}\n\n/** Collects sync metrics over time. */\nexport interface MetricsCollector {\n recordPull(name: string, durationMs: number, metrics?: SyncMetrics): void\n recordPush(name: string, durationMs: number, metrics?: SyncMetrics): void\n recordConflict(name: string): void\n getSummary(): Record<string, { totalPulls: number; totalPushes: number; avgDurationMs: number; totalBytes: number; totalConflicts: number }>\n reset(): void\n}\n\n/** Create a metrics collector that accumulates sync statistics. */\nexport function createMetricsCollector(): MetricsCollector {\n const stores = new Map<string, StoreSummary>()\n\n function ensureStore(name: string): StoreSummary {\n let s = stores.get(name)\n if (!s) {\n s = { totalPulls: 0, totalPushes: 0, totalDurationMs: 0, totalBytes: 0, totalConflicts: 0 }\n stores.set(name, s)\n }\n return s\n }\n\n return {\n recordPull(name, durationMs, metrics) {\n const s = ensureStore(name)\n s.totalPulls++\n s.totalDurationMs += durationMs\n if (metrics?.bytesTransferred) s.totalBytes += metrics.bytesTransferred\n },\n recordPush(name, durationMs, metrics) {\n const s = ensureStore(name)\n s.totalPushes++\n s.totalDurationMs += durationMs\n if (metrics?.bytesTransferred) s.totalBytes += metrics.bytesTransferred\n },\n recordConflict(name) {\n ensureStore(name).totalConflicts++\n },\n getSummary() {\n const result: Record<string, { totalPulls: number; totalPushes: number; avgDurationMs: number; totalBytes: number; totalConflicts: number }> = {}\n for (const [name, s] of stores) {\n const totalOps = s.totalPulls + s.totalPushes\n result[name] = {\n totalPulls: s.totalPulls,\n totalPushes: s.totalPushes,\n avgDurationMs: totalOps > 0 ? Math.round(s.totalDurationMs / totalOps) : 0,\n totalBytes: s.totalBytes,\n totalConflicts: s.totalConflicts,\n }\n }\n return result\n },\n reset() {\n stores.clear()\n },\n }\n}\n", "/** A function that migrates data from one schema version to the next. */\nexport type MigrationFn = (data: Record<string, unknown>) => Record<string, unknown>\n\nexport interface MigrationConfig {\n /** The current schema version of the application. */\n currentVersion: number\n /** Map of version number to the migration that upgrades FROM that version. */\n migrations: Record<number, MigrationFn>\n}\n\n/**\n * Creates a migration runner that upgrades documents to the current schema version.\n *\n * Given a document with `_schemaVersion`, applies each migration in sequence\n * until the document reaches `currentVersion`. Throws if the document version\n * is ahead of the app (forward compatibility guard).\n */\nexport function createMigrator(\n config: MigrationConfig,\n): (data: Record<string, unknown>) => Record<string, unknown> {\n // Eagerly validate the migration chain\n for (let v = 1; v < config.currentVersion; v++) {\n if (!config.migrations[v]) {\n throw new Error(`Missing migration for version ${v} -> ${v + 1}`)\n }\n }\n\n return (data) => {\n const version = typeof data._schemaVersion === \"number\" ? data._schemaVersion : 1\n\n if (version > config.currentVersion) {\n throw new Error(\n `Document schema version ${version} is newer than app version ${config.currentVersion}. Update the app.`,\n )\n }\n\n if (version === config.currentVersion) return data\n\n let result = { ...data }\n for (let v = version; v < config.currentVersion; v++) {\n const fn = config.migrations[v]\n if (!fn) {\n throw new Error(`Missing migration for version ${v} -> ${v + 1}`)\n }\n try {\n result = fn(result)\n } catch (err) {\n throw new Error(\n `Migration from version ${v} to ${v + 1} failed: ${err instanceof Error ? err.message : String(err)}`,\n { cause: err },\n )\n }\n }\n result._schemaVersion = config.currentVersion\n return result\n }\n}\n", "import type { ConflictResolver } from \"./types.js\"\n\n/** Metadata about which fields were affected during conflict resolution. */\nexport interface ConflictMeta {\n /** Field names that differed between local and remote. */\n conflictedFields: string[]\n /** How the conflict was resolved. */\n resolvedBy: \"local\" | \"remote\" | \"merged\"\n /** Timestamp of resolution. */\n timestamp: number\n}\n\n/** Conflict resolver that also returns metadata about the resolution. */\nexport type ConflictResolverWithMeta = (\n local: Record<string, unknown>,\n remote: Record<string, unknown>,\n) => { data: Record<string, unknown>; meta: ConflictMeta }\n\n/** Shallow structural comparison of two values. Handles objects, arrays, and primitives. */\nfunction shallowEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true\n if (a == null || b == null) return a === b\n if (typeof a !== typeof b) return false\n if (typeof a !== \"object\") return false\n\n if (Array.isArray(a) !== Array.isArray(b)) return false\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false\n return a.every((v, i) => shallowEqual(v, b[i]))\n }\n\n const aObj = a as Record<string, unknown>\n const bObj = b as Record<string, unknown>\n const aKeys = Object.keys(aObj)\n const bKeys = Object.keys(bObj)\n if (aKeys.length !== bKeys.length) return false\n return aKeys.every((k) => shallowEqual(aObj[k], bObj[k]))\n}\n\n/**\n * Wrap a standard ConflictResolver to also return metadata about which fields conflicted.\n * Compares local and remote keys to detect differing fields.\n */\nexport function withConflictMeta(resolver: ConflictResolver): ConflictResolverWithMeta {\n return (local, remote) => {\n const conflictedFields: string[] = []\n const allKeys = new Set([...Object.keys(local), ...Object.keys(remote)])\n for (const key of allKeys) {\n const lv = local[key]\n const rv = remote[key]\n if (!shallowEqual(lv, rv)) {\n conflictedFields.push(key)\n }\n }\n\n const data = resolver(local, remote)\n\n // Determine how it was resolved using structural comparison\n let resolvedBy: \"local\" | \"remote\" | \"merged\" = \"merged\"\n if (shallowEqual(data, local)) resolvedBy = \"local\"\n else if (shallowEqual(data, remote)) resolvedBy = \"remote\"\n\n return {\n data,\n meta: {\n conflictedFields,\n resolvedBy,\n timestamp: Date.now(),\n },\n }\n }\n}\n\n/** Compare two timestamp values. Handles both numeric (epoch) and string (ISO-8601) timestamps. */\nfunction compareTimestamps(a: unknown, b: unknown): boolean {\n if (typeof a === \"number\" && typeof b === \"number\") return a >= b\n return String(a ?? \"\") >= String(b ?? \"\")\n}\n\n/**\n * Creates a conflict resolver that merges arrays by ID with per-item\n * timestamp comparison, and uses document-level timestamp for scalars.\n *\n * For arrays: builds a union of both sets keyed by `idKey`. When both\n * sides have the same item, the one with the newer `timestampKey` wins.\n * For scalars: the document with the newer `documentTimestampKey` wins.\n *\n * @example\n * ```ts\n * const merge = createUnionMerge()\n * const sync = new SyncManager({ ..., onConflict: merge })\n * ```\n */\nexport function createUnionMerge(options?: {\n /** Key used to identify items in arrays (default: \"id\"). */\n idKey?: string\n /** Key used for per-item timestamp comparison (default: \"updatedAt\"). */\n timestampKey?: string\n /** Key used for document-level timestamp comparison (default: \"timestamp\"). */\n documentTimestampKey?: string\n}): ConflictResolver {\n const idKey = options?.idKey ?? \"id\"\n const tsKey = options?.timestampKey ?? \"updatedAt\"\n const docTsKey = options?.documentTimestampKey ?? \"timestamp\"\n\n return (local, remote) => {\n const result: Record<string, unknown> = {}\n const localNewer = compareTimestamps(local[docTsKey], remote[docTsKey])\n const allKeys = new Set([...Object.keys(local), ...Object.keys(remote)])\n\n for (const key of allKeys) {\n const lv = local[key]\n const rv = remote[key]\n\n // Both sides have arrays \u2014 attempt ID-based union\n if (Array.isArray(lv) && Array.isArray(rv)) {\n const map = new Map<unknown, Record<string, unknown>>()\n\n // Seed with remote items\n for (const item of rv) {\n if (item && typeof item === \"object\" && idKey in item) {\n map.set((item as Record<string, unknown>)[idKey], item as Record<string, unknown>)\n } else {\n map.set(Symbol(), item as Record<string, unknown>)\n }\n }\n\n // Overlay local items (per-item timestamp wins)\n for (const item of lv) {\n if (item && typeof item === \"object\" && idKey in item) {\n const localItem = item as Record<string, unknown>\n const id = localItem[idKey]\n const remoteItem = map.get(id)\n if (!remoteItem) {\n map.set(id, localItem)\n } else {\n if (compareTimestamps(localItem[tsKey], remoteItem[tsKey])) {\n map.set(id, localItem)\n }\n }\n } else {\n map.set(Symbol(), item as Record<string, unknown>)\n }\n }\n\n result[key] = [...map.values()]\n } else if (lv !== undefined && rv !== undefined) {\n // Scalar: document-level timestamp wins\n result[key] = localNewer ? lv : rv\n } else {\n // Only one side has the key\n result[key] = lv ?? rv\n }\n }\n\n return result\n }\n}\n\n/**\n * Creates a conflict resolver that handles soft-deleted items (tombstones).\n * Extends union merge with tombstone awareness: if an item exists on one side\n * with a `deletedAtKey` set, that deletion is respected even if the other side\n * still has the item alive \u2014 as long as the deletion timestamp is newer.\n */\nexport function createSoftDeleteResolver(options?: {\n idKey?: string\n timestampKey?: string\n documentTimestampKey?: string\n /** Key marking an item as deleted (default: \"_deletedAt\"). */\n deletedAtKey?: string\n}): ConflictResolver {\n const idKey = options?.idKey ?? \"id\"\n const tsKey = options?.timestampKey ?? \"updatedAt\"\n const deletedAtKey = options?.deletedAtKey ?? \"_deletedAt\"\n const baseMerge = createUnionMerge(options)\n\n return (local, remote) => {\n const merged = baseMerge(local, remote)\n\n // Build a tombstone map from both sides: id \u2192 deletedAt timestamp\n const tombstones = new Map<unknown, unknown>()\n for (const source of [local, remote]) {\n for (const key of Object.keys(source)) {\n const arr = source[key]\n if (!Array.isArray(arr)) continue\n for (const item of arr) {\n if (item && typeof item === \"object\" && idKey in item && deletedAtKey in item) {\n const rec = item as Record<string, unknown>\n const id = rec[idKey]\n const deletedAt = rec[deletedAtKey]\n if (typeof deletedAt === \"number\" || typeof deletedAt === \"string\") {\n const existing = tombstones.get(id)\n if (existing == null || compareTimestamps(deletedAt, existing)) tombstones.set(id, deletedAt)\n }\n }\n }\n }\n }\n\n // For merged arrays, ensure tombstoned items stay deleted\n // (don't resurrect an item if its tombstone is newer than its updatedAt)\n for (const key of Object.keys(merged)) {\n const value = merged[key]\n if (!Array.isArray(value)) continue\n\n merged[key] = value.filter((item) => {\n if (!item || typeof item !== \"object\" || !(idKey in item)) return true\n const rec = item as Record<string, unknown>\n const id = rec[idKey]\n const deletedAt = tombstones.get(id)\n if (deletedAt == null) return true\n // Keep the item if it has a deletedAt (it's the tombstone itself)\n if (rec[deletedAtKey] != null) return true\n // Filter out alive items that have a newer tombstone\n return compareTimestamps(rec[tsKey], deletedAt) && rec[tsKey] !== deletedAt\n })\n }\n\n return merged\n }\n}\n\n/**\n * Simple resolver: the document with the newer timestamp wins entirely.\n * No per-field or per-item merging.\n */\nexport function timestampWinner(\n timestampKey = \"timestamp\",\n): ConflictResolver {\n return (local, remote) => {\n return compareTimestamps(local[timestampKey], remote[timestampKey])\n ? local\n : remote\n }\n}\n\n/**\n * Remove expired tombstones from an array of items.\n * Items with a `deletedAtKey` older than `ttlMs` are pruned.\n *\n * @param items - Array of items, some with a deletedAt timestamp\n * @param ttlMs - Time-to-live in ms for tombstones (default: 30 days)\n * @param deletedAtKey - Key marking deletion timestamp (default: \"_deletedAt\")\n */\nexport function pruneTombstones<T extends Record<string, unknown>>(\n items: T[],\n ttlMs = 30 * 24 * 60 * 60 * 1000,\n deletedAtKey = \"_deletedAt\",\n): T[] {\n const cutoff = Date.now() - ttlMs\n return items.filter((item) => {\n const deletedAt = item[deletedAtKey]\n if (deletedAt == null) return true\n if (typeof deletedAt === \"number\") return deletedAt > cutoff\n if (typeof deletedAt === \"string\") return new Date(deletedAt).getTime() > cutoff\n return false\n })\n}\n", "export interface Snapshot {\n timestamp: number\n label: string\n data: string\n}\n\nexport interface SnapshotHistoryOptions {\n /** Maximum number of snapshots to retain. Oldest are trimmed first. Default: 20. */\n maxSnapshots?: number\n /** localStorage key for persistence. Pass to enable auto-save/load. */\n storageKey?: string\n}\n\nexport class SnapshotHistory {\n private snapshots: Snapshot[] = []\n private readonly maxSnapshots: number\n private readonly storageKey: string | undefined\n\n constructor(options?: SnapshotHistoryOptions) {\n this.maxSnapshots = options?.maxSnapshots ?? 20\n this.storageKey = options?.storageKey\n\n if (this.storageKey) {\n try {\n const raw = localStorage.getItem(this.storageKey)\n if (raw) {\n const parsed = JSON.parse(raw)\n if (Array.isArray(parsed)) this.snapshots = parsed\n }\n } catch { /* corrupted or unavailable \u2014 start fresh */ }\n }\n }\n\n /** Take a labeled snapshot of the given data. */\n take(label: string, data: Record<string, unknown>): void {\n this.snapshots.push({\n timestamp: Date.now(),\n label,\n data: JSON.stringify(data),\n })\n if (this.snapshots.length > this.maxSnapshots) {\n this.snapshots = this.snapshots.slice(-this.maxSnapshots)\n }\n this.persist()\n }\n\n /** Restore data from a snapshot at the given index. Returns undefined if index is invalid or data is corrupt. */\n restore(index: number): Record<string, unknown> | undefined {\n const snapshot = this.snapshots[index]\n if (!snapshot) return undefined\n try {\n return JSON.parse(snapshot.data)\n } catch {\n return undefined\n }\n }\n\n /** List available snapshots (metadata only, no data payload). */\n list(): Array<{ timestamp: number; label: string }> {\n return this.snapshots.map(({ timestamp, label }) => ({ timestamp, label }))\n }\n\n /** Clear all snapshots. */\n clear(): void {\n this.snapshots = []\n this.persist()\n }\n\n private persist(): void {\n if (!this.storageKey) return\n try {\n localStorage.setItem(this.storageKey, JSON.stringify(this.snapshots))\n } catch { /* quota exceeded \u2014 skip silently */ }\n }\n}\n", "/** Minimal state needed by polling utilities. */\nexport interface PollableState {\n online: boolean\n syncing: boolean\n}\n\nconst DEFAULT_INTERVALS: Record<string, number> = {\n \"slow-2g\": 120_000,\n \"2g\": 60_000,\n \"3g\": 30_000,\n \"4g\": 10_000,\n}\n\nconst DEFAULT_FALLBACK_MS = 15_000\n\n/**\n * Start periodic pulling at a fixed interval.\n * Skips pulls when offline or already syncing.\n * Returns a cleanup function that stops polling.\n */\nexport function startPolling(\n pullFn: () => Promise<void>,\n getState: () => PollableState,\n intervalMs = 30_000,\n): () => void {\n const timer = setInterval(() => {\n const { online, syncing } = getState()\n if (online && !syncing) pullFn().catch((err) => { console.error(\"[Starfish] poll failed:\", err) })\n }, intervalMs)\n\n return () => clearInterval(timer)\n}\n\nexport interface AdaptivePollingOptions {\n /** Override the base interval in ms. If set, skips network quality detection. */\n intervalMs?: number\n /** Custom mapping from effectiveType to interval in ms. */\n intervals?: Record<string, number>\n}\n\nexport interface AdaptivePollingControls {\n pause: () => void\n resume: () => void\n stop: () => void\n}\n\n/**\n * Start polling with adaptive intervals based on network quality.\n * Uses the Network Information API (`navigator.connection.effectiveType`) when available.\n * Returns controls to pause, resume, or stop polling.\n */\nexport function startAdaptivePolling(\n pullFn: () => Promise<void>,\n getState: () => PollableState,\n options?: AdaptivePollingOptions,\n): AdaptivePollingControls {\n let intervalMs: number\n\n if (options?.intervalMs != null) {\n intervalMs = options.intervalMs\n } else {\n const intervals = options?.intervals ?? DEFAULT_INTERVALS\n let effectiveType: string | undefined\n if (typeof navigator !== \"undefined\" && \"connection\" in navigator) {\n effectiveType = (navigator as unknown as { connection: { effectiveType?: string } }).connection.effectiveType\n }\n intervalMs = (effectiveType != null ? intervals[effectiveType] : undefined) ?? DEFAULT_FALLBACK_MS\n }\n\n let paused = false\n\n const timer = setInterval(() => {\n if (paused) return\n const { online, syncing } = getState()\n if (online && !syncing) pullFn().catch((err) => { console.error(\"[Starfish] adaptive poll failed:\", err) })\n }, intervalMs)\n\n return {\n pause: () => { paused = true },\n resume: () => { paused = false },\n stop: () => clearInterval(timer),\n }\n}\n", "/**\n * Request deduplication: prevents multiple concurrent identical GET requests.\n * If a GET request is in-flight for a URL, subsequent identical GET requests\n * return the same Promise. POST/PUT/DELETE/PATCH are never deduped.\n */\nexport function createDedupFetch(\n baseFetch: typeof globalThis.fetch = globalThis.fetch.bind(globalThis),\n): typeof globalThis.fetch {\n const inflightGets = new Map<string, Promise<Response>>()\n\n return (async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {\n const method = (init?.method ?? \"GET\").toUpperCase()\n\n // Only dedup GET requests\n if (method !== \"GET\") {\n return baseFetch(input, init)\n }\n\n const url = typeof input === \"string\"\n ? input\n : input instanceof URL\n ? input.toString()\n : (input as Request).url\n\n const existing = inflightGets.get(url)\n if (existing) {\n // Return a clone \u2014 the original is reserved for cloning only\n return existing.then((res) => res.clone())\n }\n\n // Store a promise that resolves to a response we keep solely for cloning.\n // The first caller also gets a clone, ensuring the \"master\" body is never consumed.\n const promise = baseFetch(input, init)\n .then((res) => res)\n .finally(() => {\n inflightGets.delete(url)\n })\n\n inflightGets.set(url, promise)\n\n // First caller also gets a clone so the cached response body stays unconsumed\n return promise.then((res) => res.clone())\n }) as typeof globalThis.fetch\n}\n", "/**\n * Read-modify-write a document with hash-CAS conflict retry.\n *\n * The everyday way to atomically edit a synced document: pull the current\n * version, apply a pure `mutator` to its data, push the result with the read\n * hash, and retry on a {@link ConflictError} (a concurrent writer moved the hash)\n * by re-reading FRESH server state and re-applying the mutator. A missing\n * document (404) is surfaced to the mutator as `{ data: null, hash: null }` so it\n * can create the doc on first write.\n *\n * This replaces the ad-hoc `for (attempt\u2026) { pull; mutate; try push catch\n * ConflictError }` loop that applications otherwise hand-roll around every\n * editable doc. The `mutator` MUST be idempotent \u2014 it re-runs on each retry \u2014 and\n * returns `null` to signal a no-op (nothing changed; skip the write).\n */\nimport { StarfishClient } from \"./client.js\"\nimport { ConflictError, StarfishHttpError } from \"./types.js\"\n\n/** The current state handed to a {@link DocMutator}: the document data (or `null`\n * when the doc does not exist yet) and the hash to base the next push on. */\nexport interface DocState<T> {\n data: T | null\n hash: string | null\n}\n\n/**\n * Pure transform from the current document to the next. Return the full next\n * document body to write, or `null` for a no-op (the write is skipped). Runs once\n * per attempt on freshly-pulled state, so it must be idempotent.\n */\nexport type DocMutator<T> = (cur: DocState<T>) => T | null\n\nexport interface MutateDocOptions {\n /** Max push attempts before a persistent conflict propagates. Default 3. */\n maxAttempts?: number\n}\n\n/**\n * Atomically read-modify-write the document at `path`. Returns the document that\n * was written, or `null` if the mutator signalled a no-op. Throws the underlying\n * error on a non-conflict failure, or a {@link ConflictError} if every attempt\n * raced and lost.\n */\nexport async function mutateDoc<T extends Record<string, unknown> = Record<string, unknown>>(\n client: StarfishClient,\n path: string,\n mutator: DocMutator<T>,\n options: MutateDocOptions = {},\n): Promise<T | null> {\n const maxAttempts = Math.max(1, options.maxAttempts ?? 3)\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n let data: T | null = null\n let hash: string | null = null\n try {\n const res = await client.pull(path)\n data = (res.data as T) ?? null\n hash = res.hash ?? null\n } catch (err) {\n // A 404 means the doc does not exist yet \u2014 hand the mutator a null state so\n // it can create it. Any other HTTP/transport error propagates.\n if (!(err instanceof StarfishHttpError) || err.status !== 404) throw err\n }\n const next = mutator({ data, hash })\n if (next === null) return null\n try {\n await client.push(path, next, hash)\n return next\n } catch (err) {\n if (err instanceof ConflictError && attempt < maxAttempts - 1) continue\n throw err\n }\n }\n // Unreachable: the final attempt either returns or re-throws above.\n throw new ConflictError()\n}\n", "/** Encryption modes supported by the Starfish server. */\nexport type EncryptionMode = \"none\" | \"delegated\"\n\n/** Append-only configuration exposed via GET /config. */\nexport interface AppendOnlyClientInfo {\n /** Append-only strategy. Only `\"by_timestamp\"` is currently supported. */\n type: \"by_timestamp\"\n /** Array field name in the stored document. Defaults to \"items\". */\n field?: string\n /** false = no storage write (replaces queueOnly). true/absent = append to array. */\n persist?: boolean\n}\n\n/** Append-only configuration exposed via GET /config. */\nexport interface AppendOnlyClientInfo {\n /** Array field name in the stored document. Defaults to \"items\". */\n field?: string\n /** false = no storage write (replaces queueOnly). true/absent = append to array. */\n persist?: boolean\n /** When true, server validates client's baseHash against hash(lastItem). */\n checkLastItem?: boolean\n}\n\n/** Per-collection metadata returned by GET /config. */\nexport interface CollectionClientInfo {\n name: string\n maxBodyBytes: number\n encryption: EncryptionMode\n allowedMimeTypes: string[]\n pullOnly?: boolean\n pushOnly?: boolean\n appendOnly?: AppendOnlyClientInfo\n ttlMs?: number\n forceFullFetch?: boolean\n}\n\n/** Response shape of GET /config. */\nexport interface ConfigResponse {\n collections: CollectionClientInfo[]\n namespaces?: Record<string, { collections: CollectionClientInfo[] }>\n}\n\n/**\n * Fetch the server's collection manifest from GET /config.\n *\n * @param baseUrl - Base URL of the Starfish server (e.g. `\"https://api.example.com/v1\"`).\n * @param options.headers - Optional request headers (e.g. `Authorization`).\n * @throws {Error} if the server returns a non-2xx response.\n */\nexport async function fetchServerConfig(\n baseUrl: string,\n options?: { headers?: Record<string, string> },\n): Promise<ConfigResponse> {\n const url = `${baseUrl.replace(/\\/$/, \"\")}/config`\n const res = await fetch(url, {\n method: \"GET\",\n headers: options?.headers,\n })\n if (!res.ok) {\n throw new Error(`fetchServerConfig: ${res.status} ${res.statusText}`)\n }\n return res.json() as Promise<ConfigResponse>\n}\n", "/**\n * IndexedDB-based storage adapter for Zustand persistence.\n * Implements the same interface as Zustand's StateStorage (getItem/setItem/removeItem).\n * Supports larger data than localStorage (typically 50MB+).\n */\n\nexport interface IndexedDBStorageOptions {\n /** Database name. Default: \"starfish\" */\n dbName?: string\n /** Object store name. Default: \"state\" */\n storeName?: string\n}\n\nexport interface AsyncStateStorage {\n getItem: (name: string) => Promise<string | null>\n setItem: (name: string, value: string) => Promise<void>\n removeItem: (name: string) => Promise<void>\n}\n\nfunction openDB(dbName: string, storeName: string): Promise<IDBDatabase> {\n return new Promise((resolve, reject) => {\n const request = indexedDB.open(dbName, 1)\n request.onupgradeneeded = () => {\n const db = request.result\n if (!db.objectStoreNames.contains(storeName)) {\n db.createObjectStore(storeName)\n }\n }\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => reject(request.error)\n })\n}\n\nfunction idbRequest<T>(request: IDBRequest<T>): Promise<T> {\n return new Promise((resolve, reject) => {\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => reject(request.error)\n })\n}\n\nexport function createIndexedDBStorage(\n opts?: IndexedDBStorageOptions,\n): AsyncStateStorage {\n const dbName = opts?.dbName ?? \"starfish\"\n const storeName = opts?.storeName ?? \"state\"\n let dbPromise: Promise<IDBDatabase> | null = null\n\n function getDB(): Promise<IDBDatabase> {\n if (!dbPromise) {\n dbPromise = openDB(dbName, storeName).catch((err) => {\n dbPromise = null // Reset so next call retries\n throw err\n })\n }\n return dbPromise\n }\n\n return {\n async getItem(name: string): Promise<string | null> {\n const db = await getDB()\n const tx = db.transaction(storeName, \"readonly\")\n const store = tx.objectStore(storeName)\n const result = await idbRequest(store.get(name))\n return result ?? null\n },\n\n async setItem(name: string, value: string): Promise<void> {\n const db = await getDB()\n const tx = db.transaction(storeName, \"readwrite\")\n const store = tx.objectStore(storeName)\n await idbRequest(store.put(value, name))\n },\n\n async removeItem(name: string): Promise<void> {\n const db = await getDB()\n const tx = db.transaction(storeName, \"readwrite\")\n const store = tx.objectStore(storeName)\n await idbRequest(store.delete(name))\n },\n }\n}\n", "/**\n * KV-backed {@link PullCache} factory.\n *\n * {@link createKvPullCache} adapts any {@link AsyncStateStorage} (or any\n * object with `getItem`/`setItem`) into a {@link PullCache} that the\n * `StarfishClient` can use as its offline read-through cache.\n *\n * Why this matters: the client's `cache` option is ciphertext-at-rest by\n * construction \u2014 it stores the raw sealed server response and only decrypts\n * in memory on read. Backing the cache with the platform's own KV (AsyncStorage\n * on React Native, IndexedDB via `createIndexedDBStorage`, a custom adapter)\n * gives offline-first reads without exposing plaintext to the OS storage layer.\n *\n * @example\n * ```ts\n * import AsyncStorage from \"@react-native-async-storage/async-storage\"\n * import { StarfishClient } from \"@drakkar.software/starfish-client\"\n * import { createKvPullCache } from \"@drakkar.software/starfish-client\"\n *\n * const client = new StarfishClient({\n * baseUrl: \"https://api.example.com\",\n * cache: createKvPullCache(AsyncStorage, { prefix: \"sf:\", maxAgeMs: 30 * 24 * 60 * 60 * 1000 }),\n * })\n * ```\n */\nimport type { PullCache } from \"./types.js\"\n\n/** A storage backend that `createKvPullCache` can wrap. */\nexport interface KvStore {\n getItem(key: string): Promise<string | null> | string | null\n setItem(key: string, value: string): Promise<unknown> | unknown\n removeItem?: (key: string) => Promise<unknown> | unknown\n}\n\n/** Options for {@link createKvPullCache}. */\nexport interface KvPullCacheOptions {\n /**\n * Key prefix for cache entries (default `\"starfish.pullcache.\"`). Change\n * when sharing a KV store with other data to avoid key collisions.\n */\n prefix?: string\n /**\n * Maximum age in milliseconds for a cached snapshot. When set, an entry\n * older than `maxAgeMs` is returned as `null` (a cache MISS) so the next\n * pull goes to the network rather than serving arbitrarily stale data.\n *\n * Each entry stores a `_cachedAt` wall-clock timestamp; expiry is checked on\n * every `get`. Omit (default) for entries that never expire \u2014 recommended\n * for offline-first apps where any last-synced data beats none.\n */\n maxAgeMs?: number\n}\n\ninterface CacheEntry {\n payload: string\n _cachedAt: number\n}\n\n/**\n * Adapt a KV store into a {@link PullCache} for `StarfishClient`.\n *\n * The adapter serialises the cached pull payload (itself a JSON string) into\n * an outer JSON envelope that tracks the wall-clock write time for optional\n * max-age expiry. Reading a legacy entry without `_cachedAt` treats it as\n * fresh (backward-compatible with plain-string caches).\n *\n * All `get`/`set` errors are swallowed (the {@link PullCache} contract\n * requires implementations not to throw) \u2014 a failing KV store simply\n * degrades to \"no cache\" without crashing the app.\n */\nexport function createKvPullCache(kv: KvStore, opts: KvPullCacheOptions = {}): PullCache {\n const prefix = opts.prefix ?? \"starfish.pullcache.\"\n const maxAgeMs = opts.maxAgeMs\n\n return {\n async get(key: string): Promise<string | null> {\n try {\n const raw = await kv.getItem(prefix + key)\n if (raw === null || raw === undefined) return null\n\n // Try to read the new envelope format `{\"payload\":\"\u2026\",\"_cachedAt\":n}`.\n // Fall back to treating the raw string as the payload directly for\n // backward-compatibility with plain-string caches written before this\n // library was used.\n let payload: string\n let cachedAt: number | undefined\n try {\n const envelope = JSON.parse(raw) as Partial<CacheEntry>\n if (typeof envelope.payload === \"string\") {\n payload = envelope.payload\n cachedAt = envelope._cachedAt\n } else {\n // Plain-string format: the raw value is the payload.\n payload = raw\n }\n } catch {\n payload = raw\n }\n\n if (maxAgeMs !== undefined && cachedAt !== undefined) {\n if (Date.now() - cachedAt > maxAgeMs) return null\n }\n\n return payload\n } catch {\n return null\n }\n },\n\n async set(key: string, value: string): Promise<void> {\n try {\n const envelope: CacheEntry = { payload: value, _cachedAt: Date.now() }\n await kv.setItem(prefix + key, JSON.stringify(envelope))\n } catch {\n // Swallow \u2014 storage failures degrade to \"no cache\", not a crash.\n }\n },\n }\n}\n", "/**\n * Data export/import helpers for Starfish sync data.\n * Supports JSON and CSV formats.\n */\n\nexport interface ExportOptions {\n /** Output format. Default: \"json\" */\n format?: \"json\" | \"csv\"\n /** Pretty-print JSON output. Default: false */\n pretty?: boolean\n}\n\n/**\n * Export data to a string representation.\n * JSON: serializes the full object.\n * CSV: flattens top-level keys into columns. Array values are JSON-encoded.\n */\nexport function exportData(\n data: Record<string, unknown>,\n opts?: ExportOptions,\n): string {\n const format = opts?.format ?? \"json\"\n\n if (format === \"json\") {\n return opts?.pretty\n ? JSON.stringify(data, null, 2)\n : JSON.stringify(data)\n }\n\n // CSV export: each top-level key becomes a column\n return toCsv(data)\n}\n\n/**\n * Import data from a string representation.\n */\nexport function importData(\n raw: string,\n format: \"json\" | \"csv\" = \"json\",\n): Record<string, unknown> {\n if (format === \"json\") {\n const parsed = JSON.parse(raw)\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n throw new Error(\"Expected a JSON object\")\n }\n return parsed as Record<string, unknown>\n }\n\n return fromCsv(raw)\n}\n\n/**\n * Export data to a Blob suitable for download.\n */\nexport function exportToBlob(\n data: Record<string, unknown>,\n opts?: ExportOptions,\n): Blob {\n const format = opts?.format ?? \"json\"\n const content = exportData(data, opts)\n const mimeType = format === \"csv\" ? \"text/csv;charset=utf-8\" : \"application/json;charset=utf-8\"\n return new Blob([content], { type: mimeType })\n}\n\nfunction toCsv(data: Record<string, unknown>): string {\n const keys = Object.keys(data)\n const header = keys.map(escapeCsvField).join(\",\")\n\n const values = keys.map((k) => {\n const v = data[k]\n if (v === null || v === undefined) return \"\"\n if (typeof v === \"object\") return escapeCsvField(JSON.stringify(v))\n return escapeCsvField(String(v))\n })\n\n return `${header}\\n${values.join(\",\")}`\n}\n\nfunction fromCsv(raw: string): Record<string, unknown> {\n const lines = raw.trim().split(\"\\n\")\n if (lines.length < 2) {\n throw new Error(\"CSV must have at least a header row and a data row\")\n }\n\n const headers = parseCsvLine(lines[0]!)\n const values = parseCsvLine(lines[1]!)\n\n const result: Record<string, unknown> = {}\n for (let i = 0; i < headers.length; i++) {\n const key = headers[i]!\n const val = values[i] ?? \"\"\n // Try to parse JSON values\n try {\n result[key] = JSON.parse(val)\n } catch {\n result[key] = val\n }\n }\n return result\n}\n\nfunction escapeCsvField(field: string): string {\n if (field.includes(\",\") || field.includes('\"') || field.includes(\"\\n\")) {\n return `\"${field.replace(/\"/g, '\"\"')}\"`\n }\n return field\n}\n\nfunction parseCsvLine(line: string): string[] {\n const result: string[] = []\n let current = \"\"\n let inQuotes = false\n\n for (let i = 0; i < line.length; i++) {\n const ch = line[i]!\n if (inQuotes) {\n if (ch === '\"' && line[i + 1] === '\"') {\n current += '\"'\n i++\n } else if (ch === '\"') {\n inQuotes = false\n } else {\n current += ch\n }\n } else {\n if (ch === '\"') {\n inQuotes = true\n } else if (ch === \",\") {\n result.push(current)\n current = \"\"\n } else {\n current += ch\n }\n }\n }\n result.push(current)\n return result\n}\n", "/**\n * Background Sync API integration for pending changes.\n * Uses the Web Background Sync API to retry failed sync operations\n * when connectivity is restored, even if the app is closed.\n */\n\nexport interface BackgroundSyncOptions {\n /** Sync event tag. Default: \"starfish-sync\" */\n tag?: string\n}\n\n/** Check if the Background Sync API is supported in the current environment. */\nexport function isBackgroundSyncSupported(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n \"serviceWorker\" in navigator &&\n \"SyncManager\" in globalThis\n )\n}\n\n/**\n * Register a background sync event with the active service worker.\n * Returns true if registration succeeded, false if not supported or no active SW.\n */\nexport async function registerBackgroundSync(\n opts?: BackgroundSyncOptions,\n): Promise<boolean> {\n if (!isBackgroundSyncSupported()) return false\n\n const tag = opts?.tag ?? \"starfish-sync\"\n\n try {\n const registration = await navigator.serviceWorker.ready\n // @ts-expect-error - SyncManager types may not be available\n await registration.sync.register(tag)\n return true\n } catch {\n return false\n }\n}\n", "/**\n * Service Worker utilities for offline support and PWA functionality.\n */\n\nexport interface ServiceWorkerOptions {\n /** Scope for the service worker registration. */\n scope?: string\n /** Called when an updated service worker is available. */\n onUpdate?: (registration: ServiceWorkerRegistration) => void\n}\n\n/** Check if service workers are supported in the current environment. */\nexport function isServiceWorkerSupported(): boolean {\n return typeof navigator !== \"undefined\" && \"serviceWorker\" in navigator\n}\n\n/**\n * Register a service worker for offline support.\n * Returns the registration, or null if not supported.\n */\nexport async function registerServiceWorker(\n scriptUrl: string,\n opts?: ServiceWorkerOptions,\n): Promise<ServiceWorkerRegistration | null> {\n if (!isServiceWorkerSupported()) return null\n\n try {\n const registration = await navigator.serviceWorker.register(scriptUrl, {\n scope: opts?.scope,\n })\n\n if (opts?.onUpdate) {\n registration.onupdatefound = () => {\n const installingWorker = registration.installing\n if (installingWorker) {\n installingWorker.onstatechange = () => {\n if (\n installingWorker.state === \"installed\" &&\n navigator.serviceWorker.controller\n ) {\n opts.onUpdate!(registration)\n }\n }\n }\n }\n }\n\n return registration\n } catch {\n return null\n }\n}\n\n/** Unregister all service worker registrations. Returns true if any were unregistered. */\nexport async function unregisterServiceWorkers(): Promise<boolean> {\n if (!isServiceWorkerSupported()) return false\n\n try {\n const registrations = await navigator.serviceWorker.getRegistrations()\n let unregistered = false\n for (const registration of registrations) {\n const result = await registration.unregister()\n if (result) unregistered = true\n }\n return unregistered\n } catch {\n return false\n }\n}\n", "/**\n * React Suspense integration for Starfish sync data.\n * Creates resources that throw Promises while loading (Suspense protocol).\n */\n\ntype SuspenseStatus = \"pending\" | \"resolved\" | \"rejected\"\n\ninterface SuspenseResource<T> {\n /** Read the resource value. Throws a Promise while pending (Suspense protocol). */\n read(): T\n}\n\n/**\n * Create a Suspense-compatible resource from an async fetcher.\n * The first call to `read()` triggers the fetch. While loading, `read()` throws\n * a Promise (which React Suspense catches to show a fallback). Once resolved,\n * `read()` returns the value synchronously.\n *\n * @example\n * ```tsx\n * const resource = createSuspenseResource(() => syncManager.pull())\n * function MyComponent() {\n * const data = resource.read() // throws while loading, returns data when ready\n * return <div>{JSON.stringify(data)}</div>\n * }\n * ```\n */\nexport function createSuspenseResource<T>(\n fetcher: () => Promise<T>,\n): SuspenseResource<T> {\n let status: SuspenseStatus = \"pending\"\n let result: T\n let error: unknown\n let promise: Promise<void> | null = null\n\n function init(): Promise<void> {\n if (promise) return promise\n promise = fetcher().then(\n (value) => {\n status = \"resolved\"\n result = value\n },\n (err) => {\n status = \"rejected\"\n error = err\n },\n )\n return promise\n }\n\n return {\n read(): T {\n switch (status) {\n case \"pending\":\n throw init()\n case \"resolved\":\n return result\n case \"rejected\":\n throw error\n }\n },\n }\n}\n", "import type { StoreApi } from \"zustand/vanilla\"\nimport type { StarfishStore } from \"./bindings/zustand.js\"\nimport type { SyncManager } from \"./sync.js\"\n\n// \u2500\u2500 Shared types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface DebouncedSyncOptions {\n /**\n * How long to wait after the last `notify()` call before pushing (default: 2000 ms).\n * Shorter values reduce latency; longer values batch more edits into a single push.\n */\n delayMs?: number\n /**\n * Emit a warning when the estimated encrypted payload exceeds this byte count (default: 900 KB).\n * The estimate multiplies the JSON size by 1.34 (base64 overhead for encrypted blobs).\n * Set to `Infinity` to disable.\n */\n warnBytes?: number\n /**\n * Block the push when the estimated encrypted payload exceeds this byte count (default: 1 MB).\n * Prevents cryptic 413 errors from the server. Set to `Infinity` to disable.\n */\n maxBytes?: number\n /**\n * Serialize store data to a sync document before pushing.\n * Called inside the debounce timer, so it always captures the latest state.\n * If omitted, `store.getState().data` is used as-is.\n */\n serialize?: (currentData: Record<string, unknown>) => Record<string, unknown>\n /**\n * Called when the estimated payload size exceeds `warnBytes` but is still below `maxBytes`.\n * Use to show a warning in the UI.\n */\n onSizeWarning?: (estimatedBytes: number) => void\n /**\n * Called when the estimated payload size exceeds `maxBytes`.\n * The push is blocked. Use to alert the user that data needs to be pruned.\n * If omitted, a console error is printed.\n */\n onSizeExceeded?: (estimatedBytes: number) => void\n}\n\nexport interface DebouncedSync {\n /**\n * Schedule a push. If called again within `delayMs`, the timer resets.\n * Safe to call on every domain store mutation.\n */\n notify: () => void\n /** Cancel any pending debounced push. Does not affect an already-in-flight push. */\n cancel: () => void\n}\n\nexport interface DebouncedPushOptions {\n /**\n * How long to wait after the last `notify()` call before pushing (default: 2000 ms).\n */\n delayMs?: number\n /**\n * Required: provides the document to push when the debounce timer fires.\n * Called inside the timer so it always captures the latest state.\n */\n serialize: () => Record<string, unknown>\n /**\n * Emit a warning when the estimated encrypted payload exceeds this byte count (default: 900 KB).\n * Set to `Infinity` to disable.\n */\n warnBytes?: number\n /**\n * Block the push when the estimated encrypted payload exceeds this byte count (default: 1 MB).\n * Set to `Infinity` to disable.\n */\n maxBytes?: number\n /**\n * Called when the estimated payload size exceeds `warnBytes` but is below `maxBytes`.\n */\n onSizeWarning?: (estimatedBytes: number) => void\n /**\n * Called when the estimated payload size exceeds `maxBytes`. The push is blocked.\n * If omitted, a console error is printed.\n */\n onSizeExceeded?: (estimatedBytes: number) => void\n /**\n * Called when `syncManager.push()` throws. Default: `console.warn`.\n */\n onError?: (err: unknown) => void\n}\n\nexport interface DebouncedPush {\n /**\n * Schedule a push. If called again within `delayMs`, the timer resets.\n */\n notify: () => void\n /** Cancel any pending debounced push. Does not affect an already-in-flight push. */\n cancel: () => void\n}\n\n// \u2500\u2500 Implementation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst DEFAULT_DELAY_MS = 2000\nconst DEFAULT_WARN_BYTES = 900 * 1024 // 900 KB\nconst DEFAULT_MAX_BYTES = 1024 * 1024 // 1 MB\n\ninterface SizeGuardOptions {\n warnBytes: number\n maxBytes: number\n onSizeWarning?: (bytes: number) => void\n onSizeExceeded?: (bytes: number) => void\n}\n\n/** Returns true if the push should be blocked. */\nfunction checkPayloadSize(doc: Record<string, unknown>, opts: SizeGuardOptions): boolean {\n // Estimate encrypted payload size. AES-GCM output is similar to input size;\n // base64 encoding adds ~33% overhead, plus a small IV/tag overhead.\n const estimatedBytes = Math.ceil(JSON.stringify(doc).length * 1.34)\n\n if (estimatedBytes > opts.maxBytes) {\n if (opts.onSizeExceeded) {\n opts.onSizeExceeded(estimatedBytes)\n } else {\n console.error(\n `[starfish] Push blocked: estimated payload ${(estimatedBytes / 1024).toFixed(0)} KB ` +\n `exceeds limit of ${(opts.maxBytes / 1024).toFixed(0)} KB. Prune your data before syncing.`,\n )\n }\n return true\n }\n\n if (estimatedBytes > opts.warnBytes) {\n if (opts.onSizeWarning) {\n opts.onSizeWarning(estimatedBytes)\n } else {\n console.warn(\n `[starfish] Payload approaching limit: estimated ${(estimatedBytes / 1024).toFixed(0)} KB ` +\n `(warn threshold: ${(opts.warnBytes / 1024).toFixed(0)} KB).`,\n )\n }\n }\n\n return false\n}\n\n/**\n * Creates a debounced push helper that coalesces rapid mutations into a single sync.\n *\n * Designed to be called on every domain store mutation (e.g., every keystroke).\n * The push is delayed by `delayMs` after the **last** call, so typing quickly\n * results in one push, not one per character.\n *\n * Also estimates the encrypted payload size before pushing and warns / blocks\n * if it approaches the server's body size limit.\n *\n * ```ts\n * const { notify } = createDebouncedSync(starfishStore, {\n * serialize: () => ({ tasks: taskStore.getState().tasks }),\n * })\n *\n * // Call on every domain store mutation:\n * taskStore.subscribe(() => notify())\n * ```\n */\nexport function createDebouncedSync(\n store: StoreApi<StarfishStore>,\n options: DebouncedSyncOptions = {},\n): DebouncedSync {\n const {\n delayMs = DEFAULT_DELAY_MS,\n warnBytes = DEFAULT_WARN_BYTES,\n maxBytes = DEFAULT_MAX_BYTES,\n serialize,\n onSizeWarning,\n onSizeExceeded,\n } = options\n\n let timer: ReturnType<typeof setTimeout> | null = null\n\n function cancel(): void {\n if (timer !== null) {\n clearTimeout(timer)\n timer = null\n }\n }\n\n function notify(): void {\n cancel()\n timer = setTimeout(() => {\n timer = null\n const current = store.getState().data\n const doc = serialize ? serialize(current) : current\n\n if (checkPayloadSize(doc, { warnBytes, maxBytes, onSizeWarning, onSizeExceeded })) return\n\n store.getState().set(() => doc)\n }, delayMs)\n }\n\n return { notify, cancel }\n}\n\n/**\n * Creates a debounced push helper that calls `syncManager.push()` directly,\n * without requiring a Zustand store.\n *\n * Use this for one-way publishing workflows: public pages, derived snapshots,\n * or any case where you want to push data without a full `createStarfishStore` setup.\n *\n * ```ts\n * const syncManager = new SyncManager({ client, pullPath, pushPath })\n *\n * const { notify, cancel } = createDebouncedPush(syncManager, {\n * serialize: () => buildPublicPageDocument(),\n * })\n *\n * // Push after every relevant store mutation:\n * planningStore.subscribe(() => notify())\n *\n * // Clean up on teardown:\n * cancel()\n * ```\n */\nexport function createDebouncedPush(\n syncManager: SyncManager,\n options: DebouncedPushOptions,\n): DebouncedPush {\n const {\n delayMs = DEFAULT_DELAY_MS,\n warnBytes = DEFAULT_WARN_BYTES,\n maxBytes = DEFAULT_MAX_BYTES,\n serialize,\n onSizeWarning,\n onSizeExceeded,\n onError,\n } = options\n\n let timer: ReturnType<typeof setTimeout> | null = null\n\n function cancel(): void {\n if (timer !== null) {\n clearTimeout(timer)\n timer = null\n }\n }\n\n function notify(): void {\n cancel()\n timer = setTimeout(() => {\n timer = null\n const doc = serialize()\n\n if (checkPayloadSize(doc, { warnBytes, maxBytes, onSizeWarning, onSizeExceeded })) return\n\n syncManager.push(doc).catch((err: unknown) => {\n if (onError) {\n onError(err)\n } else {\n console.warn(\"[starfish] Push failed:\", err)\n }\n })\n }, delayMs)\n }\n\n return { notify, cancel }\n}\n", "import type { StoreApi } from \"zustand/vanilla\"\nimport type { StarfishStore, StarfishLogStore } from \"./bindings/zustand.js\"\n\n// \u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Minimal interface matching React Native's `AppState` module.\n * Pass `AppState` from `react-native` directly.\n */\nexport interface AppStateModule {\n addEventListener: (\n type: \"change\",\n listener: (state: string) => void,\n ) => { remove: () => void }\n}\n\n/**\n * Minimal interface matching `@react-native-community/netinfo`'s default export.\n * Pass `NetInfo` from `@react-native-community/netinfo` directly.\n */\nexport interface NetInfoModule {\n addEventListener: (\n listener: (state: { isConnected: boolean | null }) => void,\n ) => () => void\n}\n\nexport interface MobileLifecycleDeps {\n /** React Native `AppState` module. */\n appState: AppStateModule\n /**\n * Optional: NetInfo module from `@react-native-community/netinfo`.\n * When provided, connectivity changes are forwarded to `store.getState().setOnline()`.\n */\n netInfo?: NetInfoModule\n}\n\nexport interface MobileLifecycleOptions {\n /**\n * Pull remote changes when the app returns to the foreground.\n * Only pulls if the store is online and not already syncing.\n * Default: `true`.\n */\n pullOnForeground?: boolean\n /**\n * Flush dirty data when the app transitions to the background.\n * Only flushes if the store has unsaved changes.\n * Default: `true`.\n */\n flushOnBackground?: boolean\n}\n\n// \u2500\u2500 Implementation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Wires React Native app lifecycle events to a Starfish store.\n *\n * - **Background**: flushes pending changes before the OS suspends the app.\n * - **Foreground**: pulls remote changes when the user returns to the app.\n * - **NetInfo**: forwards connectivity changes to `store.getState().setOnline()`.\n *\n * Uses dependency injection so no `react-native` or `netinfo` imports are needed\n * in this package. Pass the modules directly:\n *\n * ```ts\n * import { AppState } from \"react-native\"\n * import NetInfo from \"@react-native-community/netinfo\"\n * import { createMobileLifecycle } from \"@drakkar.software/starfish-client\"\n *\n * // Call once, after the store is created:\n * const cleanup = createMobileLifecycle(\n * store,\n * { appState: AppState, netInfo: NetInfo },\n * )\n *\n * // In a React component (e.g. root layout):\n * useEffect(() => cleanup, [])\n * ```\n *\n * @returns A cleanup function that removes all event listeners.\n */\nexport function createMobileLifecycle(\n store: StoreApi<StarfishStore>,\n deps: MobileLifecycleDeps,\n options: MobileLifecycleOptions = {},\n): () => void {\n const { pullOnForeground = true, flushOnBackground = true } = options\n\n const appSub = deps.appState.addEventListener(\"change\", (appState) => {\n if (appState === \"background\" && flushOnBackground) {\n if (store.getState().dirty) {\n store.getState().flush().catch((err) => { console.error(\"[Starfish] background flush failed:\", err) })\n }\n } else if (appState === \"active\" && pullOnForeground) {\n const { online, syncing } = store.getState()\n if (online && !syncing) {\n store.getState().pull().catch((err) => { console.error(\"[Starfish] foreground pull failed:\", err) })\n }\n }\n // \"inactive\" (iOS transition) and other states are intentionally ignored\n })\n\n let netUnsub: (() => void) | null = null\n if (deps.netInfo) {\n netUnsub = deps.netInfo.addEventListener(({ isConnected }) => {\n store.getState().setOnline(!!isConnected)\n })\n }\n\n return () => {\n appSub.remove()\n netUnsub?.()\n }\n}\n\n// \u2500\u2500 Append-only log lifecycle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface AppendLogLifecycleOptions {\n /**\n * Pull new elements when the app returns to the foreground.\n * Only pulls if the store is online and not already loading.\n * Default: `true`.\n */\n pullOnForeground?: boolean\n}\n\n/**\n * Wires React Native app lifecycle events to an append-log store\n * (`createStarfishLog`). A log is read-only, so this only pulls on foreground\n * (there is nothing to flush on background). NetInfo connectivity changes are\n * forwarded to `store.getState().setOnline()`.\n *\n * ```ts\n * import { AppState } from \"react-native\"\n * import NetInfo from \"@react-native-community/netinfo\"\n * import { createStarfishLog, createAppendLogMobileLifecycle } from \"@drakkar.software/starfish-client\"\n *\n * const store = createStarfishLog({ cursor })\n * const cleanup = createAppendLogMobileLifecycle(store, { appState: AppState, netInfo: NetInfo })\n * useEffect(() => cleanup, [])\n * ```\n *\n * @returns A cleanup function that removes all event listeners.\n */\nexport function createAppendLogMobileLifecycle(\n store: StoreApi<StarfishLogStore>,\n deps: MobileLifecycleDeps,\n options: AppendLogLifecycleOptions = {},\n): () => void {\n const { pullOnForeground = true } = options\n\n const appSub = deps.appState.addEventListener(\"change\", (appState) => {\n if (appState === \"active\" && pullOnForeground) {\n const { online, loading } = store.getState()\n if (online && !loading) {\n store.getState().pull().catch((err) => { console.error(\"[Starfish] foreground log pull failed:\", err) })\n }\n }\n })\n\n let netUnsub: (() => void) | null = null\n if (deps.netInfo) {\n netUnsub = deps.netInfo.addEventListener(({ isConnected }) => {\n store.getState().setOnline(!!isConnected)\n })\n }\n\n return () => {\n appSub.remove()\n netUnsub?.()\n }\n}\n", "// \u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Serializer/deserializer pair for one slice of application state.\n *\n * `serialize` snapshots the current state into a plain object.\n * `restore` applies a snapshot (potentially from a different app version after migration).\n */\nexport interface StoreSlice<T = unknown> {\n /**\n * Snapshot the current state of this slice into a serializable value.\n * Called during `serialize()`.\n */\n serialize: () => T\n /**\n * Apply a snapshot to this slice.\n * Called during `restore()` \u2014 data may be from an older schema version after migration.\n */\n restore: (data: T) => void\n}\n\n/**\n * A versioned backup document produced by `MultiStoreSync.serialize()`.\n * Safe to pass to `store.set()` as the Starfish sync document.\n */\nexport interface BackupDocument<T = Record<string, unknown>> {\n /** Schema version declared in `createMultiStoreSync`. */\n version: number\n /** Unix timestamp (ms) when this backup was created. */\n timestamp: number\n /** Serialized slice data, keyed by slice name. */\n data: T\n}\n\n/**\n * A migration function that transforms data from one version to the next.\n * Receives the full `data` object and must return an updated `data` object.\n * Only the `data` field is passed; `version` and `timestamp` are managed automatically.\n */\nexport type MultiStoreMigrationFn = (data: Record<string, unknown>) => Record<string, unknown>\n\nexport interface MultiStoreSyncOptions<T extends Record<string, unknown>> {\n /**\n * Named slices to include in the backup document.\n * Each slice provides `serialize()` and `restore()` methods.\n *\n * @example\n * ```ts\n * slices: {\n * tasks: {\n * serialize: () => taskStore.getState().tasks,\n * restore: (data) => taskStore.setState({ tasks: data }),\n * },\n * settings: {\n * serialize: () => settingsStore.getState().settings,\n * restore: (data) => settingsStore.setState({ settings: data }),\n * },\n * }\n * ```\n */\n slices: { [K in keyof T]: StoreSlice<T[K]> }\n /**\n * Current schema version. Increment when slices are added, renamed, or their shape changes.\n * Used to detect forward-incompatible documents from future app versions.\n */\n version: number\n /**\n * Optional migration chain. Key is the version number that produced the data;\n * value is a function that upgrades it to the next version.\n *\n * Migrations run sequentially from the document version up to the current version.\n *\n * @example\n * ```ts\n * migrations: {\n * 1: (data) => ({ ...data, settings: { ...data.settings, theme: \"light\" } }),\n * 2: (data) => ({ ...data, tasks: data.todos, todos: undefined }),\n * }\n * ```\n */\n migrations?: Record<number, MultiStoreMigrationFn>\n}\n\n/**\n * Returned by `createMultiStoreSync`. Serialize and restore coordinated multi-store state.\n */\nexport interface MultiStoreSync<T extends Record<string, unknown>> {\n /**\n * Snapshot all slices into a `BackupDocument`.\n * Pass the result to `starfishStore.getState().set(() => multiSync.serialize())`.\n */\n serialize: () => BackupDocument<T>\n /**\n * Apply a `BackupDocument` to all slices, running migrations as needed.\n *\n * Throws if the document version is newer than the current version (forward-incompatible).\n * Silently migrates older documents.\n */\n restore: (doc: BackupDocument) => void\n /** Current schema version as declared in options. */\n readonly version: number\n}\n\n// \u2500\u2500 Implementation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Creates a multi-store sync coordinator.\n *\n * Collects multiple application stores into a single Starfish sync document,\n * with versioned schema migrations for backward compatibility.\n *\n * ```ts\n * const multiSync = createMultiStoreSync({\n * slices: {\n * tasks: {\n * serialize: () => taskStore.getState().tasks,\n * restore: (tasks) => taskStore.setState({ tasks }),\n * },\n * settings: {\n * serialize: () => settingsStore.getState().settings,\n * restore: (settings) => settingsStore.setState({ settings }),\n * },\n * },\n * version: 2,\n * migrations: {\n * // data from version 1 \u2192 upgrade to version 2\n * 1: (data) => ({ ...data, settings: { ...(data.settings as object), darkMode: false } }),\n * },\n * })\n *\n * // Push:\n * starfishStore.getState().set(() => multiSync.serialize())\n *\n * // Restore on pull (pass as onRemoteUpdate to createStarfishStore):\n * createStarfishStore({\n * name: \"app\",\n * syncManager,\n * onRemoteUpdate: (doc) => multiSync.restore(doc as BackupDocument),\n * })\n * ```\n */\nexport function createMultiStoreSync<T extends Record<string, unknown>>(\n options: MultiStoreSyncOptions<T>,\n): MultiStoreSync<T> {\n const { slices, version, migrations = {} } = options\n\n // Validate migration chain at construction time (fail fast)\n for (const fromVersion of Object.keys(migrations)) {\n const v = Number(fromVersion)\n if (isNaN(v) || v < 1) {\n throw new Error(`Migration key must be a positive integer, got: \"${fromVersion}\"`)\n }\n }\n\n function serialize(): BackupDocument<T> {\n const data = {} as T\n for (const key of Object.keys(slices) as Array<keyof T>) {\n data[key] = slices[key].serialize() as T[typeof key]\n }\n return { version, timestamp: Date.now(), data }\n }\n\n function restore(doc: BackupDocument): void {\n if (typeof doc !== \"object\" || doc === null) {\n throw new Error(\"restore: expected a BackupDocument object\")\n }\n\n const docVersion = doc.version ?? 1\n\n if (typeof docVersion !== \"number\" || !Number.isInteger(docVersion) || docVersion < 1) {\n throw new Error(`restore: invalid document version: ${String(doc.version)}`)\n }\n\n if (docVersion > version) {\n throw new Error(\n `restore: document version ${docVersion} is newer than current version ${version}. ` +\n `Update the app to restore this backup.`,\n )\n }\n\n // Run migrations sequentially from docVersion up to current version\n let data: Record<string, unknown> =\n typeof doc.data === \"object\" && doc.data !== null\n ? { ...(doc.data as Record<string, unknown>) }\n : {}\n\n for (let v = docVersion; v < version; v++) {\n const migration = migrations[v]\n if (!migration) continue\n try {\n data = migration(data)\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err)\n throw new Error(`restore: migration from version ${v} to ${v + 1} failed: ${msg}`)\n }\n }\n\n // Restore each slice\n for (const key of Object.keys(slices) as Array<keyof T>) {\n const sliceData = data[key as string]\n if (sliceData !== undefined) {\n slices[key].restore(sliceData as T[typeof key])\n }\n }\n }\n\n return { serialize, restore, version }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["stableStringify", "AUTHOR_PUBKEY_FIELD", "AUTHOR_SIGNATURE_FIELD", "AUTHOR_PUBKEY_FIELD", "AUTHOR_SIGNATURE_FIELD"]
7
7
  }