selfstore 1.8.34 → 1.8.36

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.
Files changed (38) hide show
  1. package/README.md +6 -2
  2. package/dist/advanced.d.ts +9 -9
  3. package/dist/advanced.js +2 -2
  4. package/dist/backups.d.ts +4 -4
  5. package/dist/{cache-D8Z4u5Jz.d.ts → cache-DBIn_Uo8.d.ts} +9 -1
  6. package/dist/{chunk-A2RRJLIF.js → chunk-3P2W3XQD.js} +36 -23
  7. package/dist/chunk-3P2W3XQD.js.map +1 -0
  8. package/dist/{chunk-HQSK7HK3.js → chunk-JE3BLKXO.js} +86 -32
  9. package/dist/chunk-JE3BLKXO.js.map +1 -0
  10. package/dist/{chunk-VRWAV3GI.js → chunk-UU4PS3UH.js} +19 -9
  11. package/dist/chunk-UU4PS3UH.js.map +1 -0
  12. package/dist/{connect-B9f58dFZ.d.ts → connect-CdlhiTBy.d.ts} +4 -4
  13. package/dist/{drive-D5YLXmp_.d.ts → drive-BCRil91Q.d.ts} +2 -2
  14. package/dist/flows.d.ts +4 -4
  15. package/dist/flows.js +1 -1
  16. package/dist/groups.d.ts +2 -2
  17. package/dist/households.d.ts +7 -0
  18. package/dist/households.js +13 -3
  19. package/dist/households.js.map +1 -1
  20. package/dist/index.d.ts +9 -9
  21. package/dist/index.js +3 -3
  22. package/dist/{local-CmS0xFkK.d.ts → local-CpoBeJWk.d.ts} +2 -2
  23. package/dist/{manager-DCV_7B6M.d.ts → manager-0puQEpZc.d.ts} +2 -2
  24. package/dist/passkey.d.ts +6 -4
  25. package/dist/passkey.js +5 -4
  26. package/dist/passkey.js.map +1 -1
  27. package/dist/proof.d.ts +6 -6
  28. package/dist/{s3-mHLrEywJ.d.ts → s3-kikEHI-g.d.ts} +2 -2
  29. package/dist/{simple-Bnh2j78j.d.ts → simple-BmUQpsBM.d.ts} +6 -6
  30. package/dist/{store-DSvZwwr1.d.ts → store-bp1VRW7R.d.ts} +1 -1
  31. package/dist/sync.js +1 -1
  32. package/dist/widgets.d.ts +5 -5
  33. package/dist/widgets.js +1 -1
  34. package/llms.txt +2 -1
  35. package/package.json +1 -1
  36. package/dist/chunk-A2RRJLIF.js.map +0 -1
  37. package/dist/chunk-HQSK7HK3.js.map +0 -1
  38. package/dist/chunk-VRWAV3GI.js.map +0 -1
package/dist/index.d.ts CHANGED
@@ -1,15 +1,15 @@
1
- export { C as CacheUnlock, a as ConnectOutcome, F as FileBytes, P as PutFileInput, b as SimpleOptions, S as SimpleRecord, c as SimpleStore, s as selfstore } from './simple-Bnh2j78j.js';
2
- export { h as changePassword, g as gisDriveAuth, p as pickFromDisk, f as preloadGoogleIdentity, s as saveToDisk } from './local-CmS0xFkK.js';
3
- export { a as DriveAuth } from './drive-D5YLXmp_.js';
4
- export { S as S3Config, W as WebdavConfig } from './s3-mHLrEywJ.js';
5
- import { a as Snapshot, H as Header } from './store-DSvZwwr1.js';
1
+ export { C as CacheUnlock, a as ConnectOutcome, F as FileBytes, P as PutFileInput, b as SimpleOptions, S as SimpleRecord, c as SimpleStore, s as selfstore } from './simple-BmUQpsBM.js';
2
+ export { h as changePassword, g as gisDriveAuth, p as pickFromDisk, f as preloadGoogleIdentity, s as saveToDisk } from './local-CpoBeJWk.js';
3
+ export { a as DriveAuth } from './drive-BCRil91Q.js';
4
+ export { S as S3Config, W as WebdavConfig } from './s3-kikEHI-g.js';
5
+ import { a as Snapshot, H as Header } from './store-bp1VRW7R.js';
6
6
  import { EncodeOptions } from './advanced.js';
7
- export { L as LocalStore, e as LocalStoreState, h as Mode, i as PasswordCheck, b as PasswordPolicy, j as PasswordRequirement, o as SelfstoreError, p as SelfstoreErrorCode, q as Severity, c as SnapshotFile, r as StatusAction, d as StatusDescriptor, t as StorageState, S as StoreError, T as TargetKind, x as checkPasswordPolicy, C as errorLabelKey, G as isSelfstoreError } from './store-DSvZwwr1.js';
7
+ export { L as LocalStore, e as LocalStoreState, h as Mode, i as PasswordCheck, b as PasswordPolicy, j as PasswordRequirement, o as SelfstoreError, p as SelfstoreErrorCode, q as Severity, c as SnapshotFile, r as StatusAction, d as StatusDescriptor, t as StorageState, S as StoreError, T as TargetKind, x as checkPasswordPolicy, C as errorLabelKey, G as isSelfstoreError } from './store-bp1VRW7R.js';
8
8
  export { SyncConfig } from './sync.js';
9
- import './cache-D8Z4u5Jz.js';
10
- import './connect-B9f58dFZ.js';
9
+ import './cache-DBIn_Uo8.js';
10
+ import './connect-CdlhiTBy.js';
11
11
  import './machine-Dhbx3Eut.js';
12
- import './manager-DCV_7B6M.js';
12
+ import './manager-0puQEpZc.js';
13
13
 
14
14
  declare const BACKUP_EXTENSION = ".zip";
15
15
  declare const BACKUP_MIME = "application/zip";
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { indexedDbCache, memoryCache, gisDriveAuth, isLockableCache, createLocalStore } from './chunk-HQSK7HK3.js';
2
- export { gisDriveAuth, preloadGoogleIdentity } from './chunk-HQSK7HK3.js';
3
- import { idOf } from './chunk-A2RRJLIF.js';
1
+ import { indexedDbCache, memoryCache, gisDriveAuth, isLockableCache, createLocalStore } from './chunk-JE3BLKXO.js';
2
+ export { gisDriveAuth, preloadGoogleIdentity } from './chunk-JE3BLKXO.js';
3
+ import { idOf } from './chunk-3P2W3XQD.js';
4
4
  import { saveToDisk } from './chunk-XAONTUEC.js';
5
5
  export { pickFromDisk, saveToDisk } from './chunk-XAONTUEC.js';
6
6
  import './chunk-FJGWTBAL.js';
@@ -1,5 +1,5 @@
1
- import { a as Snapshot, E as EncodeOptions, H as Header } from './store-DSvZwwr1.js';
2
- import { a as DriveAuth } from './drive-D5YLXmp_.js';
1
+ import { a as Snapshot, E as EncodeOptions, H as Header } from './store-bp1VRW7R.js';
2
+ import { a as DriveAuth } from './drive-BCRil91Q.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
- import { K as KV } from './cache-D8Z4u5Jz.js';
2
- import { B as BackupTarget, L as LocalStore } from './store-DSvZwwr1.js';
1
+ import { K as KV } from './cache-DBIn_Uo8.js';
2
+ import { B as BackupTarget, L as LocalStore } from './store-bp1VRW7R.js';
3
3
  import { F as FlowStore } from './machine-Dhbx3Eut.js';
4
4
 
5
5
  /** What the destination reports about a listed file (one cheap metadata call). */
package/dist/passkey.d.ts CHANGED
@@ -63,8 +63,9 @@ interface PasskeyUnlock {
63
63
  enrolled(): boolean;
64
64
  /**
65
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.
66
+ * user cancelled, when PRF did not yield a key here, or when this device
67
+ * refused to store the sealed secret - fail closed, the host keeps its
68
+ * password prompt and should say so rather than pretend.
68
69
  */
69
70
  enroll(secret: string): Promise<boolean>;
70
71
  /**
@@ -77,9 +78,10 @@ interface PasskeyUnlock {
77
78
  * Drop the sealed secret from this device. The platform credential itself
78
79
  * is NOT deleted - the operating system owns it, and only the user can
79
80
  * remove it from their passkey manager. Say that in the interface rather
80
- * than implying a full erasure.
81
+ * than implying a full erasure. `false` when the storage refused the
82
+ * removal: the enrolment is still there, and the host must not say it is off.
81
83
  */
82
- forget(): void;
84
+ forget(): boolean;
83
85
  }
84
86
  declare function passkeyUnlock(options: PasskeyUnlockOptions): PasskeyUnlock;
85
87
 
package/dist/passkey.js CHANGED
@@ -45,10 +45,12 @@ function passkeyUnlock(options) {
45
45
  }
46
46
  function write(entry) {
47
47
  try {
48
- if (typeof localStorage === "undefined") return;
48
+ if (typeof localStorage === "undefined") return entry === null;
49
49
  if (entry) localStorage.setItem(storageKey, JSON.stringify(entry));
50
50
  else localStorage.removeItem(storageKey);
51
+ return true;
51
52
  } catch {
53
+ return false;
52
54
  }
53
55
  }
54
56
  async function derive(credentialId) {
@@ -118,8 +120,7 @@ function passkeyUnlock(options) {
118
120
  await sealKey(created.secret),
119
121
  new TextEncoder().encode(secret)
120
122
  );
121
- write({ credentialId: created.credentialId, iv: toB64(iv), sealed: toB64(sealed) });
122
- return true;
123
+ return write({ credentialId: created.credentialId, iv: toB64(iv), sealed: toB64(sealed) });
123
124
  } catch {
124
125
  return false;
125
126
  }
@@ -142,7 +143,7 @@ function passkeyUnlock(options) {
142
143
  }
143
144
  },
144
145
  forget() {
145
- write(null);
146
+ return write(null);
146
147
  }
147
148
  };
148
149
  }
@@ -1 +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"]}
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, when PRF did not yield a key here, or when this device\n\t * refused to store the sealed secret - fail closed, the host keeps its\n\t * 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. `false` when the storage refused the\n\t * removal: the enrolment is still there, and the host must not say it is off.\n\t */\n\tforget(): boolean;\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\t/** Whether the storage took the write. A blocked or full storage used to be\n\t * swallowed, so `enroll()` announced an enrolment nothing remembered. */\n\tfunction write(entry: StoredEnrolment | null): boolean {\n\t\ttry {\n\t\t\tif (typeof localStorage === 'undefined') return entry === null;\n\t\t\tif (entry) localStorage.setItem(storageKey, JSON.stringify(entry));\n\t\t\telse localStorage.removeItem(storageKey);\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\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\treturn write({ credentialId: created.credentialId, iv: toB64(iv), sealed: toB64(sealed) });\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(): boolean {\n\t\t\treturn write(null);\n\t\t}\n\t};\n}\n"]}
package/dist/proof.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import { SimpleRecord, FileBytes } from './index.js';
2
- import './store-DSvZwwr1.js';
3
- import './cache-D8Z4u5Jz.js';
2
+ import './store-bp1VRW7R.js';
3
+ import './cache-DBIn_Uo8.js';
4
4
  import './sync.js';
5
- import './connect-B9f58dFZ.js';
6
- import './drive-D5YLXmp_.js';
7
- import './s3-mHLrEywJ.js';
5
+ import './connect-CdlhiTBy.js';
6
+ import './drive-BCRil91Q.js';
7
+ import './s3-kikEHI-g.js';
8
8
  import './machine-Dhbx3Eut.js';
9
- import './manager-DCV_7B6M.js';
9
+ import './manager-0puQEpZc.js';
10
10
 
11
11
  /**
12
12
  * What a notary hands back for one digest. `evidence` is the notary's own
@@ -1,5 +1,5 @@
1
- import { B as BackupTarget, P as PeerSource } from './store-DSvZwwr1.js';
2
- import { K as KV } from './cache-D8Z4u5Jz.js';
1
+ import { B as BackupTarget, P as PeerSource } from './store-bp1VRW7R.js';
2
+ import { K as KV } from './cache-DBIn_Uo8.js';
3
3
 
4
4
  /**
5
5
  * WebDAV backup target: the store re-writes one .selfstore file on a WebDAV
@@ -1,10 +1,10 @@
1
- import { a as Snapshot, b as PasswordPolicy, c as SnapshotFile, B as BackupTarget, d as StatusDescriptor, S as StoreError, e as LocalStoreState, L as LocalStore } from './store-DSvZwwr1.js';
2
- import { L as LocalCache, K as KV } from './cache-D8Z4u5Jz.js';
1
+ import { a as Snapshot, b as PasswordPolicy, c as SnapshotFile, B as BackupTarget, d as StatusDescriptor, S as StoreError, e as LocalStoreState, L as LocalStore } from './store-bp1VRW7R.js';
2
+ import { L as LocalCache, K as KV } from './cache-DBIn_Uo8.js';
3
3
  import { SyncConfig } from './sync.js';
4
- import { R as ResumeOffer, C as ConnectTargets } from './connect-B9f58dFZ.js';
5
- import { f as BackupsManager } from './manager-DCV_7B6M.js';
6
- import { a as DriveAuth } from './drive-D5YLXmp_.js';
7
- import { W as WebdavConfig, S as S3Config } from './s3-mHLrEywJ.js';
4
+ import { R as ResumeOffer, C as ConnectTargets } from './connect-CdlhiTBy.js';
5
+ import { f as BackupsManager } from './manager-0puQEpZc.js';
6
+ import { a as DriveAuth } from './drive-BCRil91Q.js';
7
+ import { W as WebdavConfig, S as S3Config } from './s3-kikEHI-g.js';
8
8
 
9
9
  /** What a file body may be handed in as. */
10
10
  type FileBytes = Uint8Array | ArrayBuffer | Blob;
@@ -1,4 +1,4 @@
1
- import { G as GroupIdentity, S as SignedManifest, L as LocalCache } from './cache-D8Z4u5Jz.js';
1
+ import { G as GroupIdentity, S as SignedManifest, L as LocalCache } from './cache-DBIn_Uo8.js';
2
2
  import { CollectionChanges, Conflict, SyncConfig } from './sync.js';
3
3
 
4
4
  /** A binary attachment (image, PDF, any bytes). */
package/dist/sync.js CHANGED
@@ -1,4 +1,4 @@
1
- export { changes, createMeta, createNode, detectConflicts, merge, stamp } from './chunk-A2RRJLIF.js';
1
+ export { changes, createMeta, createNode, detectConflicts, merge, stamp } from './chunk-3P2W3XQD.js';
2
2
  import './chunk-FJGWTBAL.js';
3
3
  import './chunk-CSYEIXNY.js';
4
4
  //# sourceMappingURL=sync.js.map
package/dist/widgets.d.ts CHANGED
@@ -1,12 +1,12 @@
1
- import { S as StoreLike } from './connect-B9f58dFZ.js';
1
+ import { S as StoreLike } from './connect-CdlhiTBy.js';
2
2
  import { FlowHost, ConnectTargets, ConnectFlowOptions, ConnectKind, ConnectFlow } from './flows.js';
3
3
  import { ShareEngine, ShareLevel, ShareFlow, JoinEngine, JoinFlow, ReplicaFlow } from './flows.js';
4
4
  import { BackupsManager } from './backups.js';
5
- import './store-DSvZwwr1.js';
6
- import './cache-D8Z4u5Jz.js';
5
+ import './store-bp1VRW7R.js';
6
+ import './cache-DBIn_Uo8.js';
7
7
  import './sync.js';
8
- import './drive-D5YLXmp_.js';
9
- import './s3-mHLrEywJ.js';
8
+ import './drive-BCRil91Q.js';
9
+ import './s3-kikEHI-g.js';
10
10
  import './machine-Dhbx3Eut.js';
11
11
 
12
12
  /** Flat key -> string map; widgets merge a partial override over EN defaults. */
package/dist/widgets.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { saveToDisk, datedName } from './chunk-XAONTUEC.js';
2
- import { connectFlow, shareFlow, joinFlow } from './chunk-VRWAV3GI.js';
2
+ import { connectFlow, shareFlow, joinFlow } from './chunk-UU4PS3UH.js';
3
3
  import './chunk-TNYL6FNW.js';
4
4
  import { isOpenSupported } from './chunk-HVHQLPP6.js';
5
5
  import './chunk-2BNTCXQN.js';
package/llms.txt CHANGED
@@ -422,7 +422,8 @@ MANIFEST_ROLLBACK. Needs WebCrypto Ed25519+X25519 (evergreen browsers, Node
422
422
  ## Household sharing (mirror model), 'selfstore/households'
423
423
 
424
424
  Read-write sharing without re-keying anyone's backup: `createHouseholdGroup({
425
- store, kv, backend, storageKey? })`. The admin calls
425
+ store, kv, sealedKv?, backend, storageKey? })` - pass `cache.sealedKv` so K and the
426
+ invitation capability rest sealed, not in the clear kv. The admin calls
426
427
  startShare() (refused on an encrypted store - the honest rule), publishes an
427
428
  invitation via the backend; a visitor opens it (openIncoming) and join()s:
428
429
  their own file stays theirs, a dedicated copy file mirrors it under K, and the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "selfstore",
3
- "version": "1.8.34",
3
+ "version": "1.8.36",
4
4
  "description": "Local-first storage for browser apps: an IndexedDB working copy, portable encrypted ZIP backups (AES-256-GCM over Argon2id), durable homes (disk, Google Drive, WebDAV) and a serverless multi-device merge.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/sync/hlc.ts","../src/sync/merge.ts"],"names":[],"mappings":"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","file":"chunk-A2RRJLIF.js","sourcesContent":["/**\n * Hybrid Logical Clock: wall time + a counter + a node id, giving a\n * deterministic total order that tolerates clock skew. Encoded as a fixed-width\n * string so a plain string comparison equals causal order.\n */\n\nexport type Hlc = string;\n\ninterface Parts {\n\twall: number;\n\tcounter: number;\n\tnode: string;\n}\n\nconst WALL_W = 15;\nconst CTR_W = 6;\n\nfunction encode(p: Parts): Hlc {\n\treturn `${String(p.wall).padStart(WALL_W, '0')}|${String(p.counter).padStart(CTR_W, '0')}|${p.node}`;\n}\n\nfunction decode(h: Hlc): Parts {\n\tconst i = h.indexOf('|');\n\tconst j = h.indexOf('|', i + 1);\n\treturn { wall: Number(h.slice(0, i)), counter: Number(h.slice(i + 1, j)), node: h.slice(j + 1) };\n}\n\n/**\n * A fresh random replica id (one per device). Not a secret - but two replicas\n * sharing an id lose the tiebreak that makes the order total, so it comes from\n * the same source as everything else here. `randomUUID` is exposed only in a\n * secure context; `getRandomValues` is not, which is why the fallback is that\n * one and never `Math.random`.\n */\nexport function createNode(): string {\n\tif (typeof crypto === 'undefined') {\n\t\tthrow new TypeError(\n\t\t\t'createNode(): this platform provides no WebCrypto, so no replica id can be minted ' +\n\t\t\t\t'(evergreen browsers, Node 20+).'\n\t\t);\n\t}\n\tif (crypto.randomUUID) return crypto.randomUUID();\n\tconst bytes = new Uint8Array(16);\n\tcrypto.getRandomValues(bytes);\n\treturn [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('');\n}\n\n/**\n * How far ahead of local wall time a clock minted elsewhere may sit before this\n * replica refuses it.\n *\n * A hybrid logical clock absorbs the highest value it sees, so ONE impossible\n * wall time does three things at once: it wins every last-writer-wins\n * comparison from then on, it raises the clock of every replica that merges it\n * (and of every replica those merge with, in turn), and it keeps doing so after\n * whoever sent it has been removed from the group. Five minutes leaves room for\n * a device whose clock was never synchronised; nothing legitimate needs more.\n */\nexport const MAX_DRIFT_MS = 5 * 60_000;\n\n/** Whether `h` claims a wall time further ahead than the drift bound allows. */\nexport function driftedAhead(\n\th: Hlc,\n\twallNow: number = Date.now(),\n\tmaxDriftMs: number = MAX_DRIFT_MS\n): boolean {\n\treturn hlcWall(h) > wallNow + maxDriftMs;\n}\n\n/** Issue a clock for a local event. Monotonic per node even if the wall clock moves back. */\nexport function issue(prev: Hlc | null, node: string, wallNow: number = Date.now()): Hlc {\n\tconst p = prev ? decode(prev) : null;\n\tconst prevWall = p ? p.wall : 0;\n\tconst wall = Math.max(wallNow, prevWall);\n\tconst counter = wall === prevWall && p ? p.counter + 1 : 0;\n\treturn encode({ wall, counter, node });\n}\n\n/** Fold a clock observed from another replica into the local one. */\nexport function receive(\n\tlocal: Hlc | null,\n\tremote: Hlc,\n\tnode: string,\n\twallNow: number = Date.now()\n): Hlc {\n\tconst l = local ? decode(local) : { wall: 0, counter: 0, node };\n\tconst r = decode(remote);\n\tconst wall = Math.max(wallNow, l.wall, r.wall);\n\tlet counter: number;\n\tif (wall === l.wall && wall === r.wall) counter = Math.max(l.counter, r.counter) + 1;\n\telse if (wall === l.wall) counter = l.counter + 1;\n\telse if (wall === r.wall) counter = r.counter + 1;\n\telse counter = 0;\n\treturn encode({ wall, counter, node });\n}\n\n/** The wall-time component (ms since epoch) encoded in a clock, for age-based\n * pruning (tombstone GC). Not a precise event time - HLC wall can run ahead of\n * real time under skew - but a safe lower bound on \"how old is this at least\". */\nexport function hlcWall(h: Hlc): number {\n\treturn Number(h.slice(0, h.indexOf('|')));\n}\n\n/** Total order: <0 if a precedes b, >0 if a follows b, 0 only if identical. */\nexport function compare(a: Hlc, b: Hlc): number {\n\tif (a < b) return -1;\n\tif (a > b) return 1;\n\treturn 0;\n}\n\n/** The later of two clocks. */\nexport function max(a: Hlc | null, b: Hlc | null): Hlc {\n\tif (!a) return b as Hlc;\n\tif (!b) return a;\n\treturn compare(a, b) >= 0 ? a : b;\n}\n","/**\n * Schema-agnostic convergent merge for plain JSON. It keeps a small sidecar of\n * metadata (one logical clock per record) and reconciles two replica states with\n * well-known set semantics. Documents stay plain objects: identity is a\n * configured id field, change detection is by content hash, nothing is injected.\n *\n * Per-collection strategy:\n * 'lww-set' records keyed by id; later write per id wins; deletes tombstoned.\n * 'lww-map' records keyed by id, merged field BY field: concurrent edits\n * to different fields of one record both survive; the same field\n * goes to the later clock. Deletes stay record-level.\n * 'grow-set' append-only; entries immutable; union, never a conflict.\n * 'lww-register' a single value; later write wins.\n * 'manual' surface concurrent edits to the same id instead of resolving.\n *\n * For collaborative rich text, store an Automerge/Yjs document as a binary file\n * in the snapshot instead; this engine stops short of sequence CRDTs.\n */\n\nimport { type Hlc, issue, max, compare, createNode, hlcWall, driftedAhead } from './hlc';\nimport { SelfstoreError } from '../selfstore/errors';\nimport { byCodePoint } from '../ordering';\n\nexport type { Hlc } from './hlc';\nexport type Id = string;\nexport type MergeStrategy = 'lww-set' | 'lww-map' | 'grow-set' | 'lww-register' | 'manual';\n\nexport interface SyncConfig {\n\t/** Field holding each record's stable id (default 'id'). Dot-notation allowed. */\n\tidField?: string;\n\t/** Per-collection id path override for records whose id is not at the top level. */\n\tids?: Record<string, string>;\n\t/** Strategy per collection name; anything unlisted uses `fallback`. */\n\tstrategies?: Record<string, MergeStrategy>;\n\t/** Strategy for unlisted collections (default 'lww-set'). */\n\tfallback?: MergeStrategy;\n}\n\n/** Sidecar metadata for one collection, keyed by record id. */\nexport interface ColMeta {\n\tclocks: Record<Id, Hlc>;\n\tdeleted: Record<Id, Hlc>;\n\thashes: Record<Id, number>;\n\t/** 'lww-map' only: per-field [clock, contentHash], so concurrent edits to\n\t * different fields of one record merge instead of one side losing. */\n\tfields?: Record<Id, Record<string, [Hlc, number]>>;\n}\n\n/** Everything one replica tracks. Travels with the data so the other side can merge. */\nexport interface SyncMeta {\n\tnode: string;\n\tclock: Hlc | null;\n\tcols: Record<string, ColMeta>;\n}\n\n/** A same-id concurrent edit the 'manual' strategy refused to auto-resolve.\n * `local`/`remote` carry the two full versions during the session; consumers\n * persisting conflicts (e.g. the store's journal) redact them, so they are\n * absent after a reload - never write user records to unencrypted storage. */\nexport interface Conflict {\n\tcollection: string;\n\tid: Id;\n\tlocal?: unknown;\n\tremote?: unknown;\n\t/** The side kept as the provisional value (so it still converges). */\n\tkept: 'local' | 'remote';\n}\n\nexport interface MergeResult {\n\tcollections: Record<string, unknown[]>;\n\tmeta: SyncMeta;\n\tconflicts: Conflict[];\n}\n\nfunction stableStringify(v: unknown): string {\n\tif (v === null || typeof v !== 'object') return JSON.stringify(v) ?? 'null';\n\tif (Array.isArray(v)) return '[' + v.map(stableStringify).join(',') + ']';\n\tconst o = v as Record<string, unknown>;\n\treturn (\n\t\t'{' +\n\t\tObject.keys(o)\n\t\t\t.sort(byCodePoint)\n\t\t\t.map((k) => JSON.stringify(k) + ':' + stableStringify(o[k]))\n\t\t\t.join(',') +\n\t\t'}'\n\t);\n}\n\n/** FNV-1a 32-bit hash, enough to detect that a record changed. */\nfunction hash(v: unknown): number {\n\tconst s = stableStringify(v);\n\tlet h = 0x811c9dc5;\n\tfor (let i = 0; i < s.length; i++) {\n\t\th ^= s.charCodeAt(i);\n\t\th = Math.imul(h, 0x01000193);\n\t}\n\treturn h >>> 0;\n}\n\nfunction emptyCol(): ColMeta {\n\treturn { clocks: {}, deleted: {}, hashes: {} };\n}\n\nfunction cloneCol(c: ColMeta | undefined): ColMeta {\n\tif (!c) return emptyCol();\n\tconst out: ColMeta = {\n\t\tclocks: { ...c.clocks },\n\t\tdeleted: { ...c.deleted },\n\t\thashes: { ...c.hashes }\n\t};\n\tif (c.fields) {\n\t\tout.fields = {};\n\t\tfor (const [id, f] of Object.entries(c.fields)) out.fields[id] = { ...f };\n\t}\n\treturn out;\n}\n\nfunction strategyOf(config: SyncConfig, collection: string): MergeStrategy {\n\treturn config.strategies?.[collection] ?? config.fallback ?? 'lww-set';\n}\n\n/** The id of a record at the path the config gives its collection, dotted\n * paths included; null when there is no string there. */\nexport function idOf(config: SyncConfig, collection: string, doc: unknown): Id | null {\n\tconst path = config.ids?.[collection] ?? config.idField ?? 'id';\n\tlet v: unknown = doc;\n\tfor (const key of path.split('.')) {\n\t\tif (v === null || typeof v !== 'object') return null;\n\t\tv = (v as Record<string, unknown>)[key];\n\t}\n\treturn typeof v === 'string' ? v : null;\n}\n\n/** The id path this config resolves for a collection (for diagnostics). */\nexport function idPathFor(config: SyncConfig, collection: string): string {\n\treturn config.ids?.[collection] ?? config.idField ?? 'id';\n}\n\n/** Records the engine cannot track because they have no string id at the\n * configured path. Identity is a string id field, so such a record is invisible\n * to stamp/merge - it would silently never replicate. Returns the count per\n * collection (nonzero entries only), for a dev-time warning. A number or missing\n * id is the classic footgun: give each record a string id, or map the field via\n * `sync.ids`. */\nexport function unsyncableCounts(\n\tcollections: Record<string, unknown[]>,\n\tconfig: SyncConfig = {}\n): Record<string, number> {\n\tconst out: Record<string, number> = {};\n\tfor (const [name, docs] of Object.entries(collections)) {\n\t\tlet n = 0;\n\t\tfor (const doc of docs ?? []) if (idOf(config, name, doc) === null) n++;\n\t\tif (n > 0) out[name] = n;\n\t}\n\treturn out;\n}\n\nexport function createMeta(node: string = createNode()): SyncMeta {\n\treturn { node, clock: null, cols: {} };\n}\n\n/**\n * Prune tombstones (delete markers) whose clock is older than `beforeWall` (a\n * wall-time ms cutoff, e.g. `Date.now() - horizon`). Tombstones otherwise\n * accumulate forever, since a delete must be remembered to out-vote a stale\n * replica still holding the record.\n *\n * Safe only when the cutoff is older than the longest a replica can stay offline\n * before it next syncs: a replica that never saw a delete, and whose tombstone\n * has been GC'd everywhere, will resurrect that record on its next merge. Pick a\n * horizon comfortably beyond your worst-case offline window. Returns new meta\n * (input untouched); present-record clocks are untouched.\n */\nexport function gcTombstones(meta: SyncMeta, beforeWall: number): SyncMeta {\n\tconst cols: Record<string, ColMeta> = {};\n\tfor (const [name, col] of Object.entries(meta.cols)) {\n\t\tconst next = cloneCol(col);\n\t\tfor (const [id, clk] of Object.entries(next.deleted)) {\n\t\t\tif (hlcWall(clk) < beforeWall) delete next.deleted[id];\n\t\t}\n\t\tcols[name] = next;\n\t}\n\treturn { node: meta.node, clock: meta.clock, cols };\n}\n\n/**\n * Diff `collections` against the metadata's content hashes, assign a fresh clock\n * to every added or changed record, and tombstone records that disappeared.\n * Returns new metadata (input untouched). Call on every local save.\n */\ntype Tick = () => Hlc;\n\n/** Stamp each present record: a fresh clock for anything added or changed (by\n * content hash). Returns the set of ids seen, for the tombstone pass. */\nfunction stampPresent(\n\tcol: ColMeta,\n\tpresent: unknown[],\n\tconfig: SyncConfig,\n\tname: string,\n\ttick: Tick\n): Set<Id> {\n\tconst fieldLevel = strategyOf(config, name) === 'lww-map';\n\tconst seen = new Set<Id>();\n\tfor (const doc of present) {\n\t\tconst id = idOf(config, name, doc);\n\t\tif (id === null) continue;\n\t\tseen.add(id);\n\t\tconst h = hash(doc);\n\t\tif (col.hashes[id] !== h) {\n\t\t\tcol.clocks[id] = tick();\n\t\t\tcol.hashes[id] = h;\n\t\t\tdelete col.deleted[id];\n\t\t\tif (fieldLevel && isRecord(doc)) stampFields(col, id, doc, tick);\n\t\t}\n\t}\n\treturn seen;\n}\n\n/** Stamp each added/changed field of one record ('lww-map'): a field keeps its\n * clock until its own content changes, so field clocks stay independent and a\n * merge can tell which side last touched which field. */\nfunction stampFields(col: ColMeta, id: Id, doc: Record<string, unknown>, tick: Tick): void {\n\tconst rec = ((col.fields ??= {})[id] ??= {});\n\tfor (const key of Object.keys(doc)) {\n\t\tconst h = hash(doc[key]);\n\t\tif (rec[key]?.[1] !== h) rec[key] = [tick(), h];\n\t}\n\tfor (const key of Object.keys(rec)) if (!(key in doc)) delete rec[key];\n}\n\n/** Tombstone every previously-tracked id that is no longer present. */\nfunction tombstoneMissing(col: ColMeta, seen: Set<Id>, tick: Tick): void {\n\tfor (const id of Object.keys(col.clocks)) {\n\t\tif (!seen.has(id)) {\n\t\t\tcol.deleted[id] = tick();\n\t\t\tdelete col.clocks[id];\n\t\t\tdelete col.hashes[id];\n\t\t\tdelete col.fields?.[id];\n\t\t}\n\t}\n}\n\nexport function stamp(\n\tmeta: SyncMeta,\n\tcollections: Record<string, unknown[]>,\n\tconfig: SyncConfig,\n\twallNow: number = Date.now()\n): SyncMeta {\n\tlet clock = meta.clock;\n\tconst tick: Tick = () => (clock = issue(clock, meta.node, wallNow));\n\tconst cols: Record<string, ColMeta> = {};\n\tconst names = new Set([...Object.keys(meta.cols), ...Object.keys(collections)]);\n\n\tfor (const name of names) {\n\t\tconst strategy = strategyOf(config, name);\n\t\tconst col = cloneCol(meta.cols[name]);\n\t\tconst present = collections[name] ?? [];\n\n\t\tconst seen = stampPresent(col, present, config, name, tick);\n\t\tif (strategy !== 'grow-set') tombstoneMissing(col, seen, tick);\n\t\tcols[name] = col;\n\t}\n\treturn { node: meta.node, clock, cols };\n}\n\n/** One replica's full state: the collections plus their sync metadata. What\n * `merge` and `detectConflicts` take on each side. */\nexport interface ReplicaState {\n\tcollections: Record<string, unknown[]>;\n\tmeta: SyncMeta;\n}\ntype State = ReplicaState;\n\n/**\n * Merge replica `b` into `a` (commutative for the convergent strategies). The\n * optional `base` lets 'manual' tell a true concurrent conflict from a one-sided\n * change; without it, 'manual' errs toward surfacing.\n */\n/** Every clock a replica carries, flattened: meta, records, tombstones, fields. */\nfunction* allClocks(meta: SyncMeta): Generator<Hlc> {\n\tif (meta.clock) yield meta.clock;\n\tfor (const col of Object.values(meta.cols)) {\n\t\tyield* Object.values(col.clocks);\n\t\tyield* Object.values(col.deleted);\n\t\tfor (const fields of Object.values(col.fields ?? {})) {\n\t\t\tfor (const [clock] of Object.values(fields)) yield clock;\n\t\t}\n\t}\n}\n\n/**\n * Refuse a replica whose clocks cannot be real. `b` is the incoming side by\n * signature, and only it is checked: a local replica that already absorbed a\n * bad clock must still be able to merge, or the fix would strand exactly the\n * stores it exists to protect. Refusing rather than clamping is deliberate -\n * a silently lowered clock would leave two replicas each convinced they hold\n * the later write.\n */\nfunction assertNoDrift(meta: SyncMeta, wallNow: number): void {\n\tfor (const clock of allClocks(meta)) {\n\t\tif (driftedAhead(clock, wallNow)) {\n\t\t\tthrow new SelfstoreError(\n\t\t\t\t'CLOCK_DRIFT',\n\t\t\t\t`This copy carries a clock ${Math.round((hlcWall(clock) - wallNow) / 60_000)} minutes ` +\n\t\t\t\t\t'ahead of this device. Check the clock of the device that wrote it: merging would ' +\n\t\t\t\t\t'let those writes win every future conflict, on every device.'\n\t\t\t);\n\t\t}\n\t}\n}\n\nexport function merge(a: State, b: State, config: SyncConfig, base?: SyncMeta): MergeResult {\n\tassertNoDrift(b.meta, Date.now());\n\tconst conflicts: Conflict[] = [];\n\tconst outCols: Record<string, unknown[]> = {};\n\tconst outMeta: SyncMeta = {\n\t\tnode: a.meta.node,\n\t\tclock: max(a.meta.clock, b.meta.clock),\n\t\tcols: {}\n\t};\n\n\tconst names = new Set([\n\t\t...Object.keys(a.collections),\n\t\t...Object.keys(b.collections),\n\t\t...Object.keys(a.meta.cols),\n\t\t...Object.keys(b.meta.cols)\n\t]);\n\n\tfor (const name of names) {\n\t\tconst merged = mergeCollection(name, a, b, config, base, conflicts);\n\t\toutCols[name] = merged.docs;\n\t\toutMeta.cols[name] = merged.col;\n\t}\n\n\treturn { collections: outCols, meta: outMeta, conflicts };\n}\n\n/**\n * Concurrent same-id edits that a convergent strategy resolves silently by keeping\n * the later clock. Informational only: it never changes what `merge` keeps, it\n * reports which records both replicas changed since `base` (the loser's version is\n * the one merge dropped). Empty without a base, since two-way state alone cannot\n * tell a concurrent edit from a one-sided change.\n */\nexport function detectConflicts(\n\ta: State,\n\tb: State,\n\tconfig: SyncConfig,\n\tbase?: SyncMeta\n): Conflict[] {\n\tif (!base) return [];\n\tconst out: Conflict[] = [];\n\tconst names = new Set([...Object.keys(a.collections), ...Object.keys(b.collections)]);\n\tfor (const name of names) {\n\t\tconst strategy = strategyOf(config, name);\n\t\tconst aDocs = byId(config, name, a.collections[name]);\n\t\tconst bDocs = byId(config, name, b.collections[name]);\n\t\tconst aMeta = a.meta.cols[name] ?? emptyCol();\n\t\tconst bMeta = b.meta.cols[name] ?? emptyCol();\n\t\tconst baseMeta = base.cols[name];\n\t\tfor (const id of new Set<Id>([...aDocs.keys(), ...bDocs.keys()])) {\n\t\t\tconst aClk = aMeta.clocks[id] ?? null;\n\t\t\tconst bClk = bMeta.clocks[id] ?? null;\n\t\t\tconst winnerSide = pickWinner(aClk, bClk, aDocs.has(id));\n\t\t\t// lww-map merges different-field edits cleanly: only the same field\n\t\t\t// changed on both sides is a real (auto-resolved) conflict.\n\t\t\tconst conflict =\n\t\t\t\tstrategy === 'lww-map'\n\t\t\t\t\t? fieldConflict(\n\t\t\t\t\t\t\tname,\n\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\taDocs,\n\t\t\t\t\t\t\tbDocs,\n\t\t\t\t\t\t\taMeta.fields?.[id],\n\t\t\t\t\t\t\tbMeta.fields?.[id],\n\t\t\t\t\t\t\tbaseMeta?.fields?.[id],\n\t\t\t\t\t\t\twinnerSide\n\t\t\t\t\t\t)\n\t\t\t\t\t: manualConflict(name, id, aDocs, bDocs, aClk, bClk, baseMeta, winnerSide);\n\t\t\tif (conflict) out.push(conflict);\n\t\t}\n\t}\n\treturn out;\n}\n\n/** The 'lww-map' conflict check: a Conflict only when the same field changed on\n * both sides since base (different-field edits merge without loss). */\nfunction fieldConflict(\n\tname: string,\n\tid: Id,\n\taDocs: Map<Id, unknown>,\n\tbDocs: Map<Id, unknown>,\n\taF: Record<string, [Hlc, number]> | undefined,\n\tbF: Record<string, [Hlc, number]> | undefined,\n\tbaseF: Record<string, [Hlc, number]> | undefined,\n\twinnerSide: 'local' | 'remote'\n): Conflict | null {\n\tif (!(aDocs.has(id) && bDocs.has(id) && aF && bF)) return null;\n\tfor (const key of Object.keys(aF)) {\n\t\tconst a = aF[key];\n\t\tconst b = bF[key];\n\t\tif (!b || a[0] === b[0]) continue;\n\t\tconst baseClk = baseF?.[key]?.[0] ?? null;\n\t\tconst aChanged = !baseClk || compare(a[0], baseClk) > 0;\n\t\tconst bChanged = !baseClk || compare(b[0], baseClk) > 0;\n\t\tif (aChanged && bChanged)\n\t\t\treturn {\n\t\t\t\tcollection: name,\n\t\t\t\tid,\n\t\t\t\tlocal: aDocs.get(id),\n\t\t\t\tremote: bDocs.get(id),\n\t\t\t\tkept: winnerSide\n\t\t\t};\n\t}\n\treturn null;\n}\n\n/** The 'manual' concurrent-edit check for one record: a Conflict when both sides\n * changed the same id since `base` (or, without a base, both simply hold it and\n * their clocks differ). Pure; null when there is nothing to surface. */\nfunction manualConflict(\n\tname: string,\n\tid: Id,\n\taDocs: Map<Id, unknown>,\n\tbDocs: Map<Id, unknown>,\n\taClk: Hlc | null,\n\tbClk: Hlc | null,\n\tbaseMeta: ColMeta | undefined,\n\twinnerSide: 'local' | 'remote'\n): Conflict | null {\n\tif (!(aDocs.has(id) && bDocs.has(id) && aClk && bClk && aClk !== bClk)) return null;\n\tconst baseClk = baseMeta?.clocks[id] ?? null;\n\tconst aChanged = !baseClk || compare(aClk, baseClk) > 0;\n\tconst bChanged = !baseClk || compare(bClk, baseClk) > 0;\n\tif (!(aChanged && bChanged)) return null;\n\treturn { collection: name, id, local: aDocs.get(id), remote: bDocs.get(id), kept: winnerSide };\n}\n\n/** Reconcile every id of one collection across the two replicas. */\nfunction mergeCollection(\n\tname: string,\n\ta: State,\n\tb: State,\n\tconfig: SyncConfig,\n\tbase: SyncMeta | undefined,\n\tconflicts: Conflict[]\n): { docs: unknown[]; col: ColMeta } {\n\tconst strategy = strategyOf(config, name);\n\tconst aDocs = byId(config, name, a.collections[name]);\n\tconst bDocs = byId(config, name, b.collections[name]);\n\tconst aMeta = a.meta.cols[name] ?? emptyCol();\n\tconst bMeta = b.meta.cols[name] ?? emptyCol();\n\tconst baseMeta = base?.cols[name];\n\tconst col = emptyCol();\n\tconst docs: unknown[] = [];\n\n\tconst ids = new Set<Id>([\n\t\t...Object.keys(aMeta.clocks),\n\t\t...Object.keys(bMeta.clocks),\n\t\t...Object.keys(aMeta.deleted),\n\t\t...Object.keys(bMeta.deleted),\n\t\t...aDocs.keys(),\n\t\t...bDocs.keys()\n\t]);\n\n\tfor (const id of ids) {\n\t\tconst aClk = aMeta.clocks[id] ?? null;\n\t\tconst bClk = bMeta.clocks[id] ?? null;\n\t\tconst aliveClk = max(aClk, bClk);\n\t\tconst deadClk = max(aMeta.deleted[id] ?? null, bMeta.deleted[id] ?? null);\n\n\t\tconst deleteWins =\n\t\t\tstrategy !== 'grow-set' && deadClk && (!aliveClk || compare(deadClk, aliveClk) > 0);\n\n\t\tif (deleteWins) {\n\t\t\tcol.deleted[id] = deadClk as Hlc;\n\t\t\tcontinue;\n\t\t}\n\t\tif (!aliveClk) {\n\t\t\tif (deadClk) col.deleted[id] = deadClk;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst aHas = aDocs.has(id);\n\t\tconst winnerSide = pickWinner(aClk, bClk, aHas);\n\t\tlet doc = winnerSide === 'local' ? aDocs.get(id) : bDocs.get(id);\n\n\t\tif (strategy === 'manual') {\n\t\t\tconst conflict = manualConflict(name, id, aDocs, bDocs, aClk, bClk, baseMeta, winnerSide);\n\t\t\tif (conflict) conflicts.push(conflict);\n\t\t}\n\n\t\tif (strategy === 'lww-map') {\n\t\t\tconst merged = mergeFields(id, aDocs.get(id), bDocs.get(id), aMeta, bMeta, winnerSide);\n\t\t\tif (merged) {\n\t\t\t\tdoc = merged.doc;\n\t\t\t\t(col.fields ??= {})[id] = merged.fields;\n\t\t\t} else {\n\t\t\t\t// One side has no document: the record-level winner stands; carry its\n\t\t\t\t// field metadata forward so later merges stay field-aware.\n\t\t\t\tconst f = (winnerSide === 'local' ? aMeta : bMeta).fields?.[id];\n\t\t\t\tif (f) (col.fields ??= {})[id] = { ...f };\n\t\t\t}\n\t\t}\n\n\t\tif (doc !== undefined) {\n\t\t\tdocs.push(doc);\n\t\t\tcol.clocks[id] = aliveClk;\n\t\t\tcol.hashes[id] = hash(doc);\n\t\t}\n\t}\n\n\treturn { docs, col };\n}\n\n/** Field-by-field merge of one record ('lww-map'): each field goes to the side\n * whose field clock is later; a field only one side stamped keeps that side's\n * value; unstamped fields (written under a record-level strategy) follow the\n * record-level winner.\n * Null when either document is missing (the record-level result stands). */\nfunction mergeFields(\n\tid: Id,\n\taDoc: unknown,\n\tbDoc: unknown,\n\taMeta: ColMeta,\n\tbMeta: ColMeta,\n\twinnerSide: 'local' | 'remote'\n): { doc: Record<string, unknown>; fields: Record<string, [Hlc, number]> } | null {\n\tif (!isRecord(aDoc) || !isRecord(bDoc)) return null;\n\tconst aF = aMeta.fields?.[id] ?? {};\n\tconst bF = bMeta.fields?.[id] ?? {};\n\tconst doc: Record<string, unknown> = {};\n\tconst fields: Record<string, [Hlc, number]> = {};\n\tfor (const key of new Set([...Object.keys(aDoc), ...Object.keys(bDoc)])) {\n\t\tconst a = aF[key] ?? null;\n\t\tconst b = bF[key] ?? null;\n\t\tconst side = pickFieldSide(a, b, winnerSide);\n\t\tconst src = side === 'local' ? aDoc : bDoc;\n\t\tif (!(key in src)) continue; // the winning side removed the field: drop it\n\t\tdoc[key] = src[key];\n\t\tconst stamped = side === 'local' ? a : b;\n\t\tif (stamped) fields[key] = stamped;\n\t}\n\treturn { doc, fields };\n}\n\n/** Which side supplies one field of an 'lww-map' record: the later field clock\n * when both sides stamped it, the stamping side when only one did, else the\n * record-level winner (unstamped fields follow the record). */\nfunction pickFieldSide(\n\ta: [Hlc, number] | null,\n\tb: [Hlc, number] | null,\n\twinnerSide: 'local' | 'remote'\n): 'local' | 'remote' {\n\tif (a && b) return compare(a[0], b[0]) >= 0 ? 'local' : 'remote';\n\tif (a) return 'local';\n\tif (b) return 'remote';\n\treturn winnerSide;\n}\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n\treturn typeof v === 'object' && v !== null && !Array.isArray(v);\n}\n\n/** Which side's value to keep for a record. With both clocks present, the later\n * clock wins (ties to local); otherwise the side that actually holds the doc. */\nfunction pickWinner(aClk: Hlc | null, bClk: Hlc | null, aHas: boolean): 'local' | 'remote' {\n\tif (aClk && bClk) return compare(aClk, bClk) >= 0 ? 'local' : 'remote';\n\treturn aHas ? 'local' : 'remote';\n}\n\nfunction byId(\n\tconfig: SyncConfig,\n\tcollection: string,\n\tdocs: unknown[] | undefined\n): Map<Id, unknown> {\n\tconst m = new Map<Id, unknown>();\n\tfor (const d of docs ?? []) {\n\t\tconst id = idOf(config, collection, d);\n\t\tif (id !== null) m.set(id, d);\n\t}\n\treturn m;\n}\n\n/** What a merge did to one collection, counted by record. */\nexport interface CollectionChanges {\n\tadded: number;\n\tupdated: number;\n\tremoved: number;\n}\n\n/** Per-collection record counts of what changed between two states (same id\n * resolution as the merge). Collections with no change are omitted, so an empty\n * result means the states are equivalent. Feeds the sync journal. */\nexport function changes(\n\tbefore: Record<string, unknown[]>,\n\tafter: Record<string, unknown[]>,\n\tconfig: SyncConfig\n): Record<string, CollectionChanges> {\n\tconst out: Record<string, CollectionChanges> = {};\n\tfor (const name of new Set([...Object.keys(before), ...Object.keys(after)])) {\n\t\tconst b = byId(config, name, before[name]);\n\t\tconst a = byId(config, name, after[name]);\n\t\tlet added = 0;\n\t\tlet updated = 0;\n\t\tlet removed = 0;\n\t\tfor (const [id, doc] of a) {\n\t\t\tif (!b.has(id)) added++;\n\t\t\telse if (hash(doc) !== hash(b.get(id))) updated++;\n\t\t}\n\t\tfor (const id of b.keys()) if (!a.has(id)) removed++;\n\t\tif (added || updated || removed) out[name] = { added, updated, removed };\n\t}\n\treturn out;\n}\n"]}