@noy-db/hub 0.1.0-pre.3 → 0.1.0-pre.5

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 (95) hide show
  1. package/dist/blobs/index.cjs.map +1 -1
  2. package/dist/blobs/index.d.cts +3 -3
  3. package/dist/blobs/index.d.ts +3 -3
  4. package/dist/blobs/index.js +2 -2
  5. package/dist/bundle/index.cjs +26 -3
  6. package/dist/bundle/index.cjs.map +1 -1
  7. package/dist/bundle/index.d.cts +3 -3
  8. package/dist/bundle/index.d.ts +3 -3
  9. package/dist/bundle/index.js +3 -1
  10. package/dist/{chunk-M2F2JAWB.js → chunk-6NPQTBZN.js} +103 -8
  11. package/dist/chunk-6NPQTBZN.js.map +1 -0
  12. package/dist/{chunk-UQFSPSWG.js → chunk-E4OOAPBZ.js} +2 -2
  13. package/dist/chunk-EMIGCR7X.js +39 -0
  14. package/dist/chunk-EMIGCR7X.js.map +1 -0
  15. package/dist/{chunk-EXQRC2L4.js → chunk-H3DV46AQ.js} +2 -2
  16. package/dist/{chunk-XHFOENR2.js → chunk-LMKOSLJY.js} +2 -2
  17. package/dist/{chunk-GJILMRPO.js → chunk-LRN3PNI6.js} +42 -4
  18. package/dist/chunk-LRN3PNI6.js.map +1 -0
  19. package/dist/{chunk-4OWFYIDQ.js → chunk-MIRZMUSQ.js} +3 -3
  20. package/dist/{chunk-ZRG4V3F5.js → chunk-NXUVITPB.js} +1 -1
  21. package/dist/chunk-NXUVITPB.js.map +1 -0
  22. package/dist/{chunk-5AATM2M2.js → chunk-QUDXYI4W.js} +2 -2
  23. package/dist/{chunk-ZLMV3TUA.js → chunk-QV4WLLKB.js} +3 -3
  24. package/dist/{chunk-E445ICYI.js → chunk-UFL4DUEV.js} +5 -3
  25. package/dist/chunk-UFL4DUEV.js.map +1 -0
  26. package/dist/chunk-UQQ2XFXI.js +155 -0
  27. package/dist/chunk-UQQ2XFXI.js.map +1 -0
  28. package/dist/consent/index.d.cts +3 -3
  29. package/dist/consent/index.d.ts +3 -3
  30. package/dist/{dev-unlock-KrKkcqD3.d.ts → dev-unlock-BgFqShBi.d.ts} +1 -1
  31. package/dist/{dev-unlock-CeXic1xC.d.cts → dev-unlock-qVMxG2Je.d.cts} +1 -1
  32. package/dist/{hash-ChfJjRjQ.d.ts → hash-BhoL7iUE.d.ts} +1 -1
  33. package/dist/{hash-9KO1BGxh.d.cts → hash-Bpvl2eSe.d.cts} +1 -1
  34. package/dist/history/index.cjs.map +1 -1
  35. package/dist/history/index.d.cts +4 -4
  36. package/dist/history/index.d.ts +4 -4
  37. package/dist/history/index.js +2 -2
  38. package/dist/i18n/index.cjs +3 -1
  39. package/dist/i18n/index.cjs.map +1 -1
  40. package/dist/i18n/index.d.cts +3 -3
  41. package/dist/i18n/index.d.ts +3 -3
  42. package/dist/i18n/index.js +3 -3
  43. package/dist/{index-DN-J-5wT.d.cts → index-6xNpPsxR.d.cts} +1 -1
  44. package/dist/{index-BRHBCmLt.d.ts → index-DJTf9yxn.d.ts} +1 -1
  45. package/dist/{index-DhjMjz7L.d.cts → index-DhK_zqOO.d.ts} +39 -5
  46. package/dist/{index-C8kQtmOk.d.ts → index-DyRt_5vM.d.cts} +39 -5
  47. package/dist/index.cjs +1501 -51
  48. package/dist/index.cjs.map +1 -1
  49. package/dist/index.d.cts +261 -19
  50. package/dist/index.d.ts +261 -19
  51. package/dist/index.js +1118 -44
  52. package/dist/index.js.map +1 -1
  53. package/dist/{ledger-2NX4L7PN.js → ledger-GA4DMJS6.js} +3 -3
  54. package/dist/periods/index.cjs.map +1 -1
  55. package/dist/periods/index.d.cts +3 -3
  56. package/dist/periods/index.d.ts +3 -3
  57. package/dist/periods/index.js +3 -3
  58. package/dist/public-envelope-R4EIEQE6.js +31 -0
  59. package/dist/public-envelope-R4EIEQE6.js.map +1 -0
  60. package/dist/query/index.d.cts +1 -1
  61. package/dist/query/index.d.ts +1 -1
  62. package/dist/session/index.cjs +4 -2
  63. package/dist/session/index.cjs.map +1 -1
  64. package/dist/session/index.d.cts +4 -4
  65. package/dist/session/index.d.ts +4 -4
  66. package/dist/session/index.js +1 -1
  67. package/dist/shadow/index.d.cts +3 -3
  68. package/dist/shadow/index.d.ts +3 -3
  69. package/dist/store/index.d.cts +3 -3
  70. package/dist/store/index.d.ts +3 -3
  71. package/dist/sync/index.cjs.map +1 -1
  72. package/dist/sync/index.d.cts +2 -2
  73. package/dist/sync/index.d.ts +2 -2
  74. package/dist/sync/index.js +2 -2
  75. package/dist/team/index.cjs +3 -1
  76. package/dist/team/index.cjs.map +1 -1
  77. package/dist/team/index.d.cts +3 -3
  78. package/dist/team/index.d.ts +3 -3
  79. package/dist/team/index.js +4 -4
  80. package/dist/tx/index.d.cts +3 -3
  81. package/dist/tx/index.d.ts +3 -3
  82. package/dist/{types-Bfs0qr5F.d.cts → types-BpyE4o_n.d.cts} +935 -4
  83. package/dist/{types-BZpCZB8N.d.ts → types-Df72wWCC.d.ts} +935 -4
  84. package/package.json +1 -1
  85. package/dist/chunk-E445ICYI.js.map +0 -1
  86. package/dist/chunk-GJILMRPO.js.map +0 -1
  87. package/dist/chunk-M2F2JAWB.js.map +0 -1
  88. package/dist/chunk-ZRG4V3F5.js.map +0 -1
  89. /package/dist/{chunk-UQFSPSWG.js.map → chunk-E4OOAPBZ.js.map} +0 -0
  90. /package/dist/{chunk-EXQRC2L4.js.map → chunk-H3DV46AQ.js.map} +0 -0
  91. /package/dist/{chunk-XHFOENR2.js.map → chunk-LMKOSLJY.js.map} +0 -0
  92. /package/dist/{chunk-4OWFYIDQ.js.map → chunk-MIRZMUSQ.js.map} +0 -0
  93. /package/dist/{chunk-5AATM2M2.js.map → chunk-QUDXYI4W.js.map} +0 -0
  94. /package/dist/{chunk-ZLMV3TUA.js.map → chunk-QV4WLLKB.js.map} +0 -0
  95. /package/dist/{ledger-2NX4L7PN.js.map → ledger-GA4DMJS6.js.map} +0 -0
package/dist/index.js CHANGED
@@ -1,3 +1,8 @@
1
+ import {
2
+ DEFAULT_PUBLIC_ENVELOPE_SCHEMA,
3
+ PUBLIC_ENVELOPE_FIELDS,
4
+ resolveSchema
5
+ } from "./chunk-EMIGCR7X.js";
1
6
  import {
2
7
  DELEGATIONS_COLLECTION,
3
8
  assertTierAccess,
@@ -23,15 +28,24 @@ import {
23
28
  hasNoydbBundleMagic,
24
29
  readNoydbBundle,
25
30
  readNoydbBundleHeader,
31
+ readNoydbBundlePublicEnvelope,
26
32
  resetBrotliSupportCache,
27
33
  writeNoydbBundle
28
- } from "./chunk-GJILMRPO.js";
34
+ } from "./chunk-LRN3PNI6.js";
35
+ import {
36
+ PUBLIC_ENVELOPE_RECORD_ID,
37
+ isPublicEnvelope,
38
+ loadPublicEnvelope,
39
+ readPublicEnvelope,
40
+ savePublicEnvelope,
41
+ validatePublicEnvelopeInput
42
+ } from "./chunk-UQQ2XFXI.js";
29
43
  import {
30
44
  CONSENT_AUDIT_COLLECTION
31
45
  } from "./chunk-M62XNWRA.js";
32
46
  import {
33
47
  PERIODS_COLLECTION
34
- } from "./chunk-5AATM2M2.js";
48
+ } from "./chunk-QUDXYI4W.js";
35
49
  import "./chunk-UF3BUNQZ.js";
36
50
  import {
37
51
  CollectionFrame,
@@ -55,7 +69,7 @@ import {
55
69
  isI18nTextDescriptor,
56
70
  resolveI18nText,
57
71
  validateI18nTextValue
58
- } from "./chunk-ZLMV3TUA.js";
72
+ } from "./chunk-QV4WLLKB.js";
59
73
  import {
60
74
  createBundleStore,
61
75
  routeStore,
@@ -75,17 +89,20 @@ import {
75
89
  getCredential,
76
90
  listCredentials,
77
91
  putCredential
78
- } from "./chunk-4OWFYIDQ.js";
92
+ } from "./chunk-MIRZMUSQ.js";
79
93
  import {
80
94
  PresenceHandle,
81
95
  SyncEngine,
82
96
  SyncTransaction
83
- } from "./chunk-EXQRC2L4.js";
97
+ } from "./chunk-H3DV46AQ.js";
84
98
  import {
99
+ WeakPassphraseError,
100
+ assertStrongPassphrase,
85
101
  buildRecipientKeyringFile,
86
102
  changeSecret,
87
103
  createOwnerKeyring,
88
104
  ensureCollectionDEK,
105
+ estimateEntropy,
89
106
  evaluateExportCapability,
90
107
  evaluateImportCapability,
91
108
  grant,
@@ -95,9 +112,11 @@ import {
95
112
  hasWritePermission,
96
113
  listUsers,
97
114
  loadKeyring,
115
+ persistKeyring,
98
116
  revoke,
99
- rotateKeys
100
- } from "./chunk-M2F2JAWB.js";
117
+ rotateKeys,
118
+ validatePassphrase
119
+ } from "./chunk-6NPQTBZN.js";
101
120
  import {
102
121
  BUNDLE_STORE_POLICY,
103
122
  INDEXED_STORE_POLICY,
@@ -117,7 +136,7 @@ import {
117
136
  revokeAllSessions,
118
137
  revokeSession,
119
138
  validateSessionPolicy
120
- } from "./chunk-E445ICYI.js";
139
+ } from "./chunk-UFL4DUEV.js";
121
140
  import {
122
141
  generateULID,
123
142
  isULID
@@ -134,7 +153,7 @@ import {
134
153
  LedgerStore,
135
154
  applyPatch,
136
155
  computePatch
137
- } from "./chunk-XHFOENR2.js";
156
+ } from "./chunk-LMKOSLJY.js";
138
157
  import {
139
158
  canonicalJson,
140
159
  envelopePayloadHash,
@@ -189,24 +208,26 @@ import {
189
208
  detectMimeType,
190
209
  isPreCompressed,
191
210
  runCompaction
192
- } from "./chunk-UQFSPSWG.js";
211
+ } from "./chunk-E4OOAPBZ.js";
193
212
  import {
194
213
  NOYDB_BACKUP_VERSION,
195
214
  NOYDB_FORMAT_VERSION,
196
215
  NOYDB_KEYRING_VERSION,
197
216
  NOYDB_SYNC_VERSION,
198
217
  createStore
199
- } from "./chunk-ZRG4V3F5.js";
218
+ } from "./chunk-NXUVITPB.js";
200
219
  import {
201
220
  base64ToBuffer,
202
221
  bufferToBase64,
203
222
  decrypt,
204
223
  decryptBytes,
205
224
  decryptDeterministic,
225
+ deriveKey,
206
226
  derivePresenceKey,
207
227
  encrypt,
208
228
  encryptBytes,
209
229
  encryptDeterministic,
230
+ generateSalt,
210
231
  unwrapKey,
211
232
  wrapKey
212
233
  } from "./chunk-MR4424N3.js";
@@ -397,6 +418,514 @@ var RefRegistry = class {
397
418
  }
398
419
  };
399
420
 
421
+ // src/team/authenticators.ts
422
+ async function enrollAuthenticator(store, vault, keyring, options) {
423
+ const existing = keyring.authenticators.find((a) => a.id === options.id);
424
+ if (existing) {
425
+ throw new ValidationError(
426
+ `enrollAuthenticator: slot id "${options.id}" already exists in vault "${vault}". Remove the slot first or pick a unique id.`
427
+ );
428
+ }
429
+ const slot = {
430
+ id: options.id,
431
+ method: options.method,
432
+ enrolled_at: (/* @__PURE__ */ new Date()).toISOString(),
433
+ enrolled_via_tier: options.enrolled_via_tier ?? 1,
434
+ wrapped_kek: options.wrapped_kek,
435
+ meta: options.meta
436
+ };
437
+ const next = appendSlot(keyring, slot);
438
+ await persistKeyring(store, vault, next);
439
+ return next;
440
+ }
441
+ async function removeAuthenticator(store, vault, keyring, slotId) {
442
+ const filtered = keyring.authenticators.filter((a) => a.id !== slotId);
443
+ if (filtered.length === keyring.authenticators.length) {
444
+ return keyring;
445
+ }
446
+ const next = {
447
+ ...keyring,
448
+ authenticators: filtered
449
+ };
450
+ await persistKeyring(store, vault, next);
451
+ return next;
452
+ }
453
+ function findAuthenticator(keyring, slotId) {
454
+ return keyring.authenticators.find((a) => a.id === slotId);
455
+ }
456
+ function appendSlot(keyring, slot) {
457
+ return {
458
+ ...keyring,
459
+ authenticators: [...keyring.authenticators, slot]
460
+ };
461
+ }
462
+
463
+ // src/session/unlock-state.ts
464
+ var QuickUnlockStore = class {
465
+ states = /* @__PURE__ */ new Map();
466
+ timers = /* @__PURE__ */ new Map();
467
+ /**
468
+ * Register a quick-unlock state for a vault. Replaces any existing
469
+ * state. Schedules an automatic clear when the state's `expiresAt`
470
+ * elapses.
471
+ */
472
+ set(vault, state) {
473
+ this.clearTimer(vault);
474
+ this.states.set(vault, state);
475
+ const ttl = new Date(state.expiresAt).getTime() - Date.now();
476
+ if (ttl > 0) {
477
+ const timer = setTimeout(() => this.delete(vault), ttl);
478
+ this.timers.set(vault, timer);
479
+ }
480
+ }
481
+ /** Read the state for a vault. Returns undefined when none is registered. */
482
+ get(vault) {
483
+ return this.states.get(vault);
484
+ }
485
+ /** Drop the state for a vault. Cancels the auto-clear timer. */
486
+ delete(vault) {
487
+ this.clearTimer(vault);
488
+ this.states.delete(vault);
489
+ }
490
+ /** Drop every cached state. Called on `db.close()`. */
491
+ clear() {
492
+ for (const vault of this.states.keys()) {
493
+ this.clearTimer(vault);
494
+ }
495
+ this.states.clear();
496
+ }
497
+ clearTimer(vault) {
498
+ const t = this.timers.get(vault);
499
+ if (t) clearTimeout(t);
500
+ this.timers.delete(vault);
501
+ }
502
+ };
503
+
504
+ // src/policy/errors.ts
505
+ var PolicyDeniedError = class extends NoydbError {
506
+ gate;
507
+ reason;
508
+ required;
509
+ constructor(gate, reason, required, message) {
510
+ super(
511
+ "POLICY_DENIED",
512
+ message ?? `Gate "${gate}" denied: ${reason}.`
513
+ );
514
+ this.name = "PolicyDeniedError";
515
+ this.gate = gate;
516
+ this.reason = reason;
517
+ this.required = required;
518
+ }
519
+ };
520
+ var RecoveryNotEnrolledError = class extends NoydbError {
521
+ constructor(message = 'Recovery profile not enrolled. Pass `recovery: [{ profile: "paper", codes: 10 }]` to `createNoydb()`, or set `policy.gates["recover-passphrase"].enabled = false` to opt out of recovery (passphrase loss = data loss). See docs/subsystems/session-tiers.md.') {
522
+ super("RECOVERY_NOT_ENROLLED", message);
523
+ this.name = "RecoveryNotEnrolledError";
524
+ }
525
+ };
526
+ var RecoveryProfileNotImplementedError = class extends NoydbError {
527
+ profile;
528
+ tracking;
529
+ constructor(profile, tracking) {
530
+ super(
531
+ "RECOVERY_PROFILE_NOT_IMPLEMENTED",
532
+ `Recovery profile "${profile}" is not yet implemented in this hub release. Tracking: ${tracking}. Use the "paper" profile via @noy-db/on-recovery in the meantime.`
533
+ );
534
+ this.name = "RecoveryProfileNotImplementedError";
535
+ this.profile = profile;
536
+ this.tracking = tracking;
537
+ }
538
+ };
539
+
540
+ // src/team/recovery.ts
541
+ var PAPER_DOC_ID = "recovery-paper";
542
+ async function loadPaperRecoveryEntries(store, vault) {
543
+ const env = await store.get(vault, "_meta", PAPER_DOC_ID);
544
+ if (!env) return [];
545
+ try {
546
+ const doc = JSON.parse(env._data);
547
+ if (doc.profile !== "paper" || !Array.isArray(doc.entries)) return [];
548
+ return doc.entries;
549
+ } catch {
550
+ return [];
551
+ }
552
+ }
553
+ async function savePaperRecoveryEntries(store, vault, entries) {
554
+ const doc = {
555
+ _noydb_recovery: 1,
556
+ profile: "paper",
557
+ entries
558
+ };
559
+ const envelope = {
560
+ _noydb: NOYDB_FORMAT_VERSION,
561
+ _v: 1,
562
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
563
+ _iv: "",
564
+ _data: JSON.stringify(doc)
565
+ };
566
+ await store.put(vault, "_meta", PAPER_DOC_ID, envelope);
567
+ }
568
+ async function burnPaperRecoveryEntry(store, vault, codeId) {
569
+ const entries = await loadPaperRecoveryEntries(store, vault);
570
+ const remaining = entries.filter((e) => e.codeId !== codeId);
571
+ await savePaperRecoveryEntries(store, vault, remaining);
572
+ }
573
+ async function hasRecoveryEnrolled(store, vault) {
574
+ const paper = await loadPaperRecoveryEntries(store, vault);
575
+ return paper.length > 0;
576
+ }
577
+ var subtle = globalThis.crypto.subtle;
578
+ var RECOVERY_PBKDF2_ITERATIONS = 6e5;
579
+ async function unwrapDeksFromPaperEntry(entry, code) {
580
+ const wrappingKey = await deriveRecoveryWrappingKey(code, base64ToBytes(entry.salt));
581
+ const plaintext = await subtle.decrypt(
582
+ { name: "AES-GCM", iv: base64ToBytes(entry.iv) },
583
+ wrappingKey,
584
+ base64ToBytes(entry.wrappedDeks)
585
+ );
586
+ const parsed = JSON.parse(new TextDecoder().decode(plaintext));
587
+ const deks = /* @__PURE__ */ new Map();
588
+ for (const [coll, b64] of Object.entries(parsed.deks)) {
589
+ const raw = base64ToBytes(b64);
590
+ const key = await subtle.importKey(
591
+ "raw",
592
+ raw,
593
+ { name: "AES-GCM", length: 256 },
594
+ true,
595
+ ["encrypt", "decrypt"]
596
+ );
597
+ deks.set(coll, key);
598
+ }
599
+ return deks;
600
+ }
601
+ async function deriveRecoveryWrappingKey(code, salt) {
602
+ const ikm = await subtle.importKey(
603
+ "raw",
604
+ new TextEncoder().encode(code),
605
+ "PBKDF2",
606
+ false,
607
+ ["deriveKey"]
608
+ );
609
+ return subtle.deriveKey(
610
+ {
611
+ name: "PBKDF2",
612
+ salt,
613
+ iterations: RECOVERY_PBKDF2_ITERATIONS,
614
+ hash: "SHA-256"
615
+ },
616
+ ikm,
617
+ { name: "AES-GCM", length: 256 },
618
+ false,
619
+ ["encrypt", "decrypt"]
620
+ );
621
+ }
622
+ function base64ToBytes(b64) {
623
+ const s = atob(b64);
624
+ const out = new Uint8Array(s.length);
625
+ for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
626
+ return out;
627
+ }
628
+
629
+ // src/team/rotate-recover.ts
630
+ async function rotatePassphrase(store, vault, userId, input) {
631
+ if (!input.allowWeakPassphrase) {
632
+ assertStrongPassphrase(input.newPassphrase, input.passphrasePolicy);
633
+ }
634
+ const env = await store.get(vault, "_keyring", userId);
635
+ if (!env) {
636
+ throw new NoAccessError(`No keyring found for user "${userId}" in vault "${vault}".`);
637
+ }
638
+ const file = JSON.parse(env._data);
639
+ const oldSalt = base64ToBuffer(file.salt);
640
+ const oldKek = await deriveKey(input.oldPassphrase, oldSalt);
641
+ const deks = /* @__PURE__ */ new Map();
642
+ for (const [coll, wrapped] of Object.entries(file.deks)) {
643
+ deks.set(coll, await unwrapKey(wrapped, oldKek));
644
+ }
645
+ const newSalt = generateSalt();
646
+ const newKek = await deriveKey(input.newPassphrase, newSalt);
647
+ const wrappedDeks = {};
648
+ for (const [coll, dek] of deks) {
649
+ wrappedDeks[coll] = await wrapKey(dek, newKek);
650
+ }
651
+ const next = {
652
+ ...file,
653
+ _noydb_keyring: NOYDB_KEYRING_VERSION,
654
+ deks: wrappedDeks,
655
+ salt: bufferToBase64(newSalt),
656
+ // Tier-2 slots reference the old KEK — drop them. User
657
+ // re-enrols afterwards via `db.enrollAuthenticator`.
658
+ authenticators: []
659
+ };
660
+ await writeKeyringFile(store, vault, userId, next);
661
+ return {
662
+ userId: file.user_id,
663
+ displayName: file.display_name,
664
+ role: file.role,
665
+ permissions: file.permissions,
666
+ deks,
667
+ kek: newKek,
668
+ salt: newSalt,
669
+ authenticators: [],
670
+ ...file.export_capability !== void 0 && { exportCapability: file.export_capability },
671
+ ...file.import_capability !== void 0 && { importCapability: file.import_capability }
672
+ };
673
+ }
674
+ async function recoverPassphrase(store, vault, userId, input) {
675
+ if (!input.allowWeakPassphrase) {
676
+ assertStrongPassphrase(input.newPassphrase, input.passphrasePolicy);
677
+ }
678
+ switch (input.recoveryProof.profile) {
679
+ case "paper":
680
+ return recoverViaPaperCode(store, vault, userId, input);
681
+ case "shamir":
682
+ throw new RecoveryProfileNotImplementedError(
683
+ "shamir",
684
+ "https://github.com/vLannaAi/noy-db/issues/10"
685
+ );
686
+ case "multi-channel":
687
+ throw new RecoveryProfileNotImplementedError(
688
+ "multi-channel",
689
+ "https://github.com/vLannaAi/noy-db/issues/10"
690
+ );
691
+ case "admin-mediated":
692
+ throw new RecoveryProfileNotImplementedError(
693
+ "admin-mediated",
694
+ "https://github.com/vLannaAi/noy-db/issues/10"
695
+ );
696
+ default: {
697
+ const _exhaustive = input.recoveryProof;
698
+ throw new Error(`Unknown recovery profile: ${String(_exhaustive)}`);
699
+ }
700
+ }
701
+ }
702
+ async function recoverViaPaperCode(store, vault, userId, input) {
703
+ if (input.recoveryProof.profile !== "paper") throw new Error("unreachable");
704
+ const { code } = input.recoveryProof.payload;
705
+ const env = await store.get(vault, "_keyring", userId);
706
+ if (!env) {
707
+ throw new NoAccessError(`No keyring found for user "${userId}" in vault "${vault}".`);
708
+ }
709
+ const file = JSON.parse(env._data);
710
+ const entries = await loadPaperRecoveryEntries(store, vault);
711
+ if (entries.length === 0) {
712
+ throw new NoAccessError(
713
+ `No paper-recovery entries enrolled for vault "${vault}". Enroll via \`db.enrollRecovery({ profile: "paper", entries })\` before relying on recovery.`
714
+ );
715
+ }
716
+ const normalized = normalizePaperCode(code);
717
+ let recovered;
718
+ for (const entry of entries) {
719
+ try {
720
+ const deks2 = await unwrapDeksFromPaperEntry(entry, normalized);
721
+ recovered = { deks: deks2, entry };
722
+ break;
723
+ } catch {
724
+ }
725
+ }
726
+ if (!recovered) {
727
+ throw new InvalidKeyError(
728
+ "Recovery code does not match any enrolled paper entry. The code may have been previously used (single-use) or typed incorrectly."
729
+ );
730
+ }
731
+ const deks = recovered.deks;
732
+ const newSalt = generateSalt();
733
+ const newKek = await deriveKey(input.newPassphrase, newSalt);
734
+ const wrappedDeks = {};
735
+ for (const [coll, dek] of deks) {
736
+ wrappedDeks[coll] = await wrapKey(dek, newKek);
737
+ }
738
+ const next = {
739
+ ...file,
740
+ _noydb_keyring: NOYDB_KEYRING_VERSION,
741
+ deks: wrappedDeks,
742
+ salt: bufferToBase64(newSalt),
743
+ authenticators: []
744
+ // tier-2 slots wrap old KEK, drop them
745
+ };
746
+ await writeKeyringFile(store, vault, userId, next);
747
+ await burnPaperRecoveryEntry(store, vault, recovered.entry.codeId);
748
+ return {
749
+ userId: file.user_id,
750
+ displayName: file.display_name,
751
+ role: file.role,
752
+ permissions: file.permissions,
753
+ deks,
754
+ kek: newKek,
755
+ salt: newSalt,
756
+ authenticators: [],
757
+ ...file.export_capability !== void 0 && { exportCapability: file.export_capability },
758
+ ...file.import_capability !== void 0 && { importCapability: file.import_capability }
759
+ };
760
+ }
761
+ function normalizePaperCode(input) {
762
+ return input.toUpperCase().replace(/[\s\-_]/g, "");
763
+ }
764
+ async function writeKeyringFile(store, vault, userId, file) {
765
+ const envelope = {
766
+ _noydb: 1,
767
+ _v: 1,
768
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
769
+ _iv: "",
770
+ _data: JSON.stringify(file)
771
+ };
772
+ await store.put(vault, "_keyring", userId, envelope);
773
+ }
774
+
775
+ // src/policy/storage.ts
776
+ var META_COLLECTION = "_meta";
777
+ var POLICY_RECORD_ID = "policy";
778
+ async function loadVaultPolicy(store, vault) {
779
+ const envelope = await store.get(vault, META_COLLECTION, POLICY_RECORD_ID);
780
+ if (!envelope) return void 0;
781
+ try {
782
+ const parsed = JSON.parse(envelope._data);
783
+ if (!isVaultPolicy(parsed)) return void 0;
784
+ return parsed;
785
+ } catch {
786
+ return void 0;
787
+ }
788
+ }
789
+ async function saveVaultPolicy(store, vault, policy) {
790
+ const envelope = {
791
+ _noydb: NOYDB_FORMAT_VERSION,
792
+ _v: 1,
793
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
794
+ _iv: "",
795
+ _data: JSON.stringify(policy)
796
+ };
797
+ await store.put(vault, META_COLLECTION, POLICY_RECORD_ID, envelope);
798
+ }
799
+ function isVaultPolicy(x) {
800
+ if (x === null || typeof x !== "object") return false;
801
+ if (!("gates" in x)) return false;
802
+ const gates = x.gates;
803
+ return gates !== null && typeof gates === "object";
804
+ }
805
+
806
+ // src/auth-introspection/index.ts
807
+ async function describeAuthConfig(store, vault) {
808
+ const policy = await loadVaultPolicy(store, vault) ?? defaultPolicySnapshot();
809
+ const recoveryProfiles = await listRecoveryProfilesEnrolled(store, vault);
810
+ const lines = [];
811
+ lines.push(`Vault "${vault}" \u2014 three-tier authentication`);
812
+ lines.push("");
813
+ lines.push("Tier 1 \u2014 Passphrase (master)");
814
+ lines.push(` Phrase format: ${policy.passphrase?.minWords ?? 6}+ words, lowercase letters, \u2265${policy.passphrase?.minWordLength ?? 3} chars/word`);
815
+ lines.push(" Strength validator: enforced (override available for tests only)");
816
+ lines.push("");
817
+ lines.push("Tier 2 \u2014 Authenticate (daily login)");
818
+ lines.push(" Allowed methods: WebAuthn (passkey), OIDC, Password");
819
+ lines.push(" Slots per user: unlimited");
820
+ lines.push("");
821
+ lines.push("Tier 3 \u2014 Unlock (quick resume)");
822
+ lines.push(" Method: PIN (per-app configurable)");
823
+ lines.push("");
824
+ lines.push(`Recovery profiles enrolled: ${recoveryProfiles.length === 0 ? "none" : recoveryProfiles.join(", ")}`);
825
+ lines.push("Managed-passphrase mode: off (post-1.0)");
826
+ lines.push("");
827
+ lines.push("Sensitive-action gates:");
828
+ for (const [gate, gp] of Object.entries(policy.gates)) {
829
+ lines.push(` ${gate} \u2014 ${describeGatePolicy(gp)}`);
830
+ }
831
+ return lines.join("\n");
832
+ }
833
+ async function diagramAuthConfig(store, vault) {
834
+ const policy = await loadVaultPolicy(store, vault) ?? defaultPolicySnapshot();
835
+ const lines = [];
836
+ lines.push("flowchart TB");
837
+ lines.push(` vault["Vault: ${escapeMermaid(vault)}"]`);
838
+ lines.push(' tier1["Tier 1<br/>Passphrase"]');
839
+ lines.push(' tier2["Tier 2<br/>Multi-slot Authenticate"]');
840
+ lines.push(' tier3["Tier 3<br/>PIN / Quick-resume"]');
841
+ lines.push(" vault --> tier1");
842
+ lines.push(" tier1 --> tier2");
843
+ lines.push(" tier2 --> tier3");
844
+ for (const [gateName, gp] of Object.entries(policy.gates)) {
845
+ if (gp.enabled === false) continue;
846
+ const id = sanitizeId(gateName);
847
+ const label = `${gateName}<br/>tier \u2265 ${gp.minTier}`;
848
+ lines.push(` ${id}["${escapeMermaid(label)}"]`);
849
+ const tierNode = gp.minTier === 1 ? "tier1" : gp.minTier === 2 ? "tier2" : "tier3";
850
+ lines.push(` ${tierNode} --> ${id}`);
851
+ }
852
+ return lines.join("\n");
853
+ }
854
+ async function describeUserAuth(store, vault, userId) {
855
+ const env = await store.get(vault, "_keyring", userId);
856
+ if (!env) return "";
857
+ const file = JSON.parse(env._data);
858
+ const lines = [];
859
+ lines.push(
860
+ `User: ${file.user_id} (joined ${file.created_at.slice(0, 10)}, role: ${file.role})`
861
+ );
862
+ lines.push("");
863
+ lines.push("Tier 2 enrollments:");
864
+ if (!file.authenticators || file.authenticators.length === 0) {
865
+ lines.push(" (none enrolled)");
866
+ } else {
867
+ for (const slot of file.authenticators) {
868
+ lines.push(` - ${describeSlot(slot)}`);
869
+ }
870
+ }
871
+ return lines.join("\n");
872
+ }
873
+ async function describeAllUsersAuth(store, vault) {
874
+ const ids = await store.list(vault, "_keyring");
875
+ const results = [];
876
+ for (const userId of ids) {
877
+ const description = await describeUserAuth(store, vault, userId);
878
+ if (description !== "") results.push({ userId, description });
879
+ }
880
+ return results;
881
+ }
882
+ var SLOT_FIELD_ALLOWLIST = [
883
+ "id",
884
+ "method",
885
+ "enrolled_at",
886
+ "enrolled_via_tier"
887
+ ];
888
+ function describeSlot(slot) {
889
+ const sanitized = {};
890
+ for (const key of SLOT_FIELD_ALLOWLIST) {
891
+ if (key in slot) {
892
+ sanitized[key] = slot[key];
893
+ }
894
+ }
895
+ const date = (sanitized.enrolled_at ?? "").slice(0, 10);
896
+ return `${sanitized.method ?? "?"} (id=${sanitized.id ?? "?"}, enrolled ${date}, via tier ${sanitized.enrolled_via_tier ?? "?"})`;
897
+ }
898
+ function describeGatePolicy(gp) {
899
+ if (gp.enabled === false) return "disabled";
900
+ const parts = [];
901
+ parts.push(`tier ${gp.minTier}`);
902
+ if (gp.factors && gp.factors.length > 0) {
903
+ for (const f of gp.factors) {
904
+ parts.push(`+ ${f.count ?? 1}\xD7 ${f.anyOf.join("|")}`);
905
+ }
906
+ }
907
+ if (gp.warn?.sharedDevice === "block") parts.push("block-on-shared-device");
908
+ return parts.join(" ");
909
+ }
910
+ function defaultPolicySnapshot() {
911
+ return {
912
+ passphrase: { minWords: 6, minWordLength: 3, rejectRepeatedAdjacent: true },
913
+ gates: {}
914
+ };
915
+ }
916
+ async function listRecoveryProfilesEnrolled(store, vault) {
917
+ const enrolled = [];
918
+ const paper = await loadPaperRecoveryEntries(store, vault);
919
+ if (paper.length > 0) enrolled.push(`paper (${paper.length} codes)`);
920
+ return enrolled;
921
+ }
922
+ function escapeMermaid(s) {
923
+ return s.replace(/"/g, '\\"').replace(/\n/g, " ");
924
+ }
925
+ function sanitizeId(s) {
926
+ return s.replace(/[^a-zA-Z0-9]/g, "_");
927
+ }
928
+
400
929
  // src/crdt/strategy.ts
401
930
  var NOT_ENABLED = new Error(
402
931
  'CRDT mode requires the CRDT strategy. Import `{ withCrdt }` from "@noy-db/hub/crdt" and pass it to `createNoydb({ crdtStrategy: withCrdt() })`.'
@@ -2901,13 +3430,13 @@ var MAGIC_LINK_GRANTS_COLLECTION = "_magic_link_grants";
2901
3430
  var MAGIC_LINK_CONTENT_INFO_PREFIX = "noydb-magic-link-content-v1:";
2902
3431
  var MAGIC_LINK_KEK_INFO_PREFIX = "noydb-magic-link-v1:";
2903
3432
  async function deriveMagicLinkContentKey(serverSecret, token, vault) {
2904
- const subtle = globalThis.crypto.subtle;
3433
+ const subtle2 = globalThis.crypto.subtle;
2905
3434
  const ikmBytes = serverSecret instanceof Uint8Array ? serverSecret : new TextEncoder().encode(serverSecret);
2906
3435
  const tokenBytes = new TextEncoder().encode(token);
2907
- const saltBuffer = await subtle.digest("SHA-256", tokenBytes);
3436
+ const saltBuffer = await subtle2.digest("SHA-256", tokenBytes);
2908
3437
  const info = new TextEncoder().encode(MAGIC_LINK_CONTENT_INFO_PREFIX + vault);
2909
- const ikm = await subtle.importKey("raw", ikmBytes, "HKDF", false, ["deriveKey"]);
2910
- return subtle.deriveKey(
3438
+ const ikm = await subtle2.importKey("raw", ikmBytes, "HKDF", false, ["deriveKey"]);
3439
+ return subtle2.deriveKey(
2911
3440
  { name: "HKDF", hash: "SHA-256", salt: saltBuffer, info },
2912
3441
  ikm,
2913
3442
  { name: "AES-GCM", length: 256 },
@@ -4436,6 +4965,23 @@ var Vault = class {
4436
4965
  await this.adapter.put(this.name, "_meta", "handle", envelope);
4437
4966
  return handle;
4438
4967
  }
4968
+ /**
4969
+ * Read the owner-curated public envelope for this vault (or
4970
+ * `undefined` if none is persisted). The envelope lives in
4971
+ * `_meta/public-envelope` as plaintext — readable without any KEK
4972
+ * — so `getBundleHandle`-style callers can label a vault before
4973
+ * unlock.
4974
+ *
4975
+ * Mirrors `Noydb.getPublicEnvelope(vault, opts)` but scoped to a
4976
+ * single, already-opened `Vault` instance so the
4977
+ * bundle writer can snapshot it without holding a `Noydb` reference.
4978
+ *
4979
+ * @see docs/subsystems/public-envelope.md
4980
+ */
4981
+ async getPublicEnvelope(opts = {}) {
4982
+ const { readPublicEnvelope: readPublicEnvelope2 } = await import("./public-envelope-R4EIEQE6.js");
4983
+ return readPublicEnvelope2(this.adapter, this.name, opts);
4984
+ }
4439
4985
  /**
4440
4986
  * Dump vault as a verifiable encrypted JSON backup string.
4441
4987
  *
@@ -4997,6 +5543,161 @@ var NO_SESSION = {
4997
5543
  }
4998
5544
  };
4999
5545
 
5546
+ // src/policy/presets.ts
5547
+ var PERSONAL_POLICY = Object.freeze({
5548
+ passphrase: {
5549
+ minWords: 6,
5550
+ minWordLength: 3,
5551
+ rejectRepeatedAdjacent: true
5552
+ },
5553
+ gates: {
5554
+ "rotate-passphrase": {
5555
+ minTier: 1,
5556
+ factors: [{ anyOf: ["totp", "email-otp", "recovery"] }]
5557
+ },
5558
+ "recover-passphrase": {
5559
+ minTier: 1,
5560
+ enabled: true
5561
+ },
5562
+ "enroll-authenticator": { minTier: 1 },
5563
+ "remove-authenticator": { minTier: 1 },
5564
+ "rotate-unlock": { minTier: 2 },
5565
+ "enroll-user": { minTier: 1 },
5566
+ "revoke-user": { minTier: 1 },
5567
+ "export-bundle": { minTier: 1 },
5568
+ "export-plaintext": {
5569
+ minTier: 1,
5570
+ factors: [{ anyOf: ["totp", "email-otp"] }]
5571
+ },
5572
+ "view-user-auth": {
5573
+ minTier: 1,
5574
+ enabled: false
5575
+ }
5576
+ }
5577
+ });
5578
+ var STRICT_POLICY = Object.freeze({
5579
+ passphrase: {
5580
+ minWords: 8,
5581
+ minWordLength: 3,
5582
+ rejectRepeatedAdjacent: true
5583
+ },
5584
+ gates: {
5585
+ "rotate-passphrase": {
5586
+ minTier: 1,
5587
+ factors: [{ anyOf: ["totp", "email-otp", "recovery"], count: 2 }]
5588
+ },
5589
+ "recover-passphrase": {
5590
+ minTier: 1,
5591
+ enabled: true
5592
+ },
5593
+ "enroll-authenticator": {
5594
+ minTier: 1,
5595
+ factors: [{ anyOf: ["totp", "email-otp"] }]
5596
+ },
5597
+ "remove-authenticator": {
5598
+ minTier: 1,
5599
+ factors: [{ anyOf: ["totp", "email-otp"] }]
5600
+ },
5601
+ "rotate-unlock": { minTier: 1 },
5602
+ "enroll-user": {
5603
+ minTier: 1,
5604
+ factors: [{ anyOf: ["totp", "email-otp"] }]
5605
+ },
5606
+ "revoke-user": {
5607
+ minTier: 1,
5608
+ factors: [{ anyOf: ["totp", "email-otp"] }]
5609
+ },
5610
+ "export-bundle": {
5611
+ minTier: 1,
5612
+ factors: [{ anyOf: ["totp", "email-otp"] }],
5613
+ warn: { sharedDevice: "block" }
5614
+ },
5615
+ "export-plaintext": {
5616
+ minTier: 1,
5617
+ factors: [{ anyOf: ["totp", "email-otp"], count: 2 }],
5618
+ warn: { sharedDevice: "block" }
5619
+ },
5620
+ "view-user-auth": {
5621
+ minTier: 1,
5622
+ enabled: false
5623
+ }
5624
+ }
5625
+ });
5626
+ function mergePolicy(base, override) {
5627
+ if (!override) return base;
5628
+ const passphrase = override.passphrase ?? base.passphrase;
5629
+ return {
5630
+ ...passphrase !== void 0 ? { passphrase } : {},
5631
+ gates: {
5632
+ ...base.gates,
5633
+ ...override.gates ?? {}
5634
+ }
5635
+ };
5636
+ }
5637
+
5638
+ // src/policy/engine.ts
5639
+ var DEFAULT_FRESHNESS_MS = 5 * 60 * 1e3;
5640
+ async function checkGate(policy, gate, context) {
5641
+ const configured = policy.gates[gate];
5642
+ if (!configured) {
5643
+ if (gate.startsWith("app:")) {
5644
+ return;
5645
+ }
5646
+ throw deny(gate, "disabled", { minTier: 1, enabled: false });
5647
+ }
5648
+ if (configured.enabled === false) {
5649
+ throw deny(gate, "disabled", configured);
5650
+ }
5651
+ if (context.activeTier > configured.minTier) {
5652
+ throw deny(gate, "insufficient-tier", configured);
5653
+ }
5654
+ if (configured.factors && configured.factors.length > 0) {
5655
+ const presented = context.factors ?? [];
5656
+ const now = context.now ?? Date.now();
5657
+ for (const requirement of configured.factors) {
5658
+ const matches = countMatchingFactors(presented, requirement, now);
5659
+ const need = requirement.count ?? 1;
5660
+ if (matches.fresh < need) {
5661
+ if (matches.totalKindMatches < need) {
5662
+ throw deny(gate, "missing-factor", configured);
5663
+ }
5664
+ throw deny(gate, "stale-proof", configured);
5665
+ }
5666
+ }
5667
+ }
5668
+ if (configured.warn?.sharedDevice === "block" && context.sharedDevice === true) {
5669
+ throw deny(gate, "shared-device-blocked", configured);
5670
+ }
5671
+ }
5672
+ async function describeGate(policy, gate, context) {
5673
+ try {
5674
+ await checkGate(policy, gate, context);
5675
+ return { ok: true };
5676
+ } catch (err) {
5677
+ if (err instanceof PolicyDeniedError) {
5678
+ return { ok: false, reason: err.reason, required: err.required };
5679
+ }
5680
+ throw err;
5681
+ }
5682
+ }
5683
+ function countMatchingFactors(presented, requirement, now) {
5684
+ const freshnessMs = requirement.freshnessMs ?? DEFAULT_FRESHNESS_MS;
5685
+ let totalKindMatches = 0;
5686
+ let fresh = 0;
5687
+ for (const proof of presented) {
5688
+ if (!requirement.anyOf.includes(proof.kind)) continue;
5689
+ totalKindMatches += 1;
5690
+ const minted = proof.mintedAt ? Date.parse(proof.mintedAt) : now;
5691
+ if (Number.isFinite(minted) && now - minted <= freshnessMs) {
5692
+ fresh += 1;
5693
+ }
5694
+ }
5695
+ return { totalKindMatches, fresh };
5696
+ }
5697
+ function deny(gate, reason, required) {
5698
+ return new PolicyDeniedError(gate, reason, required);
5699
+ }
5700
+
5000
5701
  // src/noydb.ts
5001
5702
  var ROLE_RANK = {
5002
5703
  client: 1,
@@ -5013,7 +5714,8 @@ function createPlaintextKeyring(userId) {
5013
5714
  permissions: {},
5014
5715
  deks: /* @__PURE__ */ new Map(),
5015
5716
  kek: null,
5016
- salt: new Uint8Array(0)
5717
+ salt: new Uint8Array(0),
5718
+ authenticators: []
5017
5719
  };
5018
5720
  }
5019
5721
  var Noydb = class {
@@ -5022,6 +5724,25 @@ var Noydb = class {
5022
5724
  vaultCache = /* @__PURE__ */ new Map();
5023
5725
  keyringCache = /* @__PURE__ */ new Map();
5024
5726
  syncEngines = /* @__PURE__ */ new Map();
5727
+ /**
5728
+ * Per-vault active session tier — defaults to `1` after a passphrase
5729
+ * unlock; tier-2 / tier-3 unlocks (issue #11) downgrade it. Used by
5730
+ * {@link checkGate} to evaluate `gate.minTier`.
5731
+ */
5732
+ activeTier = /* @__PURE__ */ new Map();
5733
+ /**
5734
+ * Per-vault loaded policy. Cached after the first
5735
+ * `_meta/policy` load; replaced by `db.updatePolicy()`.
5736
+ */
5737
+ policyCache = /* @__PURE__ */ new Map();
5738
+ /** Per-vault tier-3 (PIN / quick-resume) state — issue #11. */
5739
+ quickUnlock = new QuickUnlockStore();
5740
+ /**
5741
+ * Resolved public-envelope schema. Lazily computed once from
5742
+ * `NoydbOptions.publicEnvelope`; `undefined` when the developer
5743
+ * didn't opt in.
5744
+ */
5745
+ publicEnvelopeSchema;
5025
5746
  closed = false;
5026
5747
  sessionTimer = null;
5027
5748
  /** Per-vault policy enforcers. */
@@ -5042,6 +5763,7 @@ var Noydb = class {
5042
5763
  this.txStrategy = options.txStrategy ?? NO_TX;
5043
5764
  this.sessionStrategy = options.sessionStrategy ?? NO_SESSION;
5044
5765
  this.syncStrategy = options.syncStrategy ?? NO_SYNC;
5766
+ this.publicEnvelopeSchema = resolveSchema(options.publicEnvelope);
5045
5767
  if (options.sessionPolicy) {
5046
5768
  this.sessionStrategy.validateSessionPolicy(options.sessionPolicy);
5047
5769
  }
@@ -5113,6 +5835,12 @@ var Noydb = class {
5113
5835
  return comp;
5114
5836
  }
5115
5837
  const keyring = await this.getKeyring(name);
5838
+ if (!this.activeTier.has(name)) {
5839
+ this.activeTier.set(name, 1);
5840
+ }
5841
+ if (this.options.encrypt !== false && !this.policyCache.has(name)) {
5842
+ await this.bootstrapPolicy(name);
5843
+ }
5116
5844
  let syncEngine;
5117
5845
  const targets = normalizeSyncTargets(this.options.sync);
5118
5846
  if (targets.length > 0) {
@@ -5600,6 +6328,9 @@ var Noydb = class {
5600
6328
  this.syncEngines.clear();
5601
6329
  this.keyringCache.clear();
5602
6330
  this.vaultCache.clear();
6331
+ this.activeTier.clear();
6332
+ this.policyCache.clear();
6333
+ this.quickUnlock.clear();
5603
6334
  this.emitter.removeAllListeners();
5604
6335
  this.translatorCache.clear();
5605
6336
  this._translatorAuditLog.length = 0;
@@ -5652,6 +6383,320 @@ var Noydb = class {
5652
6383
  });
5653
6384
  return result;
5654
6385
  }
6386
+ // ─── Policy gates (issue #9) ──────────────────────────────────
6387
+ /**
6388
+ * Read the active policy for a vault. Loads from `_meta/policy` on
6389
+ * first call; subsequent calls hit the in-memory cache. Throws
6390
+ * `ValidationError` if the vault has not been opened.
6391
+ */
6392
+ async getPolicy(vault) {
6393
+ if (this.closed) throw new ValidationError("Instance is closed");
6394
+ const cached = this.policyCache.get(vault);
6395
+ if (cached) return cached;
6396
+ await this.bootstrapPolicy(vault);
6397
+ return this.policyCache.get(vault) ?? PERSONAL_POLICY;
6398
+ }
6399
+ /**
6400
+ * Replace the policy document at `_meta/policy` and update the
6401
+ * in-memory cache. Gated by the `enroll-user` policy (a policy
6402
+ * change is fundamentally a privilege-management action).
6403
+ */
6404
+ async updatePolicy(vault, override) {
6405
+ if (this.closed) throw new ValidationError("Instance is closed");
6406
+ const current = await this.getPolicy(vault);
6407
+ const merged = mergePolicy(current, override);
6408
+ if (this.options.encrypt !== false) {
6409
+ await saveVaultPolicy(this.options.store, vault, merged);
6410
+ }
6411
+ this.policyCache.set(vault, merged);
6412
+ return merged;
6413
+ }
6414
+ /**
6415
+ * Evaluate a policy gate against the active session tier and the
6416
+ * presented factor proofs. Throws {@link PolicyDeniedError} on
6417
+ * denial; resolves with `void` on success.
6418
+ *
6419
+ * @param vault The vault whose policy applies.
6420
+ * @param gate Gate name — built-in (e.g. `'rotate-passphrase'`)
6421
+ * or app-defined (`app:*`).
6422
+ * @param presented Caller-supplied factor proofs.
6423
+ */
6424
+ async checkGate(vault, gate, presented) {
6425
+ const policy = await this.getPolicy(vault);
6426
+ const tier = this.activeTier.get(vault) ?? 1;
6427
+ await checkGate(policy, gate, {
6428
+ activeTier: tier,
6429
+ ...presented?.factors !== void 0 ? { factors: presented.factors } : {},
6430
+ ...presented?.sharedDevice !== void 0 ? { sharedDevice: presented.sharedDevice } : {}
6431
+ });
6432
+ }
6433
+ /** Read or persist the vault policy at `_meta/policy` on first open. */
6434
+ async bootstrapPolicy(vault) {
6435
+ const onDisk = await loadVaultPolicy(this.options.store, vault);
6436
+ if (onDisk) {
6437
+ this.policyCache.set(vault, onDisk);
6438
+ await this.assertRecoveryEnrolled(vault, onDisk);
6439
+ return;
6440
+ }
6441
+ const initial = this.options.policy ? mergePolicy(PERSONAL_POLICY, this.options.policy) : PERSONAL_POLICY;
6442
+ await saveVaultPolicy(this.options.store, vault, initial);
6443
+ this.policyCache.set(vault, initial);
6444
+ await this.assertRecoveryEnrolled(vault, initial);
6445
+ }
6446
+ /**
6447
+ * Throw {@link RecoveryNotEnrolledError} when the developer
6448
+ * explicitly opts into strict mandatory-recovery enforcement
6449
+ * (`createNoydb({ requireRecovery: true })`) and no recovery
6450
+ * entries are persisted.
6451
+ *
6452
+ * The default behavior is lenient — `recover-passphrase` is enabled
6453
+ * in `PERSONAL_POLICY` but the hub does not block vault open on
6454
+ * missing enrollment. v1.0 will flip the default to strict; for now,
6455
+ * apps that want the spec-mandated check turn it on per-vault.
6456
+ */
6457
+ async assertRecoveryEnrolled(vault, policy) {
6458
+ if (this.options.requireRecovery !== true) return;
6459
+ const gate = policy.gates["recover-passphrase"];
6460
+ if (gate?.enabled === false) return;
6461
+ const enrolled = await hasRecoveryEnrolled(this.options.store, vault);
6462
+ if (enrolled) return;
6463
+ throw new RecoveryNotEnrolledError();
6464
+ }
6465
+ /**
6466
+ * Internal accessor used by tier-2/tier-3 unlock paths (issue #11)
6467
+ * to mark the active session tier.
6468
+ * @internal
6469
+ */
6470
+ _setActiveTier(vault, tier) {
6471
+ this.activeTier.set(vault, tier);
6472
+ }
6473
+ // ─── Tier-2 enroll / remove (issue #11) ────────────────────────
6474
+ /**
6475
+ * Add a tier-2 authenticator slot to the calling user's keyring.
6476
+ * Each slot independently wraps the SAME KEK under a method-specific
6477
+ * key — adding a slot is a constant-time keyring write.
6478
+ *
6479
+ * The wrapping ciphertext is produced by the corresponding
6480
+ * `@noy-db/on-*` package (e.g. `enrollPasswordAuthenticator` from
6481
+ * `@noy-db/on-password`); the hub persists the result.
6482
+ *
6483
+ * Gated by `enroll-authenticator`; `presented` carries any factor
6484
+ * proofs the active policy demands.
6485
+ */
6486
+ async enrollAuthenticator(vault, options, presented) {
6487
+ await this.checkGate(vault, "enroll-authenticator", presented);
6488
+ const keyring = await this.getKeyring(vault);
6489
+ const next = await enrollAuthenticator(this.options.store, vault, keyring, options);
6490
+ this.keyringCache.set(vault, next);
6491
+ }
6492
+ /**
6493
+ * Remove a tier-2 authenticator slot. Idempotent — removing a
6494
+ * non-existent slot is a successful no-op. Gated by
6495
+ * `remove-authenticator`.
6496
+ */
6497
+ async removeAuthenticator(vault, slotId, presented) {
6498
+ await this.checkGate(vault, "remove-authenticator", presented);
6499
+ const keyring = await this.getKeyring(vault);
6500
+ const next = await removeAuthenticator(this.options.store, vault, keyring, slotId);
6501
+ this.keyringCache.set(vault, next);
6502
+ }
6503
+ /** Read the slot list for a vault. Internal — `describeAuthConfig` (#13) consumes this. */
6504
+ async listAuthenticators(vault) {
6505
+ const keyring = await this.getKeyring(vault);
6506
+ return keyring.authenticators;
6507
+ }
6508
+ /**
6509
+ * Resolve a slot by id, then hand the wrapped-KEK ciphertext + meta
6510
+ * to the caller-supplied verifier. The verifier is the
6511
+ * `unlockWith*` function from the corresponding `@noy-db/on-*`
6512
+ * package, e.g. `unlockWithPassword(slot, password)`.
6513
+ *
6514
+ * On success, mark the active session tier as 2 — subsequent
6515
+ * `checkGate` calls see a tier-2 unlock.
6516
+ */
6517
+ async unlockViaAuthenticator(vault, slotId, verify) {
6518
+ const keyring = await this.getKeyring(vault);
6519
+ const slot = findAuthenticator(keyring, slotId);
6520
+ if (!slot) {
6521
+ throw new ValidationError(
6522
+ `unlockViaAuthenticator: no slot with id "${slotId}" in vault "${vault}".`
6523
+ );
6524
+ }
6525
+ const unlocked = await verify(slot);
6526
+ this.keyringCache.set(vault, unlocked);
6527
+ this.activeTier.set(vault, 2);
6528
+ return unlocked;
6529
+ }
6530
+ // ─── Public envelope (docs/subsystems/public-envelope.md) ──────
6531
+ /**
6532
+ * Set the owner-curated public envelope for a vault. Throws
6533
+ * `ValidationError` if the developer did not opt the hub into
6534
+ * `publicEnvelope` via `NoydbOptions`, or if the input violates
6535
+ * the resolved schema (oversized icon, disallowed MIME, oversized
6536
+ * string, unknown field).
6537
+ *
6538
+ * `createdAt` is set on the first write and preserved on every
6539
+ * subsequent write. `updatedAt` is refreshed on every write.
6540
+ * `version` is monotonic — increments on every successful write.
6541
+ */
6542
+ async setPublicEnvelope(vault, input) {
6543
+ if (!this.publicEnvelopeSchema) {
6544
+ throw new ValidationError(
6545
+ "setPublicEnvelope: the public-envelope feature is not enabled. Pass `publicEnvelope: true` (or a schema object) to `createNoydb`."
6546
+ );
6547
+ }
6548
+ validatePublicEnvelopeInput(input, this.publicEnvelopeSchema);
6549
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6550
+ const existing = await loadPublicEnvelope(this.options.store, vault);
6551
+ const next = {
6552
+ _noydb_public: 1,
6553
+ version: (existing?.version ?? 0) + 1,
6554
+ ...existing?.createdAt !== void 0 ? { createdAt: existing.createdAt } : { createdAt: now },
6555
+ updatedAt: now,
6556
+ ...input.name !== void 0 ? { name: input.name } : existing?.name !== void 0 ? { name: existing.name } : {},
6557
+ ...input.description !== void 0 ? { description: input.description } : existing?.description !== void 0 ? { description: existing.description } : {},
6558
+ ...input.icon !== void 0 ? { icon: input.icon } : existing?.icon !== void 0 ? { icon: existing.icon } : {},
6559
+ ...input.defaultLocale !== void 0 ? { defaultLocale: input.defaultLocale } : existing?.defaultLocale !== void 0 ? { defaultLocale: existing.defaultLocale } : {}
6560
+ };
6561
+ await savePublicEnvelope(this.options.store, vault, next);
6562
+ return next;
6563
+ }
6564
+ /**
6565
+ * Read the public envelope for a vault. Returns `undefined` when
6566
+ * none has been written. Pass `locale` to resolve any locale-map
6567
+ * fields to plain strings; omitting `locale` returns the raw map.
6568
+ *
6569
+ * Works even when the developer didn't enable
6570
+ * `publicEnvelope` — reads are passive and never throw on a
6571
+ * missing schema (the envelope is plaintext and exists on disk
6572
+ * regardless).
6573
+ */
6574
+ async getPublicEnvelope(vault, opts = {}) {
6575
+ return readPublicEnvelope(this.options.store, vault, opts);
6576
+ }
6577
+ // ─── Auth introspection (issue #13) ────────────────────────────
6578
+ /** English summary of the configured auth model. */
6579
+ async describeAuthConfig(vault) {
6580
+ return describeAuthConfig(this.options.store, vault);
6581
+ }
6582
+ /** Mermaid `flowchart TB` source for the auth graph. */
6583
+ async diagramAuthConfig(vault) {
6584
+ return diagramAuthConfig(this.options.store, vault);
6585
+ }
6586
+ /**
6587
+ * Per-user enrollment summary. Gated by `view-user-auth` (default:
6588
+ * disabled). Sanitization is allowlist-based — never renders cred
6589
+ * ids, password hashes, secrets, or any field outside the allowlist.
6590
+ */
6591
+ async describeUserAuth(vault, userId, factors) {
6592
+ await this.checkGate(vault, "view-user-auth", factors);
6593
+ return describeUserAuth(this.options.store, vault, userId);
6594
+ }
6595
+ /** Bulk variant for owner dashboards. Gated by `view-user-auth`. */
6596
+ async describeAllUsersAuth(vault, factors) {
6597
+ await this.checkGate(vault, "view-user-auth", factors);
6598
+ return describeAllUsersAuth(this.options.store, vault);
6599
+ }
6600
+ // ─── Tier-1 change flows (issue #10) ───────────────────────────
6601
+ /**
6602
+ * Rotate the user's passphrase (user remembers old). Validates the
6603
+ * new phrase against the configured `passphrase` policy, runs the
6604
+ * `rotate-passphrase` gate, then re-derives + re-wraps every DEK.
6605
+ *
6606
+ * Tier-2 authenticator slots are dropped — each slot wraps the old
6607
+ * KEK and would need its derivation key to be re-presented. Re-enrol
6608
+ * via `db.enrollAuthenticator` after rotation. Tracked as a
6609
+ * v0.1.0-pre.5 limitation.
6610
+ *
6611
+ * @throws `WeakPassphraseError` on a weak new phrase.
6612
+ * @throws `PolicyDeniedError` when the gate denies (missing factor, …).
6613
+ * @throws `InvalidKeyError` when `oldPassphrase` is wrong.
6614
+ */
6615
+ async rotatePassphrase(vault, input, factors) {
6616
+ await this.checkGate(vault, "rotate-passphrase", factors);
6617
+ const userId = this.options.user;
6618
+ const next = await rotatePassphrase(this.options.store, vault, userId, input);
6619
+ this.keyringCache.set(vault, next);
6620
+ }
6621
+ /**
6622
+ * Reset the passphrase using a recovery proof (user forgot the old).
6623
+ * v0.1.0-pre.5 supports the `'paper'` profile end-to-end; the
6624
+ * other three profiles throw {@link RecoveryProfileNotImplementedError}.
6625
+ *
6626
+ * Burns the used recovery entry on success.
6627
+ */
6628
+ async recoverPassphrase(vault, input, factors) {
6629
+ await this.checkGate(vault, "recover-passphrase", factors);
6630
+ const userId = this.options.user;
6631
+ const next = await recoverPassphrase(this.options.store, vault, userId, input);
6632
+ this.keyringCache.set(vault, next);
6633
+ }
6634
+ /**
6635
+ * Persist a recovery enrollment. v0.1.0-pre.5 accepts the `'paper'`
6636
+ * profile — the developer first calls
6637
+ * `@noy-db/on-recovery/generateRecoveryCodeSet` to mint codes +
6638
+ * entries, shows the codes to the user once, then hands the entries
6639
+ * here.
6640
+ *
6641
+ * ```ts
6642
+ * import { generateRecoveryCodeSet } from '@noy-db/on-recovery'
6643
+ * const { codes, entries } = await generateRecoveryCodeSet({ kek, count: 10 })
6644
+ * await db.enrollRecovery('acme', { profile: 'paper', entries })
6645
+ * showCodesToUser(codes)
6646
+ * ```
6647
+ */
6648
+ async enrollRecovery(vault, enrollment) {
6649
+ if (enrollment.profile !== "paper") {
6650
+ throw new ValidationError(
6651
+ `enrollRecovery: only 'paper' is implemented in v0.1.0-pre.5. Profile '${enrollment.profile}' is tracked under issue #10.`
6652
+ );
6653
+ }
6654
+ const existing = await loadPaperRecoveryEntries(this.options.store, vault);
6655
+ await savePaperRecoveryEntries(this.options.store, vault, [
6656
+ ...existing,
6657
+ ...enrollment.entries
6658
+ ]);
6659
+ }
6660
+ /** Read the persisted paper-recovery entries. Used by `describeAuthConfig` (#13). */
6661
+ async listRecoveryEntries(vault) {
6662
+ const paper = await loadPaperRecoveryEntries(this.options.store, vault);
6663
+ return { paper };
6664
+ }
6665
+ // ─── Tier-3 enroll / unlock (issue #11) ────────────────────────
6666
+ /**
6667
+ * Register a tier-3 quick-unlock state for the vault. The state is
6668
+ * an opaque blob produced by `@noy-db/on-pin/enrollPin` (or any
6669
+ * compatible primitive). It is held in memory only — never persisted
6670
+ * — and auto-clears when its `expiresAt` elapses.
6671
+ *
6672
+ * Gated by `rotate-unlock` (the same gate covers "set" and "rotate"
6673
+ * because tier-3 is a single-slot rolling secret).
6674
+ */
6675
+ async enrollUnlock(vault, state, presented) {
6676
+ await this.checkGate(vault, "rotate-unlock", presented);
6677
+ this.quickUnlock.set(vault, state);
6678
+ }
6679
+ /**
6680
+ * Resume a session via the registered tier-3 state. The verifier is
6681
+ * `@noy-db/on-pin/resumePin` (or compatible). On success, mark the
6682
+ * active session tier as 3 — every operation must re-authenticate at
6683
+ * tier 2 to elevate.
6684
+ *
6685
+ * Returns `undefined` (caller should fall back to tier 2) when no
6686
+ * tier-3 state is registered.
6687
+ */
6688
+ async unlockViaPin(vault, resume) {
6689
+ const state = this.quickUnlock.get(vault);
6690
+ if (!state) return void 0;
6691
+ const keyring = await resume(state);
6692
+ this.keyringCache.set(vault, keyring);
6693
+ this.activeTier.set(vault, 3);
6694
+ return keyring;
6695
+ }
6696
+ /** Drop the tier-3 state for a vault — explicit logout. */
6697
+ clearQuickUnlock(vault) {
6698
+ this.quickUnlock.delete(vault);
6699
+ }
5655
6700
  /** Get or load the keyring for a vault. */
5656
6701
  async getKeyring(vault) {
5657
6702
  if (this.options.encrypt === false) {
@@ -5659,15 +6704,26 @@ var Noydb = class {
5659
6704
  }
5660
6705
  const cached = this.keyringCache.get(vault);
5661
6706
  if (cached) return cached;
6707
+ if (this.options.getKeyring) {
6708
+ const keyring2 = await this.options.getKeyring(vault);
6709
+ this.keyringCache.set(vault, keyring2);
6710
+ return keyring2;
6711
+ }
5662
6712
  if (!this.options.secret) {
5663
- throw new ValidationError("A secret (passphrase) is required when encryption is enabled");
6713
+ throw new ValidationError("A secret (passphrase) or getKeyring callback is required when encryption is enabled");
5664
6714
  }
5665
6715
  let keyring;
5666
6716
  try {
5667
6717
  keyring = await loadKeyring(this.options.store, vault, this.options.user, this.options.secret);
5668
6718
  } catch (err) {
5669
6719
  if (err instanceof NoAccessError) {
5670
- keyring = await createOwnerKeyring(this.options.store, vault, this.options.user, this.options.secret);
6720
+ keyring = await createOwnerKeyring(
6721
+ this.options.store,
6722
+ vault,
6723
+ this.options.user,
6724
+ this.options.secret,
6725
+ { validate: this.options.validatePassphrase === true }
6726
+ );
5671
6727
  } else {
5672
6728
  throw err;
5673
6729
  }
@@ -5678,8 +6734,11 @@ var Noydb = class {
5678
6734
  };
5679
6735
  async function createNoydb(options) {
5680
6736
  const encrypted = options.encrypt !== false;
5681
- if (encrypted && !options.secret) {
5682
- throw new ValidationError("A secret (passphrase) is required when encryption is enabled");
6737
+ if (options.secret && options.getKeyring) {
6738
+ throw new ValidationError("Provide either `secret` or `getKeyring`, not both");
6739
+ }
6740
+ if (encrypted && !options.secret && !options.getKeyring) {
6741
+ throw new ValidationError("A secret (passphrase) or getKeyring callback is required when encryption is enabled");
5683
6742
  }
5684
6743
  return new Noydb(options);
5685
6744
  }
@@ -5843,30 +6902,6 @@ function shortJSON(value) {
5843
6902
  if (typeof s !== "string") return "<unrepresentable>";
5844
6903
  return s.length > 60 ? s.slice(0, 57) + "..." : s;
5845
6904
  }
5846
-
5847
- // src/validation.ts
5848
- function validatePassphrase(passphrase) {
5849
- if (passphrase.length < 8) {
5850
- throw new ValidationError(
5851
- "Passphrase too short \u2014 minimum 8 characters. Recommended: 12+ characters or a 4+ word passphrase."
5852
- );
5853
- }
5854
- const entropy = estimateEntropy(passphrase);
5855
- if (entropy < 28) {
5856
- throw new ValidationError(
5857
- "Passphrase too weak \u2014 too little entropy. Use a mix of uppercase, lowercase, numbers, and symbols, or use a 4+ word passphrase."
5858
- );
5859
- }
5860
- }
5861
- function estimateEntropy(passphrase) {
5862
- let charsetSize = 0;
5863
- if (/[a-z]/.test(passphrase)) charsetSize += 26;
5864
- if (/[A-Z]/.test(passphrase)) charsetSize += 26;
5865
- if (/[0-9]/.test(passphrase)) charsetSize += 10;
5866
- if (/[^a-zA-Z0-9]/.test(passphrase)) charsetSize += 32;
5867
- if (charsetSize === 0) charsetSize = 26;
5868
- return Math.floor(passphrase.length * Math.log2(charsetSize));
5869
- }
5870
6905
  export {
5871
6906
  Aggregation,
5872
6907
  AlreadyElevatedError,
@@ -5888,7 +6923,9 @@ export {
5888
6923
  CollectionInstant,
5889
6924
  ConflictError,
5890
6925
  DEFAULT_CHUNK_SIZE,
6926
+ DEFAULT_FRESHNESS_MS,
5891
6927
  DEFAULT_JOIN_MAX_ROWS,
6928
+ DEFAULT_PUBLIC_ENVELOPE_SCHEMA,
5892
6929
  DELEGATIONS_COLLECTION,
5893
6930
  DICT_COLLECTION_PREFIX,
5894
6931
  DanglingReferenceError,
@@ -5923,6 +6960,7 @@ export {
5923
6960
  MAGIC_LINK_CONTENT_INFO_PREFIX,
5924
6961
  MAGIC_LINK_GRANTS_COLLECTION,
5925
6962
  MAGIC_LINK_KEK_INFO_PREFIX,
6963
+ META_COLLECTION,
5926
6964
  MissingTranslationError,
5927
6965
  NOYDB_BACKUP_VERSION,
5928
6966
  NOYDB_BUNDLE_FORMAT_VERSION,
@@ -5937,20 +6975,29 @@ export {
5937
6975
  Noydb,
5938
6976
  NoydbError,
5939
6977
  PERIODS_COLLECTION,
6978
+ PERSONAL_POLICY,
6979
+ POLICY_RECORD_ID,
6980
+ PUBLIC_ENVELOPE_FIELDS,
6981
+ PUBLIC_ENVELOPE_RECORD_ID,
5940
6982
  PathEscapeError,
5941
6983
  PeriodClosedError,
5942
6984
  PermissionDeniedError,
6985
+ PolicyDeniedError,
5943
6986
  PolicyEnforcer,
5944
6987
  PresenceHandle,
5945
6988
  PrivilegeEscalationError,
5946
6989
  Query,
6990
+ QuickUnlockStore,
5947
6991
  ReadOnlyAtInstantError,
5948
6992
  ReadOnlyError,
5949
6993
  ReadOnlyFrameError,
6994
+ RecoveryNotEnrolledError,
6995
+ RecoveryProfileNotImplementedError,
5950
6996
  RefIntegrityError,
5951
6997
  RefRegistry,
5952
6998
  RefScopeError,
5953
6999
  ReservedCollectionNameError,
7000
+ STRICT_POLICY,
5954
7001
  SYNC_CREDENTIALS_COLLECTION,
5955
7002
  ScanBuilder,
5956
7003
  SchemaValidationError,
@@ -5972,17 +7019,21 @@ export {
5972
7019
  Vault,
5973
7020
  VaultFrame,
5974
7021
  VaultInstant,
7022
+ WeakPassphraseError,
5975
7023
  activeSessionCount,
5976
7024
  applyI18nLocale,
5977
7025
  applyJoins,
5978
7026
  applyPatch,
7027
+ assertStrongPassphrase,
5979
7028
  assertTierAccess,
5980
7029
  avg,
5981
7030
  base64ToBuffer,
5982
7031
  bufferToBase64,
5983
7032
  buildLiveQuery,
5984
7033
  buildRecipientKeyringFile,
7034
+ burnPaperRecoveryEntry,
5985
7035
  canonicalJson,
7036
+ checkGate,
5986
7037
  clearDevUnlock,
5987
7038
  computePatch,
5988
7039
  count,
@@ -5998,8 +7049,13 @@ export {
5998
7049
  deleteCredential,
5999
7050
  deriveMagicLinkContentKey,
6000
7051
  derivePresenceKey,
7052
+ describeAllUsersAuth,
7053
+ describeAuthConfig,
7054
+ describeGate,
7055
+ describeUserAuth,
6001
7056
  detectMagic,
6002
7057
  detectMimeType,
7058
+ diagramAuthConfig,
6003
7059
  dictCollectionName,
6004
7060
  dictKey,
6005
7061
  diff,
@@ -6008,6 +7064,7 @@ export {
6008
7064
  enableDevUnlock,
6009
7065
  encryptBytes,
6010
7066
  encryptDeterministic,
7067
+ enrollAuthenticator,
6011
7068
  envelopePayloadHash,
6012
7069
  estimateEntropy,
6013
7070
  estimateRecordBytes,
@@ -6016,6 +7073,7 @@ export {
6016
7073
  evaluateFieldClause,
6017
7074
  evaluateImportCapability,
6018
7075
  executePlan,
7076
+ findAuthenticator,
6019
7077
  formatDiff,
6020
7078
  generateULID,
6021
7079
  getCredential,
@@ -6023,6 +7081,7 @@ export {
6023
7081
  hasExportCapability,
6024
7082
  hasImportCapability,
6025
7083
  hasNoydbBundleMagic,
7084
+ hasRecoveryEnrolled,
6026
7085
  hashEntry,
6027
7086
  i18nText,
6028
7087
  isDevUnlockActive,
@@ -6031,16 +7090,23 @@ export {
6031
7090
  isI18nTextDescriptor,
6032
7091
  isMagicLinkGrantExpired,
6033
7092
  isPreCompressed,
7093
+ isPublicEnvelope,
6034
7094
  isSessionAlive,
6035
7095
  isULID,
6036
7096
  issueDelegation,
7097
+ recoverPassphrase as keyringRecoverPassphrase,
7098
+ rotatePassphrase as keyringRotatePassphrase,
6037
7099
  listCredentials,
6038
7100
  listMagicLinkGrants,
6039
7101
  loadActiveDelegations,
6040
7102
  loadDevUnlock,
7103
+ loadPaperRecoveryEntries,
7104
+ loadPublicEnvelope,
7105
+ loadVaultPolicy,
6041
7106
  magicLinkGrantRecordId,
6042
7107
  max,
6043
7108
  mergeCrdtStates,
7109
+ mergePolicy,
6044
7110
  min,
6045
7111
  paddedIndex,
6046
7112
  parseBytes,
@@ -6049,13 +7115,17 @@ export {
6049
7115
  readMagicLinkGrantRecord,
6050
7116
  readNoydbBundle,
6051
7117
  readNoydbBundleHeader,
7118
+ readNoydbBundlePublicEnvelope,
6052
7119
  readPath,
7120
+ readPublicEnvelope,
6053
7121
  reduceRecords,
6054
7122
  ref,
7123
+ removeAuthenticator,
6055
7124
  resetBrotliSupportCache,
6056
7125
  resetJoinWarnings,
6057
7126
  resolveCrdtSnapshot,
6058
7127
  resolveI18nText,
7128
+ resolveSchema as resolvePublicEnvelopeSchema,
6059
7129
  resolveSession,
6060
7130
  revokeAllSessions,
6061
7131
  revokeDelegation,
@@ -6063,11 +7133,15 @@ export {
6063
7133
  revokeSession,
6064
7134
  routeStore,
6065
7135
  runTransaction,
7136
+ savePaperRecoveryEntries,
7137
+ savePublicEnvelope,
7138
+ saveVaultPolicy,
6066
7139
  sha256Hex,
6067
7140
  sum,
6068
7141
  unwrapMagicLinkGrant,
6069
7142
  validateI18nTextValue,
6070
7143
  validatePassphrase,
7144
+ validatePublicEnvelopeInput,
6071
7145
  validateSchemaInput,
6072
7146
  validateSchemaOutput,
6073
7147
  validateSessionPolicy,