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

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 (77) hide show
  1. package/dist/blobs/index.cjs.map +1 -1
  2. package/dist/blobs/index.d.cts +2 -2
  3. package/dist/blobs/index.d.ts +2 -2
  4. package/dist/blobs/index.js +2 -2
  5. package/dist/bundle/index.d.cts +2 -2
  6. package/dist/bundle/index.d.ts +2 -2
  7. package/dist/bundle/index.js +3 -3
  8. package/dist/{chunk-QUDXYI4W.js → chunk-3WCRU7TI.js} +2 -2
  9. package/dist/{chunk-6NPQTBZN.js → chunk-6IJQ27XN.js} +112 -4
  10. package/dist/chunk-6IJQ27XN.js.map +1 -0
  11. package/dist/{chunk-NXUVITPB.js → chunk-B6HF6NTZ.js} +1 -1
  12. package/dist/{chunk-NXUVITPB.js.map → chunk-B6HF6NTZ.js.map} +1 -1
  13. package/dist/{chunk-H3DV46AQ.js → chunk-CL37QSND.js} +2 -2
  14. package/dist/{chunk-LRN3PNI6.js → chunk-FAAWLVTF.js} +2 -2
  15. package/dist/{chunk-UQQ2XFXI.js → chunk-GILMPJXB.js} +2 -2
  16. package/dist/{chunk-MIRZMUSQ.js → chunk-INSJBB5W.js} +3 -3
  17. package/dist/{chunk-E4OOAPBZ.js → chunk-KPF2HHPI.js} +2 -2
  18. package/dist/{chunk-LMKOSLJY.js → chunk-N2LMZKLR.js} +2 -2
  19. package/dist/{chunk-QV4WLLKB.js → chunk-NZ4XCIKS.js} +3 -3
  20. package/dist/consent/index.d.cts +2 -2
  21. package/dist/consent/index.d.ts +2 -2
  22. package/dist/{dev-unlock-BgFqShBi.d.ts → dev-unlock-CcJ1qIi7.d.ts} +1 -1
  23. package/dist/{dev-unlock-qVMxG2Je.d.cts → dev-unlock-Dk14V6lX.d.cts} +1 -1
  24. package/dist/{hash-BhoL7iUE.d.ts → hash-1Xsqx1jl.d.ts} +1 -1
  25. package/dist/{hash-Bpvl2eSe.d.cts → hash-h_2U3TFb.d.cts} +1 -1
  26. package/dist/history/index.cjs.map +1 -1
  27. package/dist/history/index.d.cts +3 -3
  28. package/dist/history/index.d.ts +3 -3
  29. package/dist/history/index.js +2 -2
  30. package/dist/i18n/index.cjs.map +1 -1
  31. package/dist/i18n/index.d.cts +2 -2
  32. package/dist/i18n/index.d.ts +2 -2
  33. package/dist/i18n/index.js +3 -3
  34. package/dist/{index-DyRt_5vM.d.cts → index-Cvb0efA_.d.cts} +1 -1
  35. package/dist/{index-DhK_zqOO.d.ts → index-DZn6Yick.d.ts} +1 -1
  36. package/dist/index.cjs +505 -4
  37. package/dist/index.cjs.map +1 -1
  38. package/dist/index.d.cts +60 -6
  39. package/dist/index.d.ts +60 -6
  40. package/dist/index.js +409 -13
  41. package/dist/index.js.map +1 -1
  42. package/dist/{ledger-GA4DMJS6.js → ledger-5V67MAIL.js} +3 -3
  43. package/dist/periods/index.cjs.map +1 -1
  44. package/dist/periods/index.d.cts +2 -2
  45. package/dist/periods/index.d.ts +2 -2
  46. package/dist/periods/index.js +3 -3
  47. package/dist/{public-envelope-R4EIEQE6.js → public-envelope-DFJZHXVH.js} +3 -3
  48. package/dist/session/index.d.cts +3 -3
  49. package/dist/session/index.d.ts +3 -3
  50. package/dist/shadow/index.d.cts +2 -2
  51. package/dist/shadow/index.d.ts +2 -2
  52. package/dist/store/index.d.cts +2 -2
  53. package/dist/store/index.d.ts +2 -2
  54. package/dist/sync/index.cjs.map +1 -1
  55. package/dist/sync/index.d.cts +1 -1
  56. package/dist/sync/index.d.ts +1 -1
  57. package/dist/sync/index.js +2 -2
  58. package/dist/team/index.cjs.map +1 -1
  59. package/dist/team/index.d.cts +2 -2
  60. package/dist/team/index.d.ts +2 -2
  61. package/dist/team/index.js +4 -4
  62. package/dist/tx/index.d.cts +2 -2
  63. package/dist/tx/index.d.ts +2 -2
  64. package/dist/{types-Df72wWCC.d.ts → types-D-6bmD2c.d.ts} +378 -2
  65. package/dist/{types-BpyE4o_n.d.cts → types-D3QLmhlk.d.cts} +378 -2
  66. package/package.json +1 -1
  67. package/dist/chunk-6NPQTBZN.js.map +0 -1
  68. /package/dist/{chunk-QUDXYI4W.js.map → chunk-3WCRU7TI.js.map} +0 -0
  69. /package/dist/{chunk-H3DV46AQ.js.map → chunk-CL37QSND.js.map} +0 -0
  70. /package/dist/{chunk-LRN3PNI6.js.map → chunk-FAAWLVTF.js.map} +0 -0
  71. /package/dist/{chunk-UQQ2XFXI.js.map → chunk-GILMPJXB.js.map} +0 -0
  72. /package/dist/{chunk-MIRZMUSQ.js.map → chunk-INSJBB5W.js.map} +0 -0
  73. /package/dist/{chunk-E4OOAPBZ.js.map → chunk-KPF2HHPI.js.map} +0 -0
  74. /package/dist/{chunk-LMKOSLJY.js.map → chunk-N2LMZKLR.js.map} +0 -0
  75. /package/dist/{chunk-QV4WLLKB.js.map → chunk-NZ4XCIKS.js.map} +0 -0
  76. /package/dist/{ledger-GA4DMJS6.js.map → ledger-5V67MAIL.js.map} +0 -0
  77. /package/dist/{public-envelope-R4EIEQE6.js.map → public-envelope-DFJZHXVH.js.map} +0 -0
package/dist/index.js CHANGED
@@ -31,7 +31,7 @@ import {
31
31
  readNoydbBundlePublicEnvelope,
32
32
  resetBrotliSupportCache,
33
33
  writeNoydbBundle
34
- } from "./chunk-LRN3PNI6.js";
34
+ } from "./chunk-FAAWLVTF.js";
35
35
  import {
36
36
  PUBLIC_ENVELOPE_RECORD_ID,
37
37
  isPublicEnvelope,
@@ -39,13 +39,13 @@ import {
39
39
  readPublicEnvelope,
40
40
  savePublicEnvelope,
41
41
  validatePublicEnvelopeInput
42
- } from "./chunk-UQQ2XFXI.js";
42
+ } from "./chunk-GILMPJXB.js";
43
43
  import {
44
44
  CONSENT_AUDIT_COLLECTION
45
45
  } from "./chunk-M62XNWRA.js";
46
46
  import {
47
47
  PERIODS_COLLECTION
48
- } from "./chunk-QUDXYI4W.js";
48
+ } from "./chunk-3WCRU7TI.js";
49
49
  import "./chunk-UF3BUNQZ.js";
50
50
  import {
51
51
  CollectionFrame,
@@ -69,7 +69,7 @@ import {
69
69
  isI18nTextDescriptor,
70
70
  resolveI18nText,
71
71
  validateI18nTextValue
72
- } from "./chunk-QV4WLLKB.js";
72
+ } from "./chunk-NZ4XCIKS.js";
73
73
  import {
74
74
  createBundleStore,
75
75
  routeStore,
@@ -89,18 +89,22 @@ import {
89
89
  getCredential,
90
90
  listCredentials,
91
91
  putCredential
92
- } from "./chunk-MIRZMUSQ.js";
92
+ } from "./chunk-INSJBB5W.js";
93
93
  import {
94
94
  PresenceHandle,
95
95
  SyncEngine,
96
96
  SyncTransaction
97
- } from "./chunk-H3DV46AQ.js";
97
+ } from "./chunk-CL37QSND.js";
98
98
  import {
99
+ USER_ENVELOPE_COLLECTION,
100
+ USER_ENVELOPE_MAX_BYTES,
101
+ UserEnvelopeOversizedError,
99
102
  WeakPassphraseError,
100
103
  assertStrongPassphrase,
101
104
  buildRecipientKeyringFile,
102
105
  changeSecret,
103
106
  createOwnerKeyring,
107
+ deleteUserEnvelope,
104
108
  ensureCollectionDEK,
105
109
  estimateEntropy,
106
110
  evaluateExportCapability,
@@ -110,13 +114,17 @@ import {
110
114
  hasExportCapability,
111
115
  hasImportCapability,
112
116
  hasWritePermission,
117
+ listUserEnvelopeIds,
113
118
  listUsers,
119
+ listUsersWithEnvelopes,
114
120
  loadKeyring,
121
+ loadUserEnvelope,
115
122
  persistKeyring,
116
123
  revoke,
117
124
  rotateKeys,
125
+ saveUserEnvelope,
118
126
  validatePassphrase
119
- } from "./chunk-6NPQTBZN.js";
127
+ } from "./chunk-6IJQ27XN.js";
120
128
  import {
121
129
  BUNDLE_STORE_POLICY,
122
130
  INDEXED_STORE_POLICY,
@@ -153,7 +161,7 @@ import {
153
161
  LedgerStore,
154
162
  applyPatch,
155
163
  computePatch
156
- } from "./chunk-LMKOSLJY.js";
164
+ } from "./chunk-N2LMZKLR.js";
157
165
  import {
158
166
  canonicalJson,
159
167
  envelopePayloadHash,
@@ -208,14 +216,14 @@ import {
208
216
  detectMimeType,
209
217
  isPreCompressed,
210
218
  runCompaction
211
- } from "./chunk-E4OOAPBZ.js";
219
+ } from "./chunk-KPF2HHPI.js";
212
220
  import {
213
221
  NOYDB_BACKUP_VERSION,
214
222
  NOYDB_FORMAT_VERSION,
215
223
  NOYDB_KEYRING_VERSION,
216
224
  NOYDB_SYNC_VERSION,
217
225
  createStore
218
- } from "./chunk-NXUVITPB.js";
226
+ } from "./chunk-B6HF6NTZ.js";
219
227
  import {
220
228
  base64ToBuffer,
221
229
  bufferToBase64,
@@ -772,6 +780,220 @@ async function writeKeyringFile(store, vault, userId, file) {
772
780
  await store.put(vault, "_keyring", userId, envelope);
773
781
  }
774
782
 
783
+ // src/meta/user-envelope/api.ts
784
+ var UserApi = class {
785
+ constructor(adapter, vaultName, writerKeyringId, getDek, checkGate2) {
786
+ this.adapter = adapter;
787
+ this.vaultName = vaultName;
788
+ this.writerKeyringId = writerKeyringId;
789
+ this.getDek = getDek;
790
+ this.checkGate = checkGate2;
791
+ }
792
+ adapter;
793
+ vaultName;
794
+ writerKeyringId;
795
+ getDek;
796
+ checkGate;
797
+ /** keyringId → set of listeners. Wildcard '*' fires on every change. */
798
+ listeners = /* @__PURE__ */ new Map();
799
+ // ─── Write-self ──────────────────────────────────────────────────────
800
+ /** Read the writer's own envelope. Returns null if never written. */
801
+ async me() {
802
+ const dek = await this.getDek();
803
+ return loadUserEnvelope(this.adapter, this.vaultName, this.writerKeyringId, dek);
804
+ }
805
+ /**
806
+ * Deep-merge a partial patch into the writer's own envelope. Creates
807
+ * the envelope on first call. Optimistic-concurrency safe — a stale
808
+ * `_v` (parallel writer on another device) throws `ConflictError`.
809
+ *
810
+ * Gated by the `edit-own-profile` policy gate (default `minTier: 3`).
811
+ * Pass `presented` to satisfy tightened policies that require a
812
+ * factor proof (e.g. STRICT_POLICY's TOTP requirement).
813
+ */
814
+ async updateMe(patch, presented) {
815
+ if (this.checkGate) await this.checkGate("edit-own-profile", presented);
816
+ const dek = await this.getDek();
817
+ const current = await loadUserEnvelope(
818
+ this.adapter,
819
+ this.vaultName,
820
+ this.writerKeyringId,
821
+ dek
822
+ );
823
+ const merged = current ? deepMerge(current.data, patch) : patch;
824
+ const written = await saveUserEnvelope(
825
+ this.adapter,
826
+ this.vaultName,
827
+ this.writerKeyringId,
828
+ merged,
829
+ dek,
830
+ current?._v ?? 0
831
+ );
832
+ this.fireChange(this.writerKeyringId, written);
833
+ return written;
834
+ }
835
+ /**
836
+ * Replace the writer's own envelope with `payload`. Use sparingly —
837
+ * `updateMe` is the canonical mutation. No `expectedVersion` check;
838
+ * callers explicitly take last-write-wins semantics.
839
+ *
840
+ * Gated by `edit-own-profile`. See `updateMe` for `presented` usage.
841
+ */
842
+ async setMe(payload, presented) {
843
+ if (this.checkGate) await this.checkGate("edit-own-profile", presented);
844
+ const dek = await this.getDek();
845
+ const written = await saveUserEnvelope(
846
+ this.adapter,
847
+ this.vaultName,
848
+ this.writerKeyringId,
849
+ payload,
850
+ dek
851
+ );
852
+ this.fireChange(this.writerKeyringId, written);
853
+ return written;
854
+ }
855
+ // ─── Read-anyone ─────────────────────────────────────────────────────
856
+ /**
857
+ * Read another principal's envelope by their keyringId. Returns null
858
+ * if the principal exists but has no envelope yet, or if the
859
+ * keyringId does not exist at all.
860
+ *
861
+ * Gated by `view-team-profiles` (default `minTier: 2`) — but ONLY for
862
+ * cross-principal reads. Reading your own envelope (`keyringId ===
863
+ * self`) is never gated; that's just `me()` written long-form.
864
+ */
865
+ async get(keyringId, presented) {
866
+ if (this.checkGate && keyringId !== this.writerKeyringId) {
867
+ await this.checkGate("view-team-profiles", presented);
868
+ }
869
+ const dek = await this.getDek();
870
+ return loadUserEnvelope(this.adapter, this.vaultName, keyringId, dek);
871
+ }
872
+ /**
873
+ * Read every persisted envelope in the vault. Order is store-defined.
874
+ *
875
+ * Gated by `view-team-profiles`. Default policy (`minTier: 2`) lets
876
+ * any authenticated session read all envelopes. Two privacy-strict
877
+ * opt-outs:
878
+ *
879
+ * - `view-team-profiles.enabled: false` → list() returns only the
880
+ * caller's own envelope (silent self-fallback, no thrown error).
881
+ * - `view-team-profiles.minTier: 1` + insufficient tier → throws
882
+ * `PolicyDeniedError` with `reason: 'insufficient-tier'`. The
883
+ * caller is expected to elevate, not silently degrade.
884
+ *
885
+ * The asymmetry is deliberate: `enabled: false` is a deliberate
886
+ * design choice ("nobody sees teammate profiles in this app");
887
+ * `insufficient-tier` is "you need to authenticate further". Different
888
+ * UX prompts for different intents.
889
+ */
890
+ async list(presented) {
891
+ if (this.checkGate) {
892
+ try {
893
+ await this.checkGate("view-team-profiles", presented);
894
+ } catch (err) {
895
+ if (err instanceof PolicyDeniedError && err.reason === "disabled") {
896
+ const me = await this.me();
897
+ return me ? [me] : [];
898
+ }
899
+ throw err;
900
+ }
901
+ }
902
+ const dek = await this.getDek();
903
+ const ids = await listUserEnvelopeIds(this.adapter, this.vaultName);
904
+ const envelopes = await Promise.all(
905
+ ids.map((id) => loadUserEnvelope(this.adapter, this.vaultName, id, dek))
906
+ );
907
+ return envelopes.filter((e) => e !== null);
908
+ }
909
+ // ─── Reactive ────────────────────────────────────────────────────────
910
+ /**
911
+ * Listen for changes to a specific keyringId's envelope. The callback
912
+ * fires synchronously after every successful local `updateMe` /
913
+ * `setMe` for that principal.
914
+ *
915
+ * Cross-instance changes (a teammate edits their profile on their
916
+ * device, the sync engine pulls the diff onto this device) will fire
917
+ * subscribers when the sync layer replays the write through this API.
918
+ * In v1, subscribers do NOT fire on raw store changes — wire your sync
919
+ * layer to call back through `vault.user.setMe` / `updateMe` if you
920
+ * need that.
921
+ *
922
+ * Pass keyringId `'*'` to fire on every change in the vault.
923
+ */
924
+ subscribe(keyringId, cb) {
925
+ let listeners = this.listeners.get(keyringId);
926
+ if (!listeners) {
927
+ listeners = /* @__PURE__ */ new Set();
928
+ this.listeners.set(keyringId, listeners);
929
+ }
930
+ const wrapped = cb;
931
+ listeners.add(wrapped);
932
+ return () => {
933
+ listeners?.delete(wrapped);
934
+ if (listeners && listeners.size === 0) {
935
+ this.listeners.delete(keyringId);
936
+ }
937
+ };
938
+ }
939
+ /**
940
+ * Reactive handle that caches the current value and re-reads on every
941
+ * change for the given keyringId. Convenient for framework bindings:
942
+ *
943
+ * const live = vault.user.live<UserShape>(vault.userId)
944
+ * live.subscribe(env => render(env?.data))
945
+ *
946
+ * Initial value is `null` until the first `current()` call materializes
947
+ * it via `vault.user.get()`. Call `stop()` when done to release the
948
+ * subscription.
949
+ */
950
+ live(keyringId) {
951
+ let value = null;
952
+ let primed = false;
953
+ const unsubscribe = this.subscribe(keyringId, (env) => {
954
+ value = env;
955
+ });
956
+ return {
957
+ current() {
958
+ if (!primed) {
959
+ primed = true;
960
+ }
961
+ return value;
962
+ },
963
+ subscribe: (cb) => this.subscribe(keyringId, cb),
964
+ stop: unsubscribe
965
+ };
966
+ }
967
+ // ─── Internal: change emission ───────────────────────────────────────
968
+ fireChange(keyringId, env) {
969
+ const targeted = this.listeners.get(keyringId);
970
+ if (targeted) for (const l of targeted) l(env);
971
+ const wildcard = this.listeners.get("*");
972
+ if (wildcard) for (const l of wildcard) l(env);
973
+ }
974
+ };
975
+ function deepMerge(source, patch) {
976
+ if (!isPlainObject(source) || !isPlainObject(patch)) {
977
+ return patch;
978
+ }
979
+ const out = { ...source };
980
+ for (const [key, patchVal] of Object.entries(patch)) {
981
+ const sourceVal = source[key];
982
+ if (isPlainObject(sourceVal) && isPlainObject(patchVal)) {
983
+ out[key] = deepMerge(sourceVal, patchVal);
984
+ } else {
985
+ out[key] = patchVal;
986
+ }
987
+ }
988
+ return out;
989
+ }
990
+ function isPlainObject(x) {
991
+ if (x === null || typeof x !== "object") return false;
992
+ if (Array.isArray(x)) return false;
993
+ const proto = Object.getPrototypeOf(x);
994
+ return proto === Object.prototype || proto === null;
995
+ }
996
+
775
997
  // src/policy/storage.ts
776
998
  var META_COLLECTION = "_meta";
777
999
  var POLICY_RECORD_ID = "policy";
@@ -3561,6 +3783,19 @@ var Vault = class {
3561
3783
  i18nStrategy;
3562
3784
  syncStrategy;
3563
3785
  getDEK;
3786
+ /**
3787
+ * Per-principal user envelope API.
3788
+ *
3789
+ * - Write-self: `me()`, `updateMe(patch)`, `setMe(payload)` — always
3790
+ * target this vault session's keyringId. There is no method to write
3791
+ * another principal's envelope (own-only write rule, structural).
3792
+ * - Read-anyone: `get(keyringId)`, `list()` — read other principals'
3793
+ * envelopes, subject to the `view-team-profiles` policy gate (#22).
3794
+ * - Reactive: `subscribe(id, cb)`, `live(id)` — fire on local writes.
3795
+ *
3796
+ * @see docs/superpowers/specs/2026-05-05-user-envelope-design.md
3797
+ */
3798
+ user;
3564
3799
  /**
3565
3800
  * Optional callback that re-derives an UnlockedKeyring from the
3566
3801
  * adapter using the active user's passphrase. Called by `load()`
@@ -3693,6 +3928,13 @@ var Vault = class {
3693
3928
  this.locale = opts.locale;
3694
3929
  this.translateText = opts.plaintextTranslator;
3695
3930
  this.getDEK = this.makeGetDEK();
3931
+ this.user = new UserApi(
3932
+ this.adapter,
3933
+ this.name,
3934
+ this.keyring.userId,
3935
+ () => this.getDEK(USER_ENVELOPE_COLLECTION),
3936
+ (gate, presented) => this.noydb.checkGate(this.name, gate, presented)
3937
+ );
3696
3938
  }
3697
3939
  /**
3698
3940
  * Construct (or reconstruct) the lazy DEK resolver. Captures the
@@ -4979,7 +5221,7 @@ var Vault = class {
4979
5221
  * @see docs/subsystems/public-envelope.md
4980
5222
  */
4981
5223
  async getPublicEnvelope(opts = {}) {
4982
- const { readPublicEnvelope: readPublicEnvelope2 } = await import("./public-envelope-R4EIEQE6.js");
5224
+ const { readPublicEnvelope: readPublicEnvelope2 } = await import("./public-envelope-DFJZHXVH.js");
4983
5225
  return readPublicEnvelope2(this.adapter, this.name, opts);
4984
5226
  }
4985
5227
  /**
@@ -5572,7 +5814,17 @@ var PERSONAL_POLICY = Object.freeze({
5572
5814
  "view-user-auth": {
5573
5815
  minTier: 1,
5574
5816
  enabled: false
5575
- }
5817
+ },
5818
+ // ─── User envelope gates (#22) ────────────────────────────────────
5819
+ // edit-own-profile: tier 3 floor — any active session can edit their
5820
+ // own profile/preferences. Tightening to require a TOTP for
5821
+ // profile changes is a one-line override.
5822
+ // view-team-profiles: tier 2 floor — an authenticated session can
5823
+ // read teammates' profiles (display names, avatars, locales).
5824
+ // Setting `enabled: false` makes vault.user.list() return only
5825
+ // self (privacy-strict opt-out).
5826
+ "edit-own-profile": { minTier: 3 },
5827
+ "view-team-profiles": { minTier: 2 }
5576
5828
  }
5577
5829
  });
5578
5830
  var STRICT_POLICY = Object.freeze({
@@ -5620,7 +5872,16 @@ var STRICT_POLICY = Object.freeze({
5620
5872
  "view-user-auth": {
5621
5873
  minTier: 1,
5622
5874
  enabled: false
5623
- }
5875
+ },
5876
+ // ─── User envelope gates (#22) ────────────────────────────────────
5877
+ // STRICT: profile edits require a TOTP/email-OTP factor (typical
5878
+ // shared-workstation hardening — your name/avatar shouldn't change
5879
+ // without a fresh second-factor proof).
5880
+ "edit-own-profile": {
5881
+ minTier: 2,
5882
+ factors: [{ anyOf: ["totp", "email-otp"] }]
5883
+ },
5884
+ "view-team-profiles": { minTier: 2 }
5624
5885
  }
5625
5886
  });
5626
5887
  function mergePolicy(base, override) {
@@ -6311,6 +6572,36 @@ var Noydb = class {
6311
6572
  off(event, handler) {
6312
6573
  this.emitter.off(event, handler);
6313
6574
  }
6575
+ /**
6576
+ * Soft-lock a single vault: clear its in-memory keyring, DEKs, vault
6577
+ * instance, sync engine, policy enforcer, and active-tier entry —
6578
+ * WITHOUT destroying the `Noydb` instance.
6579
+ *
6580
+ * Designed for "lock screen" UX: the user taps **Lock** and DEKs are
6581
+ * scrubbed from memory immediately, but the same `Noydb` instance can
6582
+ * be re-unlocked via {@link unlockViaAuthenticator} (tier 2) or
6583
+ * {@link unlockViaPin} (tier 3) without re-running `createNoydb`.
6584
+ *
6585
+ * **QuickUnlock state is preserved.** That's the whole point — the
6586
+ * user can still resume via PIN without a full credential re-prompt.
6587
+ * The on-disk `_meta/policy` document is also kept in cache (it
6588
+ * survives lock; nothing about it changes when DEKs are scrubbed).
6589
+ *
6590
+ * No-op when `vault` is not currently in cache (idempotent).
6591
+ *
6592
+ * Unblocks vLannaAi/niwat#33.
6593
+ *
6594
+ * @see #17
6595
+ */
6596
+ lockVault(vault) {
6597
+ this.syncEngines.get(vault)?.stopAutoSync();
6598
+ this.syncEngines.delete(vault);
6599
+ this.policyEnforcers.get(vault)?.destroy();
6600
+ this.policyEnforcers.delete(vault);
6601
+ this.keyringCache.delete(vault);
6602
+ this.vaultCache.delete(vault);
6603
+ this.activeTier.delete(vault);
6604
+ }
6314
6605
  close() {
6315
6606
  this.closed = true;
6316
6607
  if (this.sessionTimer) {
@@ -6505,6 +6796,92 @@ var Noydb = class {
6505
6796
  const keyring = await this.getKeyring(vault);
6506
6797
  return keyring.authenticators;
6507
6798
  }
6799
+ /**
6800
+ * Native WebAuthn enrollment using the **real** internal keyring (#16).
6801
+ *
6802
+ * Why this exists: when a consumer is using `createNoydb({ secret })`,
6803
+ * they cannot reach the live `UnlockedKeyring` to feed it to
6804
+ * `enrollWebAuthn(keyring, vault, opts)` from `@noy-db/on-webauthn`.
6805
+ * Constructing a synthetic keyring (the previous workaround) produces
6806
+ * a slot whose `wrapped_kek` references the synthetic payload, not
6807
+ * the live session — so `unlockViaAuthenticator()` later replaces the
6808
+ * live DEK map with stale wrapped DEKs and every decrypt fails.
6809
+ *
6810
+ * This method runs `ceremony` with the REAL keyring (still in
6811
+ * `keyringCache`). The ceremony performs the WebAuthn enrollment and
6812
+ * returns the slot options that hub then persists via the standard
6813
+ * tier-2 enrollAuthenticator path.
6814
+ *
6815
+ * Layering note: hub does not import `@noy-db/on-webauthn` (that
6816
+ * would invert the dep graph). The consumer wires it in:
6817
+ *
6818
+ * ```ts
6819
+ * import { enrollWebAuthn } from '@noy-db/on-webauthn'
6820
+ *
6821
+ * await db.enrollWebAuthn('demo', async (keyring) => {
6822
+ * const e = await enrollWebAuthn(keyring, 'demo', { rp: {...} })
6823
+ * return {
6824
+ * id: `webauthn-${e.credentialId.slice(0, 8)}`,
6825
+ * method: 'webauthn',
6826
+ * wrapped_kek: e.wrappedPayload,
6827
+ * meta: {
6828
+ * credentialId: e.credentialId,
6829
+ * wrapIv: e.wrapIv,
6830
+ * prfUsed: e.prfUsed,
6831
+ * beFlag: e.beFlag,
6832
+ * requireSingleDevice: e.requireSingleDevice,
6833
+ * },
6834
+ * }
6835
+ * })
6836
+ * ```
6837
+ *
6838
+ * Returns the WebAuthn `credentialId` (extracted from `meta.credentialId`)
6839
+ * for the caller's lookup index (a bootstrap vault, a PublicEnvelope,
6840
+ * a server-side allowlist).
6841
+ *
6842
+ * Gated by `enroll-authenticator` like `enrollAuthenticator()` itself.
6843
+ *
6844
+ * @see #16
6845
+ */
6846
+ async enrollWebAuthn(vault, ceremony, presented) {
6847
+ await this.checkGate(vault, "enroll-authenticator", presented);
6848
+ const keyring = await this.getKeyring(vault);
6849
+ const slotOptions = await ceremony(keyring);
6850
+ if (slotOptions.method !== "webauthn") {
6851
+ throw new ValidationError(
6852
+ `enrollWebAuthn: ceremony returned method "${slotOptions.method}"; expected "webauthn". Use db.enrollAuthenticator() for non-webauthn methods.`
6853
+ );
6854
+ }
6855
+ const credentialId = slotOptions.meta.credentialId;
6856
+ if (typeof credentialId !== "string" || credentialId.length === 0) {
6857
+ throw new ValidationError(
6858
+ "enrollWebAuthn: ceremony result must include `meta.credentialId` (base64 string). See @noy-db/on-webauthn enrollWebAuthn() return shape."
6859
+ );
6860
+ }
6861
+ const next = await enrollAuthenticator(this.options.store, vault, keyring, slotOptions);
6862
+ this.keyringCache.set(vault, next);
6863
+ return { credentialId };
6864
+ }
6865
+ /**
6866
+ * Filter the slot list to webauthn-method slots only. Useful for
6867
+ * "you have N WebAuthn credentials enrolled" UI surfaces and for
6868
+ * deciding when a new device prompt should appear. Identity is
6869
+ * `id` + `enrolled_at`; the `meta.credentialId` (base64) is used by
6870
+ * `allowCredentials` at unlock time.
6871
+ *
6872
+ * @see #16
6873
+ */
6874
+ async listWebAuthnSlots(vault) {
6875
+ const keyring = await this.getKeyring(vault);
6876
+ return keyring.authenticators.filter((a) => a.method === "webauthn").map((a) => {
6877
+ const credentialId = a.meta.credentialId;
6878
+ return {
6879
+ id: a.id,
6880
+ enrolledAt: a.enrolled_at,
6881
+ credentialId: typeof credentialId === "string" ? credentialId : ""
6882
+ };
6883
+ });
6884
+ }
6508
6885
  /**
6509
6886
  * Resolve a slot by id, then hand the wrapped-KEK ciphertext + meta
6510
6887
  * to the caller-supplied verifier. The verifier is the
@@ -6724,6 +7101,15 @@ var Noydb = class {
6724
7101
  this.options.secret,
6725
7102
  { validate: this.options.validatePassphrase === true }
6726
7103
  );
7104
+ } else if (err instanceof InvalidKeyError && this.options.onInvalidKey === "reset") {
7105
+ await this.options.store.delete(vault, "_keyring", this.options.user);
7106
+ keyring = await createOwnerKeyring(
7107
+ this.options.store,
7108
+ vault,
7109
+ this.options.user,
7110
+ this.options.secret,
7111
+ { validate: this.options.validatePassphrase === true }
7112
+ );
6727
7113
  } else {
6728
7114
  throw err;
6729
7115
  }
@@ -7015,6 +7401,10 @@ export {
7015
7401
  TxCollection,
7016
7402
  TxContext,
7017
7403
  TxVault,
7404
+ USER_ENVELOPE_COLLECTION,
7405
+ USER_ENVELOPE_MAX_BYTES,
7406
+ UserApi,
7407
+ UserEnvelopeOversizedError,
7018
7408
  ValidationError,
7019
7409
  Vault,
7020
7410
  VaultFrame,
@@ -7047,6 +7437,7 @@ export {
7047
7437
  decryptDeterministic,
7048
7438
  dekKey,
7049
7439
  deleteCredential,
7440
+ deleteUserEnvelope,
7050
7441
  deriveMagicLinkContentKey,
7051
7442
  derivePresenceKey,
7052
7443
  describeAllUsersAuth,
@@ -7098,10 +7489,14 @@ export {
7098
7489
  rotatePassphrase as keyringRotatePassphrase,
7099
7490
  listCredentials,
7100
7491
  listMagicLinkGrants,
7492
+ listUserEnvelopeIds,
7493
+ listUsers,
7494
+ listUsersWithEnvelopes,
7101
7495
  loadActiveDelegations,
7102
7496
  loadDevUnlock,
7103
7497
  loadPaperRecoveryEntries,
7104
7498
  loadPublicEnvelope,
7499
+ loadUserEnvelope,
7105
7500
  loadVaultPolicy,
7106
7501
  magicLinkGrantRecordId,
7107
7502
  max,
@@ -7135,6 +7530,7 @@ export {
7135
7530
  runTransaction,
7136
7531
  savePaperRecoveryEntries,
7137
7532
  savePublicEnvelope,
7533
+ saveUserEnvelope,
7138
7534
  saveVaultPolicy,
7139
7535
  sha256Hex,
7140
7536
  sum,