selfstore 1.8.19 → 1.8.21

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 (89) hide show
  1. package/README.md +107 -12
  2. package/dist/advanced.d.ts +23 -8
  3. package/dist/advanced.js +23 -12
  4. package/dist/advanced.js.map +1 -1
  5. package/dist/backups.d.ts +4 -4
  6. package/dist/backups.js +8 -6
  7. package/dist/{cache-lock-KWJUORTS.js → cache-lock-5RNG6CIK.js} +4 -3
  8. package/dist/cache-lock-5RNG6CIK.js.map +1 -0
  9. package/dist/{chunk-SKWHKM25.js → chunk-23UAUYSW.js} +40 -4
  10. package/dist/chunk-23UAUYSW.js.map +1 -0
  11. package/dist/chunk-3BWBBGFW.js +29 -0
  12. package/dist/chunk-3BWBBGFW.js.map +1 -0
  13. package/dist/{chunk-W22V5CRM.js → chunk-3KAAPYPO.js} +3 -3
  14. package/dist/{chunk-W22V5CRM.js.map → chunk-3KAAPYPO.js.map} +1 -1
  15. package/dist/chunk-757ZOSUI.js +27 -0
  16. package/dist/chunk-757ZOSUI.js.map +1 -0
  17. package/dist/{chunk-DCEXWSM6.js → chunk-AUH35AAQ.js} +3 -3
  18. package/dist/{chunk-DCEXWSM6.js.map → chunk-AUH35AAQ.js.map} +1 -1
  19. package/dist/{chunk-TQH7ZYID.js → chunk-DJZ5S3FI.js} +31 -14
  20. package/dist/chunk-DJZ5S3FI.js.map +1 -0
  21. package/dist/{chunk-HOG2IKEP.js → chunk-EBCQ7ZG7.js} +4 -4
  22. package/dist/{chunk-HOG2IKEP.js.map → chunk-EBCQ7ZG7.js.map} +1 -1
  23. package/dist/chunk-FJGWTBAL.js +6 -0
  24. package/dist/chunk-FJGWTBAL.js.map +1 -0
  25. package/dist/chunk-GW5CYP4C.js +212 -0
  26. package/dist/chunk-GW5CYP4C.js.map +1 -0
  27. package/dist/{chunk-MGDXR3RB.js → chunk-HHPJPEEG.js} +6 -5
  28. package/dist/chunk-HHPJPEEG.js.map +1 -0
  29. package/dist/{chunk-J5MFXJU6.js → chunk-JTNFZV2B.js} +78 -26
  30. package/dist/chunk-JTNFZV2B.js.map +1 -0
  31. package/dist/chunk-JZHWAX34.js +178 -0
  32. package/dist/chunk-JZHWAX34.js.map +1 -0
  33. package/dist/chunk-LUNYYH6D.js +223 -0
  34. package/dist/chunk-LUNYYH6D.js.map +1 -0
  35. package/dist/{chunk-LKIRWVXM.js → chunk-LYO36VYB.js} +12 -11
  36. package/dist/chunk-LYO36VYB.js.map +1 -0
  37. package/dist/{chunk-O3Y6FORN.js → chunk-OY2A5DCB.js} +4 -3
  38. package/dist/chunk-OY2A5DCB.js.map +1 -0
  39. package/dist/chunk-QSLHEE2L.js +13 -0
  40. package/dist/chunk-QSLHEE2L.js.map +1 -0
  41. package/dist/{chunk-3OBJOMQ2.js → chunk-RREJ2MFN.js} +4 -28
  42. package/dist/chunk-RREJ2MFN.js.map +1 -0
  43. package/dist/chunk-XMC67RNM.js +29 -0
  44. package/dist/chunk-XMC67RNM.js.map +1 -0
  45. package/dist/{connect-BTVzRJY8.d.ts → connect-Bob2mofK.d.ts} +3 -3
  46. package/dist/{drive-CY_Vsj7V.d.ts → drive-86CHuTij.d.ts} +63 -2
  47. package/dist/flows.d.ts +5 -5
  48. package/dist/flows.js +17 -15
  49. package/dist/flows.js.map +1 -1
  50. package/dist/groups.js +4 -3
  51. package/dist/groups.js.map +1 -1
  52. package/dist/households.d.ts +1 -1
  53. package/dist/index.d.ts +66 -10
  54. package/dist/index.js +73 -18
  55. package/dist/index.js.map +1 -1
  56. package/dist/kdf-worker.js +7 -2
  57. package/dist/kdf-worker.js.map +1 -1
  58. package/dist/{local-Dh6eGiK9.d.ts → local-8H61XxbL.d.ts} +2 -2
  59. package/dist/local-XET4G32B.js +7 -0
  60. package/dist/{local-DPS35F5C.js.map → local-XET4G32B.js.map} +1 -1
  61. package/dist/{manager-DhoIbJyg.d.ts → manager-Di6lNE4E.d.ts} +1 -1
  62. package/dist/manager-drive-YLSLQ63O.js +10 -0
  63. package/dist/{manager-drive-4WLKOOXO.js.map → manager-drive-YLSLQ63O.js.map} +1 -1
  64. package/dist/s3-43RWNCV3.js +11 -0
  65. package/dist/s3-43RWNCV3.js.map +1 -0
  66. package/dist/{s3-0qqIyphl.d.ts → s3-BsbVn3fw.d.ts} +1 -1
  67. package/dist/{store-Cs6QIzzG.d.ts → store-CIbiNoiE.d.ts} +11 -3
  68. package/dist/sync.d.ts +7 -6
  69. package/dist/sync.js +3 -1
  70. package/dist/webdav-EEQKX5TZ.js +9 -0
  71. package/dist/webdav-EEQKX5TZ.js.map +1 -0
  72. package/dist/widgets.d.ts +51 -10
  73. package/dist/widgets.js +134 -34
  74. package/dist/widgets.js.map +1 -1
  75. package/llms.txt +51 -4
  76. package/package.json +12 -5
  77. package/skills/selfstore/SKILL.md +115 -0
  78. package/dist/cache-lock-KWJUORTS.js.map +0 -1
  79. package/dist/chunk-3OBJOMQ2.js.map +0 -1
  80. package/dist/chunk-J5MFXJU6.js.map +0 -1
  81. package/dist/chunk-LKIRWVXM.js.map +0 -1
  82. package/dist/chunk-MGDXR3RB.js.map +0 -1
  83. package/dist/chunk-O3Y6FORN.js.map +0 -1
  84. package/dist/chunk-SKWHKM25.js.map +0 -1
  85. package/dist/chunk-TKWU52LK.js +0 -607
  86. package/dist/chunk-TKWU52LK.js.map +0 -1
  87. package/dist/chunk-TQH7ZYID.js.map +0 -1
  88. package/dist/local-DPS35F5C.js +0 -6
  89. package/dist/manager-drive-4WLKOOXO.js +0 -8
@@ -0,0 +1,29 @@
1
+ import { SelfstoreError } from './chunk-XMC67RNM.js';
2
+
3
+ // src/persistence/targets/servers.ts
4
+ async function load(what, mod) {
5
+ try {
6
+ return await mod();
7
+ } catch (e) {
8
+ throw new SelfstoreError(
9
+ "TARGET_UNAVAILABLE",
10
+ `The ${what} support could not be loaded (${e instanceof Error ? e.message : String(e)}).`
11
+ );
12
+ }
13
+ }
14
+ async function webdavConnect(opts) {
15
+ return (await load("WebDAV", () => import('./webdav-EEQKX5TZ.js'))).connect(opts);
16
+ }
17
+ async function webdavFromSession(opts) {
18
+ return (await load("WebDAV", () => import('./webdav-EEQKX5TZ.js'))).fromSession(opts);
19
+ }
20
+ async function s3Connect(opts) {
21
+ return (await load("S3", () => import('./s3-43RWNCV3.js'))).connect(opts);
22
+ }
23
+ async function s3FromSession(opts) {
24
+ return (await load("S3", () => import('./s3-43RWNCV3.js'))).fromSession(opts);
25
+ }
26
+
27
+ export { s3Connect, s3FromSession, webdavConnect, webdavFromSession };
28
+ //# sourceMappingURL=chunk-3BWBBGFW.js.map
29
+ //# sourceMappingURL=chunk-3BWBBGFW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/persistence/targets/servers.ts"],"names":[],"mappings":";;;AA4BA,eAAe,IAAA,CAAQ,MAAc,GAAA,EAAmC;AACvE,EAAA,IAAI;AACH,IAAA,OAAO,MAAM,GAAA,EAAI;AAAA,EAClB,SAAS,CAAA,EAAG;AACX,IAAA,MAAM,IAAI,cAAA;AAAA,MACT,oBAAA;AAAA,MACA,CAAA,IAAA,EAAO,IAAI,CAAA,8BAAA,EAAiC,CAAA,YAAa,QAAQ,CAAA,CAAE,OAAA,GAAU,MAAA,CAAO,CAAC,CAAC,CAAA,EAAA;AAAA,KACvF;AAAA,EACD;AACD;AAEA,eAAsB,cAAc,IAAA,EAA0D;AAC7F,EAAA,OAAA,CAAQ,MAAM,KAAK,QAAA,EAAU,MAAM,OAAO,sBAAU,CAAC,CAAA,EAAG,OAAA,CAAQ,IAAI,CAAA;AACrE;AAEA,eAAsB,kBAAkB,IAAA,EAAgD;AACvF,EAAA,OAAA,CAAQ,MAAM,KAAK,QAAA,EAAU,MAAM,OAAO,sBAAU,CAAC,CAAA,EAAG,WAAA,CAAY,IAAI,CAAA;AACzE;AAEA,eAAsB,UAAU,IAAA,EAAsD;AACrF,EAAA,OAAA,CAAQ,MAAM,KAAK,IAAA,EAAM,MAAM,OAAO,kBAAM,CAAC,CAAA,EAAG,OAAA,CAAQ,IAAI,CAAA;AAC7D;AAEA,eAAsB,cAAc,IAAA,EAAgD;AACnF,EAAA,OAAA,CAAQ,MAAM,KAAK,IAAA,EAAM,MAAM,OAAO,kBAAM,CAAC,CAAA,EAAG,WAAA,CAAY,IAAI,CAAA;AACjE","file":"chunk-3BWBBGFW.js","sourcesContent":["// The two destinations that reach a server the user runs - WebDAV and an\n// S3-compatible bucket - loaded on the gesture that uses them instead of with\n// the module that offers them.\n//\n// Why these two and not all four: Drive and the disk file both open a picker or\n// a consent popup, and a browser only allows that inside the transient\n// activation of the user's click. An `await import()` before the call can spend\n// that activation and turn a working button into a silently blocked popup. A\n// WebDAV or S3 connection is plain fetch behind a form the user already\n// submitted - nothing to preserve, so the chunk can arrive late.\n//\n// What it buys: an app that offers Drive and a disk file - the common case -\n// stops shipping the SigV4 request signer and the WebDAV client. Before this,\n// importing the connect journey (so: any of the widgets) pulled both in whether\n// or not the app named them, which made \"destinations are opt-in\" true of the\n// API and false of the bytes.\n//\n// `selfstore/advanced` keeps its STATIC `webdavTarget` / `s3Target`: naming one\n// of those is the opt-in, and an app that wrote the import wants the code.\n\nimport { SelfstoreError } from '../../selfstore/errors';\nimport type { BackupTarget } from '../target';\nimport type { KV } from '../cache';\nimport type { WebdavConnectOptions } from './webdav';\nimport type { S3ConnectOptions } from './s3';\n\n/** A chunk that does not arrive is the destination not answering: same code,\n * same retry, rather than a bare Error escaping a public path. */\nasync function load<T>(what: string, mod: () => Promise<T>): Promise<T> {\n\ttry {\n\t\treturn await mod();\n\t} catch (e) {\n\t\tthrow new SelfstoreError(\n\t\t\t'TARGET_UNAVAILABLE',\n\t\t\t`The ${what} support could not be loaded (${e instanceof Error ? e.message : String(e)}).`\n\t\t);\n\t}\n}\n\nexport async function webdavConnect(opts: WebdavConnectOptions): Promise<BackupTarget | null> {\n\treturn (await load('WebDAV', () => import('./webdav'))).connect(opts);\n}\n\nexport async function webdavFromSession(opts: { kv: KV }): Promise<BackupTarget | null> {\n\treturn (await load('WebDAV', () => import('./webdav'))).fromSession(opts);\n}\n\nexport async function s3Connect(opts: S3ConnectOptions): Promise<BackupTarget | null> {\n\treturn (await load('S3', () => import('./s3'))).connect(opts);\n}\n\nexport async function s3FromSession(opts: { kv: KV }): Promise<BackupTarget | null> {\n\treturn (await load('S3', () => import('./s3'))).fromSession(opts);\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { SelfstoreError, isSelfstoreError, errorLabelKey } from './chunk-3OBJOMQ2.js';
1
+ import { SelfstoreError, isSelfstoreError, errorLabelKey } from './chunk-XMC67RNM.js';
2
2
 
3
3
  // src/flows/machine.ts
4
4
  function makeMachine(initial) {
@@ -42,5 +42,5 @@ function toStoreError(e) {
42
42
  }
43
43
 
44
44
  export { makeMachine, toStoreError, withDeadline };
45
- //# sourceMappingURL=chunk-W22V5CRM.js.map
46
- //# sourceMappingURL=chunk-W22V5CRM.js.map
45
+ //# sourceMappingURL=chunk-3KAAPYPO.js.map
46
+ //# sourceMappingURL=chunk-3KAAPYPO.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/flows/machine.ts"],"names":[],"mappings":";;;AAuBO,SAAS,YAA8B,OAAA,EAAwB;AACrE,EAAA,IAAI,QAAA,GAAW,OAAA;AACf,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAoB;AAC1C,EAAA,OAAO;AAAA,IACN,IAAI,QAAA,GAAc;AACjB,MAAA,OAAO,QAAA;AAAA,IACR,CAAA;AAAA,IACA,IAAI,KAAA,EAAyB;AAC5B,MAAA,QAAA,GAAW,EAAE,GAAG,QAAA,EAAU,GAAG,KAAA,EAAM;AACnC,MAAA,SAAA,CAAU,OAAA,CAAQ,CAAC,EAAA,KAAO,EAAA,CAAG,QAAQ,CAAC,CAAA;AAAA,IACvC,CAAA;AAAA,IACA,UAAU,QAAA,EAAsC;AAC/C,MAAA,SAAA,CAAU,IAAI,QAAQ,CAAA;AACtB,MAAA,QAAA,CAAS,QAAQ,CAAA;AACjB,MAAA,OAAO,MAAM,SAAA,CAAU,MAAA,CAAO,QAAQ,CAAA;AAAA,IACvC;AAAA,GACD;AACD;AAOA,eAAsB,YAAA,CAAgB,IAAA,EAAkB,EAAA,EAAY,IAAA,EAA0B;AAC7F,EAAA,IAAI,KAAA;AACJ,EAAA,MAAM,IAAA,GAAO,IAAI,OAAA,CAAe,CAAC,UAAU,MAAA,KAAW;AACrD,IAAA,KAAA,GAAQ,UAAA;AAAA,MACP,MACC,MAAA,CAAO,IAAI,cAAA,CAAe,oBAAA,EAAsB,GAAG,IAAI,CAAA,uBAAA,EAA0B,EAAE,CAAA,GAAA,CAAK,CAAC,CAAA;AAAA,MAC1F;AAAA,KACD;AAAA,EACD,CAAC,CAAA;AACD,EAAA,IAAI;AACH,IAAA,OAAO,MAAM,OAAA,CAAQ,IAAA,CAAK,CAAC,IAAA,EAAM,IAAI,CAAC,CAAA;AAAA,EACvC,CAAA,SAAE;AACD,IAAA,YAAA,CAAa,KAAK,CAAA;AAAA,EACnB;AACD;AAMO,SAAS,aAAa,CAAA,EAAwB;AACpD,EAAA,IAAI,gBAAA,CAAiB,CAAC,CAAA,EAAG;AACxB,IAAA,OAAO,EAAE,IAAA,EAAM,CAAA,CAAE,IAAA,EAAM,QAAA,EAAU,aAAA,CAAc,CAAA,CAAE,IAAI,CAAA,EAAG,OAAA,EAAS,CAAA,CAAE,OAAA,EAAQ;AAAA,EAC5E;AACA,EAAA,MAAM,UAAU,CAAA,YAAa,KAAA,GAAQ,CAAA,CAAE,OAAA,GAAU,OAAO,CAAC,CAAA;AACzD,EAAA,OAAO,EAAE,IAAA,EAAM,oBAAA,EAAsB,UAAU,aAAA,CAAc,oBAAoB,GAAG,OAAA,EAAQ;AAC7F","file":"chunk-W22V5CRM.js","sourcesContent":["// Shared plumbing for the flows: a tiny observable snapshot (fits the\n// Svelte store contract and React's useSyncExternalStore), a deadline\n// wrapper for network legs, and the normalizer that turns anything thrown\n// into the stable { code, labelKey, message } shape.\n\nimport type { StoreError } from '../persistence/store';\nimport { SelfstoreError, errorLabelKey, isSelfstoreError } from '../selfstore/errors';\n\n/** What every flow exposes: the current snapshot plus a subscription.\n * `subscribe` calls the listener IMMEDIATELY with the current snapshot and on\n * every change after - that is the Svelte store contract, so a flow works as\n * `$flow` in Svelte; React reads `useSyncExternalStore(flow.subscribe, () =>\n * flow.snapshot)`; everyone else just re-renders in the callback. */\nexport interface FlowStore<T> {\n\treadonly snapshot: T;\n\tsubscribe(listener: (snapshot: T) => void): () => void;\n}\n\nexport interface Machine<T> extends FlowStore<T> {\n\t/** Merge a partial into the snapshot and notify. */\n\tset(patch: Partial<T>): void;\n}\n\nexport function makeMachine<T extends object>(initial: T): Machine<T> {\n\tlet snapshot = initial;\n\tconst listeners = new Set<(s: T) => void>();\n\treturn {\n\t\tget snapshot(): T {\n\t\t\treturn snapshot;\n\t\t},\n\t\tset(patch: Partial<T>): void {\n\t\t\tsnapshot = { ...snapshot, ...patch };\n\t\t\tlisteners.forEach((fn) => fn(snapshot));\n\t\t},\n\t\tsubscribe(listener: (s: T) => void): () => void {\n\t\t\tlisteners.add(listener);\n\t\t\tlistener(snapshot);\n\t\t\treturn () => listeners.delete(listener);\n\t\t}\n\t};\n}\n\n/** Bound a network leg so a stuck destination surfaces as a retryable error\n * instead of a flow frozen on a spinner. The rejection is TARGET_UNAVAILABLE -\n * transient by contract - because \"did not answer in time\" says nothing about\n * access being lost. Never wrap a human step (a consent popup, a picker):\n * people are allowed to be slower than a deadline. */\nexport async function withDeadline<T>(work: Promise<T>, ms: number, what: string): Promise<T> {\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\tconst gate = new Promise<never>((_resolve, reject) => {\n\t\ttimer = setTimeout(\n\t\t\t() =>\n\t\t\t\treject(new SelfstoreError('TARGET_UNAVAILABLE', `${what} did not answer within ${ms}ms.`)),\n\t\t\tms\n\t\t);\n\t});\n\ttry {\n\t\treturn await Promise.race([work, gate]);\n\t} finally {\n\t\tclearTimeout(timer);\n\t}\n}\n\n/** Normalize anything thrown into the stable StoreError shape the whole\n * library reports with: show `labelKey`, log `message`. Unknown errors map to\n * TARGET_UNAVAILABLE (transient, retryable) - the flow never invents new\n * codes for the app to branch on. */\nexport function toStoreError(e: unknown): StoreError {\n\tif (isSelfstoreError(e)) {\n\t\treturn { code: e.code, labelKey: errorLabelKey(e.code), message: e.message };\n\t}\n\tconst message = e instanceof Error ? e.message : String(e);\n\treturn { code: 'TARGET_UNAVAILABLE', labelKey: errorLabelKey('TARGET_UNAVAILABLE'), message };\n}\n"]}
1
+ {"version":3,"sources":["../src/flows/machine.ts"],"names":[],"mappings":";;;AAuBO,SAAS,YAA8B,OAAA,EAAwB;AACrE,EAAA,IAAI,QAAA,GAAW,OAAA;AACf,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAoB;AAC1C,EAAA,OAAO;AAAA,IACN,IAAI,QAAA,GAAc;AACjB,MAAA,OAAO,QAAA;AAAA,IACR,CAAA;AAAA,IACA,IAAI,KAAA,EAAyB;AAC5B,MAAA,QAAA,GAAW,EAAE,GAAG,QAAA,EAAU,GAAG,KAAA,EAAM;AACnC,MAAA,SAAA,CAAU,OAAA,CAAQ,CAAC,EAAA,KAAO,EAAA,CAAG,QAAQ,CAAC,CAAA;AAAA,IACvC,CAAA;AAAA,IACA,UAAU,QAAA,EAAsC;AAC/C,MAAA,SAAA,CAAU,IAAI,QAAQ,CAAA;AACtB,MAAA,QAAA,CAAS,QAAQ,CAAA;AACjB,MAAA,OAAO,MAAM,SAAA,CAAU,MAAA,CAAO,QAAQ,CAAA;AAAA,IACvC;AAAA,GACD;AACD;AAOA,eAAsB,YAAA,CAAgB,IAAA,EAAkB,EAAA,EAAY,IAAA,EAA0B;AAC7F,EAAA,IAAI,KAAA;AACJ,EAAA,MAAM,IAAA,GAAO,IAAI,OAAA,CAAe,CAAC,UAAU,MAAA,KAAW;AACrD,IAAA,KAAA,GAAQ,UAAA;AAAA,MACP,MACC,MAAA,CAAO,IAAI,cAAA,CAAe,oBAAA,EAAsB,GAAG,IAAI,CAAA,uBAAA,EAA0B,EAAE,CAAA,GAAA,CAAK,CAAC,CAAA;AAAA,MAC1F;AAAA,KACD;AAAA,EACD,CAAC,CAAA;AACD,EAAA,IAAI;AACH,IAAA,OAAO,MAAM,OAAA,CAAQ,IAAA,CAAK,CAAC,IAAA,EAAM,IAAI,CAAC,CAAA;AAAA,EACvC,CAAA,SAAE;AACD,IAAA,YAAA,CAAa,KAAK,CAAA;AAAA,EACnB;AACD;AAMO,SAAS,aAAa,CAAA,EAAwB;AACpD,EAAA,IAAI,gBAAA,CAAiB,CAAC,CAAA,EAAG;AACxB,IAAA,OAAO,EAAE,IAAA,EAAM,CAAA,CAAE,IAAA,EAAM,QAAA,EAAU,aAAA,CAAc,CAAA,CAAE,IAAI,CAAA,EAAG,OAAA,EAAS,CAAA,CAAE,OAAA,EAAQ;AAAA,EAC5E;AACA,EAAA,MAAM,UAAU,CAAA,YAAa,KAAA,GAAQ,CAAA,CAAE,OAAA,GAAU,OAAO,CAAC,CAAA;AACzD,EAAA,OAAO,EAAE,IAAA,EAAM,oBAAA,EAAsB,UAAU,aAAA,CAAc,oBAAoB,GAAG,OAAA,EAAQ;AAC7F","file":"chunk-3KAAPYPO.js","sourcesContent":["// Shared plumbing for the flows: a tiny observable snapshot (fits the\n// Svelte store contract and React's useSyncExternalStore), a deadline\n// wrapper for network legs, and the normalizer that turns anything thrown\n// into the stable { code, labelKey, message } shape.\n\nimport type { StoreError } from '../persistence/store';\nimport { SelfstoreError, errorLabelKey, isSelfstoreError } from '../selfstore/errors';\n\n/** What every flow exposes: the current snapshot plus a subscription.\n * `subscribe` calls the listener IMMEDIATELY with the current snapshot and on\n * every change after - that is the Svelte store contract, so a flow works as\n * `$flow` in Svelte; React reads `useSyncExternalStore(flow.subscribe, () =>\n * flow.snapshot)`; everyone else just re-renders in the callback. */\nexport interface FlowStore<T> {\n\treadonly snapshot: T;\n\tsubscribe(listener: (snapshot: T) => void): () => void;\n}\n\nexport interface Machine<T> extends FlowStore<T> {\n\t/** Merge a partial into the snapshot and notify. */\n\tset(patch: Partial<T>): void;\n}\n\nexport function makeMachine<T extends object>(initial: T): Machine<T> {\n\tlet snapshot = initial;\n\tconst listeners = new Set<(s: T) => void>();\n\treturn {\n\t\tget snapshot(): T {\n\t\t\treturn snapshot;\n\t\t},\n\t\tset(patch: Partial<T>): void {\n\t\t\tsnapshot = { ...snapshot, ...patch };\n\t\t\tlisteners.forEach((fn) => fn(snapshot));\n\t\t},\n\t\tsubscribe(listener: (s: T) => void): () => void {\n\t\t\tlisteners.add(listener);\n\t\t\tlistener(snapshot);\n\t\t\treturn () => listeners.delete(listener);\n\t\t}\n\t};\n}\n\n/** Bound a network leg so a stuck destination surfaces as a retryable error\n * instead of a flow frozen on a spinner. The rejection is TARGET_UNAVAILABLE -\n * transient by contract - because \"did not answer in time\" says nothing about\n * access being lost. Never wrap a human step (a consent popup, a picker):\n * people are allowed to be slower than a deadline. */\nexport async function withDeadline<T>(work: Promise<T>, ms: number, what: string): Promise<T> {\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\tconst gate = new Promise<never>((_resolve, reject) => {\n\t\ttimer = setTimeout(\n\t\t\t() =>\n\t\t\t\treject(new SelfstoreError('TARGET_UNAVAILABLE', `${what} did not answer within ${ms}ms.`)),\n\t\t\tms\n\t\t);\n\t});\n\ttry {\n\t\treturn await Promise.race([work, gate]);\n\t} finally {\n\t\tclearTimeout(timer);\n\t}\n}\n\n/** Normalize anything thrown into the stable StoreError shape the whole\n * library reports with: show `labelKey`, log `message`. Unknown errors map to\n * TARGET_UNAVAILABLE (transient, retryable) - the flow never invents new\n * codes for the app to branch on. */\nexport function toStoreError(e: unknown): StoreError {\n\tif (isSelfstoreError(e)) {\n\t\treturn { code: e.code, labelKey: errorLabelKey(e.code), message: e.message };\n\t}\n\tconst message = e instanceof Error ? e.message : String(e);\n\treturn { code: 'TARGET_UNAVAILABLE', labelKey: errorLabelKey('TARGET_UNAVAILABLE'), message };\n}\n"]}
@@ -0,0 +1,27 @@
1
+ import { SelfstoreError } from './chunk-XMC67RNM.js';
2
+
3
+ // src/persistence/targets/abort.ts
4
+ function boundedSignal(deadlineMs, life) {
5
+ const deadline = AbortSignal.timeout(deadlineMs);
6
+ if (!life) return deadline;
7
+ if (life.aborted) return life;
8
+ const ctl = new AbortController();
9
+ deadline.addEventListener("abort", () => ctl.abort(deadline.reason), { once: true });
10
+ life.addEventListener("abort", () => ctl.abort(life.reason), { once: true });
11
+ return ctl.signal;
12
+ }
13
+ function lifeLine() {
14
+ let ctl = new AbortController();
15
+ return {
16
+ current: () => ctl.signal,
17
+ cut() {
18
+ const c = ctl;
19
+ ctl = new AbortController();
20
+ c.abort(new SelfstoreError("TARGET_UNAVAILABLE", "Request aborted: target detaching."));
21
+ }
22
+ };
23
+ }
24
+
25
+ export { boundedSignal, lifeLine };
26
+ //# sourceMappingURL=chunk-757ZOSUI.js.map
27
+ //# sourceMappingURL=chunk-757ZOSUI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/persistence/targets/abort.ts"],"names":[],"mappings":";;;AAWO,SAAS,aAAA,CAAc,YAAoB,IAAA,EAAiC;AAClF,EAAA,MAAM,QAAA,GAAW,WAAA,CAAY,OAAA,CAAQ,UAAU,CAAA;AAC/C,EAAA,IAAI,CAAC,MAAM,OAAO,QAAA;AAClB,EAAA,IAAI,IAAA,CAAK,SAAS,OAAO,IAAA;AACzB,EAAA,MAAM,GAAA,GAAM,IAAI,eAAA,EAAgB;AAChC,EAAA,QAAA,CAAS,gBAAA,CAAiB,OAAA,EAAS,MAAM,GAAA,CAAI,KAAA,CAAM,QAAA,CAAS,MAAM,CAAA,EAAG,EAAE,IAAA,EAAM,IAAA,EAAM,CAAA;AACnF,EAAA,IAAA,CAAK,gBAAA,CAAiB,OAAA,EAAS,MAAM,GAAA,CAAI,KAAA,CAAM,IAAA,CAAK,MAAM,CAAA,EAAG,EAAE,IAAA,EAAM,IAAA,EAAM,CAAA;AAC3E,EAAA,OAAO,GAAA,CAAI,MAAA;AACZ;AAMO,SAAS,QAAA,GAAoD;AACnE,EAAA,IAAI,GAAA,GAAM,IAAI,eAAA,EAAgB;AAC9B,EAAA,OAAO;AAAA,IACN,OAAA,EAAS,MAAM,GAAA,CAAI,MAAA;AAAA,IACnB,GAAA,GAAY;AACX,MAAA,MAAM,CAAA,GAAI,GAAA;AACV,MAAA,GAAA,GAAM,IAAI,eAAA,EAAgB;AAC1B,MAAA,CAAA,CAAE,KAAA,CAAM,IAAI,cAAA,CAAe,oBAAA,EAAsB,oCAAoC,CAAC,CAAA;AAAA,IACvF;AAAA,GACD;AACD","file":"chunk-757ZOSUI.js","sourcesContent":["/**\n * Deadline + life-signal composition for target requests. Every target fetch\n * carries a per-request deadline; composing it with the target's LIFE signal\n * lets abortInFlight() (the user detaching the destination) cut a suspended\n * request NOW instead of letting the detach wait behind the deadline. The\n * composition is done by hand (not AbortSignal.any) so behaviour is identical\n * on every engine the library supports.\n */\n\nimport { SelfstoreError } from '../../selfstore';\n\nexport function boundedSignal(deadlineMs: number, life?: AbortSignal): AbortSignal {\n\tconst deadline = AbortSignal.timeout(deadlineMs);\n\tif (!life) return deadline;\n\tif (life.aborted) return life;\n\tconst ctl = new AbortController();\n\tdeadline.addEventListener('abort', () => ctl.abort(deadline.reason), { once: true });\n\tlife.addEventListener('abort', () => ctl.abort(life.reason), { once: true });\n\treturn ctl.signal;\n}\n\n/** A target's resettable LIFE line. cut() aborts everything bound to the\n * CURRENT signal (with the standard transient detach reason) and re-arms,\n * so work started after the cut lives normally. cut is `this`-free: targets\n * expose it directly as their abortInFlight. */\nexport function lifeLine(): { current(): AbortSignal; cut(): void } {\n\tlet ctl = new AbortController();\n\treturn {\n\t\tcurrent: () => ctl.signal,\n\t\tcut(): void {\n\t\t\tconst c = ctl;\n\t\t\tctl = new AbortController();\n\t\t\tc.abort(new SelfstoreError('TARGET_UNAVAILABLE', 'Request aborted: target detaching.'));\n\t\t}\n\t};\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { BACKUP_EXTENSION, BACKUP_MIME } from './chunk-MGDXR3RB.js';
1
+ import { BACKUP_EXTENSION, BACKUP_MIME } from './chunk-HHPJPEEG.js';
2
2
 
3
3
  // src/selfstore/targets/local.ts
4
4
  async function pickSaveHandle(filename) {
@@ -61,5 +61,5 @@ function pickFromDisk() {
61
61
  }
62
62
 
63
63
  export { datedName, downloadBlob, pickFromDisk, pickSaveHandle, saveToDisk, writeToHandle };
64
- //# sourceMappingURL=chunk-DCEXWSM6.js.map
65
- //# sourceMappingURL=chunk-DCEXWSM6.js.map
64
+ //# sourceMappingURL=chunk-AUH35AAQ.js.map
65
+ //# sourceMappingURL=chunk-AUH35AAQ.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/selfstore/targets/local.ts"],"names":[],"mappings":"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","file":"chunk-DCEXWSM6.js","sourcesContent":["/**\n * Local-disk helpers: the DOM edge of selfstore, kept out of the core so the\n * core stays environment-agnostic and testable.\n */\n\nimport { BACKUP_EXTENSION, BACKUP_MIME } from '../index';\n\n/** The slice of FileSystemFileHandle this module needs. */\nexport interface SaveHandle {\n\tcreateWritable(): Promise<{ write(d: Blob): Promise<void>; close(): Promise<void> }>;\n}\n\n/**\n * Ask the user where to save, and nothing else.\n *\n * Split out because ORDER decides whether the dialog opens at all. The picker\n * needs the transient user activation of the click, and that activation expires\n * while an encrypted backup is being built (Argon2id is deliberately slow). Ask\n * first, build after, and the dialog is still allowed; build first and the\n * browser refuses it. Hosts that saw a silent download instead of the save\n * dialog were hitting exactly that.\n *\n * Null when the browser has no picker at all (Firefox, Safari) or when it\n * refuses for any reason other than a cancellation - the caller then falls back\n * to a download. Throws nothing; a cancelled dialog is reported as CANCELLED.\n */\nexport async function pickSaveHandle(\n\tfilename: string\n): Promise<{ handle: SaveHandle } | 'CANCELLED' | null> {\n\tconst picker = (\n\t\twindow as unknown as {\n\t\t\tshowSaveFilePicker?: (o: unknown) => Promise<SaveHandle>;\n\t\t}\n\t).showSaveFilePicker;\n\tif (!picker) return null;\n\ttry {\n\t\tconst handle = await picker({\n\t\t\tsuggestedName: filename,\n\t\t\ttypes: [{ description: 'selfstore backup', accept: { [BACKUP_MIME]: [BACKUP_EXTENSION] } }]\n\t\t});\n\t\treturn { handle };\n\t} catch (e) {\n\t\tif (e instanceof DOMException && e.name === 'AbortError') return 'CANCELLED';\n\t\t// Anything else (no activation left, policy): the download still works.\n\t\treturn null;\n\t}\n}\n\n/** Write a blob through a handle obtained earlier by pickSaveHandle. */\nexport async function writeToHandle(handle: SaveHandle, blob: Blob): Promise<void> {\n\tconst writable = await handle.createWritable();\n\tawait writable.write(blob);\n\tawait writable.close();\n}\n\n/** Hand the blob to the browser's download machinery. No cancel to observe:\n * once taken, the host has done all it can. */\nexport function downloadBlob(blob: Blob, filename: string): void {\n\tconst url = URL.createObjectURL(blob);\n\tconst a = document.createElement('a');\n\ta.href = url;\n\ta.download = filename;\n\tdocument.body.appendChild(a);\n\ta.click();\n\ta.remove();\n\tURL.revokeObjectURL(url);\n}\n\n/**\n * Save a backup blob to disk via the File System Access API, else a download.\n *\n * Resolves TRUE when bytes were handed to the browser, FALSE when the user\n * closed the save dialog. Callers must branch on it: a host that records \"last\n * backup: today\" after a cancelled dialog tells the user they are safe when no\n * file exists, and that lie only surfaces the day they need the file.\n *\n * Takes a ready blob, so the dialog opens only after the blob was built. When\n * that blob is an encrypted backup, prefer asking first: see pickSaveHandle.\n */\nexport async function saveToDisk(blob: Blob, filename: string): Promise<boolean> {\n\tconst picked = await pickSaveHandle(filename);\n\tif (picked === 'CANCELLED') return false;\n\tif (picked) {\n\t\tawait writeToHandle(picked.handle, blob);\n\t\treturn true;\n\t}\n\tdownloadBlob(blob, datedName(filename));\n\treturn true;\n}\n\n/**\n * The same name, stamped to the minute.\n *\n * Used only on the download path, and the reasoning is worth stating because it\n * looks backwards. Through a handle the app rewrites the SAME file, so the name\n * must stay stable. A download never replaces anything: the browser appends\n * \" (1)\", \" (2)\", and no web API can stop it. The pile forms either way - so it\n * may as well be readable. A dated series says at a glance which one is the\n * latest and lets the user step back to an earlier one; \"backup (7).zip\" says\n * neither when, nor in what order.\n */\nexport function datedName(filename: string, when = new Date()): string {\n\tconst deux = (n: number) => String(n).padStart(2, '0');\n\tconst stamp =\n\t\t`${when.getFullYear()}-${deux(when.getMonth() + 1)}-${deux(when.getDate())}` +\n\t\t`-${deux(when.getHours())}h${deux(when.getMinutes())}`;\n\tconst dot = filename.lastIndexOf('.');\n\treturn dot <= 0\n\t\t? `${filename}-${stamp}`\n\t\t: `${filename.slice(0, dot)}-${stamp}${filename.slice(dot)}`;\n}\n\n/** Prompt the user to pick a backup file from disk. Resolves null if cancelled. */\nexport function pickFromDisk(): Promise<File | null> {\n\treturn new Promise((resolve) => {\n\t\tconst input = document.createElement('input');\n\t\tinput.type = 'file';\n\t\t// octet-stream: some providers serve a downloaded ZIP under that type.\n\t\tinput.accept = '.zip,application/zip,application/octet-stream';\n\t\tinput.onchange = () => resolve(input.files?.[0] ?? null);\n\t\t// Without this, a cancelled dialog leaves the promise pending forever.\n\t\tinput.oncancel = () => resolve(null);\n\t\t// Some browsers need the input in the DOM for the dialog to open.\n\t\tinput.style.display = 'none';\n\t\tdocument.body.appendChild(input);\n\t\tinput.click();\n\t\tinput.remove();\n\t});\n}\n"]}
1
+ {"version":3,"sources":["../src/selfstore/targets/local.ts"],"names":[],"mappings":"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","file":"chunk-AUH35AAQ.js","sourcesContent":["/**\n * Local-disk helpers: the DOM edge of selfstore, kept out of the core so the\n * core stays environment-agnostic and testable.\n */\n\nimport { BACKUP_EXTENSION, BACKUP_MIME } from '../index';\n\n/** The slice of FileSystemFileHandle this module needs. */\nexport interface SaveHandle {\n\tcreateWritable(): Promise<{ write(d: Blob): Promise<void>; close(): Promise<void> }>;\n}\n\n/**\n * Ask the user where to save, and nothing else.\n *\n * Split out because ORDER decides whether the dialog opens at all. The picker\n * needs the transient user activation of the click, and that activation expires\n * while an encrypted backup is being built (Argon2id is deliberately slow). Ask\n * first, build after, and the dialog is still allowed; build first and the\n * browser refuses it. Hosts that saw a silent download instead of the save\n * dialog were hitting exactly that.\n *\n * Null when the browser has no picker at all (Firefox, Safari) or when it\n * refuses for any reason other than a cancellation - the caller then falls back\n * to a download. Throws nothing; a cancelled dialog is reported as CANCELLED.\n */\nexport async function pickSaveHandle(\n\tfilename: string\n): Promise<{ handle: SaveHandle } | 'CANCELLED' | null> {\n\tconst picker = (\n\t\twindow as unknown as {\n\t\t\tshowSaveFilePicker?: (o: unknown) => Promise<SaveHandle>;\n\t\t}\n\t).showSaveFilePicker;\n\tif (!picker) return null;\n\ttry {\n\t\tconst handle = await picker({\n\t\t\tsuggestedName: filename,\n\t\t\ttypes: [{ description: 'selfstore backup', accept: { [BACKUP_MIME]: [BACKUP_EXTENSION] } }]\n\t\t});\n\t\treturn { handle };\n\t} catch (e) {\n\t\tif (e instanceof DOMException && e.name === 'AbortError') return 'CANCELLED';\n\t\t// Anything else (no activation left, policy): the download still works.\n\t\treturn null;\n\t}\n}\n\n/** Write a blob through a handle obtained earlier by pickSaveHandle. */\nexport async function writeToHandle(handle: SaveHandle, blob: Blob): Promise<void> {\n\tconst writable = await handle.createWritable();\n\tawait writable.write(blob);\n\tawait writable.close();\n}\n\n/** Hand the blob to the browser's download machinery. No cancel to observe:\n * once taken, the host has done all it can. */\nexport function downloadBlob(blob: Blob, filename: string): void {\n\tconst url = URL.createObjectURL(blob);\n\tconst a = document.createElement('a');\n\ta.href = url;\n\ta.download = filename;\n\tdocument.body.appendChild(a);\n\ta.click();\n\ta.remove();\n\tURL.revokeObjectURL(url);\n}\n\n/**\n * Save a backup blob to disk via the File System Access API, else a download.\n *\n * Resolves TRUE when bytes were handed to the browser, FALSE when the user\n * closed the save dialog. Callers must branch on it: a host that records \"last\n * backup: today\" after a cancelled dialog tells the user they are safe when no\n * file exists, and that lie only surfaces the day they need the file.\n *\n * Takes a ready blob, so the dialog opens only after the blob was built. When\n * that blob is an encrypted backup, prefer asking first: see pickSaveHandle.\n */\nexport async function saveToDisk(blob: Blob, filename: string): Promise<boolean> {\n\tconst picked = await pickSaveHandle(filename);\n\tif (picked === 'CANCELLED') return false;\n\tif (picked) {\n\t\tawait writeToHandle(picked.handle, blob);\n\t\treturn true;\n\t}\n\tdownloadBlob(blob, datedName(filename));\n\treturn true;\n}\n\n/**\n * The same name, stamped to the minute.\n *\n * Used only on the download path, and the reasoning is worth stating because it\n * looks backwards. Through a handle the app rewrites the SAME file, so the name\n * must stay stable. A download never replaces anything: the browser appends\n * \" (1)\", \" (2)\", and no web API can stop it. The pile forms either way - so it\n * may as well be readable. A dated series says at a glance which one is the\n * latest and lets the user step back to an earlier one; \"backup (7).zip\" says\n * neither when, nor in what order.\n */\nexport function datedName(filename: string, when = new Date()): string {\n\tconst deux = (n: number) => String(n).padStart(2, '0');\n\tconst stamp =\n\t\t`${when.getFullYear()}-${deux(when.getMonth() + 1)}-${deux(when.getDate())}` +\n\t\t`-${deux(when.getHours())}h${deux(when.getMinutes())}`;\n\tconst dot = filename.lastIndexOf('.');\n\treturn dot <= 0\n\t\t? `${filename}-${stamp}`\n\t\t: `${filename.slice(0, dot)}-${stamp}${filename.slice(dot)}`;\n}\n\n/** Prompt the user to pick a backup file from disk. Resolves null if cancelled. */\nexport function pickFromDisk(): Promise<File | null> {\n\treturn new Promise((resolve) => {\n\t\tconst input = document.createElement('input');\n\t\tinput.type = 'file';\n\t\t// octet-stream: some providers serve a downloaded ZIP under that type.\n\t\tinput.accept = '.zip,application/zip,application/octet-stream';\n\t\tinput.onchange = () => resolve(input.files?.[0] ?? null);\n\t\t// Without this, a cancelled dialog leaves the promise pending forever.\n\t\tinput.oncancel = () => resolve(null);\n\t\t// Some browsers need the input in the DOM for the dialog to open.\n\t\tinput.style.display = 'none';\n\t\tdocument.body.appendChild(input);\n\t\tinput.click();\n\t\tinput.remove();\n\t});\n}\n"]}
@@ -1,8 +1,10 @@
1
- import { inspect, checkPasswordPolicy, writeBox, BACKUP_MIME, readBoxWithSync, asBytes } from './chunk-MGDXR3RB.js';
1
+ import { sha256Hex } from './chunk-QSLHEE2L.js';
2
+ import { inspect, checkPasswordPolicy, writeBox, BACKUP_MIME, readBoxWithSync, asBytes } from './chunk-HHPJPEEG.js';
2
3
  import { seal, isEnvelope, unseal, newDeviceKey } from './chunk-QBCU7OOP.js';
3
- import { openManifest, GROUP_KEYING } from './chunk-O3Y6FORN.js';
4
- import { SelfstoreError, mintSlot, MAX_KEY_SLOTS, mintExternalSlot, isAuthExpired, openSlot, errorLabelKey, isSelfstoreError } from './chunk-3OBJOMQ2.js';
5
- import { createNode, createMeta, gcTombstones, stamp, merge, unsyncableCounts, idPathFor, detectConflicts, changes } from './chunk-SKWHKM25.js';
4
+ import { openManifest, GROUP_KEYING } from './chunk-OY2A5DCB.js';
5
+ import { mintSlot, MAX_KEY_SLOTS, mintExternalSlot, openSlot } from './chunk-RREJ2MFN.js';
6
+ import { createNode, createMeta, gcTombstones, stamp, merge, unsyncableCounts, idPathFor, detectConflicts, changes } from './chunk-23UAUYSW.js';
7
+ import { SelfstoreError, isAuthExpired, errorLabelKey, isSelfstoreError } from './chunk-XMC67RNM.js';
6
8
  import { openDB } from 'idb';
7
9
 
8
10
  // src/persistence/target.ts
@@ -286,13 +288,29 @@ function createLocalStore(opts) {
286
288
  )
287
289
  };
288
290
  }
289
- async function buildBlob(pre) {
290
- if (requireEncryption && !encrypted && !group) {
291
+ async function buildBlob(pre, opts2) {
292
+ if (requireEncryption && (opts2?.plaintext || !encrypted && !group)) {
291
293
  throw new SelfstoreError(
292
294
  "ENCRYPTION_REQUIRED",
293
295
  "requireEncryption is set: refusing to build a plaintext backup."
294
296
  );
295
297
  }
298
+ if (opts2?.plaintext) {
299
+ if (locked()) {
300
+ throw new SelfstoreError(
301
+ "PASSWORD_REQUIRED",
302
+ "Cannot export a readable copy while locked - unlock first."
303
+ );
304
+ }
305
+ const snapshot = pre ?? await gather();
306
+ const stamp2 = Math.max(version, schemaCeiling);
307
+ const bytes2 = await writeBox(
308
+ { collections: snapshot.collections, files: snapshot.files },
309
+ { app, appVersion, schemaVersion: stamp2, readme },
310
+ { schemaVersion: stamp2, meta }
311
+ );
312
+ return new Blob([bytes2], { type: BACKUP_MIME });
313
+ }
296
314
  const snap = pre ?? await gather();
297
315
  let env;
298
316
  if (encrypted && !group) {
@@ -616,8 +634,7 @@ function createLocalStore(opts) {
616
634
  buf.set(p, off);
617
635
  off += p.length;
618
636
  }
619
- const digest = await crypto.subtle.digest("SHA-256", buf);
620
- return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
637
+ return sha256Hex(buf);
621
638
  }
622
639
  async function doSave() {
623
640
  if (mode !== "persistent") return;
@@ -1393,8 +1410,8 @@ function createLocalStore(opts) {
1393
1410
  notify();
1394
1411
  }
1395
1412
  },
1396
- exportBlob() {
1397
- return buildBlob();
1413
+ exportBlob(opts2) {
1414
+ return buildBlob(void 0, opts2);
1398
1415
  },
1399
1416
  markDownloaded() {
1400
1417
  pendingDownload = false;
@@ -1959,7 +1976,7 @@ function indexedDbCache(name, opts) {
1959
1976
  sealKey = null;
1960
1977
  },
1961
1978
  async reseal(secret) {
1962
- const { keyFromPassword, freshCacheKdf } = await import('./cache-lock-KWJUORTS.js');
1979
+ const { keyFromPassword, freshCacheKdf } = await import('./cache-lock-5RNG6CIK.js');
1963
1980
  const previous = sealKey;
1964
1981
  if (previous === null) {
1965
1982
  throw new SelfstoreError(
@@ -1993,7 +2010,7 @@ function indexedDbCache(name, opts) {
1993
2010
  sealKey = next;
1994
2011
  },
1995
2012
  async unlock(secret) {
1996
- const { keyFromPassword, freshCacheKdf, opens } = await import('./cache-lock-KWJUORTS.js');
2013
+ const { keyFromPassword, freshCacheKdf, opens } = await import('./cache-lock-5RNG6CIK.js');
1997
2014
  const d = await db();
1998
2015
  let candidate;
1999
2016
  if (typeof secret === "string") {
@@ -2140,5 +2157,5 @@ function gisDriveAuth(opts) {
2140
2157
  }
2141
2158
 
2142
2159
  export { RESERVED_STORE_MODES, createLocalStore, deriveStatus, gisDriveAuth, indexedDbCache, isLockableCache, isReservedStoreMode, memoryCache };
2143
- //# sourceMappingURL=chunk-TQH7ZYID.js.map
2144
- //# sourceMappingURL=chunk-TQH7ZYID.js.map
2160
+ //# sourceMappingURL=chunk-DJZ5S3FI.js.map
2161
+ //# sourceMappingURL=chunk-DJZ5S3FI.js.map