@drakkar.software/starfish-client 2.1.0 → 2.2.0

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
3
  "sources": ["../src/index.ts", "../src/types.ts", "../src/client.ts", "../src/sync.ts", "../src/crypto.ts", "../src/validate.ts", "../src/logger.ts", "../src/migrate.ts", "../src/fetch.ts", "../src/resolvers.ts", "../src/history.ts", "../src/polling.ts", "../src/dedup.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", "../src/group-crypto.ts", "../src/entitlements.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 type { PullResult, PushSuccess } from \"@drakkar.software/starfish-protocol\"\n\nexport { StarfishClient } from \"./client.js\"\nexport type { BlobPullResult, BlobPushResult, AppendPullOptions } from \"./client.js\"\nexport { SyncManager } from \"./sync.js\"\nexport type { SyncManagerOptions } from \"./sync.js\"\nexport { createEncryptor, ENCRYPTED_KEY } from \"./crypto.js\"\nexport type { Encryptor } from \"./crypto.js\"\nexport {\n ConflictError,\n StarfishHttpError,\n} from \"./types.js\"\nexport type {\n StarfishClientOptions,\n AuthProvider,\n ConflictResolver,\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 } 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 { 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 } from \"./mobile-lifecycle.js\"\nexport type { AppStateModule, NetInfoModule, MobileLifecycleDeps, MobileLifecycleOptions } 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 {\n deriveGroupKeyPair,\n generateGroupKey,\n wrapGroupKey,\n unwrapGroupKey,\n createGroupKeyring,\n addGroupMember,\n rotateGroupKey,\n createGroupEncryptor,\n} from \"./group-crypto.js\"\nexport type { GroupKeyPair, EpochKeyring, GroupKeyring } from \"./group-crypto.js\"\nexport { pullEntitlements } from \"./entitlements.js\"\nexport type { PullEntitlementsOptions } from \"./entitlements.js\"\nexport type { AppendOnlyClientInfo } from \"./config.js\"\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 * Auth provider: returns headers to include in requests.\n * Called for every authenticated request (pull and push).\n */\nexport type AuthProvider = (req: {\n method: string\n path: string\n body: string | null\n}) => Record<string, string> | Promise<Record<string, string>>\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 /** Auth provider that returns headers for authenticated requests. Optional for public-read collections. */\n auth?: AuthProvider\n /** Optional fetch implementation (defaults to global fetch). */\n fetch?: typeof fetch\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", "import type { PullResult, PushSuccess } from \"@drakkar.software/starfish-protocol\"\nimport type {\n StarfishClientOptions,\n AuthProvider,\n} from \"./types.js\"\nimport { ConflictError, StarfishHttpError } from \"./types.js\"\n\nconst APPEND_DEFAULT_FIELD = \"items\"\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}\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 auth?: AuthProvider\n private readonly fetch: typeof globalThis.fetch\n\n constructor(options: StarfishClientOptions) {\n this.baseUrl = options.baseUrl.replace(/\\/$/, \"\")\n this.auth = options.auth\n this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis)\n }\n\n /** Pull synced data from the server. Returns the raw `PullResult`. */\n async pull(path: string, checkpoint?: number): 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,\n ): Promise<PullResult | T[]> {\n let url = `${this.baseUrl}${path}`\n let appendField: string | undefined\n\n if (typeof checkpointOrOptions === \"number\") {\n if (checkpointOrOptions) url += `?checkpoint=${checkpointOrOptions}`\n } else if (checkpointOrOptions != null) {\n appendField = checkpointOrOptions.appendField ?? APPEND_DEFAULT_FIELD\n const params = new URLSearchParams()\n if (checkpointOrOptions.since != null) {\n if (checkpointOrOptions.since < 0) throw new Error(\"since must be non-negative\")\n params.set(\"checkpoint\", String(checkpointOrOptions.since))\n }\n if (checkpointOrOptions.last != null) {\n if (checkpointOrOptions.last < 0) throw new Error(\"last must be non-negative\")\n params.set(\"last\", String(checkpointOrOptions.last))\n }\n if (params.size > 0) url += `?${params.toString()}`\n }\n\n const authHeaders = this.auth\n ? await this.auth({ method: \"GET\", path, body: null })\n : {}\n\n const res = await this.fetch(url, {\n method: \"GET\",\n headers: { Accept: \"application/json\", ...authHeaders },\n })\n if (!res.ok) {\n throw new StarfishHttpError(res.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 return result\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 * @param authorSignature - Optional author signature for provenance\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 authorSignature?: string\n ): Promise<PushSuccess> {\n const body = JSON.stringify({\n data,\n baseHash,\n ...(authorSignature && { authorSignature }),\n })\n\n const authHeaders = this.auth\n ? await this.auth({ method: \"POST\", path, body })\n : {}\n\n const res = await this.fetch(`${this.baseUrl}${path}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n 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 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 authHeaders = this.auth\n ? await this.auth({ method: \"GET\", path, body: null })\n : {}\n\n const res = await this.fetch(`${this.baseUrl}${path}`, {\n method: \"GET\",\n headers: { 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(\"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 const authHeaders = this.auth\n ? await this.auth({ method: \"POST\", path, body: null })\n : {}\n\n const res = await this.fetch(`${this.baseUrl}${path}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": contentType,\n 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 { PullResult } from \"@drakkar.software/starfish-protocol\"\nimport { deepMerge, stableStringify } from \"@drakkar.software/starfish-protocol\"\nimport type { ConflictResolver } from \"./types.js\"\nimport { ConflictError } from \"./types.js\"\nimport { StarfishClient } from \"./client.js\"\nimport type { Encryptor } from \"./crypto.js\"\nimport { createEncryptor } from \"./crypto.js\"\nimport type { SyncLogger } from \"./logger.js\"\nimport type { Validator } from \"./validate.js\"\nimport { ValidationError } from \"./validate.js\"\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 encryptionSecret?: string\n encryptionSalt?: string\n encryptionInfo?: string\n /**\n * Pre-created Encryptor. Use this with `createGroupEncryptor` for group encryption.\n * Takes precedence over `encryptionSecret` / `encryptionSalt` if both are provided.\n */\n encryptor?: Encryptor\n signData?: (data: string) => Promise<string>\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 signData?: (data: string) => Promise<string>\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\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.signData = options.signData\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 =\n options.encryptor ??\n (options.encryptionSecret && options.encryptionSalt\n ? createEncryptor(options.encryptionSecret, options.encryptionSalt, options.encryptionInfo)\n : null)\n }\n\n getData(): Record<string, unknown> {\n return { ...this.localData }\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 getCheckpoint(): number {\n return this.lastCheckpoint\n }\n\n async pull(): Promise<PullResult> {\n this.logger?.pullStart(this.loggerName)\n const start = performance.now()\n try {\n const result = await this.client.pull(this.pullPath, this.lastCheckpoint)\n\n if (this.encryptor) {\n const decrypted = await this.encryptor.decrypt(result.data)\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.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 payload = this.encryptor\n ? await this.encryptor.encrypt(pendingData)\n : pendingData\n\n const sig = this.signData\n ? await this.signData(stableStringify(payload))\n : undefined\n\n const result = await this.client.push(\n this.pushPath,\n payload,\n this.lastHash,\n sig\n )\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 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 const remoteData = this.encryptor\n ? await this.encryptor.decrypt(remote.data)\n : remote.data\n this.lastHash = remote.hash\n this.lastCheckpoint = remote.timestamp\n pendingData = this.onConflict(pendingData, remoteData)\n } catch (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", "import { getCrypto, getBase64, IV_BYTES, ENCRYPTED_KEY, deriveKey } from \"@drakkar.software/starfish-protocol\"\n\nconst ALGO = \"AES-GCM\"\n\nexport { ENCRYPTED_KEY }\n\n/** Encrypt/decrypt interface for client-side E2E encryption. */\nexport interface Encryptor {\n encrypt(data: Record<string, unknown>): Promise<Record<string, unknown>>\n decrypt(wrapper: Record<string, unknown>): Promise<Record<string, unknown>>\n}\n\n/**\n * Creates an Encryptor that uses AES-256-GCM with HKDF-derived keys.\n */\nexport function createEncryptor(secret: string, salt: string, info: string = \"starfish-e2e\"): Encryptor {\n if (!secret) throw new Error(\"encryptionSecret must not be empty\")\n if (!salt) throw new Error(\"encryptionSalt must not be empty\")\n const keyPromise = deriveKey(secret, salt, info)\n\n return {\n async encrypt(data: Record<string, unknown>): Promise<Record<string, unknown>> {\n const key = await keyPromise\n const c = getCrypto()\n const b64 = getBase64()\n const plaintext = new TextEncoder().encode(JSON.stringify(data))\n const iv = c.getRandomValues(new Uint8Array(IV_BYTES))\n const ciphertext = await c.subtle.encrypt({ name: ALGO, iv }, key, plaintext)\n\n const combined = new Uint8Array(iv.length + ciphertext.byteLength)\n combined.set(iv)\n combined.set(new Uint8Array(ciphertext), iv.length)\n\n return { [ENCRYPTED_KEY]: b64.encode(combined) }\n },\n\n async decrypt(wrapper: Record<string, unknown>): Promise<Record<string, unknown>> {\n const encoded = wrapper[ENCRYPTED_KEY]\n if (typeof encoded !== \"string\") {\n throw new Error(\"Expected encrypted data but received unencrypted document\")\n }\n\n const key = await keyPromise\n const c = getCrypto()\n const b64 = getBase64()\n const combined = b64.decode(encoded)\n if (combined.length < IV_BYTES) {\n throw new Error(\"Encrypted data is too short\")\n }\n const iv = combined.slice(0, IV_BYTES)\n const ciphertext = combined.slice(IV_BYTES)\n try {\n const plaintext = await c.subtle.decrypt({ name: ALGO, iv }, key, ciphertext)\n return JSON.parse(new TextDecoder().decode(plaintext))\n } catch (err) {\n throw new Error(\"Decryption failed: data may be tampered or key is incorrect\", { cause: err })\n }\n },\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", "/** Extended metrics for sync operations. */\nexport interface SyncMetrics {\n bytesTransferred?: number\n compressedSize?: number\n conflictCount?: number\n retryCount?: number\n cacheHit?: boolean\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", "/** 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 retryAfter = res.headers.get(\"Retry-After\")?.trim()\n let delay: number\n if (retryAfter) {\n const seconds = Number(retryAfter)\n if (retryAfter !== \"\" && !isNaN(seconds)) {\n delay = Math.min(seconds * 1000, maxDelay)\n } else {\n const date = Date.parse(retryAfter)\n delay = isNaN(date) ? initialDelay : Math.min(Math.max(date - Date.now(), 0), maxDelay)\n }\n } else {\n delay = Math.min(initialDelay * Math.pow(2, attempt), 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 { 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", "/** Encryption modes supported by the Starfish server. */\nexport type EncryptionMode = \"none\" | \"identity\" | \"server\" | \"delegated\"\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 clientEncrypted?: boolean\n /** Base64-encoded public key for client-side encryption, if configured on the server. */\n publicKey?: string\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 } 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", "// \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", "/**\n * Group encryption utilities for Starfish.\n *\n * Enables multiple users to share a common encrypted collection without sharing\n * a passphrase. Each member holds their own credentials; a Group Encryption Key\n * (GEK) is distributed per-member using X25519 ECDH key agreement.\n *\n * Typical flow:\n * 1. Each user calls `deriveCredentials(passphrase)` \u2014 now includes groupPublicKey / groupPrivateKey.\n * 2. Admin calls `createGroupKeyring(...)` to create a keyring document.\n * 3. Members call `createGroupEncryptor(keyringData, myIdentity, myPrivateKey)` to get an Encryptor.\n * 4. The Encryptor is passed to SyncManager via the `encryptor` option.\n */\n\nimport { x25519 } from \"@noble/curves/ed25519.js\"\nimport { getCrypto, getBase64, IV_BYTES, deriveKey } from \"@drakkar.software/starfish-protocol\"\nimport type { Encryptor } from \"./crypto.js\"\nimport { createEncryptor } from \"./crypto.js\"\n\n// \u2500\u2500 Internal helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\nfunction bytesToHex(bytes: Uint8Array): string {\n return Array.from(bytes, (b) => b.toString(16).padStart(2, \"0\")).join(\"\")\n}\n\nfunction hexToBytes(hex: string): Uint8Array {\n const bytes = new Uint8Array(hex.length / 2)\n for (let i = 0; i < bytes.length; i++) {\n bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16)\n }\n return bytes\n}\n\nconst ALGO = \"AES-GCM\"\nconst GROUP_WRAP_SALT = \"starfish-group-wrap\"\nconst GROUP_WRAP_INFO = \"starfish-group-wrap\"\nconst GROUP_ECDH_DOMAIN = \"starfish-group-ecdh\"\nconst GROUP_DATA_INFO = \"starfish-group\"\nconst GEK_BYTES = 32\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/** An ECDH key pair used for group encryption. Hex-encoded for easy serialization. */\nexport interface GroupKeyPair {\n /** Hex-encoded X25519 private key (32 bytes). Keep secret \u2014 never store on server. */\n privateKey: string\n /** Hex-encoded X25519 public key (32 bytes). Safe to publish. */\n publicKey: string\n}\n\n/** One epoch's wrapped keys: each member's GEK encrypted to their public key. */\nexport interface EpochKeyring {\n /** The admin's hex-encoded X25519 public key (used for ECDH by members). */\n adminPublicKey: string\n /** Map from member identity (userId) \u2192 base64(IV || AES-GCM(GEK)) */\n wrappedKeys: Record<string, string>\n}\n\n/** The full keyring document stored in a Starfish collection. Push this with any SyncManager. */\nexport interface GroupKeyring {\n /** The epoch number currently used for new encryptions. */\n currentEpoch: number\n /** All epochs. Members unwrap the GEK for whichever epoch a document was encrypted with. */\n epochs: Record<string, EpochKeyring>\n}\n\n// \u2500\u2500 Key derivation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Derives a deterministic X25519 key pair from a passphrase + userId.\n *\n * The derivation uses SHA-256 with a fixed domain separator so it is distinct\n * from the auth token and encryption key derivations. Same passphrase + userId\n * always produces the same key pair on any device (stateless).\n */\nexport async function deriveGroupKeyPair(passphrase: string, userId: string): Promise<GroupKeyPair> {\n const c = getCrypto()\n const enc = new TextEncoder()\n const input = enc.encode(`${passphrase}:${userId}:${GROUP_ECDH_DOMAIN}`)\n const hash = await c.subtle.digest(\"SHA-256\", input)\n const privateKeyBytes = new Uint8Array(hash)\n const publicKeyBytes = x25519.getPublicKey(privateKeyBytes)\n return { privateKey: bytesToHex(privateKeyBytes), publicKey: bytesToHex(publicKeyBytes) }\n}\n\n// \u2500\u2500 GEK generation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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/** Generates a random 256-bit Group Encryption Key as a hex string. */\nexport function generateGroupKey(): string {\n const c = getCrypto()\n return bytesToHex(c.getRandomValues(new Uint8Array(GEK_BYTES)))\n}\n\n// \u2500\u2500 Key wrapping / unwrapping \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Wraps a GEK for a specific member using ECDH key agreement.\n *\n * The wrapper (admin) and member each have an X25519 key pair. ECDH between\n * `wrapperPrivateKey` and `memberPublicKey` produces a shared secret, which is\n * used to derive an AES-256-GCM key that encrypts the GEK.\n *\n * @returns base64(IV || AES-GCM-ciphertext)\n */\nexport async function wrapGroupKey(\n gek: string,\n memberPublicKey: string,\n wrapperPrivateKey: string,\n): Promise<string> {\n const sharedSecret = x25519.getSharedSecret(hexToBytes(wrapperPrivateKey), hexToBytes(memberPublicKey))\n const wrappingKey = await deriveKey(bytesToHex(sharedSecret), GROUP_WRAP_SALT, GROUP_WRAP_INFO)\n\n const c = getCrypto()\n const b64 = getBase64()\n const iv = c.getRandomValues(new Uint8Array(IV_BYTES))\n const encrypted = await c.subtle.encrypt({ name: ALGO, iv }, wrappingKey, hexToBytes(gek).buffer as ArrayBuffer)\n\n const combined = new Uint8Array(IV_BYTES + encrypted.byteLength)\n combined.set(iv)\n combined.set(new Uint8Array(encrypted), IV_BYTES)\n return b64.encode(combined)\n}\n\n/**\n * Unwraps a GEK using the member's own private key and the admin's public key.\n *\n * ECDH between `memberPrivateKey` and `adminPublicKey` yields the same shared\n * secret as the wrapping step, so the same AES key is derived and the GEK is\n * recovered.\n *\n * @returns GEK as a hex string\n */\nexport async function unwrapGroupKey(\n wrapped: string,\n memberPrivateKey: string,\n adminPublicKey: string,\n): Promise<string> {\n const sharedSecret = x25519.getSharedSecret(hexToBytes(memberPrivateKey), hexToBytes(adminPublicKey))\n const wrappingKey = await deriveKey(bytesToHex(sharedSecret), GROUP_WRAP_SALT, GROUP_WRAP_INFO)\n\n const b64 = getBase64()\n const c = getCrypto()\n const combined = b64.decode(wrapped)\n const iv = combined.slice(0, IV_BYTES)\n const ciphertext = combined.slice(IV_BYTES)\n try {\n const decrypted = await c.subtle.decrypt({ name: ALGO, iv }, wrappingKey, ciphertext)\n return bytesToHex(new Uint8Array(decrypted))\n } catch {\n throw new Error(\"Failed to unwrap group key: decryption failed (wrong keys or corrupted data)\")\n }\n}\n\n// \u2500\u2500 Keyring management \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 new group keyring document with epoch 1.\n *\n * @param adminKeyPair The admin's key pair (from `deriveGroupKeyPair` or `deriveCredentials`)\n * @param members Map from member identity (userId) \u2192 hex public key\n * @param gek Optional GEK to use; generated randomly if omitted\n * @returns The keyring document and the raw GEK (admin keeps the GEK to add future members)\n */\nexport async function createGroupKeyring(\n adminKeyPair: GroupKeyPair,\n members: Record<string, string>,\n gek?: string,\n): Promise<{ keyring: GroupKeyring; gek: string }> {\n const resolvedGek = gek ?? generateGroupKey()\n const wrappedKeys: Record<string, string> = {}\n for (const [memberId, memberPublicKey] of Object.entries(members)) {\n wrappedKeys[memberId] = await wrapGroupKey(resolvedGek, memberPublicKey, adminKeyPair.privateKey)\n }\n const keyring: GroupKeyring = {\n currentEpoch: 1,\n epochs: {\n \"1\": { adminPublicKey: adminKeyPair.publicKey, wrappedKeys },\n },\n }\n return { keyring, gek: resolvedGek }\n}\n\n/**\n * Adds a new member to the current epoch of an existing keyring.\n *\n * The admin supplies the current GEK (returned by `createGroupKeyring` or\n * `rotateGroupKey`) and their key pair to wrap it for the new member.\n * This does NOT rotate the GEK \u2014 the new member can read all existing\n * documents encrypted with the current epoch key.\n *\n * Only the admin (whose `publicKey` matches `epochKeyring.adminPublicKey`) can\n * add members, because all wrapped entries must use the same ECDH key pair.\n */\nexport async function addGroupMember(\n keyring: GroupKeyring,\n adminKeyPair: GroupKeyPair,\n currentGek: string,\n newMemberId: string,\n newMemberPublicKey: string,\n): Promise<GroupKeyring> {\n const epochKey = String(keyring.currentEpoch)\n const epochKeyring = keyring.epochs[epochKey]\n if (!epochKeyring) throw new Error(`Epoch ${keyring.currentEpoch} not found in keyring`)\n if (epochKeyring.adminPublicKey !== adminKeyPair.publicKey) {\n throw new Error(`Provided key pair does not match the admin public key stored in epoch ${keyring.currentEpoch}`)\n }\n\n const wrapped = await wrapGroupKey(currentGek, newMemberPublicKey, adminKeyPair.privateKey)\n\n return {\n ...keyring,\n epochs: {\n ...keyring.epochs,\n [epochKey]: {\n ...epochKeyring,\n wrappedKeys: { ...epochKeyring.wrappedKeys, [newMemberId]: wrapped },\n },\n },\n }\n}\n\n/**\n * Rotates the group key, creating a new epoch.\n *\n * Used when removing a member. The removed member retains their old epoch key\n * (and can still read old documents), but cannot read new documents.\n *\n * @param remainingMembers Map from identity \u2192 hex public key for members who keep access\n */\nexport async function rotateGroupKey(\n keyring: GroupKeyring,\n adminKeyPair: GroupKeyPair,\n remainingMembers: Record<string, string>,\n newGek?: string,\n): Promise<{ keyring: GroupKeyring; gek: string }> {\n const epochKey = String(keyring.currentEpoch)\n const epochKeyring = keyring.epochs[epochKey]\n if (epochKeyring && epochKeyring.adminPublicKey !== adminKeyPair.publicKey) {\n throw new Error(\n `Provided key pair does not match the admin public key stored in epoch ${keyring.currentEpoch}`,\n )\n }\n const resolvedGek = newGek ?? generateGroupKey()\n const newEpoch = keyring.currentEpoch + 1\n const wrappedKeys: Record<string, string> = {}\n for (const [memberId, memberPublicKey] of Object.entries(remainingMembers)) {\n wrappedKeys[memberId] = await wrapGroupKey(resolvedGek, memberPublicKey, adminKeyPair.privateKey)\n }\n const newKeyring: GroupKeyring = {\n currentEpoch: newEpoch,\n epochs: {\n ...keyring.epochs,\n [String(newEpoch)]: { adminPublicKey: adminKeyPair.publicKey, wrappedKeys },\n },\n }\n return { keyring: newKeyring, gek: resolvedGek }\n}\n\n// \u2500\u2500 Encryptor factory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 an Encryptor that can decrypt any epoch and encrypts with the current epoch.\n *\n * Wire format: `{ _encrypted: \"base64(IV || ciphertext)\", _epoch: N }`\n *\n * @param keyring The keyring document fetched from Starfish\n * @param myIdentity The caller's userId (to locate their wrapped key in each epoch)\n * @param myPrivateKey The caller's hex-encoded X25519 private key\n */\nexport async function createGroupEncryptor(\n keyring: GroupKeyring,\n myIdentity: string,\n myPrivateKey: string,\n): Promise<Encryptor> {\n // Unwrap GEK for each epoch we have a key for\n const epochEncryptors = new Map<number, Encryptor>()\n for (const [epochStr, epochKeyring] of Object.entries(keyring.epochs)) {\n const epoch = parseInt(epochStr, 10)\n const wrapped = epochKeyring.wrappedKeys[myIdentity]\n if (!wrapped) continue\n const gek = await unwrapGroupKey(wrapped, myPrivateKey, epochKeyring.adminPublicKey)\n epochEncryptors.set(epoch, createEncryptor(gek, `epoch-${epoch}`, GROUP_DATA_INFO))\n }\n\n const currentEpoch = keyring.currentEpoch\n const currentEncryptor = epochEncryptors.get(currentEpoch)\n if (!currentEncryptor) {\n throw new Error(\n `No wrapped key found for identity \"${myIdentity}\" in epoch ${currentEpoch}. ` +\n `Ensure the admin has added this member to the keyring.`,\n )\n }\n\n return {\n async encrypt(data: Record<string, unknown>): Promise<Record<string, unknown>> {\n const encrypted = await currentEncryptor.encrypt(data)\n return { ...encrypted, _epoch: currentEpoch }\n },\n\n async decrypt(wrapper: Record<string, unknown>): Promise<Record<string, unknown>> {\n const epoch = typeof wrapper._epoch === \"number\" ? wrapper._epoch : currentEpoch\n const encryptor = epochEncryptors.get(epoch)\n if (!encryptor) {\n throw new Error(\n `No key available for epoch ${epoch}. ` +\n `This document was encrypted in a different epoch. ` +\n `Ensure your keyring is up to date.`,\n )\n }\n return encryptor.decrypt(wrapper)\n },\n }\n}\n", "import type { StarfishClient } from \"./client.js\"\nimport { StarfishHttpError } from \"./types.js\"\n\nexport interface PullEntitlementsOptions {\n /**\n * Path template for the entitlement document.\n * `{userId}` is replaced with the `userId` argument.\n * Defaults to `\"/pull/users/{userId}/entitlements\"`.\n */\n path?: string\n /**\n * Field name in the document `data` object that holds the feature slug array.\n * Defaults to `\"features\"`.\n */\n field?: string\n}\n\n/**\n * Fetches the list of feature slugs from a user's entitlement document.\n *\n * Returns an empty array if the document does not exist yet or the features\n * field is absent \u2014 so callers never need to handle a 404.\n *\n * ```ts\n * import { pullEntitlements } from \"@drakkar.software/starfish-client\"\n *\n * const features = await pullEntitlements(client, userId)\n * // e.g. [\"premium-package-1\", \"paid-cloud-sync\"]\n *\n * if (features.includes(\"paid-cloud-sync\")) {\n * // unlock cloud sync UI\n * }\n * ```\n *\n * The path template must match the server-side collection's `storagePath`.\n * With the recommended default config:\n * ```ts\n * { storagePath: \"users/{identity}/entitlements\" }\n * // \u2192 path: \"/pull/users/{userId}/entitlements\" (default)\n * ```\n */\nexport async function pullEntitlements(\n client: StarfishClient,\n userId: string,\n opts?: PullEntitlementsOptions,\n): Promise<string[]> {\n const path = (opts?.path ?? \"/pull/users/{userId}/entitlements\").replace(\"{userId}\", userId)\n const field = opts?.field ?? \"features\"\n\n try {\n const result = await client.pull(path)\n const list = (result.data as Record<string, unknown> | null)?.[field]\n if (!Array.isArray(list)) return []\n return list.filter((s): s is string => typeof s === \"string\")\n } catch (err) {\n if (err instanceof StarfishHttpError && err.status === 404) return []\n throw err\n }\n}\n"],
5
- "mappings": "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",
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 type { PullResult, PushSuccess } from \"@drakkar.software/starfish-protocol\"\n\nexport { StarfishClient } from \"./client.js\"\nexport type { BlobPullResult, BlobPushResult, AppendPullOptions } from \"./client.js\"\nexport { SyncManager, AbortError } from \"./sync.js\"\nexport type { SyncManagerOptions } from \"./sync.js\"\nexport { createEncryptor, ENCRYPTED_KEY } from \"./crypto.js\"\nexport type { Encryptor } from \"./crypto.js\"\nexport {\n ConflictError,\n StarfishHttpError,\n} from \"./types.js\"\nexport type {\n StarfishClientOptions,\n AuthProvider,\n ConflictResolver,\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 } 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 { 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 } from \"./mobile-lifecycle.js\"\nexport type { AppStateModule, NetInfoModule, MobileLifecycleDeps, MobileLifecycleOptions } 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 {\n deriveGroupKeyPair,\n generateGroupKey,\n wrapGroupKey,\n unwrapGroupKey,\n createGroupKeyring,\n addGroupMember,\n rotateGroupKey,\n createGroupEncryptor,\n} from \"./group-crypto.js\"\nexport type { GroupKeyPair, EpochKeyring, GroupKeyring } from \"./group-crypto.js\"\nexport { pullEntitlements } from \"./entitlements.js\"\nexport type { PullEntitlementsOptions } from \"./entitlements.js\"\nexport type { AppendOnlyClientInfo } from \"./config.js\"\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 * Auth provider: returns headers to include in requests.\n * Called for every authenticated request (pull and push).\n */\nexport type AuthProvider = (req: {\n method: string\n path: string\n body: string | null\n}) => Record<string, string> | Promise<Record<string, string>>\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 /** Auth provider that returns headers for authenticated requests. Optional for public-read collections. */\n auth?: AuthProvider\n /** Optional fetch implementation (defaults to global fetch). */\n fetch?: typeof fetch\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", "import type { PullResult, PushSuccess } from \"@drakkar.software/starfish-protocol\"\nimport type {\n StarfishClientOptions,\n AuthProvider,\n} from \"./types.js\"\nimport { ConflictError, StarfishHttpError } from \"./types.js\"\n\nconst APPEND_DEFAULT_FIELD = \"items\"\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}\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 auth?: AuthProvider\n private readonly fetch: typeof globalThis.fetch\n\n constructor(options: StarfishClientOptions) {\n this.baseUrl = options.baseUrl.replace(/\\/$/, \"\")\n this.auth = options.auth\n this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis)\n }\n\n /** Pull synced data from the server. Returns the raw `PullResult`. */\n async pull(path: string, checkpoint?: number): 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,\n ): Promise<PullResult | T[]> {\n let url = `${this.baseUrl}${path}`\n let appendField: string | undefined\n\n if (typeof checkpointOrOptions === \"number\") {\n if (checkpointOrOptions) url += `?checkpoint=${checkpointOrOptions}`\n } else if (checkpointOrOptions != null) {\n appendField = checkpointOrOptions.appendField ?? APPEND_DEFAULT_FIELD\n const params = new URLSearchParams()\n if (checkpointOrOptions.since != null) {\n if (checkpointOrOptions.since < 0) throw new Error(\"since must be non-negative\")\n params.set(\"checkpoint\", String(checkpointOrOptions.since))\n }\n if (checkpointOrOptions.last != null) {\n if (checkpointOrOptions.last < 0) throw new Error(\"last must be non-negative\")\n params.set(\"last\", String(checkpointOrOptions.last))\n }\n if (params.size > 0) url += `?${params.toString()}`\n }\n\n const authHeaders = this.auth\n ? await this.auth({ method: \"GET\", path, body: null })\n : {}\n\n const res = await this.fetch(url, {\n method: \"GET\",\n headers: { Accept: \"application/json\", ...authHeaders },\n })\n if (!res.ok) {\n throw new StarfishHttpError(res.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 return result\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 * @param authorSignature - Optional author signature for provenance\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 authorSignature?: string\n ): Promise<PushSuccess> {\n const body = JSON.stringify({\n data,\n baseHash,\n ...(authorSignature && { authorSignature }),\n })\n\n const authHeaders = this.auth\n ? await this.auth({ method: \"POST\", path, body })\n : {}\n\n const res = await this.fetch(`${this.baseUrl}${path}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n 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 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 authHeaders = this.auth\n ? await this.auth({ method: \"GET\", path, body: null })\n : {}\n\n const res = await this.fetch(`${this.baseUrl}${path}`, {\n method: \"GET\",\n headers: { 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(\"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 const authHeaders = this.auth\n ? await this.auth({ method: \"POST\", path, body: null })\n : {}\n\n const res = await this.fetch(`${this.baseUrl}${path}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": contentType,\n 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 { PullResult } from \"@drakkar.software/starfish-protocol\"\nimport { deepMerge, stableStringify } from \"@drakkar.software/starfish-protocol\"\nimport type { ConflictResolver } from \"./types.js\"\nimport { ConflictError } from \"./types.js\"\nimport { StarfishClient } from \"./client.js\"\nimport type { Encryptor } from \"./crypto.js\"\nimport { createEncryptor } from \"./crypto.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\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 encryptionSecret?: string\n encryptionSalt?: string\n encryptionInfo?: string\n /**\n * Pre-created Encryptor. Use this with `createGroupEncryptor` for group encryption.\n * Takes precedence over `encryptionSecret` / `encryptionSalt` if both are provided.\n */\n encryptor?: Encryptor\n signData?: (data: string) => Promise<string>\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 signData?: (data: string) => Promise<string>\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\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.signData = options.signData\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 =\n options.encryptor ??\n (options.encryptionSecret && options.encryptionSalt\n ? createEncryptor(options.encryptionSecret, options.encryptionSalt, options.encryptionInfo)\n : 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 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 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 const result = await this.client.pull(this.pullPath, this.lastCheckpoint)\n if (this.aborted) throw new AbortError()\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 payload = this.encryptor\n ? await this.encryptor.encrypt(pendingData)\n : pendingData\n if (this.aborted) throw new AbortError()\n\n const sig = this.signData\n ? await this.signData(stableStringify(payload))\n : undefined\n if (this.aborted) throw new AbortError()\n\n const result = await this.client.push(\n this.pushPath,\n payload,\n this.lastHash,\n sig\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", "import { getCrypto, getBase64, IV_BYTES, ENCRYPTED_KEY, deriveKey } from \"@drakkar.software/starfish-protocol\"\n\nconst ALGO = \"AES-GCM\"\n\nexport { ENCRYPTED_KEY }\n\n/** Encrypt/decrypt interface for client-side E2E encryption. */\nexport interface Encryptor {\n encrypt(data: Record<string, unknown>): Promise<Record<string, unknown>>\n decrypt(wrapper: Record<string, unknown>): Promise<Record<string, unknown>>\n}\n\n/**\n * Creates an Encryptor that uses AES-256-GCM with HKDF-derived keys.\n */\nexport function createEncryptor(secret: string, salt: string, info: string = \"starfish-e2e\"): Encryptor {\n if (!secret) throw new Error(\"encryptionSecret must not be empty\")\n if (!salt) throw new Error(\"encryptionSalt must not be empty\")\n const keyPromise = deriveKey(secret, salt, info)\n\n return {\n async encrypt(data: Record<string, unknown>): Promise<Record<string, unknown>> {\n const key = await keyPromise\n const c = getCrypto()\n const b64 = getBase64()\n const plaintext = new TextEncoder().encode(JSON.stringify(data))\n const iv = c.getRandomValues(new Uint8Array(IV_BYTES))\n const ciphertext = await c.subtle.encrypt({ name: ALGO, iv }, key, plaintext)\n\n const combined = new Uint8Array(iv.length + ciphertext.byteLength)\n combined.set(iv)\n combined.set(new Uint8Array(ciphertext), iv.length)\n\n return { [ENCRYPTED_KEY]: b64.encode(combined) }\n },\n\n async decrypt(wrapper: Record<string, unknown>): Promise<Record<string, unknown>> {\n const encoded = wrapper[ENCRYPTED_KEY]\n if (typeof encoded !== \"string\") {\n throw new Error(\"Expected encrypted data but received unencrypted document\")\n }\n\n const key = await keyPromise\n const c = getCrypto()\n const b64 = getBase64()\n const combined = b64.decode(encoded)\n if (combined.length < IV_BYTES) {\n throw new Error(\"Encrypted data is too short\")\n }\n const iv = combined.slice(0, IV_BYTES)\n const ciphertext = combined.slice(IV_BYTES)\n try {\n const plaintext = await c.subtle.decrypt({ name: ALGO, iv }, key, ciphertext)\n return JSON.parse(new TextDecoder().decode(plaintext))\n } catch (err) {\n throw new Error(\"Decryption failed: data may be tampered or key is incorrect\", { cause: err })\n }\n },\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", "/** Extended metrics for sync operations. */\nexport interface SyncMetrics {\n bytesTransferred?: number\n compressedSize?: number\n conflictCount?: number\n retryCount?: number\n cacheHit?: boolean\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", "/** 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 retryAfter = res.headers.get(\"Retry-After\")?.trim()\n let delay: number\n if (retryAfter) {\n const seconds = Number(retryAfter)\n if (retryAfter !== \"\" && !isNaN(seconds)) {\n delay = Math.min(seconds * 1000, maxDelay)\n } else {\n const date = Date.parse(retryAfter)\n delay = isNaN(date) ? initialDelay : Math.min(Math.max(date - Date.now(), 0), maxDelay)\n }\n } else {\n delay = Math.min(initialDelay * Math.pow(2, attempt), 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 { 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", "/** Encryption modes supported by the Starfish server. */\nexport type EncryptionMode = \"none\" | \"identity\" | \"server\" | \"delegated\"\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 clientEncrypted?: boolean\n /** Base64-encoded public key for client-side encryption, if configured on the server. */\n publicKey?: string\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 } 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", "// \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", "/**\n * Group encryption utilities for Starfish.\n *\n * Enables multiple users to share a common encrypted collection without sharing\n * a passphrase. Each member holds their own credentials; a Group Encryption Key\n * (GEK) is distributed per-member using X25519 ECDH key agreement.\n *\n * Typical flow:\n * 1. Each user calls `deriveCredentials(passphrase)` \u2014 now includes groupPublicKey / groupPrivateKey.\n * 2. Admin calls `createGroupKeyring(...)` to create a keyring document.\n * 3. Members call `createGroupEncryptor(keyringData, myIdentity, myPrivateKey)` to get an Encryptor.\n * 4. The Encryptor is passed to SyncManager via the `encryptor` option.\n */\n\nimport { x25519 } from \"@noble/curves/ed25519.js\"\nimport { getCrypto, getBase64, IV_BYTES, deriveKey } from \"@drakkar.software/starfish-protocol\"\nimport type { Encryptor } from \"./crypto.js\"\nimport { createEncryptor } from \"./crypto.js\"\n\n// \u2500\u2500 Internal helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\nfunction bytesToHex(bytes: Uint8Array): string {\n return Array.from(bytes, (b) => b.toString(16).padStart(2, \"0\")).join(\"\")\n}\n\nfunction hexToBytes(hex: string): Uint8Array {\n const bytes = new Uint8Array(hex.length / 2)\n for (let i = 0; i < bytes.length; i++) {\n bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16)\n }\n return bytes\n}\n\nconst ALGO = \"AES-GCM\"\nconst GROUP_WRAP_SALT = \"starfish-group-wrap\"\nconst GROUP_WRAP_INFO = \"starfish-group-wrap\"\nconst GROUP_ECDH_DOMAIN = \"starfish-group-ecdh\"\nconst GROUP_DATA_INFO = \"starfish-group\"\nconst GEK_BYTES = 32\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/** An ECDH key pair used for group encryption. Hex-encoded for easy serialization. */\nexport interface GroupKeyPair {\n /** Hex-encoded X25519 private key (32 bytes). Keep secret \u2014 never store on server. */\n privateKey: string\n /** Hex-encoded X25519 public key (32 bytes). Safe to publish. */\n publicKey: string\n}\n\n/** One epoch's wrapped keys: each member's GEK encrypted to their public key. */\nexport interface EpochKeyring {\n /** The admin's hex-encoded X25519 public key (used for ECDH by members). */\n adminPublicKey: string\n /** Map from member identity (userId) \u2192 base64(IV || AES-GCM(GEK)) */\n wrappedKeys: Record<string, string>\n}\n\n/** The full keyring document stored in a Starfish collection. Push this with any SyncManager. */\nexport interface GroupKeyring {\n /** The epoch number currently used for new encryptions. */\n currentEpoch: number\n /** All epochs. Members unwrap the GEK for whichever epoch a document was encrypted with. */\n epochs: Record<string, EpochKeyring>\n}\n\n// \u2500\u2500 Key derivation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Derives a deterministic X25519 key pair from a passphrase + userId.\n *\n * The derivation uses SHA-256 with a fixed domain separator so it is distinct\n * from the auth token and encryption key derivations. Same passphrase + userId\n * always produces the same key pair on any device (stateless).\n */\nexport async function deriveGroupKeyPair(passphrase: string, userId: string): Promise<GroupKeyPair> {\n const c = getCrypto()\n const enc = new TextEncoder()\n const input = enc.encode(`${passphrase}:${userId}:${GROUP_ECDH_DOMAIN}`)\n const hash = await c.subtle.digest(\"SHA-256\", input)\n const privateKeyBytes = new Uint8Array(hash)\n const publicKeyBytes = x25519.getPublicKey(privateKeyBytes)\n return { privateKey: bytesToHex(privateKeyBytes), publicKey: bytesToHex(publicKeyBytes) }\n}\n\n// \u2500\u2500 GEK generation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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/** Generates a random 256-bit Group Encryption Key as a hex string. */\nexport function generateGroupKey(): string {\n const c = getCrypto()\n return bytesToHex(c.getRandomValues(new Uint8Array(GEK_BYTES)))\n}\n\n// \u2500\u2500 Key wrapping / unwrapping \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Wraps a GEK for a specific member using ECDH key agreement.\n *\n * The wrapper (admin) and member each have an X25519 key pair. ECDH between\n * `wrapperPrivateKey` and `memberPublicKey` produces a shared secret, which is\n * used to derive an AES-256-GCM key that encrypts the GEK.\n *\n * @returns base64(IV || AES-GCM-ciphertext)\n */\nexport async function wrapGroupKey(\n gek: string,\n memberPublicKey: string,\n wrapperPrivateKey: string,\n): Promise<string> {\n const sharedSecret = x25519.getSharedSecret(hexToBytes(wrapperPrivateKey), hexToBytes(memberPublicKey))\n const wrappingKey = await deriveKey(bytesToHex(sharedSecret), GROUP_WRAP_SALT, GROUP_WRAP_INFO)\n\n const c = getCrypto()\n const b64 = getBase64()\n const iv = c.getRandomValues(new Uint8Array(IV_BYTES))\n const encrypted = await c.subtle.encrypt({ name: ALGO, iv }, wrappingKey, hexToBytes(gek).buffer as ArrayBuffer)\n\n const combined = new Uint8Array(IV_BYTES + encrypted.byteLength)\n combined.set(iv)\n combined.set(new Uint8Array(encrypted), IV_BYTES)\n return b64.encode(combined)\n}\n\n/**\n * Unwraps a GEK using the member's own private key and the admin's public key.\n *\n * ECDH between `memberPrivateKey` and `adminPublicKey` yields the same shared\n * secret as the wrapping step, so the same AES key is derived and the GEK is\n * recovered.\n *\n * @returns GEK as a hex string\n */\nexport async function unwrapGroupKey(\n wrapped: string,\n memberPrivateKey: string,\n adminPublicKey: string,\n): Promise<string> {\n const sharedSecret = x25519.getSharedSecret(hexToBytes(memberPrivateKey), hexToBytes(adminPublicKey))\n const wrappingKey = await deriveKey(bytesToHex(sharedSecret), GROUP_WRAP_SALT, GROUP_WRAP_INFO)\n\n const b64 = getBase64()\n const c = getCrypto()\n const combined = b64.decode(wrapped)\n const iv = combined.slice(0, IV_BYTES)\n const ciphertext = combined.slice(IV_BYTES)\n try {\n const decrypted = await c.subtle.decrypt({ name: ALGO, iv }, wrappingKey, ciphertext)\n return bytesToHex(new Uint8Array(decrypted))\n } catch {\n throw new Error(\"Failed to unwrap group key: decryption failed (wrong keys or corrupted data)\")\n }\n}\n\n// \u2500\u2500 Keyring management \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 new group keyring document with epoch 1.\n *\n * @param adminKeyPair The admin's key pair (from `deriveGroupKeyPair` or `deriveCredentials`)\n * @param members Map from member identity (userId) \u2192 hex public key\n * @param gek Optional GEK to use; generated randomly if omitted\n * @returns The keyring document and the raw GEK (admin keeps the GEK to add future members)\n */\nexport async function createGroupKeyring(\n adminKeyPair: GroupKeyPair,\n members: Record<string, string>,\n gek?: string,\n): Promise<{ keyring: GroupKeyring; gek: string }> {\n const resolvedGek = gek ?? generateGroupKey()\n const wrappedKeys: Record<string, string> = {}\n for (const [memberId, memberPublicKey] of Object.entries(members)) {\n wrappedKeys[memberId] = await wrapGroupKey(resolvedGek, memberPublicKey, adminKeyPair.privateKey)\n }\n const keyring: GroupKeyring = {\n currentEpoch: 1,\n epochs: {\n \"1\": { adminPublicKey: adminKeyPair.publicKey, wrappedKeys },\n },\n }\n return { keyring, gek: resolvedGek }\n}\n\n/**\n * Adds a new member to the current epoch of an existing keyring.\n *\n * The admin supplies the current GEK (returned by `createGroupKeyring` or\n * `rotateGroupKey`) and their key pair to wrap it for the new member.\n * This does NOT rotate the GEK \u2014 the new member can read all existing\n * documents encrypted with the current epoch key.\n *\n * Only the admin (whose `publicKey` matches `epochKeyring.adminPublicKey`) can\n * add members, because all wrapped entries must use the same ECDH key pair.\n */\nexport async function addGroupMember(\n keyring: GroupKeyring,\n adminKeyPair: GroupKeyPair,\n currentGek: string,\n newMemberId: string,\n newMemberPublicKey: string,\n): Promise<GroupKeyring> {\n const epochKey = String(keyring.currentEpoch)\n const epochKeyring = keyring.epochs[epochKey]\n if (!epochKeyring) throw new Error(`Epoch ${keyring.currentEpoch} not found in keyring`)\n if (epochKeyring.adminPublicKey !== adminKeyPair.publicKey) {\n throw new Error(`Provided key pair does not match the admin public key stored in epoch ${keyring.currentEpoch}`)\n }\n\n const wrapped = await wrapGroupKey(currentGek, newMemberPublicKey, adminKeyPair.privateKey)\n\n return {\n ...keyring,\n epochs: {\n ...keyring.epochs,\n [epochKey]: {\n ...epochKeyring,\n wrappedKeys: { ...epochKeyring.wrappedKeys, [newMemberId]: wrapped },\n },\n },\n }\n}\n\n/**\n * Rotates the group key, creating a new epoch.\n *\n * Used when removing a member. The removed member retains their old epoch key\n * (and can still read old documents), but cannot read new documents.\n *\n * @param remainingMembers Map from identity \u2192 hex public key for members who keep access\n */\nexport async function rotateGroupKey(\n keyring: GroupKeyring,\n adminKeyPair: GroupKeyPair,\n remainingMembers: Record<string, string>,\n newGek?: string,\n): Promise<{ keyring: GroupKeyring; gek: string }> {\n const epochKey = String(keyring.currentEpoch)\n const epochKeyring = keyring.epochs[epochKey]\n if (epochKeyring && epochKeyring.adminPublicKey !== adminKeyPair.publicKey) {\n throw new Error(\n `Provided key pair does not match the admin public key stored in epoch ${keyring.currentEpoch}`,\n )\n }\n const resolvedGek = newGek ?? generateGroupKey()\n const newEpoch = keyring.currentEpoch + 1\n const wrappedKeys: Record<string, string> = {}\n for (const [memberId, memberPublicKey] of Object.entries(remainingMembers)) {\n wrappedKeys[memberId] = await wrapGroupKey(resolvedGek, memberPublicKey, adminKeyPair.privateKey)\n }\n const newKeyring: GroupKeyring = {\n currentEpoch: newEpoch,\n epochs: {\n ...keyring.epochs,\n [String(newEpoch)]: { adminPublicKey: adminKeyPair.publicKey, wrappedKeys },\n },\n }\n return { keyring: newKeyring, gek: resolvedGek }\n}\n\n// \u2500\u2500 Encryptor factory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 an Encryptor that can decrypt any epoch and encrypts with the current epoch.\n *\n * Wire format: `{ _encrypted: \"base64(IV || ciphertext)\", _epoch: N }`\n *\n * @param keyring The keyring document fetched from Starfish\n * @param myIdentity The caller's userId (to locate their wrapped key in each epoch)\n * @param myPrivateKey The caller's hex-encoded X25519 private key\n */\nexport async function createGroupEncryptor(\n keyring: GroupKeyring,\n myIdentity: string,\n myPrivateKey: string,\n): Promise<Encryptor> {\n // Unwrap GEK for each epoch we have a key for\n const epochEncryptors = new Map<number, Encryptor>()\n for (const [epochStr, epochKeyring] of Object.entries(keyring.epochs)) {\n const epoch = parseInt(epochStr, 10)\n const wrapped = epochKeyring.wrappedKeys[myIdentity]\n if (!wrapped) continue\n const gek = await unwrapGroupKey(wrapped, myPrivateKey, epochKeyring.adminPublicKey)\n epochEncryptors.set(epoch, createEncryptor(gek, `epoch-${epoch}`, GROUP_DATA_INFO))\n }\n\n const currentEpoch = keyring.currentEpoch\n const currentEncryptor = epochEncryptors.get(currentEpoch)\n if (!currentEncryptor) {\n throw new Error(\n `No wrapped key found for identity \"${myIdentity}\" in epoch ${currentEpoch}. ` +\n `Ensure the admin has added this member to the keyring.`,\n )\n }\n\n return {\n async encrypt(data: Record<string, unknown>): Promise<Record<string, unknown>> {\n const encrypted = await currentEncryptor.encrypt(data)\n return { ...encrypted, _epoch: currentEpoch }\n },\n\n async decrypt(wrapper: Record<string, unknown>): Promise<Record<string, unknown>> {\n const epoch = typeof wrapper._epoch === \"number\" ? wrapper._epoch : currentEpoch\n const encryptor = epochEncryptors.get(epoch)\n if (!encryptor) {\n throw new Error(\n `No key available for epoch ${epoch}. ` +\n `This document was encrypted in a different epoch. ` +\n `Ensure your keyring is up to date.`,\n )\n }\n return encryptor.decrypt(wrapper)\n },\n }\n}\n", "import type { StarfishClient } from \"./client.js\"\nimport { StarfishHttpError } from \"./types.js\"\n\nexport interface PullEntitlementsOptions {\n /**\n * Path template for the entitlement document.\n * `{userId}` is replaced with the `userId` argument.\n * Defaults to `\"/pull/users/{userId}/entitlements\"`.\n */\n path?: string\n /**\n * Field name in the document `data` object that holds the feature slug array.\n * Defaults to `\"features\"`.\n */\n field?: string\n}\n\n/**\n * Fetches the list of feature slugs from a user's entitlement document.\n *\n * Returns an empty array if the document does not exist yet or the features\n * field is absent \u2014 so callers never need to handle a 404.\n *\n * ```ts\n * import { pullEntitlements } from \"@drakkar.software/starfish-client\"\n *\n * const features = await pullEntitlements(client, userId)\n * // e.g. [\"premium-package-1\", \"paid-cloud-sync\"]\n *\n * if (features.includes(\"paid-cloud-sync\")) {\n * // unlock cloud sync UI\n * }\n * ```\n *\n * The path template must match the server-side collection's `storagePath`.\n * With the recommended default config:\n * ```ts\n * { storagePath: \"users/{identity}/entitlements\" }\n * // \u2192 path: \"/pull/users/{userId}/entitlements\" (default)\n * ```\n */\nexport async function pullEntitlements(\n client: StarfishClient,\n userId: string,\n opts?: PullEntitlementsOptions,\n): Promise<string[]> {\n const path = (opts?.path ?? \"/pull/users/{userId}/entitlements\").replace(\"{userId}\", userId)\n const field = opts?.field ?? \"features\"\n\n try {\n const result = await client.pull(path)\n const list = (result.data as Record<string, unknown> | null)?.[field]\n if (!Array.isArray(list)) return []\n return list.filter((s): s is string => typeof s === \"string\")\n } catch (err) {\n if (err instanceof StarfishHttpError && err.status === 404) return []\n throw err\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["stableStringify", "getCrypto", "getBase64", "IV_BYTES", "deriveKey", "ALGO", "getCrypto", "deriveKey", "getBase64", "IV_BYTES"]
7
7
  }