selfstore 1.8.18 → 1.8.20

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.
@@ -1,4 +1,4 @@
1
- import { B as BackupTarget } from './store-Cs6QIzzG.js';
1
+ import { B as BackupTarget } from './store-DhrWMYg_.js';
2
2
  import { K as KV } from './cache-ilsK3Tki.js';
3
3
 
4
4
  /**
package/dist/flows.d.ts CHANGED
@@ -1,11 +1,11 @@
1
- import { b as ConnectKind, S as StoreLike, C as ConnectTargets } from './connect-BTVzRJY8.js';
2
- export { c as ConnectFlow, a as ConnectFlowOptions, d as ConnectFlowOutcome, e as ConnectResolution, f as ConnectSnapshot, g as ConnectStep, h as Connector, F as FlowHost, R as ResumeOffer, i as connectFlow } from './connect-BTVzRJY8.js';
3
- import { S as StoreError, k as ReplicaState, B as BackupTarget } from './store-Cs6QIzzG.js';
1
+ import { b as ConnectKind, S as StoreLike, C as ConnectTargets } from './connect-GY1R9c6Q.js';
2
+ export { c as ConnectFlow, a as ConnectFlowOptions, d as ConnectFlowOutcome, e as ConnectResolution, f as ConnectSnapshot, g as ConnectStep, h as Connector, F as FlowHost, R as ResumeOffer, i as connectFlow } from './connect-GY1R9c6Q.js';
3
+ import { S as StoreError, k as ReplicaState, B as BackupTarget } from './store-DhrWMYg_.js';
4
4
  import { F as FlowStore } from './machine-ALMkT3mk.js';
5
5
  export { w as withDeadline } from './machine-ALMkT3mk.js';
6
- import { W as WebdavConfig, S as S3Config } from './s3-0qqIyphl.js';
6
+ import { W as WebdavConfig, S as S3Config } from './s3-BLgf6hqR.js';
7
7
  import './cache-ilsK3Tki.js';
8
- import './drive-CY_Vsj7V.js';
8
+ import './drive-BLRsK32w.js';
9
9
  import './sync.js';
10
10
 
11
11
  /** The one replica this flow manages, as its id on the engine. */
@@ -1,4 +1,4 @@
1
- import { B as BackupTarget, P as PeerSource, L as LocalStore } from './store-Cs6QIzzG.js';
1
+ import { B as BackupTarget, P as PeerSource, L as LocalStore } from './store-DhrWMYg_.js';
2
2
  import { K as KV } from './cache-ilsK3Tki.js';
3
3
  import './sync.js';
4
4
 
package/dist/index.d.ts CHANGED
@@ -1,12 +1,12 @@
1
- import { o as Snapshot, E as EncodeOptions, H as Header, f as PasswordPolicy, B as BackupTarget, r as StatusDescriptor, S as StoreError, c as LocalStoreState, L as LocalStore } from './store-Cs6QIzzG.js';
2
- export { d as Mode, e as PasswordCheck, g as PasswordRequirement, l as SelfstoreError, m as SelfstoreErrorCode, n as Severity, p as SnapshotFile, q as StatusAction, t as StorageState, T as TargetKind, x as checkPasswordPolicy, C as errorLabelKey, G as isSelfstoreError } from './store-Cs6QIzzG.js';
1
+ import { o as Snapshot, E as EncodeOptions, H as Header, f as PasswordPolicy, B as BackupTarget, r as StatusDescriptor, S as StoreError, c as LocalStoreState, L as LocalStore } from './store-DhrWMYg_.js';
2
+ export { d as Mode, e as PasswordCheck, g as PasswordRequirement, l as SelfstoreError, m as SelfstoreErrorCode, n as Severity, p as SnapshotFile, q as StatusAction, t as StorageState, T as TargetKind, x as checkPasswordPolicy, C as errorLabelKey, G as isSelfstoreError } from './store-DhrWMYg_.js';
3
3
  import { L as LocalCache, K as KV } from './cache-ilsK3Tki.js';
4
4
  import { SyncConfig } from './sync.js';
5
- import { R as ResumeOffer, C as ConnectTargets } from './connect-BTVzRJY8.js';
6
- import { f as BackupsManager } from './manager-DhoIbJyg.js';
7
- import { a as DriveAuth } from './drive-CY_Vsj7V.js';
8
- import { W as WebdavConfig, S as S3Config } from './s3-0qqIyphl.js';
9
- export { f as changePassword, g as gisDriveAuth, p as pickFromDisk, s as saveToDisk } from './local-Dh6eGiK9.js';
5
+ import { R as ResumeOffer, C as ConnectTargets } from './connect-GY1R9c6Q.js';
6
+ import { f as BackupsManager } from './manager-BlSs6Q5S.js';
7
+ import { a as DriveAuth } from './drive-BLRsK32w.js';
8
+ import { W as WebdavConfig, S as S3Config } from './s3-BLgf6hqR.js';
9
+ export { f as changePassword, g as gisDriveAuth, p as pickFromDisk, s as saveToDisk } from './local-DrziuI_t.js';
10
10
  import './machine-ALMkT3mk.js';
11
11
 
12
12
  declare const BACKUP_EXTENSION = ".zip";
@@ -218,8 +218,22 @@ interface SimpleStore<S extends Record<string, SimpleRecord> = Record<string, Si
218
218
  unlock(password: string): Promise<boolean>;
219
219
  /** status.action === 'reconnect': re-run the destination's auth gesture. */
220
220
  reconnect(): Promise<boolean>;
221
- /** The portable backup file (a real ZIP; encrypted when protect() is on). */
222
- exportBackup(): Promise<Blob>;
221
+ /** The portable backup file (a real ZIP; encrypted when protect() is on).
222
+ *
223
+ * `{ plaintext: true }` asks for a READABLE copy of a protected store: the
224
+ * same ZIP with `selfstore.json` in the clear, which any tool can open and
225
+ * git can diff. It is the answer to "can I actually get my data out", and
226
+ * it costs nothing: the store, its password and its destination are
227
+ * untouched, so the real backup stays encrypted. Before this, the only way
228
+ * out was `unprotect()`, which rewrites the destination and leaves it
229
+ * readable until you remember to undo it.
230
+ *
231
+ * Refused while locked (there is nothing readable to write), and refused on
232
+ * a store created with `requireEncryption` - that flag means this store
233
+ * never produces cleartext, and an escape hatch would make it a lie. */
234
+ exportBackup(opts?: {
235
+ plaintext?: boolean;
236
+ }): Promise<Blob>;
223
237
  /** Download the backup (also resolves the 'manual' file mode's pending flag).
224
238
  * False when the user closed the save dialog: nothing was written, the
225
239
  * pending flag stands, and the app must not record a backup. */
package/dist/index.js CHANGED
@@ -1,6 +1,5 @@
1
- import { indexedDbCache, memoryCache, gisDriveAuth, isLockableCache, createLocalStore } from './chunk-RBIQRTZR.js';
2
- export { gisDriveAuth } from './chunk-RBIQRTZR.js';
3
- import './chunk-SKWHKM25.js';
1
+ import { indexedDbCache, memoryCache, gisDriveAuth, isLockableCache, createLocalStore } from './chunk-ULPGGZ6L.js';
2
+ export { gisDriveAuth } from './chunk-ULPGGZ6L.js';
4
3
  import { saveToDisk } from './chunk-DCEXWSM6.js';
5
4
  export { pickFromDisk, saveToDisk } from './chunk-DCEXWSM6.js';
6
5
  import { fromSession as fromSession$1, fromSession2, isSupported, connect3, connect2, connect } from './chunk-TKWU52LK.js';
@@ -12,6 +11,7 @@ import './chunk-QBCU7OOP.js';
12
11
  import './chunk-O3Y6FORN.js';
13
12
  import { SelfstoreError } from './chunk-3OBJOMQ2.js';
14
13
  export { SelfstoreError, errorLabelKey, isSelfstoreError } from './chunk-3OBJOMQ2.js';
14
+ import './chunk-SKWHKM25.js';
15
15
 
16
16
  // src/simple/simple.ts
17
17
  function idFieldOf(sync, collection) {
@@ -272,7 +272,7 @@ async function selfstore(app, options = {}) {
272
272
  unprotect: () => store.setEncryption(null),
273
273
  unlock: (password) => store.unlock(password),
274
274
  reconnect: () => store.reconnect(),
275
- exportBackup: () => store.exportBlob(),
275
+ exportBackup: (opts) => store.exportBlob(opts),
276
276
  async downloadBackup(filename) {
277
277
  const written = await saveToDisk(await store.exportBlob(), filename ?? backupName);
278
278
  if (written) store.markDownloaded();
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/simple/simple.ts"],"names":["fromSession","connect"],"mappings":"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","file":"index.js","sourcesContent":["// The simple store: one call, sensible defaults, and the store owns the data.\n//\n// const store = await selfstore('notes-app');\n// await store.put('todos', { id: 't1', text: 'hello' });\n// store.onChange(render);\n//\n// Convention over configuration: schema version 1, an IndexedDB cache named\n// after the app (in-memory where IndexedDB does not exist, so tests and SSR\n// just work), debounced auto-save, and the browser sync moments wired.\n// Every default has an option; everything deeper lives on store.advanced -\n// the full createLocalStore, same instance.\n//\n// One rule is enforced rather than documented: records need a non-empty\n// string id (the merge identifies records by it), so put() throws a\n// TypeError right away. The advanced pull-model store only logs - it cannot\n// know which collections an app considers mergeable.\n\nimport {\n\tcreateLocalStore,\n\ttype LocalStore,\n\ttype LocalStoreState,\n\ttype StoreError\n} from '../persistence/store';\nimport {\n\tindexedDbCache,\n\tmemoryCache,\n\tisLockableCache,\n\ttype KV,\n\ttype LocalCache\n} from '../persistence/cache';\nimport type { BackupTarget } from '../persistence/target';\nimport type { StatusDescriptor } from '../persistence/status';\nimport type { SyncConfig } from '../sync';\nimport type { ConnectTargets, ResumeOffer } from '../flows/connect';\nimport type { BackupsManager } from '../backups/manager';\nimport type { Snapshot, SnapshotFile } from '../selfstore/types';\nimport { SelfstoreError } from '../selfstore/errors';\nimport { restore, type PasswordPolicy } from '../selfstore';\nimport { saveToDisk } from '../selfstore/targets/local';\nimport {\n\taccount as driveAccount,\n\tconnect as driveConnect,\n\tfromSession as driveFromSession,\n\ttype DriveAuth\n} from '../persistence/targets/drive';\nimport { gisDriveAuth } from '../persistence/targets/drive-auth-gis';\nimport {\n\tconnect as fileConnect,\n\tfromSession as fileFromSession,\n\tisSupported as fileIsSupported\n} from '../persistence/targets/file';\nimport {\n\tconnect as webdavConnect,\n\tfromSession as webdavFromSession,\n\ttype WebdavConfig\n} from '../persistence/targets/webdav';\nimport { connect as s3Connect, type S3Config } from '../persistence/targets/s3';\n\n/** A record as the simple store sees it: plain JSON with a string id (the id\n * field name can be remapped per collection via `sync.ids`). */\nexport type SimpleRecord = Record<string, unknown>;\n\n/** Unlock callback for the ultra-sensitive cacheLock mode: return the session\n * secret - a password (Argon2id-derived) or an app-provided key (e.g. a passkey\n * PRF result). Called at boot; called again with { failed: true } after a wrong\n * secret so the app can re-prompt. Throw to abort the boot (user cancelled). */\nexport type CacheUnlock = (attempt: { failed: boolean }) => Promise<string | CryptoKey>;\n\n/** Options - every one of them optional, the defaults carry a real app. */\nexport interface SimpleOptions {\n\t/** Your DATA schema version (default 1). Bump it together with `migrate`. */\n\tschema?: number;\n\t/** Upgrade a snapshot written by an older schema version. */\n\tmigrate?: (fromVersion: number, snap: Snapshot) => Snapshot;\n\t/** Per-collection merge tuning (id field mapping, strategies). The default -\n\t * records keyed by `id`, later edit wins, deletes propagate - fits most apps. */\n\tsync?: SyncConfig;\n\t/**\n\t * Google Drive, when this app uses it. Two shapes for two situations:\n\t *\n\t * `{ clientId }` is the whole wiring - the library builds the browser\n\t * connection, keeps ONE session for the app (several would mean several\n\t * consent screens), reuses its token across a reload, remembers which\n\t * account holds the backup and hands that back to Google so the chooser\n\t * stops appearing. Nothing else to write.\n\t *\n\t * A `DriveAuth` instead, for an app that gets its tokens elsewhere - a\n\t * server broker holding a refresh token, a native shell.\n\t *\n\t * Either way, providing it here is also what lets a connected Drive backup\n\t * restore itself on the next start.\n\t */\n\tdrive?: DriveAuth | { clientId: string; scope?: string };\n\t/** Where the working copy lives (default: IndexedDB named after the app;\n\t * in-memory when IndexedDB does not exist, e.g. tests or SSR). */\n\tcache?: LocalCache;\n\t/** Auto-save debounce in ms. */\n\tdebounceMs?: number;\n\t/** Cross-tab coordination (default on in browsers). */\n\tmultiTab?: boolean;\n\t/** Refuse to ever write or export a plaintext backup: connecting a\n\t * destination then demands a password (or a group). Off by default. */\n\trequireEncryption?: boolean;\n\t/** Reject a backup password weaker than this policy (length, character\n\t * classes) at protect()/addKey time. Preview the rules with\n\t * checkPasswordPolicy for a live UI hint. Omit for no policy. */\n\tpasswordPolicy?: PasswordPolicy;\n\t/** Ultra-sensitive mode: seal the local cache under a key derived from a\n\t * secret held in memory (this callback), never on disk - so a copy of the\n\t * browser profile cannot read it. One unlock per session; branch it on the\n\t * app's existing login to leave the UX unchanged. Browser (IndexedDB) only. */\n\tcacheLock?: CacheUnlock;\n\t/** Wire tab focus / network return / interval / tab hide automatically\n\t * (default true in browsers). Pass false to drive syncing yourself. */\n\tautoSync?: boolean;\n}\n\n/** What connecting a destination did. 'merged': it already held a backup and\n * both sides were folded together. 'started': it was empty, this device's data\n * is now its content. 'manual': non-Chromium file fallback - offer\n * downloadBackup() instead. 'cancelled': the user closed the picker/consent. */\nexport type ConnectOutcome = 'merged' | 'started' | 'manual' | 'cancelled';\n\nexport interface SimpleStore<\n\tS extends Record<string, SimpleRecord> = Record<string, SimpleRecord>\n> {\n\t// --- Your data ---------------------------------------------------------\n\t/** Every record of a collection (treat as read-only; write via put/remove). */\n\tall<K extends keyof S & string>(collection: K): readonly S[K][];\n\t/** One record by id, or undefined. */\n\tget<K extends keyof S & string>(collection: K, id: string): S[K] | undefined;\n\t/** Insert or replace one record (auto-saves, debounced). Throws a TypeError\n\t * when the record has no non-empty string id. */\n\tput<K extends keyof S & string>(collection: K, record: S[K]): Promise<void>;\n\t/** Insert or replace many records in one save. */\n\tputAll<K extends keyof S & string>(collection: K, records: S[K][]): Promise<void>;\n\t/** Delete one record by id (propagates to other devices). Unknown id: no-op. */\n\tremove<K extends keyof S & string>(collection: K, id: string): Promise<void>;\n\t/** Empty a collection (every removal propagates). */\n\tclear<K extends keyof S & string>(collection: K): Promise<void>;\n\t/** Called after any data change: your writes, another tab, another device,\n\t * a restore. Returns an unsubscribe. */\n\tonChange(fn: () => void): () => void;\n\n\t// --- Where it lives ----------------------------------------------------\n\t/** Connect Google Drive as the durable home. An existing backup there is\n\t * MERGED with this device (multi-device); an encrypted one needs its\n\t * password up front (PASSWORD_REQUIRED is thrown before anything changes). */\n\tconnectDrive(auth: DriveAuth, opts?: { password?: string }): Promise<ConnectOutcome>;\n\t/** Connect a disk file (File System Access; falls back to manual download). */\n\tconnectFile(opts?: { password?: string }): Promise<ConnectOutcome>;\n\t/** Connect a WebDAV server (Nextcloud, ownCloud, your own). */\n\tconnectWebdav(config: WebdavConfig, opts?: { password?: string }): Promise<ConnectOutcome>;\n\t/** Connect an S3-compatible bucket you control (Amazon S3, R2, B2, MinIO). */\n\tconnectS3(config: S3Config, opts?: { password?: string }): Promise<ConnectOutcome>;\n\t/** Connect any custom BackupTarget with the same merge semantics. */\n\tconnectTarget(target: BackupTarget, opts?: { password?: string }): Promise<ConnectOutcome>;\n\t/** Back to device-only (the destination keeps its last backup). */\n\tdisconnect(): Promise<void>;\n\n\t// --- Backup copy (replica) ---------------------------------------------\n\t/** Also write the same encrypted backup to this second destination on\n\t * every save. A broken copy never gates the store (state.replicas). */\n\taddReplica(target: BackupTarget, opts?: { id?: string }): string;\n\t/** Stop writing the copy (the destination keeps its last backup). */\n\tremoveReplica(id: string): void;\n\n\t// --- Protection & the two gate gestures --------------------------------\n\t/** Encrypt the durable backup end to end with this password (reversible). */\n\tprotect(password: string): Promise<void>;\n\t/** Remove the backup password. */\n\tunprotect(): Promise<void>;\n\t/** status.action === 'unlock': supply the password to resume. */\n\tunlock(password: string): Promise<boolean>;\n\t/** status.action === 'reconnect': re-run the destination's auth gesture. */\n\treconnect(): Promise<boolean>;\n\n\t// --- Portable backups ---------------------------------------------------\n\t/** The portable backup file (a real ZIP; encrypted when protect() is on). */\n\texportBackup(): Promise<Blob>;\n\t/** Download the backup (also resolves the 'manual' file mode's pending flag).\n\t * False when the user closed the save dialog: nothing was written, the\n\t * pending flag stands, and the app must not record a backup. */\n\tdownloadBackup(filename?: string): Promise<boolean>;\n\t/** Load a backup file into this store, replacing the local data (removals\n\t * propagate like edits). Throws PASSWORD_REQUIRED / DECRYPT_FAILED. */\n\timportBackup(file: Blob | Uint8Array, opts?: { password?: string }): Promise<void>;\n\n\t// --- Observability ------------------------------------------------------\n\t/** Headless status: { state, severity, action, labelKey } - map labelKey to\n\t * your own copy. */\n\treadonly status: StatusDescriptor;\n\t/** The last problem: { code, labelKey, message } - show labelKey, log message. */\n\treadonly error: StoreError | null;\n\t/** The full underlying state (advanced reads: journal, peers, mode...). */\n\treadonly state: LocalStoreState;\n\t/** Fires on any state change (status flips included) - for framework bindings. */\n\tsubscribe(fn: () => void): () => void;\n\n\t// --- Lifecycle ----------------------------------------------------------\n\t/** Save now (called for you on tab hide when autoSync is on). */\n\tflush(): Promise<void>;\n\t/** Converge with the destination now (a user gesture like pull-to-refresh). */\n\tsync(): Promise<void>;\n\t/** Drop timers and listeners (tests, SPA teardown). */\n\tdispose(): void;\n\n\t/** The escape hatch: the full advanced store this simple one is built on -\n\t * peers, groups, custom strategies, everything (see selfstore/advanced). */\n\treadonly advanced: LocalStore;\n\n\t/** The attachment point for 'selfstore/flows': the engine plus the cache KV\n\t * and backup file name a flow needs to build destination targets. Apps on\n\t * the advanced store hand a flow the same three by themselves. */\n\treadonly flowHost: { engine: LocalStore; kv: KV; backupName: string };\n\n\t/** Who holds the destination, when it is known - the Google address behind a\n\t * Drive backup. Learned on connect and remembered across sessions, so a\n\t * surface can NAME the destination (\"Google Drive\" is not an address) and a\n\t * re-grant can skip the account chooser. Null for a file, or before a first\n\t * connect. Reading it costs nothing: no call goes out. */\n\treadonly account: string | null;\n\n\t/** The destination this device already used, ready to hand to a connect flow\n\t * as `options.resume` - so a returning visitor is offered \"reopen my backup\"\n\t * ahead of the plain destinations, instead of re-deriving a decision the\n\t * library already has on file. Null when there is nothing to resume. */\n\tresumeOffer(): ResumeOffer | null;\n\n\t/**\n\t * The several-backups manager for this store's destination, or null when\n\t * there is nothing to manage several of (no Drive configured).\n\t *\n\t * The port under it - list, open by id, create, rename, delete - was the last\n\t * thing an app still assembled from the session the store had built, and the\n\t * one place it could pick the wrong file id. Built once and remembered; the\n\t * backups module is imported on demand, so an app that never asks does not\n\t * carry it.\n\t */\n\tbackups(): Promise<BackupsManager | null>;\n\n\t/**\n\t * The destinations this store can offer, ready for a connect flow or widget:\n\t * Drive first when the app configured it, a file always.\n\t *\n\t * Consumers were assembling this by hand from the very session the store\n\t * built - and the order and the \"no client id means no Drive card\" rule were\n\t * rediscovered each time. An app wanting a different offer (a WebDAV server,\n\t * its own target) still writes its own object; this is the default, not a\n\t * ceiling.\n\t */\n\tdestinations(): ConnectTargets;\n}\n\n/** The id field a collection's records are keyed by: `sync.ids` may remap it;\n * a DOTTED path (nested id) is an advanced setup the simple check skips. */\nfunction idFieldOf(sync: SyncConfig | undefined, collection: string): string | null {\n\tconst mapped = sync?.ids?.[collection];\n\tif (mapped == null) return 'id';\n\treturn mapped.includes('.') ? null : mapped;\n}\n\nfunction requireStringId(record: SimpleRecord, field: string | null, collection: string): string {\n\tif (field === null) return ''; // nested-path ids: validated by the engine's own rules\n\tconst id = record[field];\n\tif (typeof id !== 'string' || id.length === 0) {\n\t\tthrow new TypeError(\n\t\t\t`selfstore: a record in \"${collection}\" needs a non-empty STRING \"${field}\" ` +\n\t\t\t\t`(got ${JSON.stringify(id)}). String ids are what the multi-device merge keys on; ` +\n\t\t\t\t`map another field with { sync: { ids: { ${collection}: 'yourField' } } }.`\n\t\t);\n\t}\n\treturn id;\n}\n\n/** True when a browser IndexedDB is available (SSR and plain Node lack it). */\nfunction hasIndexedDb(): boolean {\n\treturn typeof indexedDB !== 'undefined';\n}\n\n/** Told apart by shape: a DriveAuth mints tokens, a `{ clientId }` asks us to. */\nfunction isDriveAuth(v: SimpleOptions['drive']): v is DriveAuth {\n\treturn !!v && typeof (v as DriveAuth).token === 'function';\n}\n\n/** Where the connected Drive address is kept, per app: it is what stops Google\n * asking WHICH account at every re-grant. An address is not a secret, and a\n * browser that refuses storage simply gets the chooser back. */\nconst driveAccountKey = (app: string): string => `selfstore.drive.account.${app}`;\n\nfunction localStore<T>(job: (store: Storage) => T): T | undefined {\n\ttry {\n\t\tconst store = globalThis.localStorage;\n\t\treturn store ? job(store) : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction rememberedDriveAccount(app: string): string | undefined {\n\treturn localStore((s) => s.getItem(driveAccountKey(app))) ?? undefined;\n}\n\n/** Learn the account behind a live Drive session, best-effort and once: it feeds\n * the next re-grant's hint, and any surface that wants to name the destination. */\nasync function learnDriveAccount(app: string, auth: DriveAuth): Promise<void> {\n\ttry {\n\t\tconst who = await driveAccount({ auth });\n\t\tif (who.email) localStore((s) => s.setItem(driveAccountKey(app), who.email!));\n\t} catch {\n\t\t// A destination that will not say costs the hint, never the connection.\n\t}\n}\n\n/**\n * The same connection, which learns whose it is the first time it hands out a\n * token.\n *\n * Learning it took an explicit connect, so anyone whose backup was attached\n * before this existed - or attached in another session - kept a destination\n * with no name on it: the panel said \"Google Drive\" where it could have said\n * which one, and the offer to reopen that backup had no address to show, which\n * is most of what makes it recognisable.\n *\n * It cannot simply be asked for at start-up: reading the account needs a token,\n * and asking for a token with no user gesture behind it opens a popup the\n * browser blocks. So it rides along instead - every token this app obtains,\n * whatever asked for it, teaches it the address at no cost. Once known, nothing\n * is called again.\n */\nfunction selfNaming(app: string, auth: DriveAuth): DriveAuth {\n\tlet learning: Promise<void> | null = null;\n\tconst learn = (): void => {\n\t\tif (learning || rememberedDriveAccount(app)) return;\n\t\tlearning = learnDriveAccount(app, auth);\n\t};\n\treturn {\n\t\tasync token(opts) {\n\t\t\tconst value = await auth.token(opts);\n\t\t\tlearn();\n\t\t\treturn value;\n\t\t},\n\t\tasync reconnect() {\n\t\t\tconst ok = await auth.reconnect();\n\t\t\tif (ok) learn();\n\t\t\treturn ok;\n\t\t},\n\t\tforget: () => auth.forget()\n\t};\n}\n\n/**\n * Open (or create) the app's local store. Awaits the initial load, so the\n * returned store is ready: data readable, destination restored, first converge\n * done. See SimpleOptions for the defaults.\n */\nexport async function selfstore<\n\tS extends Record<string, SimpleRecord> = Record<string, SimpleRecord>\n>(app: string, options: SimpleOptions = {}): Promise<SimpleStore<S>> {\n\tif (typeof app !== 'string' || app.length === 0) {\n\t\tthrow new TypeError('selfstore: pass your app name, e.g. selfstore(\"my-app\").');\n\t}\n\n\t// The one copy of the data. The engine pulls it on save (gather) and pushes\n\t// into it on restores/folds (apply); the facade's mutators edit it in place\n\t// copy-on-write and schedule a save.\n\tlet collections: Record<string, SimpleRecord[]> = {};\n\t// Binary files ride along untouched (a backup that carries them keeps them);\n\t// managing them is an advanced-store concern.\n\tlet files: SnapshotFile[] = [];\n\n\tconst dataSubs = new Set<() => void>();\n\tconst emitData = (): void => dataSubs.forEach((fn) => fn());\n\n\tconst cache =\n\t\toptions.cache ??\n\t\t(hasIndexedDb()\n\t\t\t? indexedDbCache(app, options.cacheLock ? { lock: true } : undefined)\n\t\t\t: memoryCache());\n\tconst backupName = `${app}.zip`;\n\t// connectDrive() remembers the auth so a later restoreTarget can use it even\n\t// when options.drive was not provided up front (same session only).\n\t//\n\t// `{ clientId }` is the whole wiring done here rather than in every app: ONE\n\t// session (several would mean several consent screens), a token that survives\n\t// a reload, and the account remembered so Google stops asking which one. Each\n\t// of those was a separate decision every consumer had to discover; the app\n\t// that wrote them by hand hit all three as bugs first.\n\t/** Built at most once: two managers over one destination would each hold\n\t * their own idea of which file is active. */\n\tlet backupsManager: BackupsManager | null = null;\n\t// Wrapped whichever shape it came in as: an app that mints its own tokens\n\t// (a server broker, a native shell) has the same nameless-destination\n\t// problem, and the wrapper only ever rides along on a token already granted.\n\tlet driveAuth: DriveAuth | null = isDriveAuth(options.drive)\n\t\t? selfNaming(app, options.drive)\n\t\t: options.drive\n\t\t\t? selfNaming(\n\t\t\t\t\tapp,\n\t\t\t\t\tgisDriveAuth({\n\t\t\t\t\t\tclientId: options.drive.clientId,\n\t\t\t\t\t\tscope: options.drive.scope,\n\t\t\t\t\t\tpersist: 'session',\n\t\t\t\t\t\thint: () => rememberedDriveAccount(app)\n\t\t\t\t\t})\n\t\t\t\t)\n\t\t\t: null;\n\n\t// Ultra-sensitive: unlock the cache before the store hydrates from it. One\n\t// prompt per session, driven by the app (branch it on an existing login to\n\t// keep the UX unchanged). A wrong secret re-prompts; throw to abort.\n\tif (options.cacheLock && isLockableCache(cache)) {\n\t\tlet failed = false;\n\t\twhile (!(await cache.unlock(await options.cacheLock({ failed })))) failed = true;\n\t}\n\n\tconst store = createLocalStore({\n\t\tapp,\n\t\tschemaVersion: options.schema ?? 1,\n\t\tgather: (): Snapshot => structuredClone({ collections, files }),\n\t\tapply: (snap: Snapshot): void => {\n\t\t\tcollections = (snap.collections ?? {}) as Record<string, SimpleRecord[]>;\n\t\t\tfiles = snap.files ?? [];\n\t\t\temitData();\n\t\t},\n\t\tmigrate: options.migrate,\n\t\tsync: options.sync,\n\t\tcache,\n\t\tdebounceMs: options.debounceMs,\n\t\tmultiTab: options.multiTab,\n\t\trequireEncryption: options.requireEncryption,\n\t\tpasswordPolicy: options.passwordPolicy,\n\t\trestoreTarget: async (kind) => {\n\t\t\tif (kind === 'drive' && driveAuth) {\n\t\t\t\treturn driveFromSession({ auth: driveAuth, kv: cache.kv, fileName: backupName });\n\t\t\t}\n\t\t\tif (kind === 'file') return fileFromSession({ kv: cache.kv });\n\t\t\tif (kind === 'webdav') return webdavFromSession({ kv: cache.kv });\n\t\t\treturn null;\n\t\t}\n\t});\n\n\tawait store.init();\n\n\t// A degraded file mode is a verdict about the BROWSER, but it was persisted\n\t// like a property of the data: every later session re-read the word without\n\t// ever asking again. One session that could not open a picker - a browser\n\t// that ships the API and refuses it, a call made a moment too late, a user on\n\t// another browser that day - then pinned download-only mode forever, on a\n\t// Chromium perfectly able to hold a file, with no way back.\n\t//\n\t// Nothing writes this word except a picker that could not deliver, so a\n\t// browser that CAN deliver makes it stale. Re-examined here, in the layer\n\t// that wrote it. Dropping to 'device' is the honest state: manual mode had\n\t// already released the durable target, so there is nothing to detach - only\n\t// a stale claim to stop repeating, and a destination the host can offer again.\n\tif (store.state.targetKind === 'file-manual' && fileIsSupported()) {\n\t\tawait store.detachTarget();\n\t}\n\n\t// Auto-wired sync moments (Spring-Boot-style autoconfiguration): tab focus,\n\t// network return, a slow interval, and save-on-hide. Opt out with\n\t// { autoSync: false } to drive these yourself.\n\tconst teardown: (() => void)[] = [];\n\tif (\n\t\toptions.autoSync !== false &&\n\t\ttypeof document !== 'undefined' &&\n\t\ttypeof window !== 'undefined'\n\t) {\n\t\tconst onVisibility = (): void => {\n\t\t\tif (document.visibilityState === 'hidden') void store.flush();\n\t\t\telse void store.syncIfStale('focus');\n\t\t};\n\t\tconst onOnline = (): void => void store.syncIfStale('online');\n\t\tconst onPageHide = (): void => void store.flush();\n\t\tconst interval = setInterval(() => {\n\t\t\tif (document.visibilityState === 'visible') void store.syncIfStale('interval');\n\t\t}, 5 * 60_000);\n\t\tdocument.addEventListener('visibilitychange', onVisibility);\n\t\twindow.addEventListener('online', onOnline);\n\t\twindow.addEventListener('pagehide', onPageHide);\n\t\tteardown.push(() => {\n\t\t\tclearInterval(interval);\n\t\t\tdocument.removeEventListener('visibilitychange', onVisibility);\n\t\t\twindow.removeEventListener('online', onOnline);\n\t\t\twindow.removeEventListener('pagehide', onPageHide);\n\t\t});\n\t}\n\n\tfunction mutate(collection: string, next: SimpleRecord[]): Promise<void> {\n\t\tcollections = { ...collections, [collection]: next };\n\t\temitData();\n\t\tstore.schedule();\n\t\treturn Promise.resolve();\n\t}\n\n\tasync function connectTarget(\n\t\ttarget: BackupTarget,\n\t\topts: { password?: string } = {}\n\t): Promise<ConnectOutcome> {\n\t\tconst info = await store.inspectTarget(target);\n\t\tif (info.hasBackup) {\n\t\t\t// Another app's backup is refused BEFORE the password question: asking\n\t\t\t// first teaches the user their password \"does not work\" on a file that\n\t\t\t// was never this app's to open.\n\t\t\tif (info.app !== null && info.app !== store.app) {\n\t\t\t\tthrow new SelfstoreError(\n\t\t\t\t\t'FOREIGN_BACKUP',\n\t\t\t\t\t`This destination holds a backup written by app \"${info.app}\", not \"${store.app}\". Open it in its own application, or pick another file.`\n\t\t\t\t);\n\t\t\t}\n\t\t\t// Fail before attaching: an encrypted backup without its password would\n\t\t\t// otherwise attach locked, which is a support case, not an outcome.\n\t\t\tif (info.encrypted && !opts.password) {\n\t\t\t\tthrow new SelfstoreError(\n\t\t\t\t\t'PASSWORD_REQUIRED',\n\t\t\t\t\t'This destination holds an encrypted backup: pass its password to connect.'\n\t\t\t\t);\n\t\t\t}\n\t\t\tawait store.attachTarget(target, { password: opts.password ?? null, strategy: 'merge' });\n\t\t\treturn 'merged';\n\t\t}\n\t\tawait store.attachTarget(target, {\n\t\t\tpassword: opts.password ?? null,\n\t\t\tstrategy: 'replace-remote'\n\t\t});\n\t\treturn 'started';\n\t}\n\n\tconst simple: SimpleStore<S> = {\n\t\tall<K extends keyof S & string>(collection: K): readonly S[K][] {\n\t\t\treturn (collections[collection] ?? []) as S[K][];\n\t\t},\n\t\tget<K extends keyof S & string>(collection: K, id: string): S[K] | undefined {\n\t\t\tconst field = idFieldOf(options.sync, collection) ?? 'id';\n\t\t\treturn (collections[collection] ?? []).find((r) => r[field] === id) as S[K] | undefined;\n\t\t},\n\t\tput<K extends keyof S & string>(collection: K, record: S[K]): Promise<void> {\n\t\t\tconst field = idFieldOf(options.sync, collection);\n\t\t\tconst id = requireStringId(record, field, collection);\n\t\t\tconst rows = collections[collection] ?? [];\n\t\t\tconst at = field === null ? -1 : rows.findIndex((r) => r[field] === id);\n\t\t\treturn mutate(\n\t\t\t\tcollection,\n\t\t\t\tat === -1 ? [...rows, record] : rows.map((r, i) => (i === at ? record : r))\n\t\t\t);\n\t\t},\n\t\tputAll<K extends keyof S & string>(collection: K, records: S[K][]): Promise<void> {\n\t\t\tconst field = idFieldOf(options.sync, collection);\n\t\t\tconst byId = new Map<unknown, number>();\n\t\t\tfor (const r of records) requireStringId(r, field, collection);\n\t\t\tconst rows = [...(collections[collection] ?? [])];\n\t\t\tif (field !== null) {\n\t\t\t\trows.forEach((r, i) => byId.set(r[field], i));\n\t\t\t\tfor (const r of records) {\n\t\t\t\t\tconst at = byId.get(r[field]);\n\t\t\t\t\tif (typeof at === 'number') rows[at] = r;\n\t\t\t\t\telse rows.push(r);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\trows.push(...records);\n\t\t\t}\n\t\t\treturn mutate(collection, rows);\n\t\t},\n\t\tremove<K extends keyof S & string>(collection: K, id: string): Promise<void> {\n\t\t\tconst field = idFieldOf(options.sync, collection) ?? 'id';\n\t\t\tconst rows = collections[collection] ?? [];\n\t\t\tconst next = rows.filter((r) => r[field] !== id);\n\t\t\treturn next.length === rows.length ? Promise.resolve() : mutate(collection, next);\n\t\t},\n\t\tclear<K extends keyof S & string>(collection: K): Promise<void> {\n\t\t\tif ((collections[collection] ?? []).length === 0) return Promise.resolve();\n\t\t\treturn mutate(collection, []);\n\t\t},\n\t\tonChange(fn: () => void): () => void {\n\t\t\tdataSubs.add(fn);\n\t\t\treturn () => dataSubs.delete(fn);\n\t\t},\n\n\t\tasync connectDrive(auth: DriveAuth, opts?: { password?: string }): Promise<ConnectOutcome> {\n\t\t\tdriveAuth = auth;\n\t\t\tconst target = await driveConnect({ auth, kv: cache.kv, fileName: backupName });\n\t\t\tif (!target) return 'cancelled';\n\t\t\tconst outcome = await connectTarget(target, opts);\n\t\t\t// Learn WHICH account now that there is a session: it is the hint that\n\t\t\t// keeps the chooser away next time, and the name a panel can show. Not\n\t\t\t// awaited into the outcome - a slow metadata call must not delay the\n\t\t\t// connect the user is waiting on.\n\t\t\tif (outcome !== 'cancelled') void learnDriveAccount(app, auth);\n\t\t\treturn outcome;\n\t\t},\n\t\tasync connectFile(opts?: { password?: string }): Promise<ConnectOutcome> {\n\t\t\t// Asked twice on purpose: the first call answers from what the browser\n\t\t\t// exposes, the second from what it did when actually asked. A browser\n\t\t\t// that ships the picker and then refuses it only tells us so here.\n\t\t\tif (fileIsSupported()) {\n\t\t\t\tconst target = await fileConnect({ kv: cache.kv, fileName: backupName });\n\t\t\t\tif (target) return connectTarget(target, opts);\n\t\t\t\tif (fileIsSupported()) return 'cancelled';\n\t\t\t}\n\t\t\tawait store.setManualFile();\n\t\t\treturn 'manual';\n\t\t},\n\t\tasync connectWebdav(\n\t\t\tconfig: WebdavConfig,\n\t\t\topts?: { password?: string }\n\t\t): Promise<ConnectOutcome> {\n\t\t\tconst target = await webdavConnect({ kv: cache.kv, config });\n\t\t\tif (!target) {\n\t\t\t\tthrow new SelfstoreError(\n\t\t\t\t\t'TARGET_UNAVAILABLE',\n\t\t\t\t\t'The WebDAV server did not answer (URL, credentials or CORS).'\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn connectTarget(target, opts);\n\t\t},\n\t\tasync connectS3(config: S3Config, opts?: { password?: string }): Promise<ConnectOutcome> {\n\t\t\tconst target = await s3Connect({ kv: cache.kv, config });\n\t\t\tif (!target) {\n\t\t\t\tthrow new SelfstoreError(\n\t\t\t\t\t'TARGET_UNAVAILABLE',\n\t\t\t\t\t'The S3 endpoint did not answer (endpoint, credentials or CORS).'\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn connectTarget(target, opts);\n\t\t},\n\t\tconnectTarget,\n\t\tdisconnect: () => store.detachTarget(),\n\t\taddReplica: (target, opts) => store.attachReplica(target, opts),\n\t\tremoveReplica: (id) => store.detachReplica(id),\n\n\t\tprotect: (password: string) => store.setEncryption(password),\n\t\tunprotect: () => store.setEncryption(null),\n\t\tunlock: (password: string) => store.unlock(password),\n\t\treconnect: () => store.reconnect(),\n\n\t\texportBackup: () => store.exportBlob(),\n\t\tasync downloadBackup(filename?: string): Promise<boolean> {\n\t\t\tconst written = await saveToDisk(await store.exportBlob(), filename ?? backupName);\n\t\t\t// Clearing the pending flag on a cancelled dialog would drop the very\n\t\t\t// nudge that asks for the file the user still does not have.\n\t\t\tif (written) store.markDownloaded();\n\t\t\treturn written;\n\t\t},\n\t\tasync importBackup(file: Blob | Uint8Array, opts?: { password?: string }): Promise<void> {\n\t\t\t// The fluent read() strips reserved bookkeeping collections; removals\n\t\t\t// then propagate because the next save diffs against the sync meta.\n\t\t\tconst reader = restore(file);\n\t\t\tconst snap = opts?.password\n\t\t\t\t? await reader.withPassword(opts.password).read()\n\t\t\t\t: await reader.read();\n\t\t\tcollections = (snap.collections ?? {}) as Record<string, SimpleRecord[]>;\n\t\t\tfiles = snap.files ?? [];\n\t\t\temitData();\n\t\t\tstore.schedule();\n\t\t},\n\n\t\tget status(): StatusDescriptor {\n\t\t\treturn store.state.status;\n\t\t},\n\t\tget error(): StoreError | null {\n\t\t\treturn store.state.lastError;\n\t\t},\n\t\tget state(): LocalStoreState {\n\t\t\treturn store.state;\n\t\t},\n\t\tsubscribe: (fn: () => void) => store.subscribe(fn),\n\n\t\tflush: () => store.flush(),\n\t\tasync sync(): Promise<void> {\n\t\t\tawait store.syncNow();\n\t\t},\n\t\tdispose(): void {\n\t\t\tteardown.forEach((fn) => fn());\n\t\t\tteardown.length = 0;\n\t\t\tstore.dispose();\n\t\t},\n\n\t\tadvanced: store,\n\n\t\tflowHost: { engine: store, kv: cache.kv, backupName },\n\n\t\tget account(): string | null {\n\t\t\treturn rememberedDriveAccount(app) ?? null;\n\t\t},\n\n\t\tasync backups(): Promise<BackupsManager | null> {\n\t\t\tif (!driveAuth) return null;\n\t\t\tbackupsManager ??= await (async () => {\n\t\t\t\tconst { createBackupsManager, driveBackupsHost } = await import('../backups/manager-drive');\n\t\t\t\treturn createBackupsManager({\n\t\t\t\t\tstore,\n\t\t\t\t\tkv: cache.kv,\n\t\t\t\t\thost: driveBackupsHost({ auth: driveAuth!, kv: cache.kv, fileName: backupName }),\n\t\t\t\t\tnaming: { canonicalName: backupName }\n\t\t\t\t});\n\t\t\t})();\n\t\t\treturn backupsManager;\n\t\t},\n\n\t\tdestinations(): ConnectTargets {\n\t\t\t// Drive first when there is one: it is the answer that follows a user\n\t\t\t// from device to device. A file always, for whoever wants no account.\n\t\t\treturn driveAuth ? { drive: driveAuth, file: true } : { file: true };\n\t\t},\n\n\t\tresumeOffer(): ResumeOffer | null {\n\t\t\tconst detail = rememberedDriveAccount(app);\n\t\t\tif (!driveAuth || !detail) return null;\n\t\t\tconst auth = driveAuth;\n\t\t\treturn {\n\t\t\t\tkind: 'drive',\n\t\t\t\tdetail,\n\t\t\t\t// Reopening is not searching: fromSession takes the file id already\n\t\t\t\t// remembered, where a fresh connect resolves by name and could land on\n\t\t\t\t// another file carrying the same one.\n\t\t\t\tconnect: () => driveFromSession({ auth, kv: cache.kv, fileName: backupName })\n\t\t\t};\n\t\t}\n\t};\n\n\treturn simple;\n}\n"]}
1
+ {"version":3,"sources":["../src/simple/simple.ts"],"names":["fromSession","connect"],"mappings":"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","file":"index.js","sourcesContent":["// The simple store: one call, sensible defaults, and the store owns the data.\n//\n// const store = await selfstore('notes-app');\n// await store.put('todos', { id: 't1', text: 'hello' });\n// store.onChange(render);\n//\n// Convention over configuration: schema version 1, an IndexedDB cache named\n// after the app (in-memory where IndexedDB does not exist, so tests and SSR\n// just work), debounced auto-save, and the browser sync moments wired.\n// Every default has an option; everything deeper lives on store.advanced -\n// the full createLocalStore, same instance.\n//\n// One rule is enforced rather than documented: records need a non-empty\n// string id (the merge identifies records by it), so put() throws a\n// TypeError right away. The advanced pull-model store only logs - it cannot\n// know which collections an app considers mergeable.\n\nimport {\n\tcreateLocalStore,\n\ttype LocalStore,\n\ttype LocalStoreState,\n\ttype StoreError\n} from '../persistence/store';\nimport {\n\tindexedDbCache,\n\tmemoryCache,\n\tisLockableCache,\n\ttype KV,\n\ttype LocalCache\n} from '../persistence/cache';\nimport type { BackupTarget } from '../persistence/target';\nimport type { StatusDescriptor } from '../persistence/status';\nimport type { SyncConfig } from '../sync';\nimport type { ConnectTargets, ResumeOffer } from '../flows/connect';\nimport type { BackupsManager } from '../backups/manager';\nimport type { Snapshot, SnapshotFile } from '../selfstore/types';\nimport { SelfstoreError } from '../selfstore/errors';\nimport { restore, type PasswordPolicy } from '../selfstore';\nimport { saveToDisk } from '../selfstore/targets/local';\nimport {\n\taccount as driveAccount,\n\tconnect as driveConnect,\n\tfromSession as driveFromSession,\n\ttype DriveAuth\n} from '../persistence/targets/drive';\nimport { gisDriveAuth } from '../persistence/targets/drive-auth-gis';\nimport {\n\tconnect as fileConnect,\n\tfromSession as fileFromSession,\n\tisSupported as fileIsSupported\n} from '../persistence/targets/file';\nimport {\n\tconnect as webdavConnect,\n\tfromSession as webdavFromSession,\n\ttype WebdavConfig\n} from '../persistence/targets/webdav';\nimport { connect as s3Connect, type S3Config } from '../persistence/targets/s3';\n\n/** A record as the simple store sees it: plain JSON with a string id (the id\n * field name can be remapped per collection via `sync.ids`). */\nexport type SimpleRecord = Record<string, unknown>;\n\n/** Unlock callback for the ultra-sensitive cacheLock mode: return the session\n * secret - a password (Argon2id-derived) or an app-provided key (e.g. a passkey\n * PRF result). Called at boot; called again with { failed: true } after a wrong\n * secret so the app can re-prompt. Throw to abort the boot (user cancelled). */\nexport type CacheUnlock = (attempt: { failed: boolean }) => Promise<string | CryptoKey>;\n\n/** Options - every one of them optional, the defaults carry a real app. */\nexport interface SimpleOptions {\n\t/** Your DATA schema version (default 1). Bump it together with `migrate`. */\n\tschema?: number;\n\t/** Upgrade a snapshot written by an older schema version. */\n\tmigrate?: (fromVersion: number, snap: Snapshot) => Snapshot;\n\t/** Per-collection merge tuning (id field mapping, strategies). The default -\n\t * records keyed by `id`, later edit wins, deletes propagate - fits most apps. */\n\tsync?: SyncConfig;\n\t/**\n\t * Google Drive, when this app uses it. Two shapes for two situations:\n\t *\n\t * `{ clientId }` is the whole wiring - the library builds the browser\n\t * connection, keeps ONE session for the app (several would mean several\n\t * consent screens), reuses its token across a reload, remembers which\n\t * account holds the backup and hands that back to Google so the chooser\n\t * stops appearing. Nothing else to write.\n\t *\n\t * A `DriveAuth` instead, for an app that gets its tokens elsewhere - a\n\t * server broker holding a refresh token, a native shell.\n\t *\n\t * Either way, providing it here is also what lets a connected Drive backup\n\t * restore itself on the next start.\n\t */\n\tdrive?: DriveAuth | { clientId: string; scope?: string };\n\t/** Where the working copy lives (default: IndexedDB named after the app;\n\t * in-memory when IndexedDB does not exist, e.g. tests or SSR). */\n\tcache?: LocalCache;\n\t/** Auto-save debounce in ms. */\n\tdebounceMs?: number;\n\t/** Cross-tab coordination (default on in browsers). */\n\tmultiTab?: boolean;\n\t/** Refuse to ever write or export a plaintext backup: connecting a\n\t * destination then demands a password (or a group). Off by default. */\n\trequireEncryption?: boolean;\n\t/** Reject a backup password weaker than this policy (length, character\n\t * classes) at protect()/addKey time. Preview the rules with\n\t * checkPasswordPolicy for a live UI hint. Omit for no policy. */\n\tpasswordPolicy?: PasswordPolicy;\n\t/** Ultra-sensitive mode: seal the local cache under a key derived from a\n\t * secret held in memory (this callback), never on disk - so a copy of the\n\t * browser profile cannot read it. One unlock per session; branch it on the\n\t * app's existing login to leave the UX unchanged. Browser (IndexedDB) only. */\n\tcacheLock?: CacheUnlock;\n\t/** Wire tab focus / network return / interval / tab hide automatically\n\t * (default true in browsers). Pass false to drive syncing yourself. */\n\tautoSync?: boolean;\n}\n\n/** What connecting a destination did. 'merged': it already held a backup and\n * both sides were folded together. 'started': it was empty, this device's data\n * is now its content. 'manual': non-Chromium file fallback - offer\n * downloadBackup() instead. 'cancelled': the user closed the picker/consent. */\nexport type ConnectOutcome = 'merged' | 'started' | 'manual' | 'cancelled';\n\nexport interface SimpleStore<\n\tS extends Record<string, SimpleRecord> = Record<string, SimpleRecord>\n> {\n\t// --- Your data ---------------------------------------------------------\n\t/** Every record of a collection (treat as read-only; write via put/remove). */\n\tall<K extends keyof S & string>(collection: K): readonly S[K][];\n\t/** One record by id, or undefined. */\n\tget<K extends keyof S & string>(collection: K, id: string): S[K] | undefined;\n\t/** Insert or replace one record (auto-saves, debounced). Throws a TypeError\n\t * when the record has no non-empty string id. */\n\tput<K extends keyof S & string>(collection: K, record: S[K]): Promise<void>;\n\t/** Insert or replace many records in one save. */\n\tputAll<K extends keyof S & string>(collection: K, records: S[K][]): Promise<void>;\n\t/** Delete one record by id (propagates to other devices). Unknown id: no-op. */\n\tremove<K extends keyof S & string>(collection: K, id: string): Promise<void>;\n\t/** Empty a collection (every removal propagates). */\n\tclear<K extends keyof S & string>(collection: K): Promise<void>;\n\t/** Called after any data change: your writes, another tab, another device,\n\t * a restore. Returns an unsubscribe. */\n\tonChange(fn: () => void): () => void;\n\n\t// --- Where it lives ----------------------------------------------------\n\t/** Connect Google Drive as the durable home. An existing backup there is\n\t * MERGED with this device (multi-device); an encrypted one needs its\n\t * password up front (PASSWORD_REQUIRED is thrown before anything changes). */\n\tconnectDrive(auth: DriveAuth, opts?: { password?: string }): Promise<ConnectOutcome>;\n\t/** Connect a disk file (File System Access; falls back to manual download). */\n\tconnectFile(opts?: { password?: string }): Promise<ConnectOutcome>;\n\t/** Connect a WebDAV server (Nextcloud, ownCloud, your own). */\n\tconnectWebdav(config: WebdavConfig, opts?: { password?: string }): Promise<ConnectOutcome>;\n\t/** Connect an S3-compatible bucket you control (Amazon S3, R2, B2, MinIO). */\n\tconnectS3(config: S3Config, opts?: { password?: string }): Promise<ConnectOutcome>;\n\t/** Connect any custom BackupTarget with the same merge semantics. */\n\tconnectTarget(target: BackupTarget, opts?: { password?: string }): Promise<ConnectOutcome>;\n\t/** Back to device-only (the destination keeps its last backup). */\n\tdisconnect(): Promise<void>;\n\n\t// --- Backup copy (replica) ---------------------------------------------\n\t/** Also write the same encrypted backup to this second destination on\n\t * every save. A broken copy never gates the store (state.replicas). */\n\taddReplica(target: BackupTarget, opts?: { id?: string }): string;\n\t/** Stop writing the copy (the destination keeps its last backup). */\n\tremoveReplica(id: string): void;\n\n\t// --- Protection & the two gate gestures --------------------------------\n\t/** Encrypt the durable backup end to end with this password (reversible). */\n\tprotect(password: string): Promise<void>;\n\t/** Remove the backup password. */\n\tunprotect(): Promise<void>;\n\t/** status.action === 'unlock': supply the password to resume. */\n\tunlock(password: string): Promise<boolean>;\n\t/** status.action === 'reconnect': re-run the destination's auth gesture. */\n\treconnect(): Promise<boolean>;\n\n\t// --- Portable backups ---------------------------------------------------\n\t/** The portable backup file (a real ZIP; encrypted when protect() is on).\n\t *\n\t * `{ plaintext: true }` asks for a READABLE copy of a protected store: the\n\t * same ZIP with `selfstore.json` in the clear, which any tool can open and\n\t * git can diff. It is the answer to \"can I actually get my data out\", and\n\t * it costs nothing: the store, its password and its destination are\n\t * untouched, so the real backup stays encrypted. Before this, the only way\n\t * out was `unprotect()`, which rewrites the destination and leaves it\n\t * readable until you remember to undo it.\n\t *\n\t * Refused while locked (there is nothing readable to write), and refused on\n\t * a store created with `requireEncryption` - that flag means this store\n\t * never produces cleartext, and an escape hatch would make it a lie. */\n\texportBackup(opts?: { plaintext?: boolean }): Promise<Blob>;\n\t/** Download the backup (also resolves the 'manual' file mode's pending flag).\n\t * False when the user closed the save dialog: nothing was written, the\n\t * pending flag stands, and the app must not record a backup. */\n\tdownloadBackup(filename?: string): Promise<boolean>;\n\t/** Load a backup file into this store, replacing the local data (removals\n\t * propagate like edits). Throws PASSWORD_REQUIRED / DECRYPT_FAILED. */\n\timportBackup(file: Blob | Uint8Array, opts?: { password?: string }): Promise<void>;\n\n\t// --- Observability ------------------------------------------------------\n\t/** Headless status: { state, severity, action, labelKey } - map labelKey to\n\t * your own copy. */\n\treadonly status: StatusDescriptor;\n\t/** The last problem: { code, labelKey, message } - show labelKey, log message. */\n\treadonly error: StoreError | null;\n\t/** The full underlying state (advanced reads: journal, peers, mode...). */\n\treadonly state: LocalStoreState;\n\t/** Fires on any state change (status flips included) - for framework bindings. */\n\tsubscribe(fn: () => void): () => void;\n\n\t// --- Lifecycle ----------------------------------------------------------\n\t/** Save now (called for you on tab hide when autoSync is on). */\n\tflush(): Promise<void>;\n\t/** Converge with the destination now (a user gesture like pull-to-refresh). */\n\tsync(): Promise<void>;\n\t/** Drop timers and listeners (tests, SPA teardown). */\n\tdispose(): void;\n\n\t/** The escape hatch: the full advanced store this simple one is built on -\n\t * peers, groups, custom strategies, everything (see selfstore/advanced). */\n\treadonly advanced: LocalStore;\n\n\t/** The attachment point for 'selfstore/flows': the engine plus the cache KV\n\t * and backup file name a flow needs to build destination targets. Apps on\n\t * the advanced store hand a flow the same three by themselves. */\n\treadonly flowHost: { engine: LocalStore; kv: KV; backupName: string };\n\n\t/** Who holds the destination, when it is known - the Google address behind a\n\t * Drive backup. Learned on connect and remembered across sessions, so a\n\t * surface can NAME the destination (\"Google Drive\" is not an address) and a\n\t * re-grant can skip the account chooser. Null for a file, or before a first\n\t * connect. Reading it costs nothing: no call goes out. */\n\treadonly account: string | null;\n\n\t/** The destination this device already used, ready to hand to a connect flow\n\t * as `options.resume` - so a returning visitor is offered \"reopen my backup\"\n\t * ahead of the plain destinations, instead of re-deriving a decision the\n\t * library already has on file. Null when there is nothing to resume. */\n\tresumeOffer(): ResumeOffer | null;\n\n\t/**\n\t * The several-backups manager for this store's destination, or null when\n\t * there is nothing to manage several of (no Drive configured).\n\t *\n\t * The port under it - list, open by id, create, rename, delete - was the last\n\t * thing an app still assembled from the session the store had built, and the\n\t * one place it could pick the wrong file id. Built once and remembered; the\n\t * backups module is imported on demand, so an app that never asks does not\n\t * carry it.\n\t */\n\tbackups(): Promise<BackupsManager | null>;\n\n\t/**\n\t * The destinations this store can offer, ready for a connect flow or widget:\n\t * Drive first when the app configured it, a file always.\n\t *\n\t * Consumers were assembling this by hand from the very session the store\n\t * built - and the order and the \"no client id means no Drive card\" rule were\n\t * rediscovered each time. An app wanting a different offer (a WebDAV server,\n\t * its own target) still writes its own object; this is the default, not a\n\t * ceiling.\n\t */\n\tdestinations(): ConnectTargets;\n}\n\n/** The id field a collection's records are keyed by: `sync.ids` may remap it;\n * a DOTTED path (nested id) is an advanced setup the simple check skips. */\nfunction idFieldOf(sync: SyncConfig | undefined, collection: string): string | null {\n\tconst mapped = sync?.ids?.[collection];\n\tif (mapped == null) return 'id';\n\treturn mapped.includes('.') ? null : mapped;\n}\n\nfunction requireStringId(record: SimpleRecord, field: string | null, collection: string): string {\n\tif (field === null) return ''; // nested-path ids: validated by the engine's own rules\n\tconst id = record[field];\n\tif (typeof id !== 'string' || id.length === 0) {\n\t\tthrow new TypeError(\n\t\t\t`selfstore: a record in \"${collection}\" needs a non-empty STRING \"${field}\" ` +\n\t\t\t\t`(got ${JSON.stringify(id)}). String ids are what the multi-device merge keys on; ` +\n\t\t\t\t`map another field with { sync: { ids: { ${collection}: 'yourField' } } }.`\n\t\t);\n\t}\n\treturn id;\n}\n\n/** True when a browser IndexedDB is available (SSR and plain Node lack it). */\nfunction hasIndexedDb(): boolean {\n\treturn typeof indexedDB !== 'undefined';\n}\n\n/** Told apart by shape: a DriveAuth mints tokens, a `{ clientId }` asks us to. */\nfunction isDriveAuth(v: SimpleOptions['drive']): v is DriveAuth {\n\treturn !!v && typeof (v as DriveAuth).token === 'function';\n}\n\n/** Where the connected Drive address is kept, per app: it is what stops Google\n * asking WHICH account at every re-grant. An address is not a secret, and a\n * browser that refuses storage simply gets the chooser back. */\nconst driveAccountKey = (app: string): string => `selfstore.drive.account.${app}`;\n\nfunction localStore<T>(job: (store: Storage) => T): T | undefined {\n\ttry {\n\t\tconst store = globalThis.localStorage;\n\t\treturn store ? job(store) : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction rememberedDriveAccount(app: string): string | undefined {\n\treturn localStore((s) => s.getItem(driveAccountKey(app))) ?? undefined;\n}\n\n/** Learn the account behind a live Drive session, best-effort and once: it feeds\n * the next re-grant's hint, and any surface that wants to name the destination. */\nasync function learnDriveAccount(app: string, auth: DriveAuth): Promise<void> {\n\ttry {\n\t\tconst who = await driveAccount({ auth });\n\t\tif (who.email) localStore((s) => s.setItem(driveAccountKey(app), who.email!));\n\t} catch {\n\t\t// A destination that will not say costs the hint, never the connection.\n\t}\n}\n\n/**\n * The same connection, which learns whose it is the first time it hands out a\n * token.\n *\n * Learning it took an explicit connect, so anyone whose backup was attached\n * before this existed - or attached in another session - kept a destination\n * with no name on it: the panel said \"Google Drive\" where it could have said\n * which one, and the offer to reopen that backup had no address to show, which\n * is most of what makes it recognisable.\n *\n * It cannot simply be asked for at start-up: reading the account needs a token,\n * and asking for a token with no user gesture behind it opens a popup the\n * browser blocks. So it rides along instead - every token this app obtains,\n * whatever asked for it, teaches it the address at no cost. Once known, nothing\n * is called again.\n */\nfunction selfNaming(app: string, auth: DriveAuth): DriveAuth {\n\tlet learning: Promise<void> | null = null;\n\tconst learn = (): void => {\n\t\tif (learning || rememberedDriveAccount(app)) return;\n\t\tlearning = learnDriveAccount(app, auth);\n\t};\n\treturn {\n\t\tasync token(opts) {\n\t\t\tconst value = await auth.token(opts);\n\t\t\tlearn();\n\t\t\treturn value;\n\t\t},\n\t\tasync reconnect() {\n\t\t\tconst ok = await auth.reconnect();\n\t\t\tif (ok) learn();\n\t\t\treturn ok;\n\t\t},\n\t\tforget: () => auth.forget()\n\t};\n}\n\n/**\n * Open (or create) the app's local store. Awaits the initial load, so the\n * returned store is ready: data readable, destination restored, first converge\n * done. See SimpleOptions for the defaults.\n */\nexport async function selfstore<\n\tS extends Record<string, SimpleRecord> = Record<string, SimpleRecord>\n>(app: string, options: SimpleOptions = {}): Promise<SimpleStore<S>> {\n\tif (typeof app !== 'string' || app.length === 0) {\n\t\tthrow new TypeError('selfstore: pass your app name, e.g. selfstore(\"my-app\").');\n\t}\n\n\t// The one copy of the data. The engine pulls it on save (gather) and pushes\n\t// into it on restores/folds (apply); the facade's mutators edit it in place\n\t// copy-on-write and schedule a save.\n\tlet collections: Record<string, SimpleRecord[]> = {};\n\t// Binary files ride along untouched (a backup that carries them keeps them);\n\t// managing them is an advanced-store concern.\n\tlet files: SnapshotFile[] = [];\n\n\tconst dataSubs = new Set<() => void>();\n\tconst emitData = (): void => dataSubs.forEach((fn) => fn());\n\n\tconst cache =\n\t\toptions.cache ??\n\t\t(hasIndexedDb()\n\t\t\t? indexedDbCache(app, options.cacheLock ? { lock: true } : undefined)\n\t\t\t: memoryCache());\n\tconst backupName = `${app}.zip`;\n\t// connectDrive() remembers the auth so a later restoreTarget can use it even\n\t// when options.drive was not provided up front (same session only).\n\t//\n\t// `{ clientId }` is the whole wiring done here rather than in every app: ONE\n\t// session (several would mean several consent screens), a token that survives\n\t// a reload, and the account remembered so Google stops asking which one. Each\n\t// of those was a separate decision every consumer had to discover; the app\n\t// that wrote them by hand hit all three as bugs first.\n\t/** Built at most once: two managers over one destination would each hold\n\t * their own idea of which file is active. */\n\tlet backupsManager: BackupsManager | null = null;\n\t// Wrapped whichever shape it came in as: an app that mints its own tokens\n\t// (a server broker, a native shell) has the same nameless-destination\n\t// problem, and the wrapper only ever rides along on a token already granted.\n\tlet driveAuth: DriveAuth | null = isDriveAuth(options.drive)\n\t\t? selfNaming(app, options.drive)\n\t\t: options.drive\n\t\t\t? selfNaming(\n\t\t\t\t\tapp,\n\t\t\t\t\tgisDriveAuth({\n\t\t\t\t\t\tclientId: options.drive.clientId,\n\t\t\t\t\t\tscope: options.drive.scope,\n\t\t\t\t\t\tpersist: 'session',\n\t\t\t\t\t\thint: () => rememberedDriveAccount(app)\n\t\t\t\t\t})\n\t\t\t\t)\n\t\t\t: null;\n\n\t// Ultra-sensitive: unlock the cache before the store hydrates from it. One\n\t// prompt per session, driven by the app (branch it on an existing login to\n\t// keep the UX unchanged). A wrong secret re-prompts; throw to abort.\n\tif (options.cacheLock && isLockableCache(cache)) {\n\t\tlet failed = false;\n\t\twhile (!(await cache.unlock(await options.cacheLock({ failed })))) failed = true;\n\t}\n\n\tconst store = createLocalStore({\n\t\tapp,\n\t\tschemaVersion: options.schema ?? 1,\n\t\tgather: (): Snapshot => structuredClone({ collections, files }),\n\t\tapply: (snap: Snapshot): void => {\n\t\t\tcollections = (snap.collections ?? {}) as Record<string, SimpleRecord[]>;\n\t\t\tfiles = snap.files ?? [];\n\t\t\temitData();\n\t\t},\n\t\tmigrate: options.migrate,\n\t\tsync: options.sync,\n\t\tcache,\n\t\tdebounceMs: options.debounceMs,\n\t\tmultiTab: options.multiTab,\n\t\trequireEncryption: options.requireEncryption,\n\t\tpasswordPolicy: options.passwordPolicy,\n\t\trestoreTarget: async (kind) => {\n\t\t\tif (kind === 'drive' && driveAuth) {\n\t\t\t\treturn driveFromSession({ auth: driveAuth, kv: cache.kv, fileName: backupName });\n\t\t\t}\n\t\t\tif (kind === 'file') return fileFromSession({ kv: cache.kv });\n\t\t\tif (kind === 'webdav') return webdavFromSession({ kv: cache.kv });\n\t\t\treturn null;\n\t\t}\n\t});\n\n\tawait store.init();\n\n\t// A degraded file mode is a verdict about the BROWSER, but it was persisted\n\t// like a property of the data: every later session re-read the word without\n\t// ever asking again. One session that could not open a picker - a browser\n\t// that ships the API and refuses it, a call made a moment too late, a user on\n\t// another browser that day - then pinned download-only mode forever, on a\n\t// Chromium perfectly able to hold a file, with no way back.\n\t//\n\t// Nothing writes this word except a picker that could not deliver, so a\n\t// browser that CAN deliver makes it stale. Re-examined here, in the layer\n\t// that wrote it. Dropping to 'device' is the honest state: manual mode had\n\t// already released the durable target, so there is nothing to detach - only\n\t// a stale claim to stop repeating, and a destination the host can offer again.\n\tif (store.state.targetKind === 'file-manual' && fileIsSupported()) {\n\t\tawait store.detachTarget();\n\t}\n\n\t// Auto-wired sync moments (Spring-Boot-style autoconfiguration): tab focus,\n\t// network return, a slow interval, and save-on-hide. Opt out with\n\t// { autoSync: false } to drive these yourself.\n\tconst teardown: (() => void)[] = [];\n\tif (\n\t\toptions.autoSync !== false &&\n\t\ttypeof document !== 'undefined' &&\n\t\ttypeof window !== 'undefined'\n\t) {\n\t\tconst onVisibility = (): void => {\n\t\t\tif (document.visibilityState === 'hidden') void store.flush();\n\t\t\telse void store.syncIfStale('focus');\n\t\t};\n\t\tconst onOnline = (): void => void store.syncIfStale('online');\n\t\tconst onPageHide = (): void => void store.flush();\n\t\tconst interval = setInterval(() => {\n\t\t\tif (document.visibilityState === 'visible') void store.syncIfStale('interval');\n\t\t}, 5 * 60_000);\n\t\tdocument.addEventListener('visibilitychange', onVisibility);\n\t\twindow.addEventListener('online', onOnline);\n\t\twindow.addEventListener('pagehide', onPageHide);\n\t\tteardown.push(() => {\n\t\t\tclearInterval(interval);\n\t\t\tdocument.removeEventListener('visibilitychange', onVisibility);\n\t\t\twindow.removeEventListener('online', onOnline);\n\t\t\twindow.removeEventListener('pagehide', onPageHide);\n\t\t});\n\t}\n\n\tfunction mutate(collection: string, next: SimpleRecord[]): Promise<void> {\n\t\tcollections = { ...collections, [collection]: next };\n\t\temitData();\n\t\tstore.schedule();\n\t\treturn Promise.resolve();\n\t}\n\n\tasync function connectTarget(\n\t\ttarget: BackupTarget,\n\t\topts: { password?: string } = {}\n\t): Promise<ConnectOutcome> {\n\t\tconst info = await store.inspectTarget(target);\n\t\tif (info.hasBackup) {\n\t\t\t// Another app's backup is refused BEFORE the password question: asking\n\t\t\t// first teaches the user their password \"does not work\" on a file that\n\t\t\t// was never this app's to open.\n\t\t\tif (info.app !== null && info.app !== store.app) {\n\t\t\t\tthrow new SelfstoreError(\n\t\t\t\t\t'FOREIGN_BACKUP',\n\t\t\t\t\t`This destination holds a backup written by app \"${info.app}\", not \"${store.app}\". Open it in its own application, or pick another file.`\n\t\t\t\t);\n\t\t\t}\n\t\t\t// Fail before attaching: an encrypted backup without its password would\n\t\t\t// otherwise attach locked, which is a support case, not an outcome.\n\t\t\tif (info.encrypted && !opts.password) {\n\t\t\t\tthrow new SelfstoreError(\n\t\t\t\t\t'PASSWORD_REQUIRED',\n\t\t\t\t\t'This destination holds an encrypted backup: pass its password to connect.'\n\t\t\t\t);\n\t\t\t}\n\t\t\tawait store.attachTarget(target, { password: opts.password ?? null, strategy: 'merge' });\n\t\t\treturn 'merged';\n\t\t}\n\t\tawait store.attachTarget(target, {\n\t\t\tpassword: opts.password ?? null,\n\t\t\tstrategy: 'replace-remote'\n\t\t});\n\t\treturn 'started';\n\t}\n\n\tconst simple: SimpleStore<S> = {\n\t\tall<K extends keyof S & string>(collection: K): readonly S[K][] {\n\t\t\treturn (collections[collection] ?? []) as S[K][];\n\t\t},\n\t\tget<K extends keyof S & string>(collection: K, id: string): S[K] | undefined {\n\t\t\tconst field = idFieldOf(options.sync, collection) ?? 'id';\n\t\t\treturn (collections[collection] ?? []).find((r) => r[field] === id) as S[K] | undefined;\n\t\t},\n\t\tput<K extends keyof S & string>(collection: K, record: S[K]): Promise<void> {\n\t\t\tconst field = idFieldOf(options.sync, collection);\n\t\t\tconst id = requireStringId(record, field, collection);\n\t\t\tconst rows = collections[collection] ?? [];\n\t\t\tconst at = field === null ? -1 : rows.findIndex((r) => r[field] === id);\n\t\t\treturn mutate(\n\t\t\t\tcollection,\n\t\t\t\tat === -1 ? [...rows, record] : rows.map((r, i) => (i === at ? record : r))\n\t\t\t);\n\t\t},\n\t\tputAll<K extends keyof S & string>(collection: K, records: S[K][]): Promise<void> {\n\t\t\tconst field = idFieldOf(options.sync, collection);\n\t\t\tconst byId = new Map<unknown, number>();\n\t\t\tfor (const r of records) requireStringId(r, field, collection);\n\t\t\tconst rows = [...(collections[collection] ?? [])];\n\t\t\tif (field !== null) {\n\t\t\t\trows.forEach((r, i) => byId.set(r[field], i));\n\t\t\t\tfor (const r of records) {\n\t\t\t\t\tconst at = byId.get(r[field]);\n\t\t\t\t\tif (typeof at === 'number') rows[at] = r;\n\t\t\t\t\telse rows.push(r);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\trows.push(...records);\n\t\t\t}\n\t\t\treturn mutate(collection, rows);\n\t\t},\n\t\tremove<K extends keyof S & string>(collection: K, id: string): Promise<void> {\n\t\t\tconst field = idFieldOf(options.sync, collection) ?? 'id';\n\t\t\tconst rows = collections[collection] ?? [];\n\t\t\tconst next = rows.filter((r) => r[field] !== id);\n\t\t\treturn next.length === rows.length ? Promise.resolve() : mutate(collection, next);\n\t\t},\n\t\tclear<K extends keyof S & string>(collection: K): Promise<void> {\n\t\t\tif ((collections[collection] ?? []).length === 0) return Promise.resolve();\n\t\t\treturn mutate(collection, []);\n\t\t},\n\t\tonChange(fn: () => void): () => void {\n\t\t\tdataSubs.add(fn);\n\t\t\treturn () => dataSubs.delete(fn);\n\t\t},\n\n\t\tasync connectDrive(auth: DriveAuth, opts?: { password?: string }): Promise<ConnectOutcome> {\n\t\t\tdriveAuth = auth;\n\t\t\tconst target = await driveConnect({ auth, kv: cache.kv, fileName: backupName });\n\t\t\tif (!target) return 'cancelled';\n\t\t\tconst outcome = await connectTarget(target, opts);\n\t\t\t// Learn WHICH account now that there is a session: it is the hint that\n\t\t\t// keeps the chooser away next time, and the name a panel can show. Not\n\t\t\t// awaited into the outcome - a slow metadata call must not delay the\n\t\t\t// connect the user is waiting on.\n\t\t\tif (outcome !== 'cancelled') void learnDriveAccount(app, auth);\n\t\t\treturn outcome;\n\t\t},\n\t\tasync connectFile(opts?: { password?: string }): Promise<ConnectOutcome> {\n\t\t\t// Asked twice on purpose: the first call answers from what the browser\n\t\t\t// exposes, the second from what it did when actually asked. A browser\n\t\t\t// that ships the picker and then refuses it only tells us so here.\n\t\t\tif (fileIsSupported()) {\n\t\t\t\tconst target = await fileConnect({ kv: cache.kv, fileName: backupName });\n\t\t\t\tif (target) return connectTarget(target, opts);\n\t\t\t\tif (fileIsSupported()) return 'cancelled';\n\t\t\t}\n\t\t\tawait store.setManualFile();\n\t\t\treturn 'manual';\n\t\t},\n\t\tasync connectWebdav(\n\t\t\tconfig: WebdavConfig,\n\t\t\topts?: { password?: string }\n\t\t): Promise<ConnectOutcome> {\n\t\t\tconst target = await webdavConnect({ kv: cache.kv, config });\n\t\t\tif (!target) {\n\t\t\t\tthrow new SelfstoreError(\n\t\t\t\t\t'TARGET_UNAVAILABLE',\n\t\t\t\t\t'The WebDAV server did not answer (URL, credentials or CORS).'\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn connectTarget(target, opts);\n\t\t},\n\t\tasync connectS3(config: S3Config, opts?: { password?: string }): Promise<ConnectOutcome> {\n\t\t\tconst target = await s3Connect({ kv: cache.kv, config });\n\t\t\tif (!target) {\n\t\t\t\tthrow new SelfstoreError(\n\t\t\t\t\t'TARGET_UNAVAILABLE',\n\t\t\t\t\t'The S3 endpoint did not answer (endpoint, credentials or CORS).'\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn connectTarget(target, opts);\n\t\t},\n\t\tconnectTarget,\n\t\tdisconnect: () => store.detachTarget(),\n\t\taddReplica: (target, opts) => store.attachReplica(target, opts),\n\t\tremoveReplica: (id) => store.detachReplica(id),\n\n\t\tprotect: (password: string) => store.setEncryption(password),\n\t\tunprotect: () => store.setEncryption(null),\n\t\tunlock: (password: string) => store.unlock(password),\n\t\treconnect: () => store.reconnect(),\n\n\t\texportBackup: (opts?: { plaintext?: boolean }) => store.exportBlob(opts),\n\t\tasync downloadBackup(filename?: string): Promise<boolean> {\n\t\t\tconst written = await saveToDisk(await store.exportBlob(), filename ?? backupName);\n\t\t\t// Clearing the pending flag on a cancelled dialog would drop the very\n\t\t\t// nudge that asks for the file the user still does not have.\n\t\t\tif (written) store.markDownloaded();\n\t\t\treturn written;\n\t\t},\n\t\tasync importBackup(file: Blob | Uint8Array, opts?: { password?: string }): Promise<void> {\n\t\t\t// The fluent read() strips reserved bookkeeping collections; removals\n\t\t\t// then propagate because the next save diffs against the sync meta.\n\t\t\tconst reader = restore(file);\n\t\t\tconst snap = opts?.password\n\t\t\t\t? await reader.withPassword(opts.password).read()\n\t\t\t\t: await reader.read();\n\t\t\tcollections = (snap.collections ?? {}) as Record<string, SimpleRecord[]>;\n\t\t\tfiles = snap.files ?? [];\n\t\t\temitData();\n\t\t\tstore.schedule();\n\t\t},\n\n\t\tget status(): StatusDescriptor {\n\t\t\treturn store.state.status;\n\t\t},\n\t\tget error(): StoreError | null {\n\t\t\treturn store.state.lastError;\n\t\t},\n\t\tget state(): LocalStoreState {\n\t\t\treturn store.state;\n\t\t},\n\t\tsubscribe: (fn: () => void) => store.subscribe(fn),\n\n\t\tflush: () => store.flush(),\n\t\tasync sync(): Promise<void> {\n\t\t\tawait store.syncNow();\n\t\t},\n\t\tdispose(): void {\n\t\t\tteardown.forEach((fn) => fn());\n\t\t\tteardown.length = 0;\n\t\t\tstore.dispose();\n\t\t},\n\n\t\tadvanced: store,\n\n\t\tflowHost: { engine: store, kv: cache.kv, backupName },\n\n\t\tget account(): string | null {\n\t\t\treturn rememberedDriveAccount(app) ?? null;\n\t\t},\n\n\t\tasync backups(): Promise<BackupsManager | null> {\n\t\t\tif (!driveAuth) return null;\n\t\t\tbackupsManager ??= await (async () => {\n\t\t\t\tconst { createBackupsManager, driveBackupsHost } = await import('../backups/manager-drive');\n\t\t\t\treturn createBackupsManager({\n\t\t\t\t\tstore,\n\t\t\t\t\tkv: cache.kv,\n\t\t\t\t\thost: driveBackupsHost({ auth: driveAuth!, kv: cache.kv, fileName: backupName }),\n\t\t\t\t\tnaming: { canonicalName: backupName }\n\t\t\t\t});\n\t\t\t})();\n\t\t\treturn backupsManager;\n\t\t},\n\n\t\tdestinations(): ConnectTargets {\n\t\t\t// Drive first when there is one: it is the answer that follows a user\n\t\t\t// from device to device. A file always, for whoever wants no account.\n\t\t\treturn driveAuth ? { drive: driveAuth, file: true } : { file: true };\n\t\t},\n\n\t\tresumeOffer(): ResumeOffer | null {\n\t\t\tconst detail = rememberedDriveAccount(app);\n\t\t\tif (!driveAuth || !detail) return null;\n\t\t\tconst auth = driveAuth;\n\t\t\treturn {\n\t\t\t\tkind: 'drive',\n\t\t\t\tdetail,\n\t\t\t\t// Reopening is not searching: fromSession takes the file id already\n\t\t\t\t// remembered, where a fresh connect resolves by name and could land on\n\t\t\t\t// another file carrying the same one.\n\t\t\t\tconnect: () => driveFromSession({ auth, kv: cache.kv, fileName: backupName })\n\t\t\t};\n\t\t}\n\t};\n\n\treturn simple;\n}\n"]}
@@ -1,5 +1,5 @@
1
- import { o as Snapshot, E as EncodeOptions, H as Header } from './store-Cs6QIzzG.js';
2
- import { a as DriveAuth } from './drive-CY_Vsj7V.js';
1
+ import { o as Snapshot, E as EncodeOptions, H as Header } from './store-DhrWMYg_.js';
2
+ import { a as DriveAuth } from './drive-BLRsK32w.js';
3
3
 
4
4
  /** Serialize a snapshot to a portable backup blob (compressed; encrypted if a password is given). */
5
5
  declare function exportSnapshot(snapshot: Snapshot, opts: EncodeOptions): Promise<Blob>;
@@ -1,5 +1,5 @@
1
1
  import { K as KV } from './cache-ilsK3Tki.js';
2
- import { B as BackupTarget, L as LocalStore } from './store-Cs6QIzzG.js';
2
+ import { B as BackupTarget, L as LocalStore } from './store-DhrWMYg_.js';
3
3
  import { F as FlowStore } from './machine-ALMkT3mk.js';
4
4
 
5
5
  /** What the destination reports about a listed file (one cheap metadata call). */
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Open a store with the device instead of the keyboard: Face, fingerprint or
3
+ * Windows Hello in place of typing the password.
4
+ *
5
+ * WHAT IT IS. A platform passkey carrying the WebAuthn PRF extension (FIDO2
6
+ * hmac-secret) yields a stable 32-byte secret - deterministic for that
7
+ * credential and a fixed per-app salt, and it never leaves the authenticator.
8
+ * That secret becomes an AES-256-GCM key, under which this module seals the
9
+ * app's own secret (typically the backup password). Unlocking is then: user
10
+ * verification -> PRF secret -> the password in the clear, in memory.
11
+ *
12
+ * WHAT IT IS NOT. It does not replace the password, it carries it. The
13
+ * password keeps opening everything, on every device, forever; this is a
14
+ * convenience bound to ONE browser profile on ONE machine. Losing the passkey
15
+ * costs a typed password, never data. That asymmetry is deliberate: a second
16
+ * independent key to the data would widen the blast radius of a stolen device,
17
+ * which is the opposite of what a locked cache is for.
18
+ *
19
+ * THE PROPERTY IT PRESERVES. The sealed blob is useless on its own: a copy of
20
+ * the browser profile does not open it, because the PRF secret lives in the
21
+ * authenticator and demands user verification. That is exactly the guarantee a
22
+ * lock-mode cache exists for, so enrolling does not weaken it.
23
+ *
24
+ * OPT-IN BY CONSTRUCTION. Nothing here runs unless an app calls
25
+ * `passkeyUnlock()`. An app that never does simply has no such feature, and an
26
+ * app that does still decides when to offer it - the recommended shape is an
27
+ * explicit control the user turns on, and can turn off again with `forget()`.
28
+ *
29
+ * Requires the PRF extension: recent Chrome/Edge, Safari 18+, recent Android;
30
+ * inside a native WebView it depends on the system WebView version. Nothing
31
+ * trusts that blindly - `available()` and `enroll()` feature-detect at runtime
32
+ * and FAIL CLOSED, so the option is only ever usable where a real secret comes
33
+ * back. Dependency-free, and guarded for SSR and no-WebAuthn environments.
34
+ */
35
+ /** How the passkey is presented by the platform, and what it protects. */
36
+ interface PasskeyUnlockOptions {
37
+ /**
38
+ * Shown by the operating system's passkey prompt and stored with the
39
+ * credential. The application name, as a person would read it.
40
+ */
41
+ appName: string;
42
+ /**
43
+ * Domain separator for the PRF evaluation - NOT a secret, and it may be a
44
+ * constant: the credential's own secret is what makes the derived key
45
+ * unique. Use a stable per-app string. Changing it rotates every
46
+ * passkey-derived key at once, which orphans every existing enrolment.
47
+ */
48
+ salt: string;
49
+ /** Where the sealed secret is kept on this device (default `selfstore.passkey`). */
50
+ storageKey?: string;
51
+ }
52
+ /** The device-unlock facade. Every method is safe to call anywhere: outside a
53
+ * browser, or without WebAuthn, they report unavailable rather than throw. */
54
+ interface PasskeyUnlock {
55
+ /**
56
+ * Whether a user-verifying platform authenticator exists at all. Necessary
57
+ * but NOT sufficient: only an actual `enroll()` proves the PRF extension
58
+ * works here. Use it to decide whether to show the control, not to promise
59
+ * that it will succeed.
60
+ */
61
+ available(): Promise<boolean>;
62
+ /** Whether this device currently holds a sealed secret to reveal. */
63
+ enrolled(): boolean;
64
+ /**
65
+ * Create a platform passkey and seal `secret` behind it. `false` when the
66
+ * user cancelled, or when PRF did not yield a key here - fail closed, the
67
+ * host keeps its password prompt and should say so rather than pretend.
68
+ */
69
+ enroll(secret: string): Promise<boolean>;
70
+ /**
71
+ * Ask for Face / fingerprint / Hello and return the sealed secret. `null`
72
+ * on cancel, on a missing enrolment, or on any failure; the host then falls
73
+ * back to asking for the password. Never throws.
74
+ */
75
+ reveal(): Promise<string | null>;
76
+ /**
77
+ * Drop the sealed secret from this device. The platform credential itself
78
+ * is NOT deleted - the operating system owns it, and only the user can
79
+ * remove it from their passkey manager. Say that in the interface rather
80
+ * than implying a full erasure.
81
+ */
82
+ forget(): void;
83
+ }
84
+ declare function passkeyUnlock(options: PasskeyUnlockOptions): PasskeyUnlock;
85
+
86
+ export { type PasskeyUnlock, type PasskeyUnlockOptions, passkeyUnlock };
@@ -0,0 +1,152 @@
1
+ // src/passkey/passkey.ts
2
+ var DEFAULT_STORAGE_KEY = "selfstore.passkey";
3
+ var PRF_BYTES = 32;
4
+ function toB64(bytes) {
5
+ const b = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
6
+ let s = "";
7
+ for (const byte of b) s += String.fromCharCode(byte);
8
+ return btoa(s);
9
+ }
10
+ function fromB64(s) {
11
+ const bin = atob(s);
12
+ const out = new Uint8Array(bin.length);
13
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
14
+ return out;
15
+ }
16
+ function webauthnUsable() {
17
+ return typeof window !== "undefined" && !!window.PublicKeyCredential && typeof navigator !== "undefined" && !!navigator.credentials && typeof crypto !== "undefined" && !!crypto.subtle;
18
+ }
19
+ function prfSecret(credential) {
20
+ const first = credential?.getClientExtensionResults()?.prf?.results?.first;
21
+ if (!first) return null;
22
+ const bytes = new Uint8Array(first);
23
+ return bytes.length === PRF_BYTES ? bytes : null;
24
+ }
25
+ function sealKey(secret) {
26
+ return crypto.subtle.importKey("raw", secret, "AES-GCM", false, [
27
+ "encrypt",
28
+ "decrypt"
29
+ ]);
30
+ }
31
+ function passkeyUnlock(options) {
32
+ const storageKey = options.storageKey ?? DEFAULT_STORAGE_KEY;
33
+ const prfSalt = new TextEncoder().encode(options.salt);
34
+ function read() {
35
+ try {
36
+ if (typeof localStorage === "undefined") return null;
37
+ const raw = localStorage.getItem(storageKey);
38
+ if (!raw) return null;
39
+ const parsed = JSON.parse(raw);
40
+ const complete = typeof parsed?.credentialId === "string" && typeof parsed?.iv === "string" && typeof parsed?.sealed === "string";
41
+ return complete ? parsed : null;
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+ function write(entry) {
47
+ try {
48
+ if (typeof localStorage === "undefined") return;
49
+ if (entry) localStorage.setItem(storageKey, JSON.stringify(entry));
50
+ else localStorage.removeItem(storageKey);
51
+ } catch {
52
+ }
53
+ }
54
+ async function derive(credentialId) {
55
+ try {
56
+ const assertion = await navigator.credentials.get({
57
+ publicKey: {
58
+ challenge: crypto.getRandomValues(new Uint8Array(32)),
59
+ allowCredentials: credentialId ? [{ type: "public-key", id: fromB64(credentialId) }] : [],
60
+ userVerification: "required",
61
+ extensions: { prf: { eval: { first: prfSalt } } },
62
+ timeout: 6e4
63
+ }
64
+ });
65
+ return prfSecret(assertion);
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+ async function create() {
71
+ const credential = await navigator.credentials.create({
72
+ publicKey: {
73
+ challenge: crypto.getRandomValues(new Uint8Array(32)),
74
+ rp: { name: options.appName, id: location.hostname },
75
+ user: {
76
+ id: crypto.getRandomValues(new Uint8Array(16)),
77
+ name: options.appName,
78
+ displayName: options.appName
79
+ },
80
+ pubKeyCredParams: [
81
+ { type: "public-key", alg: -7 },
82
+ { type: "public-key", alg: -257 }
83
+ ],
84
+ authenticatorSelection: {
85
+ authenticatorAttachment: "platform",
86
+ userVerification: "required",
87
+ residentKey: "required"
88
+ },
89
+ extensions: { prf: { eval: { first: prfSalt } } },
90
+ timeout: 6e4
91
+ }
92
+ });
93
+ if (!credential) return null;
94
+ const credentialId = toB64(credential.rawId);
95
+ const secret = prfSecret(credential) ?? await derive(credentialId);
96
+ return secret ? { credentialId, secret } : null;
97
+ }
98
+ return {
99
+ async available() {
100
+ try {
101
+ if (!webauthnUsable()) return false;
102
+ return await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
103
+ } catch {
104
+ return false;
105
+ }
106
+ },
107
+ enrolled() {
108
+ return read() !== null;
109
+ },
110
+ async enroll(secret) {
111
+ if (!webauthnUsable()) return false;
112
+ try {
113
+ const created = await create();
114
+ if (!created) return false;
115
+ const iv = crypto.getRandomValues(new Uint8Array(12));
116
+ const sealed = await crypto.subtle.encrypt(
117
+ { name: "AES-GCM", iv },
118
+ await sealKey(created.secret),
119
+ new TextEncoder().encode(secret)
120
+ );
121
+ write({ credentialId: created.credentialId, iv: toB64(iv), sealed: toB64(sealed) });
122
+ return true;
123
+ } catch {
124
+ return false;
125
+ }
126
+ },
127
+ async reveal() {
128
+ const entry = read();
129
+ if (!entry || !webauthnUsable()) return null;
130
+ const secret = await derive(entry.credentialId);
131
+ if (!secret) return null;
132
+ try {
133
+ const opened = await crypto.subtle.decrypt(
134
+ { name: "AES-GCM", iv: fromB64(entry.iv) },
135
+ await sealKey(secret),
136
+ fromB64(entry.sealed)
137
+ );
138
+ return new TextDecoder().decode(opened);
139
+ } catch {
140
+ write(null);
141
+ return null;
142
+ }
143
+ },
144
+ forget() {
145
+ write(null);
146
+ }
147
+ };
148
+ }
149
+
150
+ export { passkeyUnlock };
151
+ //# sourceMappingURL=passkey.js.map
152
+ //# sourceMappingURL=passkey.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/passkey/passkey.ts"],"names":[],"mappings":"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","file":"passkey.js","sourcesContent":["/**\n * Open a store with the device instead of the keyboard: Face, fingerprint or\n * Windows Hello in place of typing the password.\n *\n * WHAT IT IS. A platform passkey carrying the WebAuthn PRF extension (FIDO2\n * hmac-secret) yields a stable 32-byte secret - deterministic for that\n * credential and a fixed per-app salt, and it never leaves the authenticator.\n * That secret becomes an AES-256-GCM key, under which this module seals the\n * app's own secret (typically the backup password). Unlocking is then: user\n * verification -> PRF secret -> the password in the clear, in memory.\n *\n * WHAT IT IS NOT. It does not replace the password, it carries it. The\n * password keeps opening everything, on every device, forever; this is a\n * convenience bound to ONE browser profile on ONE machine. Losing the passkey\n * costs a typed password, never data. That asymmetry is deliberate: a second\n * independent key to the data would widen the blast radius of a stolen device,\n * which is the opposite of what a locked cache is for.\n *\n * THE PROPERTY IT PRESERVES. The sealed blob is useless on its own: a copy of\n * the browser profile does not open it, because the PRF secret lives in the\n * authenticator and demands user verification. That is exactly the guarantee a\n * lock-mode cache exists for, so enrolling does not weaken it.\n *\n * OPT-IN BY CONSTRUCTION. Nothing here runs unless an app calls\n * `passkeyUnlock()`. An app that never does simply has no such feature, and an\n * app that does still decides when to offer it - the recommended shape is an\n * explicit control the user turns on, and can turn off again with `forget()`.\n *\n * Requires the PRF extension: recent Chrome/Edge, Safari 18+, recent Android;\n * inside a native WebView it depends on the system WebView version. Nothing\n * trusts that blindly - `available()` and `enroll()` feature-detect at runtime\n * and FAIL CLOSED, so the option is only ever usable where a real secret comes\n * back. Dependency-free, and guarded for SSR and no-WebAuthn environments.\n */\n\n/** How the passkey is presented by the platform, and what it protects. */\nexport interface PasskeyUnlockOptions {\n\t/**\n\t * Shown by the operating system's passkey prompt and stored with the\n\t * credential. The application name, as a person would read it.\n\t */\n\tappName: string;\n\t/**\n\t * Domain separator for the PRF evaluation - NOT a secret, and it may be a\n\t * constant: the credential's own secret is what makes the derived key\n\t * unique. Use a stable per-app string. Changing it rotates every\n\t * passkey-derived key at once, which orphans every existing enrolment.\n\t */\n\tsalt: string;\n\t/** Where the sealed secret is kept on this device (default `selfstore.passkey`). */\n\tstorageKey?: string;\n}\n\n/** The device-unlock facade. Every method is safe to call anywhere: outside a\n * browser, or without WebAuthn, they report unavailable rather than throw. */\nexport interface PasskeyUnlock {\n\t/**\n\t * Whether a user-verifying platform authenticator exists at all. Necessary\n\t * but NOT sufficient: only an actual `enroll()` proves the PRF extension\n\t * works here. Use it to decide whether to show the control, not to promise\n\t * that it will succeed.\n\t */\n\tavailable(): Promise<boolean>;\n\t/** Whether this device currently holds a sealed secret to reveal. */\n\tenrolled(): boolean;\n\t/**\n\t * Create a platform passkey and seal `secret` behind it. `false` when the\n\t * user cancelled, or when PRF did not yield a key here - fail closed, the\n\t * host keeps its password prompt and should say so rather than pretend.\n\t */\n\tenroll(secret: string): Promise<boolean>;\n\t/**\n\t * Ask for Face / fingerprint / Hello and return the sealed secret. `null`\n\t * on cancel, on a missing enrolment, or on any failure; the host then falls\n\t * back to asking for the password. Never throws.\n\t */\n\treveal(): Promise<string | null>;\n\t/**\n\t * Drop the sealed secret from this device. The platform credential itself\n\t * is NOT deleted - the operating system owns it, and only the user can\n\t * remove it from their passkey manager. Say that in the interface rather\n\t * than implying a full erasure.\n\t */\n\tforget(): void;\n}\n\n/** PRF extension shapes are absent from some TS DOM libs; describe what we use. */\ntype PrfInputs = { prf?: { eval?: { first: BufferSource } } };\ntype PrfOutputs = { prf?: { results?: { first?: ArrayBuffer } } };\n\n/** What one device remembers: which credential to prefer, and the sealed secret. */\ninterface StoredEnrolment {\n\tcredentialId: string;\n\tiv: string;\n\tsealed: string;\n}\n\nconst DEFAULT_STORAGE_KEY = 'selfstore.passkey';\nconst PRF_BYTES = 32;\n\nfunction toB64(bytes: ArrayBuffer | Uint8Array): string {\n\tconst b = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);\n\tlet s = '';\n\tfor (const byte of b) s += String.fromCharCode(byte);\n\treturn btoa(s);\n}\n\nfunction fromB64(s: string): Uint8Array<ArrayBuffer> {\n\tconst bin = atob(s);\n\tconst out = new Uint8Array(bin.length);\n\tfor (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n\treturn out;\n}\n\nfunction webauthnUsable(): boolean {\n\treturn (\n\t\ttypeof window !== 'undefined' &&\n\t\t!!window.PublicKeyCredential &&\n\t\ttypeof navigator !== 'undefined' &&\n\t\t!!navigator.credentials &&\n\t\ttypeof crypto !== 'undefined' &&\n\t\t!!crypto.subtle\n\t);\n}\n\nfunction prfSecret(credential: PublicKeyCredential | null): Uint8Array | null {\n\tconst first = (credential?.getClientExtensionResults() as PrfOutputs | undefined)?.prf?.results\n\t\t?.first;\n\tif (!first) return null;\n\tconst bytes = new Uint8Array(first);\n\treturn bytes.length === PRF_BYTES ? bytes : null;\n}\n\n/**\n * The 32 bytes coming out of PRF are an HMAC output, so they are already\n * uniform: import them straight as an AES-GCM key rather than running a KDF\n * over them. Stretching high-entropy material buys nothing and would only make\n * every unlock slower.\n */\nfunction sealKey(secret: Uint8Array): Promise<CryptoKey> {\n\treturn crypto.subtle.importKey('raw', secret as BufferSource, 'AES-GCM', false, [\n\t\t'encrypt',\n\t\t'decrypt'\n\t]);\n}\n\nexport function passkeyUnlock(options: PasskeyUnlockOptions): PasskeyUnlock {\n\tconst storageKey = options.storageKey ?? DEFAULT_STORAGE_KEY;\n\tconst prfSalt = new TextEncoder().encode(options.salt);\n\n\tfunction read(): StoredEnrolment | null {\n\t\ttry {\n\t\t\tif (typeof localStorage === 'undefined') return null;\n\t\t\tconst raw = localStorage.getItem(storageKey);\n\t\t\tif (!raw) return null;\n\t\t\tconst parsed = JSON.parse(raw) as StoredEnrolment;\n\t\t\tconst complete =\n\t\t\t\ttypeof parsed?.credentialId === 'string' &&\n\t\t\t\ttypeof parsed?.iv === 'string' &&\n\t\t\t\ttypeof parsed?.sealed === 'string';\n\t\t\treturn complete ? parsed : null;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tfunction write(entry: StoredEnrolment | null): void {\n\t\ttry {\n\t\t\tif (typeof localStorage === 'undefined') return;\n\t\t\tif (entry) localStorage.setItem(storageKey, JSON.stringify(entry));\n\t\t\telse localStorage.removeItem(storageKey);\n\t\t} catch {\n\t\t\t/* storage blocked: the enrolment is simply not remembered */\n\t\t}\n\t}\n\n\t/**\n\t * Re-derive the PRF secret. With a credential id it targets that one; with\n\t * none, the platform offers the user's discoverable passkeys for this site.\n\t */\n\tasync function derive(credentialId?: string): Promise<Uint8Array | null> {\n\t\ttry {\n\t\t\tconst assertion = (await navigator.credentials.get({\n\t\t\t\tpublicKey: {\n\t\t\t\t\tchallenge: crypto.getRandomValues(new Uint8Array(32)),\n\t\t\t\t\tallowCredentials: credentialId\n\t\t\t\t\t\t? [{ type: 'public-key', id: fromB64(credentialId) as BufferSource }]\n\t\t\t\t\t\t: [],\n\t\t\t\t\tuserVerification: 'required',\n\t\t\t\t\textensions: { prf: { eval: { first: prfSalt } } } as PrfInputs,\n\t\t\t\t\ttimeout: 60_000\n\t\t\t\t}\n\t\t\t})) as PublicKeyCredential | null;\n\t\t\treturn prfSecret(assertion);\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tasync function create(): Promise<{ credentialId: string; secret: Uint8Array } | null> {\n\t\tconst credential = (await navigator.credentials.create({\n\t\t\tpublicKey: {\n\t\t\t\tchallenge: crypto.getRandomValues(new Uint8Array(32)),\n\t\t\t\trp: { name: options.appName, id: location.hostname },\n\t\t\t\tuser: {\n\t\t\t\t\tid: crypto.getRandomValues(new Uint8Array(16)),\n\t\t\t\t\tname: options.appName,\n\t\t\t\t\tdisplayName: options.appName\n\t\t\t\t},\n\t\t\t\tpubKeyCredParams: [\n\t\t\t\t\t{ type: 'public-key', alg: -7 },\n\t\t\t\t\t{ type: 'public-key', alg: -257 }\n\t\t\t\t],\n\t\t\t\tauthenticatorSelection: {\n\t\t\t\t\tauthenticatorAttachment: 'platform',\n\t\t\t\t\tuserVerification: 'required',\n\t\t\t\t\tresidentKey: 'required'\n\t\t\t\t},\n\t\t\t\textensions: { prf: { eval: { first: prfSalt } } } as PrfInputs,\n\t\t\t\ttimeout: 60_000\n\t\t\t}\n\t\t})) as PublicKeyCredential | null;\n\t\tif (!credential) return null;\n\t\tconst credentialId = toB64(credential.rawId);\n\t\t// Some platforms hand the PRF result back from create(); most need a\n\t\t// second get(). Try the cheap path first to save the user one prompt.\n\t\tconst secret = prfSecret(credential) ?? (await derive(credentialId));\n\t\treturn secret ? { credentialId, secret } : null;\n\t}\n\n\treturn {\n\t\tasync available(): Promise<boolean> {\n\t\t\ttry {\n\t\t\t\tif (!webauthnUsable()) return false;\n\t\t\t\treturn await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t},\n\n\t\tenrolled(): boolean {\n\t\t\treturn read() !== null;\n\t\t},\n\n\t\tasync enroll(secret: string): Promise<boolean> {\n\t\t\tif (!webauthnUsable()) return false;\n\t\t\ttry {\n\t\t\t\tconst created = await create();\n\t\t\t\tif (!created) return false;\n\t\t\t\tconst iv = crypto.getRandomValues(new Uint8Array(12));\n\t\t\t\tconst sealed = await crypto.subtle.encrypt(\n\t\t\t\t\t{ name: 'AES-GCM', iv },\n\t\t\t\t\tawait sealKey(created.secret),\n\t\t\t\t\tnew TextEncoder().encode(secret)\n\t\t\t\t);\n\t\t\t\twrite({ credentialId: created.credentialId, iv: toB64(iv), sealed: toB64(sealed) });\n\t\t\t\treturn true;\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t},\n\n\t\tasync reveal(): Promise<string | null> {\n\t\t\tconst entry = read();\n\t\t\tif (!entry || !webauthnUsable()) return null;\n\t\t\tconst secret = await derive(entry.credentialId);\n\t\t\tif (!secret) return null;\n\t\t\ttry {\n\t\t\t\tconst opened = await crypto.subtle.decrypt(\n\t\t\t\t\t{ name: 'AES-GCM', iv: fromB64(entry.iv) as BufferSource },\n\t\t\t\t\tawait sealKey(secret),\n\t\t\t\t\tfromB64(entry.sealed) as BufferSource\n\t\t\t\t);\n\t\t\t\treturn new TextDecoder().decode(opened);\n\t\t\t} catch {\n\t\t\t\t// A wrong key or a tampered blob: an unopenable enrolment is dead\n\t\t\t\t// weight that would fail at every attempt, so drop it and let the\n\t\t\t\t// host fall back to the password.\n\t\t\t\twrite(null);\n\t\t\t\treturn null;\n\t\t\t}\n\t\t},\n\n\t\tforget(): void {\n\t\t\twrite(null);\n\t\t}\n\t};\n}\n"]}