selfstore 1.8.23 → 1.8.25

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 (86) hide show
  1. package/README.md +66 -0
  2. package/dist/advanced.d.ts +7 -7
  3. package/dist/advanced.js +15 -14
  4. package/dist/advanced.js.map +1 -1
  5. package/dist/backups.d.ts +4 -4
  6. package/dist/backups.js +9 -8
  7. package/dist/{cache-lock-5RNG6CIK.js → cache-lock-VVAB3CDB.js} +5 -4
  8. package/dist/cache-lock-VVAB3CDB.js.map +1 -0
  9. package/dist/{chunk-JTNFZV2B.js → chunk-25U6AKIX.js} +4 -4
  10. package/dist/{chunk-JTNFZV2B.js.map → chunk-25U6AKIX.js.map} +1 -1
  11. package/dist/{chunk-3KAAPYPO.js → chunk-2BNTCXQN.js} +3 -3
  12. package/dist/{chunk-3KAAPYPO.js.map → chunk-2BNTCXQN.js.map} +1 -1
  13. package/dist/{chunk-LYO36VYB.js → chunk-2PIYIIHY.js} +8 -8
  14. package/dist/{chunk-LYO36VYB.js.map → chunk-2PIYIIHY.js.map} +1 -1
  15. package/dist/{chunk-GW5CYP4C.js → chunk-2TIRUEXR.js} +3 -3
  16. package/dist/{chunk-GW5CYP4C.js.map → chunk-2TIRUEXR.js.map} +1 -1
  17. package/dist/{chunk-EBCQ7ZG7.js → chunk-5DAPYC2K.js} +4 -4
  18. package/dist/{chunk-EBCQ7ZG7.js.map → chunk-5DAPYC2K.js.map} +1 -1
  19. package/dist/{chunk-RREJ2MFN.js → chunk-5UYPKCDB.js} +5 -17
  20. package/dist/chunk-5UYPKCDB.js.map +1 -0
  21. package/dist/{chunk-DJZ5S3FI.js → chunk-AZWS7YC3.js} +21 -13
  22. package/dist/chunk-AZWS7YC3.js.map +1 -0
  23. package/dist/{chunk-XMC67RNM.js → chunk-CSYEIXNY.js} +2 -2
  24. package/dist/{chunk-XMC67RNM.js.map → chunk-CSYEIXNY.js.map} +1 -1
  25. package/dist/{chunk-OY2A5DCB.js → chunk-F4ZZEJ5H.js} +4 -4
  26. package/dist/{chunk-OY2A5DCB.js.map → chunk-F4ZZEJ5H.js.map} +1 -1
  27. package/dist/{chunk-3BWBBGFW.js → chunk-FQ5O4PHK.js} +7 -7
  28. package/dist/{chunk-3BWBBGFW.js.map → chunk-FQ5O4PHK.js.map} +1 -1
  29. package/dist/{chunk-JZHWAX34.js → chunk-GHSHIDF3.js} +4 -4
  30. package/dist/{chunk-JZHWAX34.js.map → chunk-GHSHIDF3.js.map} +1 -1
  31. package/dist/{chunk-23UAUYSW.js → chunk-GSZSZGSD.js} +3 -3
  32. package/dist/{chunk-23UAUYSW.js.map → chunk-GSZSZGSD.js.map} +1 -1
  33. package/dist/{chunk-LUNYYH6D.js → chunk-IDW3WQKF.js} +6 -6
  34. package/dist/{chunk-LUNYYH6D.js.map → chunk-IDW3WQKF.js.map} +1 -1
  35. package/dist/{chunk-757ZOSUI.js → chunk-LF3CC2P3.js} +3 -3
  36. package/dist/{chunk-757ZOSUI.js.map → chunk-LF3CC2P3.js.map} +1 -1
  37. package/dist/{chunk-AUH35AAQ.js → chunk-LQS7AMOD.js} +3 -3
  38. package/dist/{chunk-AUH35AAQ.js.map → chunk-LQS7AMOD.js.map} +1 -1
  39. package/dist/{chunk-HHPJPEEG.js → chunk-RVOBONZF.js} +7 -6
  40. package/dist/chunk-RVOBONZF.js.map +1 -0
  41. package/dist/chunk-SJ3PKQPR.js +10 -0
  42. package/dist/chunk-SJ3PKQPR.js.map +1 -0
  43. package/dist/chunk-WXI2NAPA.js +16 -0
  44. package/dist/chunk-WXI2NAPA.js.map +1 -0
  45. package/dist/{connect-Bob2mofK.d.ts → connect-DoB_DN__.d.ts} +3 -3
  46. package/dist/{drive-86CHuTij.d.ts → drive--mndByvF.d.ts} +1 -1
  47. package/dist/flows.d.ts +5 -5
  48. package/dist/flows.js +12 -11
  49. package/dist/flows.js.map +1 -1
  50. package/dist/groups.js +5 -4
  51. package/dist/groups.js.map +1 -1
  52. package/dist/households.d.ts +1 -1
  53. package/dist/index.d.ts +11 -266
  54. package/dist/index.js +21 -24
  55. package/dist/index.js.map +1 -1
  56. package/dist/{local-8H61XxbL.d.ts → local-ClXq1N2M.d.ts} +14 -3
  57. package/dist/local-GGAMRY23.js +8 -0
  58. package/dist/{local-XET4G32B.js.map → local-GGAMRY23.js.map} +1 -1
  59. package/dist/manager-drive-2IUE4WQ2.js +11 -0
  60. package/dist/{manager-drive-YLSLQ63O.js.map → manager-drive-2IUE4WQ2.js.map} +1 -1
  61. package/dist/{manager-Di6lNE4E.d.ts → manager-yCM0M0FL.d.ts} +1 -1
  62. package/dist/proof.d.ts +164 -0
  63. package/dist/proof.js +360 -0
  64. package/dist/proof.js.map +1 -0
  65. package/dist/s3-ABKUTTXE.js +12 -0
  66. package/dist/s3-ABKUTTXE.js.map +1 -0
  67. package/dist/{s3-BsbVn3fw.d.ts → s3-dG1SCnV1.d.ts} +1 -1
  68. package/dist/simple-GdtB5IrG.d.ts +267 -0
  69. package/dist/{store-CIbiNoiE.d.ts → store-CKGrIFQU.d.ts} +2 -2
  70. package/dist/sync.js +2 -2
  71. package/dist/webdav-5SHNR7CL.js +10 -0
  72. package/dist/{webdav-EEQKX5TZ.js.map → webdav-5SHNR7CL.js.map} +1 -1
  73. package/dist/widgets.d.ts +5 -5
  74. package/dist/widgets.js +12 -11
  75. package/dist/widgets.js.map +1 -1
  76. package/llms.txt +19 -2
  77. package/package.json +5 -1
  78. package/dist/cache-lock-5RNG6CIK.js.map +0 -1
  79. package/dist/chunk-DJZ5S3FI.js.map +0 -1
  80. package/dist/chunk-HHPJPEEG.js.map +0 -1
  81. package/dist/chunk-RREJ2MFN.js.map +0 -1
  82. package/dist/local-XET4G32B.js +0 -7
  83. package/dist/manager-drive-YLSLQ63O.js +0 -10
  84. package/dist/s3-43RWNCV3.js +0 -11
  85. package/dist/s3-43RWNCV3.js.map +0 -1
  86. package/dist/webdav-EEQKX5TZ.js +0 -9
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/persistence/identity.ts"],"names":[],"mappings":"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","file":"groups.js","sourcesContent":["// At-rest storage for a member identity (passwordless groups). Private keys\n// are sealed under a non-extractable device key in the same kv - defeats disk\n// forensics and partial exfiltration, not code running in the origin or a\n// copy of the whole profile (THREAT-MODEL.md).\n//\n// The opt-in passphrase lock reseals the identity under Argon2id(passphrase)\n// instead, closing the profile-copy gap at the cost of an unlock prompt. The\n// vault keeps no unlocked session state: unlock() returns the identity, the\n// caller holds it in memory, and at rest it stays sealed. A bare envelope\n// keeps meaning device mode, so pre-lock vaults load unchanged.\n//\n// The identity is one person's; move it between their devices over a trusted\n// channel (QR shown locally, encrypted note) - never through the group's\n// shared storage.\n\nimport { seal, unseal, isEnvelope, newDeviceKey, type EncEnvelope } from './cache-crypto';\nimport type { KV } from './cache';\nimport { generateIdentity, type GroupIdentity } from '../selfstore/group';\nimport { deriveKey, freshKdfParams, assertKdfBounds } from '../selfstore/crypto';\nimport type { KdfParams } from '../selfstore/types';\nimport { SelfstoreError } from '../selfstore/errors';\n\nconst VAULT_KEY = 'groupVaultKey';\nconst IDENTITY_KEY = 'groupIdentity';\n\nconst utf8 = { enc: new TextEncoder(), dec: new TextDecoder() };\n\n// Local kv bookkeeping only - not part of the interchange format (SPEC.md).\ninterface VaultLock {\n\tselfstoreVaultLock: 1;\n\tkdf: KdfParams;\n\tenvelope: EncEnvelope;\n}\n\nfunction isVaultLock(v: unknown): v is VaultLock {\n\treturn (\n\t\ttypeof v === 'object' &&\n\t\tv !== null &&\n\t\t(v as { selfstoreVaultLock?: unknown }).selfstoreVaultLock === 1\n\t);\n}\n\nconst encode = (identity: GroupIdentity): Uint8Array => utf8.enc.encode(JSON.stringify(identity));\nconst decode = (bytes: Uint8Array): GroupIdentity =>\n\tJSON.parse(utf8.dec.decode(bytes)) as GroupIdentity;\n\nexport interface IdentityVault {\n\t/** The stored identity, or null when none exists yet. On a protected\n\t * vault this throws PASSWORD_REQUIRED rather than returning null - a\n\t * locked identity exists, and null would invite loadOrCreate to mint a\n\t * second one over it. */\n\tload(): Promise<GroupIdentity | null>;\n\t/** Seal and store an identity, overwriting any previous one. Throws\n\t * PASSWORD_REQUIRED on a protected vault: a locked identity is never\n\t * silently replaced (unprotect() or clear() first). */\n\tsave(identity: GroupIdentity): Promise<void>;\n\t/** load(), creating and storing a fresh identity when absent. Same\n\t * PASSWORD_REQUIRED rule as load(). */\n\tloadOrCreate(): Promise<GroupIdentity>;\n\t/** Forget the identity on this device. Allowed even when protected -\n\t * forgetting leaks nothing, and it is the honest lost-passphrase escape\n\t * hatch (re-import from another device, or get re-invited). */\n\tclear(): Promise<void>;\n\tisProtected(): Promise<boolean>;\n\t/** Decrypt a protected vault and return the identity. Stateless: the\n\t * caller holds it in memory, the stored value stays sealed. Wrong\n\t * passphrase and tampered blob are one DECRYPT_FAILED; hostile KDF\n\t * params are refused before any derivation. */\n\tunlock(passphrase: string): Promise<GroupIdentity>;\n\t/** Reseal the identity under Argon2id(passphrase); an empty vault gets a\n\t * fresh identity, created-and-protected in one step. Changing the\n\t * passphrase is unprotect(old) then protect(new) - an already-protected\n\t * vault throws PASSWORD_REQUIRED, never a silent rekey. */\n\tprotect(passphrase: string): Promise<void>;\n\t/** Unlock with `passphrase`, then reseal under the device key. A wrong\n\t * passphrase throws DECRYPT_FAILED and leaves the vault untouched. */\n\tunprotect(passphrase: string): Promise<void>;\n}\n\n/** An identity vault over the app's kv (e.g. `indexedDbCache(app).kv`). */\nexport function identityVault(kv: KV): IdentityVault {\n\tasync function vaultKey(): Promise<CryptoKey> {\n\t\tconst existing = await kv.get<CryptoKey>(VAULT_KEY);\n\t\tif (existing) return existing;\n\t\tconst fresh = await newDeviceKey();\n\t\tawait kv.set(VAULT_KEY, fresh);\n\t\treturn fresh;\n\t}\n\n\t/** Device mode's write path: a bare envelope under the device key. */\n\tasync function sealDevice(identity: GroupIdentity): Promise<void> {\n\t\tawait kv.set(IDENTITY_KEY, await seal(await vaultKey(), encode(identity)));\n\t}\n\n\tasync function load(): Promise<GroupIdentity | null> {\n\t\tconst stored = await kv.get(IDENTITY_KEY);\n\t\tif (isVaultLock(stored)) {\n\t\t\tthrow new SelfstoreError(\n\t\t\t\t'PASSWORD_REQUIRED',\n\t\t\t\t'The identity vault is passphrase-protected. Call unlock(passphrase) - ' +\n\t\t\t\t\t'load()/loadOrCreate() never open or replace a locked identity.'\n\t\t\t);\n\t\t}\n\t\tif (!isEnvelope(stored)) return null;\n\t\treturn decode(await unseal(await vaultKey(), stored));\n\t}\n\n\tasync function save(identity: GroupIdentity): Promise<void> {\n\t\tif (isVaultLock(await kv.get(IDENTITY_KEY))) {\n\t\t\tthrow new SelfstoreError(\n\t\t\t\t'PASSWORD_REQUIRED',\n\t\t\t\t'The identity vault is passphrase-protected; save() never overwrites a locked ' +\n\t\t\t\t\t'identity. unprotect(passphrase) first, or clear() to forget it.'\n\t\t\t);\n\t\t}\n\t\tawait sealDevice(identity);\n\t}\n\n\tasync function unlock(passphrase: string): Promise<GroupIdentity> {\n\t\tconst stored = await kv.get(IDENTITY_KEY);\n\t\tif (!isVaultLock(stored)) {\n\t\t\tthrow new TypeError('unlock(): the vault is not passphrase-protected (check isProtected()).');\n\t\t}\n\t\tif (!stored.kdf || !isEnvelope(stored.envelope)) {\n\t\t\tthrow new SelfstoreError('BAD_FORMAT', 'Missing or malformed vault lock parameters.');\n\t\t}\n\t\t// Same read-side ceiling as backup decrypts: a tampered kv must not make\n\t\t// unlock allocate gigabytes (memory-hard bomb) before any passphrase check.\n\t\tassertKdfBounds(stored.kdf);\n\t\ttry {\n\t\t\treturn decode(await unseal(await deriveKey(passphrase, stored.kdf), stored.envelope));\n\t\t} catch (e) {\n\t\t\tif (e instanceof SelfstoreError) throw e;\n\t\t\tthrow new SelfstoreError('DECRYPT_FAILED', 'Wrong passphrase or tampered identity vault.');\n\t\t}\n\t}\n\n\tasync function protect(passphrase: string): Promise<void> {\n\t\tif (!passphrase) throw new TypeError('protect(): a non-empty passphrase is required.');\n\t\tconst stored = await kv.get(IDENTITY_KEY);\n\t\tif (isVaultLock(stored)) {\n\t\t\tthrow new SelfstoreError(\n\t\t\t\t'PASSWORD_REQUIRED',\n\t\t\t\t'The identity vault is already passphrase-protected. To change the passphrase: ' +\n\t\t\t\t\t'unprotect(oldPassphrase), then protect(newPassphrase).'\n\t\t\t);\n\t\t}\n\t\tconst identity = isEnvelope(stored)\n\t\t\t? decode(await unseal(await vaultKey(), stored)) // reseal the existing identity\n\t\t\t: await generateIdentity(); // empty vault: create-and-protect in one step\n\t\tconst kdf = freshKdfParams();\n\t\tconst envelope = await seal(await deriveKey(passphrase, kdf), encode(identity));\n\t\tawait kv.set(IDENTITY_KEY, { selfstoreVaultLock: 1, kdf, envelope } satisfies VaultLock);\n\t}\n\n\treturn {\n\t\tload,\n\t\tsave,\n\t\tunlock,\n\t\tprotect,\n\t\tasync loadOrCreate(): Promise<GroupIdentity> {\n\t\t\tconst existing = await load(); // PASSWORD_REQUIRED when protected: never mint over a lock\n\t\t\tif (existing) return existing;\n\t\t\tconst fresh = await generateIdentity();\n\t\t\tawait save(fresh);\n\t\t\treturn fresh;\n\t\t},\n\t\tasync clear(): Promise<void> {\n\t\t\tawait kv.del(IDENTITY_KEY);\n\t\t},\n\t\tasync isProtected(): Promise<boolean> {\n\t\t\treturn isVaultLock(await kv.get(IDENTITY_KEY));\n\t\t},\n\t\tasync unprotect(passphrase: string): Promise<void> {\n\t\t\t// unlock() enforces \"protected\" and the passphrase; only then rewrite.\n\t\t\tawait sealDevice(await unlock(passphrase));\n\t\t}\n\t};\n}\n"]}
1
+ {"version":3,"sources":["../src/persistence/identity.ts"],"names":[],"mappings":"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","file":"groups.js","sourcesContent":["// At-rest storage for a member identity (passwordless groups). Private keys\n// are sealed under a non-extractable device key in the same kv - defeats disk\n// forensics and partial exfiltration, not code running in the origin or a\n// copy of the whole profile (THREAT-MODEL.md).\n//\n// The opt-in passphrase lock reseals the identity under Argon2id(passphrase)\n// instead, closing the profile-copy gap at the cost of an unlock prompt. The\n// vault keeps no unlocked session state: unlock() returns the identity, the\n// caller holds it in memory, and at rest it stays sealed. A bare envelope\n// keeps meaning device mode, so pre-lock vaults load unchanged.\n//\n// The identity is one person's; move it between their devices over a trusted\n// channel (QR shown locally, encrypted note) - never through the group's\n// shared storage.\n\nimport { seal, unseal, isEnvelope, newDeviceKey, type EncEnvelope } from './cache-crypto';\nimport type { KV } from './cache';\nimport { generateIdentity, type GroupIdentity } from '../selfstore/group';\nimport { deriveKey, freshKdfParams, assertKdfBounds } from '../selfstore/crypto';\nimport type { KdfParams } from '../selfstore/types';\nimport { SelfstoreError } from '../selfstore/errors';\n\nconst VAULT_KEY = 'groupVaultKey';\nconst IDENTITY_KEY = 'groupIdentity';\n\nconst utf8 = { enc: new TextEncoder(), dec: new TextDecoder() };\n\n// Local kv bookkeeping only - not part of the interchange format (SPEC.md).\ninterface VaultLock {\n\tselfstoreVaultLock: 1;\n\tkdf: KdfParams;\n\tenvelope: EncEnvelope;\n}\n\nfunction isVaultLock(v: unknown): v is VaultLock {\n\treturn (\n\t\ttypeof v === 'object' &&\n\t\tv !== null &&\n\t\t(v as { selfstoreVaultLock?: unknown }).selfstoreVaultLock === 1\n\t);\n}\n\nconst encode = (identity: GroupIdentity): Uint8Array => utf8.enc.encode(JSON.stringify(identity));\nconst decode = (bytes: Uint8Array): GroupIdentity =>\n\tJSON.parse(utf8.dec.decode(bytes)) as GroupIdentity;\n\nexport interface IdentityVault {\n\t/** The stored identity, or null when none exists yet. On a protected\n\t * vault this throws PASSWORD_REQUIRED rather than returning null - a\n\t * locked identity exists, and null would invite loadOrCreate to mint a\n\t * second one over it. */\n\tload(): Promise<GroupIdentity | null>;\n\t/** Seal and store an identity, overwriting any previous one. Throws\n\t * PASSWORD_REQUIRED on a protected vault: a locked identity is never\n\t * silently replaced (unprotect() or clear() first). */\n\tsave(identity: GroupIdentity): Promise<void>;\n\t/** load(), creating and storing a fresh identity when absent. Same\n\t * PASSWORD_REQUIRED rule as load(). */\n\tloadOrCreate(): Promise<GroupIdentity>;\n\t/** Forget the identity on this device. Allowed even when protected -\n\t * forgetting leaks nothing, and it is the honest lost-passphrase escape\n\t * hatch (re-import from another device, or get re-invited). */\n\tclear(): Promise<void>;\n\tisProtected(): Promise<boolean>;\n\t/** Decrypt a protected vault and return the identity. Stateless: the\n\t * caller holds it in memory, the stored value stays sealed. Wrong\n\t * passphrase and tampered blob are one DECRYPT_FAILED; hostile KDF\n\t * params are refused before any derivation. */\n\tunlock(passphrase: string): Promise<GroupIdentity>;\n\t/** Reseal the identity under Argon2id(passphrase); an empty vault gets a\n\t * fresh identity, created-and-protected in one step. Changing the\n\t * passphrase is unprotect(old) then protect(new) - an already-protected\n\t * vault throws PASSWORD_REQUIRED, never a silent rekey. */\n\tprotect(passphrase: string): Promise<void>;\n\t/** Unlock with `passphrase`, then reseal under the device key. A wrong\n\t * passphrase throws DECRYPT_FAILED and leaves the vault untouched. */\n\tunprotect(passphrase: string): Promise<void>;\n}\n\n/** An identity vault over the app's kv (e.g. `indexedDbCache(app).kv`). */\nexport function identityVault(kv: KV): IdentityVault {\n\tasync function vaultKey(): Promise<CryptoKey> {\n\t\tconst existing = await kv.get<CryptoKey>(VAULT_KEY);\n\t\tif (existing) return existing;\n\t\tconst fresh = await newDeviceKey();\n\t\tawait kv.set(VAULT_KEY, fresh);\n\t\treturn fresh;\n\t}\n\n\t/** Device mode's write path: a bare envelope under the device key. */\n\tasync function sealDevice(identity: GroupIdentity): Promise<void> {\n\t\tawait kv.set(IDENTITY_KEY, await seal(await vaultKey(), encode(identity)));\n\t}\n\n\tasync function load(): Promise<GroupIdentity | null> {\n\t\tconst stored = await kv.get(IDENTITY_KEY);\n\t\tif (isVaultLock(stored)) {\n\t\t\tthrow new SelfstoreError(\n\t\t\t\t'PASSWORD_REQUIRED',\n\t\t\t\t'The identity vault is passphrase-protected. Call unlock(passphrase) - ' +\n\t\t\t\t\t'load()/loadOrCreate() never open or replace a locked identity.'\n\t\t\t);\n\t\t}\n\t\tif (!isEnvelope(stored)) return null;\n\t\treturn decode(await unseal(await vaultKey(), stored));\n\t}\n\n\tasync function save(identity: GroupIdentity): Promise<void> {\n\t\tif (isVaultLock(await kv.get(IDENTITY_KEY))) {\n\t\t\tthrow new SelfstoreError(\n\t\t\t\t'PASSWORD_REQUIRED',\n\t\t\t\t'The identity vault is passphrase-protected; save() never overwrites a locked ' +\n\t\t\t\t\t'identity. unprotect(passphrase) first, or clear() to forget it.'\n\t\t\t);\n\t\t}\n\t\tawait sealDevice(identity);\n\t}\n\n\tasync function unlock(passphrase: string): Promise<GroupIdentity> {\n\t\tconst stored = await kv.get(IDENTITY_KEY);\n\t\tif (!isVaultLock(stored)) {\n\t\t\tthrow new TypeError('unlock(): the vault is not passphrase-protected (check isProtected()).');\n\t\t}\n\t\tif (!stored.kdf || !isEnvelope(stored.envelope)) {\n\t\t\tthrow new SelfstoreError('BAD_FORMAT', 'Missing or malformed vault lock parameters.');\n\t\t}\n\t\t// Same read-side ceiling as backup decrypts: a tampered kv must not make\n\t\t// unlock allocate gigabytes (memory-hard bomb) before any passphrase check.\n\t\tassertKdfBounds(stored.kdf);\n\t\ttry {\n\t\t\treturn decode(await unseal(await deriveKey(passphrase, stored.kdf), stored.envelope));\n\t\t} catch (e) {\n\t\t\tif (e instanceof SelfstoreError) throw e;\n\t\t\tthrow new SelfstoreError('DECRYPT_FAILED', 'Wrong passphrase or tampered identity vault.');\n\t\t}\n\t}\n\n\tasync function protect(passphrase: string): Promise<void> {\n\t\tif (!passphrase) throw new TypeError('protect(): a non-empty passphrase is required.');\n\t\tconst stored = await kv.get(IDENTITY_KEY);\n\t\tif (isVaultLock(stored)) {\n\t\t\tthrow new SelfstoreError(\n\t\t\t\t'PASSWORD_REQUIRED',\n\t\t\t\t'The identity vault is already passphrase-protected. To change the passphrase: ' +\n\t\t\t\t\t'unprotect(oldPassphrase), then protect(newPassphrase).'\n\t\t\t);\n\t\t}\n\t\tconst identity = isEnvelope(stored)\n\t\t\t? decode(await unseal(await vaultKey(), stored)) // reseal the existing identity\n\t\t\t: await generateIdentity(); // empty vault: create-and-protect in one step\n\t\tconst kdf = freshKdfParams();\n\t\tconst envelope = await seal(await deriveKey(passphrase, kdf), encode(identity));\n\t\tawait kv.set(IDENTITY_KEY, { selfstoreVaultLock: 1, kdf, envelope } satisfies VaultLock);\n\t}\n\n\treturn {\n\t\tload,\n\t\tsave,\n\t\tunlock,\n\t\tprotect,\n\t\tasync loadOrCreate(): Promise<GroupIdentity> {\n\t\t\tconst existing = await load(); // PASSWORD_REQUIRED when protected: never mint over a lock\n\t\t\tif (existing) return existing;\n\t\t\tconst fresh = await generateIdentity();\n\t\t\tawait save(fresh);\n\t\t\treturn fresh;\n\t\t},\n\t\tasync clear(): Promise<void> {\n\t\t\tawait kv.del(IDENTITY_KEY);\n\t\t},\n\t\tasync isProtected(): Promise<boolean> {\n\t\t\treturn isVaultLock(await kv.get(IDENTITY_KEY));\n\t\t},\n\t\tasync unprotect(passphrase: string): Promise<void> {\n\t\t\t// unlock() enforces \"protected\" and the passphrase; only then rewrite.\n\t\t\tawait sealDevice(await unlock(passphrase));\n\t\t}\n\t};\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { B as BackupTarget, P as PeerSource, L as LocalStore } from './store-CIbiNoiE.js';
1
+ import { B as BackupTarget, P as PeerSource, L as LocalStore } from './store-CKGrIFQU.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,13 +1,14 @@
1
- import { o as Snapshot, E as EncodeOptions, H as Header, f as PasswordPolicy, p as SnapshotFile, B as BackupTarget, r as StatusDescriptor, S as StoreError, c as LocalStoreState, L as LocalStore } from './store-CIbiNoiE.js';
2
- export { d as Mode, e as PasswordCheck, g as PasswordRequirement, l as SelfstoreError, m as SelfstoreErrorCode, n as Severity, q as StatusAction, t as StorageState, T as TargetKind, x as checkPasswordPolicy, C as errorLabelKey, G as isSelfstoreError } from './store-CIbiNoiE.js';
3
- import { L as LocalCache, K as KV } from './cache-ilsK3Tki.js';
4
- import { SyncConfig } from './sync.js';
5
- import { R as ResumeOffer, C as ConnectTargets } from './connect-Bob2mofK.js';
6
- import { f as BackupsManager } from './manager-Di6lNE4E.js';
7
- import { a as DriveAuth } from './drive-86CHuTij.js';
8
- import { W as WebdavConfig, S as S3Config } from './s3-BsbVn3fw.js';
9
- export { f as changePassword, g as gisDriveAuth, p as pickFromDisk, s as saveToDisk } from './local-8H61XxbL.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-GdtB5IrG.js';
2
+ export { h as changePassword, g as gisDriveAuth, p as pickFromDisk, f as preloadGoogleIdentity, s as saveToDisk } from './local-ClXq1N2M.js';
3
+ export { a as DriveAuth } from './drive--mndByvF.js';
4
+ export { S as S3Config, W as WebdavConfig } from './s3-dG1SCnV1.js';
5
+ import { a as Snapshot, E as EncodeOptions, H as Header } from './store-CKGrIFQU.js';
6
+ 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-CKGrIFQU.js';
7
+ export { SyncConfig } from './sync.js';
8
+ import './cache-ilsK3Tki.js';
9
+ import './connect-DoB_DN__.js';
10
10
  import './machine-ALMkT3mk.js';
11
+ import './manager-yCM0M0FL.js';
11
12
 
12
13
  declare const BACKUP_EXTENSION = ".zip";
13
14
  declare const BACKUP_MIME = "application/zip";
@@ -96,262 +97,6 @@ declare class RestoreBuilder {
96
97
  read(): Promise<Snapshot>;
97
98
  }
98
99
 
99
- /** A record as the simple store sees it: plain JSON with a string id (the id
100
- * field name can be remapped per collection via `sync.ids`). */
101
- type SimpleRecord = Record<string, unknown>;
102
- /** What a file body may be handed in as. */
103
- type FileBytes = Uint8Array | ArrayBuffer | Blob;
104
- /** A file on its way into the store. Omit `id` to get the content hash, which
105
- * is the id that makes the multi-device union safe - see `putFile`. */
106
- interface PutFileInput {
107
- /** Defaults to the SHA-256 of the bytes. */
108
- id?: string;
109
- /** Defaults to the id. Cosmetic: nothing keys on it. */
110
- name?: string;
111
- /** Defaults to 'application/octet-stream'. */
112
- mime?: string;
113
- bytes: FileBytes;
114
- }
115
- /** Unlock callback for the ultra-sensitive cacheLock mode: return the session
116
- * secret - a password (Argon2id-derived) or an app-provided key (e.g. a passkey
117
- * PRF result). Called at boot; called again with { failed: true } after a wrong
118
- * secret so the app can re-prompt. Throw to abort the boot (user cancelled). */
119
- type CacheUnlock = (attempt: {
120
- failed: boolean;
121
- }) => Promise<string | CryptoKey>;
122
- /** Options - every one of them optional, the defaults carry a real app. */
123
- interface SimpleOptions {
124
- /** Your DATA schema version (default 1). Bump it together with `migrate`. */
125
- schema?: number;
126
- /** Upgrade a snapshot written by an older schema version. */
127
- migrate?: (fromVersion: number, snap: Snapshot) => Snapshot;
128
- /** Per-collection merge tuning (id field mapping, strategies). The default -
129
- * records keyed by `id`, later edit wins, deletes propagate - fits most apps. */
130
- sync?: SyncConfig;
131
- /**
132
- * Google Drive, when this app uses it. Two shapes for two situations:
133
- *
134
- * `{ clientId }` is the whole wiring - the library builds the browser
135
- * connection, keeps ONE session for the app (several would mean several
136
- * consent screens), reuses its token across a reload, remembers which
137
- * account holds the backup and hands that back to Google so the chooser
138
- * stops appearing. Nothing else to write.
139
- *
140
- * A `DriveAuth` instead, for an app that gets its tokens elsewhere - a
141
- * server broker holding a refresh token, a native shell.
142
- *
143
- * Either way, providing it here is also what lets a connected Drive backup
144
- * restore itself on the next start.
145
- */
146
- drive?: DriveAuth | {
147
- clientId: string;
148
- scope?: string;
149
- };
150
- /** Where the working copy lives (default: IndexedDB named after the app;
151
- * in-memory when IndexedDB does not exist, e.g. tests or SSR). */
152
- cache?: LocalCache;
153
- /** Auto-save debounce in ms. */
154
- debounceMs?: number;
155
- /** Cross-tab coordination (default on in browsers). */
156
- multiTab?: boolean;
157
- /** Refuse to ever write or export a plaintext backup: connecting a
158
- * destination then demands a password (or a group). Off by default. */
159
- requireEncryption?: boolean;
160
- /** Reject a backup password weaker than this policy (length, character
161
- * classes) at protect()/addKey time. Preview the rules with
162
- * checkPasswordPolicy for a live UI hint. Omit for no policy. */
163
- passwordPolicy?: PasswordPolicy;
164
- /** Ultra-sensitive mode: seal the local cache under a key derived from a
165
- * secret held in memory (this callback), never on disk - so a copy of the
166
- * browser profile cannot read it. One unlock per session; branch it on the
167
- * app's existing login to leave the UX unchanged. Browser (IndexedDB) only. */
168
- cacheLock?: CacheUnlock;
169
- /** Wire tab focus / network return / interval / tab hide automatically
170
- * (default true in browsers). Pass false to drive syncing yourself. */
171
- autoSync?: boolean;
172
- }
173
- /** What connecting a destination did. 'merged': it already held a backup and
174
- * both sides were folded together. 'started': it was empty, this device's data
175
- * is now its content. 'manual': non-Chromium file fallback - offer
176
- * downloadBackup() instead. 'cancelled': the user closed the picker/consent. */
177
- type ConnectOutcome = 'merged' | 'started' | 'manual' | 'cancelled';
178
- interface SimpleStore<S extends Record<string, SimpleRecord> = Record<string, SimpleRecord>> {
179
- /** Every record of a collection (treat as read-only; write via put/remove). */
180
- all<K extends keyof S & string>(collection: K): readonly S[K][];
181
- /** One record by id, or undefined. */
182
- get<K extends keyof S & string>(collection: K, id: string): S[K] | undefined;
183
- /** Insert or replace one record (auto-saves, debounced). Throws a TypeError
184
- * when the record has no non-empty string id. */
185
- put<K extends keyof S & string>(collection: K, record: S[K]): Promise<void>;
186
- /** Insert or replace many records in one save. */
187
- putAll<K extends keyof S & string>(collection: K, records: S[K][]): Promise<void>;
188
- /** Delete one record by id (propagates to other devices). Unknown id: no-op. */
189
- remove<K extends keyof S & string>(collection: K, id: string): Promise<void>;
190
- /** Empty a collection (every removal propagates). */
191
- clear<K extends keyof S & string>(collection: K): Promise<void>;
192
- /** Called after any data change: your writes, another tab, another device,
193
- * a restore. Returns an unsubscribe. */
194
- onChange(fn: () => void): () => void;
195
- /** Every file (treat as read-only; write via putFile/removeFile). */
196
- allFiles(): readonly SnapshotFile[];
197
- /** One file by id, or undefined. */
198
- getFile(id: string): SnapshotFile | undefined;
199
- /**
200
- * Store bytes alongside the collections and return the file's id.
201
- *
202
- * Files are **immutable per id**. The multi-device merge unions them by id
203
- * and has no clock to order two bodies, so when two devices hold DIFFERENT
204
- * bytes under one id, one is kept and the other is dropped - with no
205
- * conflict reported, because at that level there is nothing to compare.
206
- *
207
- * Hence the default id: the SHA-256 of the bytes. Different bytes land on
208
- * different ids and the union keeps both, which is what makes a CRDT
209
- * document (Yjs, Automerge) safe to carry here - store its UPDATES, let the
210
- * union collect them, fold them at load. Tie a file's lifetime to a record
211
- * so deleting the record tells you which files to remove.
212
- *
213
- * Naming the id yourself puts that rule in your hands: identical bytes
214
- * under an existing id are a no-op, different bytes are refused with a
215
- * TypeError. `{ replace: true }` does it anyway - right for a body only
216
- * ever written on one device (a thumbnail, a rendered export), a silent
217
- * loser as soon as two devices write it.
218
- */
219
- putFile(file: PutFileInput, opts?: {
220
- replace?: boolean;
221
- }): Promise<string>;
222
- /** Delete one file by id (propagates to other devices). Unknown id: no-op. */
223
- removeFile(id: string): Promise<void>;
224
- /** Connect Google Drive as the durable home. An existing backup there is
225
- * MERGED with this device (multi-device); an encrypted one needs its
226
- * password up front (PASSWORD_REQUIRED is thrown before anything changes). */
227
- connectDrive(auth: DriveAuth, opts?: {
228
- password?: string;
229
- }): Promise<ConnectOutcome>;
230
- /** Connect a disk file (File System Access; falls back to manual download). */
231
- connectFile(opts?: {
232
- password?: string;
233
- }): Promise<ConnectOutcome>;
234
- /** Connect a WebDAV server (Nextcloud, ownCloud, your own). */
235
- connectWebdav(config: WebdavConfig, opts?: {
236
- password?: string;
237
- }): Promise<ConnectOutcome>;
238
- /** Connect an S3-compatible bucket you control (Amazon S3, R2, B2, MinIO). */
239
- connectS3(config: S3Config, opts?: {
240
- password?: string;
241
- }): Promise<ConnectOutcome>;
242
- /** Connect any custom BackupTarget with the same merge semantics. */
243
- connectTarget(target: BackupTarget, opts?: {
244
- password?: string;
245
- }): Promise<ConnectOutcome>;
246
- /** Back to device-only (the destination keeps its last backup). */
247
- disconnect(): Promise<void>;
248
- /** Also write the same encrypted backup to this second destination on
249
- * every save. A broken copy never gates the store (state.replicas). */
250
- addReplica(target: BackupTarget, opts?: {
251
- id?: string;
252
- }): string;
253
- /** Stop writing the copy (the destination keeps its last backup). */
254
- removeReplica(id: string): void;
255
- /** Encrypt the durable backup end to end with this password (reversible). */
256
- protect(password: string): Promise<void>;
257
- /** Remove the backup password. */
258
- unprotect(): Promise<void>;
259
- /** status.action === 'unlock': supply the password to resume. */
260
- unlock(password: string): Promise<boolean>;
261
- /** status.action === 'reconnect': re-run the destination's auth gesture. */
262
- reconnect(): Promise<boolean>;
263
- /** The portable backup file (a real ZIP; encrypted when protect() is on).
264
- *
265
- * `{ plaintext: true }` asks for a READABLE copy of a protected store: the
266
- * same ZIP with `selfstore.json` in the clear, which any tool can open and
267
- * git can diff. It is the answer to "can I actually get my data out", and
268
- * it costs nothing: the store, its password and its destination are
269
- * untouched, so the real backup stays encrypted. Before this, the only way
270
- * out was `unprotect()`, which rewrites the destination and leaves it
271
- * readable until you remember to undo it.
272
- *
273
- * Refused while locked (there is nothing readable to write), and refused on
274
- * a store created with `requireEncryption` - that flag means this store
275
- * never produces cleartext, and an escape hatch would make it a lie. */
276
- exportBackup(opts?: {
277
- plaintext?: boolean;
278
- }): Promise<Blob>;
279
- /** Download the backup (also resolves the 'manual' file mode's pending flag).
280
- * False when the user closed the save dialog: nothing was written, the
281
- * pending flag stands, and the app must not record a backup. */
282
- downloadBackup(filename?: string): Promise<boolean>;
283
- /** Load a backup file into this store, replacing the local data (removals
284
- * propagate like edits). Throws PASSWORD_REQUIRED / DECRYPT_FAILED. */
285
- importBackup(file: Blob | Uint8Array, opts?: {
286
- password?: string;
287
- }): Promise<void>;
288
- /** Headless status: { state, severity, action, labelKey } - map labelKey to
289
- * your own copy. */
290
- readonly status: StatusDescriptor;
291
- /** The last problem: { code, labelKey, message } - show labelKey, log message. */
292
- readonly error: StoreError | null;
293
- /** The full underlying state (advanced reads: journal, peers, mode...). */
294
- readonly state: LocalStoreState;
295
- /** Fires on any state change (status flips included) - for framework bindings. */
296
- subscribe(fn: () => void): () => void;
297
- /** Save now (called for you on tab hide when autoSync is on). */
298
- flush(): Promise<void>;
299
- /** Converge with the destination now (a user gesture like pull-to-refresh). */
300
- sync(): Promise<void>;
301
- /** Drop timers and listeners (tests, SPA teardown). */
302
- dispose(): void;
303
- /** The escape hatch: the full advanced store this simple one is built on -
304
- * peers, groups, custom strategies, everything (see selfstore/advanced). */
305
- readonly advanced: LocalStore;
306
- /** The attachment point for 'selfstore/flows': the engine plus the cache KV
307
- * and backup file name a flow needs to build destination targets. Apps on
308
- * the advanced store hand a flow the same three by themselves. */
309
- readonly flowHost: {
310
- engine: LocalStore;
311
- kv: KV;
312
- backupName: string;
313
- };
314
- /** Who holds the destination, when it is known - the Google address behind a
315
- * Drive backup. Learned on connect and remembered across sessions, so a
316
- * surface can NAME the destination ("Google Drive" is not an address) and a
317
- * re-grant can skip the account chooser. Null for a file, or before a first
318
- * connect. Reading it costs nothing: no call goes out. */
319
- readonly account: string | null;
320
- /** The destination this device already used, ready to hand to a connect flow
321
- * as `options.resume` - so a returning visitor is offered "reopen my backup"
322
- * ahead of the plain destinations, instead of re-deriving a decision the
323
- * library already has on file. Null when there is nothing to resume. */
324
- resumeOffer(): ResumeOffer | null;
325
- /**
326
- * The several-backups manager for this store's destination, or null when
327
- * there is nothing to manage several of (no Drive configured).
328
- *
329
- * The port under it - list, open by id, create, rename, delete - was the last
330
- * thing an app still assembled from the session the store had built, and the
331
- * one place it could pick the wrong file id. Built once and remembered; the
332
- * backups module is imported on demand, so an app that never asks does not
333
- * carry it.
334
- */
335
- backups(): Promise<BackupsManager | null>;
336
- /**
337
- * The destinations this store can offer, ready for a connect flow or widget:
338
- * Drive first when the app configured it, a file always.
339
- *
340
- * Consumers were assembling this by hand from the very session the store
341
- * built - and the order and the "no client id means no Drive card" rule were
342
- * rediscovered each time. An app wanting a different offer (a WebDAV server,
343
- * its own target) still writes its own object; this is the default, not a
344
- * ceiling.
345
- */
346
- destinations(): ConnectTargets;
347
- }
348
- /**
349
- * Open (or create) the app's local store. Awaits the initial load, so the
350
- * returned store is ready: data readable, destination restored, first converge
351
- * done. See SimpleOptions for the defaults.
352
- */
353
- declare function selfstore<S extends Record<string, SimpleRecord> = Record<string, SimpleRecord>>(app: string, options?: SimpleOptions): Promise<SimpleStore<S>>;
354
-
355
100
  /**
356
101
  * Disk-file backup target for a desktop shell (Tauri, and anything exposing the
357
102
  * same filesystem and dialog calls).
@@ -425,4 +170,4 @@ declare function useDesktopFiles(b: DesktopFileBridge | null): void;
425
170
  * real path instead of asking the browser for a handle. */
426
171
  declare function hasDesktopFiles(): boolean;
427
172
 
428
- export { BACKUP_EXTENSION, BACKUP_MIME, BackupBuilder, BackupDraft, type CacheUnlock, type ConnectOutcome, type DesktopDialogFilter, type DesktopFileBridge, DriveAuth, EncryptedBackupBuilder, type FileBytes, Header, LocalStore, LocalStoreState, PasswordPolicy, type PutFileInput, RestoreBuilder, S3Config, type SimpleOptions, type SimpleRecord, type SimpleStore, Snapshot, SnapshotFile, StatusDescriptor, StoreError, SyncConfig, WebdavConfig, backup, hasDesktopFiles, restore, selfstore, useDesktopFiles };
173
+ export { BACKUP_EXTENSION, BACKUP_MIME, BackupBuilder, BackupDraft, type DesktopDialogFilter, type DesktopFileBridge, EncryptedBackupBuilder, Header, RestoreBuilder, Snapshot, backup, hasDesktopFiles, restore, useDesktopFiles };
package/dist/index.js CHANGED
@@ -1,22 +1,24 @@
1
- import { indexedDbCache, memoryCache, gisDriveAuth, isLockableCache, createLocalStore } from './chunk-DJZ5S3FI.js';
2
- export { gisDriveAuth } from './chunk-DJZ5S3FI.js';
3
- import { saveToDisk } from './chunk-AUH35AAQ.js';
4
- export { pickFromDisk, saveToDisk } from './chunk-AUH35AAQ.js';
5
- import { sha256Hex } from './chunk-QSLHEE2L.js';
6
- import { webdavFromSession, s3Connect, webdavConnect } from './chunk-3BWBBGFW.js';
7
- import { fromSession as fromSession$1, isSupported, connect } from './chunk-GW5CYP4C.js';
8
- export { hasDesktopFiles, useDesktopFiles } from './chunk-GW5CYP4C.js';
9
- import { fromSession, connect as connect$1, account } from './chunk-JTNFZV2B.js';
10
- import './chunk-757ZOSUI.js';
11
- import { restore } from './chunk-HHPJPEEG.js';
12
- export { BACKUP_EXTENSION, BACKUP_MIME, backup, changePassword, checkPasswordPolicy, restore } from './chunk-HHPJPEEG.js';
13
- import './chunk-QBCU7OOP.js';
14
- import './chunk-OY2A5DCB.js';
15
- import './chunk-RREJ2MFN.js';
16
- import './chunk-23UAUYSW.js';
1
+ import { indexedDbCache, memoryCache, gisDriveAuth, isLockableCache, createLocalStore } from './chunk-AZWS7YC3.js';
2
+ export { gisDriveAuth, preloadGoogleIdentity } from './chunk-AZWS7YC3.js';
3
+ import './chunk-GSZSZGSD.js';
4
+ import { saveToDisk } from './chunk-LQS7AMOD.js';
5
+ export { pickFromDisk, saveToDisk } from './chunk-LQS7AMOD.js';
17
6
  import './chunk-FJGWTBAL.js';
18
- import { SelfstoreError } from './chunk-XMC67RNM.js';
19
- export { SelfstoreError, errorLabelKey, isSelfstoreError } from './chunk-XMC67RNM.js';
7
+ import { webdavFromSession, s3Connect, webdavConnect } from './chunk-FQ5O4PHK.js';
8
+ import { fromSession as fromSession$1, isSupported, connect } from './chunk-2TIRUEXR.js';
9
+ export { hasDesktopFiles, useDesktopFiles } from './chunk-2TIRUEXR.js';
10
+ import { fromSession, connect as connect$1, account } from './chunk-25U6AKIX.js';
11
+ import './chunk-LF3CC2P3.js';
12
+ import { restore } from './chunk-RVOBONZF.js';
13
+ export { BACKUP_EXTENSION, BACKUP_MIME, backup, changePassword, checkPasswordPolicy, restore } from './chunk-RVOBONZF.js';
14
+ import './chunk-QBCU7OOP.js';
15
+ import './chunk-F4ZZEJ5H.js';
16
+ import './chunk-5UYPKCDB.js';
17
+ import { toBytes } from './chunk-SJ3PKQPR.js';
18
+ import { sha256Hex } from './chunk-QSLHEE2L.js';
19
+ import './chunk-WXI2NAPA.js';
20
+ import { SelfstoreError } from './chunk-CSYEIXNY.js';
21
+ export { SelfstoreError, errorLabelKey, isSelfstoreError } from './chunk-CSYEIXNY.js';
20
22
 
21
23
  // src/simple/simple.ts
22
24
  function idFieldOf(sync, collection) {
@@ -34,11 +36,6 @@ function requireStringId(record, field, collection) {
34
36
  }
35
37
  return id;
36
38
  }
37
- async function toBytes(input) {
38
- if (input instanceof Uint8Array) return input;
39
- if (input instanceof ArrayBuffer) return new Uint8Array(input);
40
- return new Uint8Array(await input.arrayBuffer());
41
- }
42
39
  function sameBytes(a, b) {
43
40
  return a.length === b.length && a.every((v, i) => v === b[i]);
44
41
  }
@@ -368,7 +365,7 @@ async function selfstore(app, options = {}) {
368
365
  async backups() {
369
366
  if (!driveAuth) return null;
370
367
  backupsManager ??= await (async () => {
371
- const { createBackupsManager, driveBackupsHost } = await import('./manager-drive-YLSLQ63O.js');
368
+ const { createBackupsManager, driveBackupsHost } = await import('./manager-drive-2IUE4WQ2.js');
372
369
  return createBackupsManager({
373
370
  store,
374
371
  kv: cache.kv,