@noy-db/hub 0.1.0-pre.10

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 (203) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +197 -0
  3. package/dist/aggregate/index.cjs +476 -0
  4. package/dist/aggregate/index.cjs.map +1 -0
  5. package/dist/aggregate/index.d.cts +38 -0
  6. package/dist/aggregate/index.d.ts +38 -0
  7. package/dist/aggregate/index.js +53 -0
  8. package/dist/aggregate/index.js.map +1 -0
  9. package/dist/blobs/index.cjs +1480 -0
  10. package/dist/blobs/index.cjs.map +1 -0
  11. package/dist/blobs/index.d.cts +45 -0
  12. package/dist/blobs/index.d.ts +45 -0
  13. package/dist/blobs/index.js +48 -0
  14. package/dist/blobs/index.js.map +1 -0
  15. package/dist/bundle/index.cjs +496 -0
  16. package/dist/bundle/index.cjs.map +1 -0
  17. package/dist/bundle/index.d.cts +7 -0
  18. package/dist/bundle/index.d.ts +7 -0
  19. package/dist/bundle/index.js +51 -0
  20. package/dist/bundle/index.js.map +1 -0
  21. package/dist/chunk-2QR2PQTT.js +217 -0
  22. package/dist/chunk-2QR2PQTT.js.map +1 -0
  23. package/dist/chunk-72UIIX3E.js +1109 -0
  24. package/dist/chunk-72UIIX3E.js.map +1 -0
  25. package/dist/chunk-A4NFZKRW.js +722 -0
  26. package/dist/chunk-A4NFZKRW.js.map +1 -0
  27. package/dist/chunk-AOYCZP2H.js +793 -0
  28. package/dist/chunk-AOYCZP2H.js.map +1 -0
  29. package/dist/chunk-CIMZBAZB.js +72 -0
  30. package/dist/chunk-CIMZBAZB.js.map +1 -0
  31. package/dist/chunk-E3AGCGJ4.js +160 -0
  32. package/dist/chunk-E3AGCGJ4.js.map +1 -0
  33. package/dist/chunk-EKX3YVCI.js +97 -0
  34. package/dist/chunk-EKX3YVCI.js.map +1 -0
  35. package/dist/chunk-EMIGCR7X.js +39 -0
  36. package/dist/chunk-EMIGCR7X.js.map +1 -0
  37. package/dist/chunk-EMMRIE3C.js +72 -0
  38. package/dist/chunk-EMMRIE3C.js.map +1 -0
  39. package/dist/chunk-EUNIORPU.js +680 -0
  40. package/dist/chunk-EUNIORPU.js.map +1 -0
  41. package/dist/chunk-FZU343FL.js +32 -0
  42. package/dist/chunk-FZU343FL.js.map +1 -0
  43. package/dist/chunk-GHGXG53C.js +795 -0
  44. package/dist/chunk-GHGXG53C.js.map +1 -0
  45. package/dist/chunk-GKA4BGJN.js +79 -0
  46. package/dist/chunk-GKA4BGJN.js.map +1 -0
  47. package/dist/chunk-HG2OWBLX.js +430 -0
  48. package/dist/chunk-HG2OWBLX.js.map +1 -0
  49. package/dist/chunk-IGAROPKM.js +34 -0
  50. package/dist/chunk-IGAROPKM.js.map +1 -0
  51. package/dist/chunk-J66GRPNH.js +111 -0
  52. package/dist/chunk-J66GRPNH.js.map +1 -0
  53. package/dist/chunk-LVMMDXFT.js +275 -0
  54. package/dist/chunk-LVMMDXFT.js.map +1 -0
  55. package/dist/chunk-M5INGEFC.js +84 -0
  56. package/dist/chunk-M5INGEFC.js.map +1 -0
  57. package/dist/chunk-NBYQNDXA.js +557 -0
  58. package/dist/chunk-NBYQNDXA.js.map +1 -0
  59. package/dist/chunk-NPC4LFV5.js +132 -0
  60. package/dist/chunk-NPC4LFV5.js.map +1 -0
  61. package/dist/chunk-NSWHB5VQ.js +1285 -0
  62. package/dist/chunk-NSWHB5VQ.js.map +1 -0
  63. package/dist/chunk-OLM4LA6K.js +392 -0
  64. package/dist/chunk-OLM4LA6K.js.map +1 -0
  65. package/dist/chunk-UAFBZWFB.js +155 -0
  66. package/dist/chunk-UAFBZWFB.js.map +1 -0
  67. package/dist/chunk-UF3BUNQZ.js +1 -0
  68. package/dist/chunk-UF3BUNQZ.js.map +1 -0
  69. package/dist/chunk-UMMAVAYW.js +17 -0
  70. package/dist/chunk-UMMAVAYW.js.map +1 -0
  71. package/dist/chunk-UPY7WLBH.js +381 -0
  72. package/dist/chunk-UPY7WLBH.js.map +1 -0
  73. package/dist/chunk-W63BWEJH.js +311 -0
  74. package/dist/chunk-W63BWEJH.js.map +1 -0
  75. package/dist/chunk-WIGI5OJK.js +90 -0
  76. package/dist/chunk-WIGI5OJK.js.map +1 -0
  77. package/dist/chunk-XNL2TKKR.js +490 -0
  78. package/dist/chunk-XNL2TKKR.js.map +1 -0
  79. package/dist/chunk-XWNUJPIS.js +367 -0
  80. package/dist/chunk-XWNUJPIS.js.map +1 -0
  81. package/dist/chunk-YWKJZZGV.js +715 -0
  82. package/dist/chunk-YWKJZZGV.js.map +1 -0
  83. package/dist/consent/index.cjs +204 -0
  84. package/dist/consent/index.cjs.map +1 -0
  85. package/dist/consent/index.d.cts +24 -0
  86. package/dist/consent/index.d.ts +24 -0
  87. package/dist/consent/index.js +23 -0
  88. package/dist/consent/index.js.map +1 -0
  89. package/dist/crdt/index.cjs +152 -0
  90. package/dist/crdt/index.cjs.map +1 -0
  91. package/dist/crdt/index.d.cts +30 -0
  92. package/dist/crdt/index.d.ts +30 -0
  93. package/dist/crdt/index.js +24 -0
  94. package/dist/crdt/index.js.map +1 -0
  95. package/dist/crypto-6PNIHP7W.js +44 -0
  96. package/dist/crypto-6PNIHP7W.js.map +1 -0
  97. package/dist/delegation-WVIVMF73.js +17 -0
  98. package/dist/delegation-WVIVMF73.js.map +1 -0
  99. package/dist/dev-unlock-D4xB0_gs.d.cts +263 -0
  100. package/dist/dev-unlock-Dz8GEbd3.d.ts +263 -0
  101. package/dist/hash--EflSV65.d.cts +63 -0
  102. package/dist/hash-CRdXYnv3.d.ts +63 -0
  103. package/dist/history/index.cjs +1215 -0
  104. package/dist/history/index.cjs.map +1 -0
  105. package/dist/history/index.d.cts +62 -0
  106. package/dist/history/index.d.ts +62 -0
  107. package/dist/history/index.js +79 -0
  108. package/dist/history/index.js.map +1 -0
  109. package/dist/i18n/index.cjs +840 -0
  110. package/dist/i18n/index.cjs.map +1 -0
  111. package/dist/i18n/index.d.cts +38 -0
  112. package/dist/i18n/index.d.ts +38 -0
  113. package/dist/i18n/index.js +68 -0
  114. package/dist/i18n/index.js.map +1 -0
  115. package/dist/index-CD1VnONm.d.cts +415 -0
  116. package/dist/index-CLRxPs-W.d.cts +1960 -0
  117. package/dist/index-CUi9wfss.d.ts +415 -0
  118. package/dist/index-DtV93TMP.d.ts +1960 -0
  119. package/dist/index.cjs +17387 -0
  120. package/dist/index.cjs.map +1 -0
  121. package/dist/index.d.cts +565 -0
  122. package/dist/index.d.ts +565 -0
  123. package/dist/index.js +7525 -0
  124. package/dist/index.js.map +1 -0
  125. package/dist/indexing/index.cjs +736 -0
  126. package/dist/indexing/index.cjs.map +1 -0
  127. package/dist/indexing/index.d.cts +36 -0
  128. package/dist/indexing/index.d.ts +36 -0
  129. package/dist/indexing/index.js +77 -0
  130. package/dist/indexing/index.js.map +1 -0
  131. package/dist/lazy-builder-BwEoBQZ9.d.ts +304 -0
  132. package/dist/lazy-builder-CZVLKh0Z.d.cts +304 -0
  133. package/dist/ledger-HBBH2NPZ.js +33 -0
  134. package/dist/ledger-HBBH2NPZ.js.map +1 -0
  135. package/dist/mime-magic-CBBSOkjm.d.cts +50 -0
  136. package/dist/mime-magic-CBBSOkjm.d.ts +50 -0
  137. package/dist/periods/index.cjs +1035 -0
  138. package/dist/periods/index.cjs.map +1 -0
  139. package/dist/periods/index.d.cts +21 -0
  140. package/dist/periods/index.d.ts +21 -0
  141. package/dist/periods/index.js +25 -0
  142. package/dist/periods/index.js.map +1 -0
  143. package/dist/predicate-SBHmi6D0.d.cts +161 -0
  144. package/dist/predicate-SBHmi6D0.d.ts +161 -0
  145. package/dist/public-envelope-TLQA6REO.js +31 -0
  146. package/dist/public-envelope-TLQA6REO.js.map +1 -0
  147. package/dist/query/index.cjs +1999 -0
  148. package/dist/query/index.cjs.map +1 -0
  149. package/dist/query/index.d.cts +3 -0
  150. package/dist/query/index.d.ts +3 -0
  151. package/dist/query/index.js +73 -0
  152. package/dist/query/index.js.map +1 -0
  153. package/dist/session/index.cjs +495 -0
  154. package/dist/session/index.cjs.map +1 -0
  155. package/dist/session/index.d.cts +45 -0
  156. package/dist/session/index.d.ts +45 -0
  157. package/dist/session/index.js +51 -0
  158. package/dist/session/index.js.map +1 -0
  159. package/dist/shadow/index.cjs +133 -0
  160. package/dist/shadow/index.cjs.map +1 -0
  161. package/dist/shadow/index.d.cts +16 -0
  162. package/dist/shadow/index.d.ts +16 -0
  163. package/dist/shadow/index.js +20 -0
  164. package/dist/shadow/index.js.map +1 -0
  165. package/dist/store/index.cjs +1083 -0
  166. package/dist/store/index.cjs.map +1 -0
  167. package/dist/store/index.d.cts +491 -0
  168. package/dist/store/index.d.ts +491 -0
  169. package/dist/store/index.js +37 -0
  170. package/dist/store/index.js.map +1 -0
  171. package/dist/strategy-BSxFXGzb.d.cts +110 -0
  172. package/dist/strategy-BSxFXGzb.d.ts +110 -0
  173. package/dist/strategy-D-SrOLCl.d.cts +548 -0
  174. package/dist/strategy-D-SrOLCl.d.ts +548 -0
  175. package/dist/sync/index.cjs +1062 -0
  176. package/dist/sync/index.cjs.map +1 -0
  177. package/dist/sync/index.d.cts +42 -0
  178. package/dist/sync/index.d.ts +42 -0
  179. package/dist/sync/index.js +28 -0
  180. package/dist/sync/index.js.map +1 -0
  181. package/dist/team/index.cjs +2606 -0
  182. package/dist/team/index.cjs.map +1 -0
  183. package/dist/team/index.d.cts +117 -0
  184. package/dist/team/index.d.ts +117 -0
  185. package/dist/team/index.js +106 -0
  186. package/dist/team/index.js.map +1 -0
  187. package/dist/tx/index.cjs +212 -0
  188. package/dist/tx/index.cjs.map +1 -0
  189. package/dist/tx/index.d.cts +20 -0
  190. package/dist/tx/index.d.ts +20 -0
  191. package/dist/tx/index.js +20 -0
  192. package/dist/tx/index.js.map +1 -0
  193. package/dist/types-DSFLtbKg.d.ts +9702 -0
  194. package/dist/types-zwwMOqkg.d.cts +9702 -0
  195. package/dist/ulid-COREQ2RQ.js +9 -0
  196. package/dist/ulid-COREQ2RQ.js.map +1 -0
  197. package/dist/util/index.cjs +230 -0
  198. package/dist/util/index.cjs.map +1 -0
  199. package/dist/util/index.d.cts +77 -0
  200. package/dist/util/index.d.ts +77 -0
  201. package/dist/util/index.js +190 -0
  202. package/dist/util/index.js.map +1 -0
  203. package/package.json +244 -0
@@ -0,0 +1,715 @@
1
+ import {
2
+ dekKey
3
+ } from "./chunk-IGAROPKM.js";
4
+ import {
5
+ assertStrongPassphrase,
6
+ ensureCollectionDEK,
7
+ mintKeyringCanary,
8
+ persistKeyring
9
+ } from "./chunk-GHGXG53C.js";
10
+ import {
11
+ NOYDB_FORMAT_VERSION,
12
+ NOYDB_KEYRING_VERSION
13
+ } from "./chunk-UMMAVAYW.js";
14
+ import {
15
+ base64ToBuffer,
16
+ bufferToBase64,
17
+ decrypt,
18
+ deriveKey,
19
+ encrypt,
20
+ generateSalt,
21
+ unwrapKey,
22
+ wrapKey
23
+ } from "./chunk-LVMMDXFT.js";
24
+ import {
25
+ DelegationTargetMissingError,
26
+ InvalidKeyError,
27
+ NoAccessError,
28
+ NoydbError,
29
+ PermissionDeniedError,
30
+ PrivilegeEscalationError,
31
+ ValidationError
32
+ } from "./chunk-NBYQNDXA.js";
33
+
34
+ // src/team/authenticators.ts
35
+ async function enrollAuthenticator(store, vault, keyring, options) {
36
+ const existing = keyring.authenticators.find((a) => a.id === options.id);
37
+ if (existing) {
38
+ throw new ValidationError(
39
+ `enrollAuthenticator: slot id "${options.id}" already exists in vault "${vault}". Remove the slot first or pick a unique id.`
40
+ );
41
+ }
42
+ const base = {
43
+ id: options.id,
44
+ method: options.method,
45
+ enrolled_at: (/* @__PURE__ */ new Date()).toISOString(),
46
+ enrolled_via_tier: options.enrolled_via_tier ?? 1,
47
+ meta: options.meta
48
+ };
49
+ const slot = options.wrapKind === "deks" ? {
50
+ ...base,
51
+ wrapKind: "deks",
52
+ wrapped_deks: options.wrapped_deks,
53
+ iv: options.iv
54
+ } : {
55
+ ...base,
56
+ wrapped_kek: options.wrapped_kek
57
+ };
58
+ const next = appendSlot(keyring, slot);
59
+ await persistKeyring(store, vault, next);
60
+ return next;
61
+ }
62
+ async function updateAuthenticator(store, vault, keyring, slotId, options) {
63
+ if (options.meta === void 0) {
64
+ throw new ValidationError(
65
+ `updateAuthenticator: at least one of meta must be provided (slotId: "${slotId}").`
66
+ );
67
+ }
68
+ const idx = keyring.authenticators.findIndex((a) => a.id === slotId);
69
+ if (idx === -1) {
70
+ throw new NoAccessError(
71
+ `updateAuthenticator: slot "${slotId}" not found in vault "${vault}".`
72
+ );
73
+ }
74
+ const existing = keyring.authenticators[idx];
75
+ const mergedMeta = { ...existing.meta };
76
+ for (const [k, v] of Object.entries(options.meta)) {
77
+ if (v === void 0) continue;
78
+ if (v === null) {
79
+ delete mergedMeta[k];
80
+ continue;
81
+ }
82
+ mergedMeta[k] = v;
83
+ }
84
+ const next = { ...existing, meta: mergedMeta };
85
+ const nextSlots = [...keyring.authenticators];
86
+ nextSlots[idx] = next;
87
+ const nextKeyring = {
88
+ ...keyring,
89
+ authenticators: nextSlots
90
+ };
91
+ await persistKeyring(store, vault, nextKeyring);
92
+ return nextKeyring;
93
+ }
94
+ async function removeAuthenticator(store, vault, keyring, slotId) {
95
+ const filtered = keyring.authenticators.filter((a) => a.id !== slotId);
96
+ if (filtered.length === keyring.authenticators.length) {
97
+ return keyring;
98
+ }
99
+ const next = {
100
+ ...keyring,
101
+ authenticators: filtered
102
+ };
103
+ await persistKeyring(store, vault, next);
104
+ return next;
105
+ }
106
+ function findAuthenticator(keyring, slotId) {
107
+ return keyring.authenticators.find((a) => a.id === slotId);
108
+ }
109
+ function appendSlot(keyring, slot) {
110
+ return {
111
+ ...keyring,
112
+ authenticators: [...keyring.authenticators, slot]
113
+ };
114
+ }
115
+
116
+ // src/policy/errors.ts
117
+ var PolicyDeniedError = class extends NoydbError {
118
+ gate;
119
+ reason;
120
+ required;
121
+ constructor(gate, reason, required, message) {
122
+ super(
123
+ "POLICY_DENIED",
124
+ message ?? `Gate "${gate}" denied: ${reason}.`
125
+ );
126
+ this.name = "PolicyDeniedError";
127
+ this.gate = gate;
128
+ this.reason = reason;
129
+ this.required = required;
130
+ }
131
+ };
132
+ var RecoveryNotEnrolledError = class extends NoydbError {
133
+ 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.') {
134
+ super("RECOVERY_NOT_ENROLLED", message);
135
+ this.name = "RecoveryNotEnrolledError";
136
+ }
137
+ };
138
+ var RecoveryProfileNotImplementedError = class extends NoydbError {
139
+ profile;
140
+ tracking;
141
+ constructor(profile, tracking) {
142
+ super(
143
+ "RECOVERY_PROFILE_NOT_IMPLEMENTED",
144
+ `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.`
145
+ );
146
+ this.name = "RecoveryProfileNotImplementedError";
147
+ this.profile = profile;
148
+ this.tracking = tracking;
149
+ }
150
+ };
151
+
152
+ // src/team/wrapped-deks.ts
153
+ var PBKDF2_ITERATIONS = 6e5;
154
+ var SALT_BYTES = 32;
155
+ var IV_BYTES = 12;
156
+ var subtle = globalThis.crypto.subtle;
157
+ async function mintWrappedDeksBlob(deks, credential) {
158
+ const salt = crypto.getRandomValues(new Uint8Array(SALT_BYTES));
159
+ const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
160
+ const wrappingKey = await deriveWrappingKey(credential, salt);
161
+ const exported = {};
162
+ for (const [coll, dek] of deks) {
163
+ const raw = await subtle.exportKey("raw", dek);
164
+ exported[coll] = bytesToBase64(new Uint8Array(raw));
165
+ }
166
+ const plaintext = new TextEncoder().encode(JSON.stringify({ deks: exported }));
167
+ const ciphertext = await subtle.encrypt(
168
+ { name: "AES-GCM", iv },
169
+ wrappingKey,
170
+ plaintext
171
+ );
172
+ return {
173
+ salt: bytesToBase64(salt),
174
+ iv: bytesToBase64(iv),
175
+ wrappedDeks: bytesToBase64(new Uint8Array(ciphertext))
176
+ };
177
+ }
178
+ async function unwrapDeksFromBlob(blob, credential) {
179
+ const wrappingKey = await deriveWrappingKey(credential, base64ToBytes(blob.salt));
180
+ const plaintext = await subtle.decrypt(
181
+ { name: "AES-GCM", iv: base64ToBytes(blob.iv) },
182
+ wrappingKey,
183
+ base64ToBytes(blob.wrappedDeks)
184
+ );
185
+ const parsed = JSON.parse(new TextDecoder().decode(plaintext));
186
+ const deks = /* @__PURE__ */ new Map();
187
+ for (const [coll, b64] of Object.entries(parsed.deks)) {
188
+ const raw = base64ToBytes(b64);
189
+ const key = await subtle.importKey(
190
+ "raw",
191
+ raw,
192
+ { name: "AES-GCM", length: 256 },
193
+ true,
194
+ ["encrypt", "decrypt"]
195
+ );
196
+ deks.set(coll, key);
197
+ }
198
+ return deks;
199
+ }
200
+ async function deriveWrappingKey(credential, salt) {
201
+ const ikm = await subtle.importKey(
202
+ "raw",
203
+ new TextEncoder().encode(credential),
204
+ "PBKDF2",
205
+ false,
206
+ ["deriveKey"]
207
+ );
208
+ return subtle.deriveKey(
209
+ {
210
+ name: "PBKDF2",
211
+ salt,
212
+ iterations: PBKDF2_ITERATIONS,
213
+ hash: "SHA-256"
214
+ },
215
+ ikm,
216
+ { name: "AES-GCM", length: 256 },
217
+ false,
218
+ ["encrypt", "decrypt"]
219
+ );
220
+ }
221
+ function bytesToBase64(b) {
222
+ let s = "";
223
+ for (const x of b) s += String.fromCharCode(x);
224
+ return btoa(s);
225
+ }
226
+ function base64ToBytes(b64) {
227
+ const s = atob(b64);
228
+ const out = new Uint8Array(s.length);
229
+ for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
230
+ return out;
231
+ }
232
+
233
+ // src/team/recovery.ts
234
+ var PAPER_DOC_ID = "recovery-paper";
235
+ async function loadPaperRecoveryEntries(store, vault) {
236
+ const env = await store.get(vault, "_meta", PAPER_DOC_ID);
237
+ if (!env) return [];
238
+ try {
239
+ const doc = JSON.parse(env._data);
240
+ if (doc.profile !== "paper" || !Array.isArray(doc.entries)) return [];
241
+ return doc.entries;
242
+ } catch {
243
+ return [];
244
+ }
245
+ }
246
+ async function savePaperRecoveryEntries(store, vault, entries) {
247
+ const doc = {
248
+ _noydb_recovery: 1,
249
+ profile: "paper",
250
+ entries
251
+ };
252
+ const envelope = {
253
+ _noydb: NOYDB_FORMAT_VERSION,
254
+ _v: 1,
255
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
256
+ _iv: "",
257
+ _data: JSON.stringify(doc)
258
+ };
259
+ await store.put(vault, "_meta", PAPER_DOC_ID, envelope);
260
+ }
261
+ async function burnPaperRecoveryEntry(store, vault, codeId) {
262
+ const entries = await loadPaperRecoveryEntries(store, vault);
263
+ const remaining = entries.filter((e) => e.codeId !== codeId);
264
+ await savePaperRecoveryEntries(store, vault, remaining);
265
+ }
266
+ async function hasRecoveryEnrolled(store, vault) {
267
+ const paper = await loadPaperRecoveryEntries(store, vault);
268
+ return paper.length > 0;
269
+ }
270
+ async function mintPaperRecoveryEntry(deks, code, codeId) {
271
+ const blob = await mintWrappedDeksBlob(deks, code);
272
+ return {
273
+ ...blob,
274
+ codeId,
275
+ enrolledAt: (/* @__PURE__ */ new Date()).toISOString()
276
+ };
277
+ }
278
+ async function unwrapDeksFromPaperEntry(entry, code) {
279
+ return unwrapDeksFromBlob(entry, code);
280
+ }
281
+
282
+ // src/team/rotate-recover.ts
283
+ async function rotatePassphrase(store, vault, userId, input) {
284
+ if (!input.allowWeakPassphrase) {
285
+ assertStrongPassphrase(input.newPassphrase, input.passphrasePolicy);
286
+ }
287
+ const env = await store.get(vault, "_keyring", userId);
288
+ if (!env) {
289
+ throw new NoAccessError(`No keyring found for user "${userId}" in vault "${vault}".`);
290
+ }
291
+ const file = JSON.parse(env._data);
292
+ const oldSalt = base64ToBuffer(file.salt);
293
+ const oldKek = await deriveKey(input.oldPassphrase, oldSalt);
294
+ const deks = /* @__PURE__ */ new Map();
295
+ for (const [coll, wrapped] of Object.entries(file.deks)) {
296
+ deks.set(coll, await unwrapKey(wrapped, oldKek));
297
+ }
298
+ const newSalt = generateSalt();
299
+ const newKek = await deriveKey(input.newPassphrase, newSalt);
300
+ const wrappedDeks = {};
301
+ for (const [coll, dek] of deks) {
302
+ wrappedDeks[coll] = await wrapKey(dek, newKek);
303
+ }
304
+ const oldSlots = file.authenticators ?? [];
305
+ const newSlots = [];
306
+ if (input.slotCeremonies && oldSlots.length > 0) {
307
+ for (const oldSlot of oldSlots) {
308
+ const ceremony = input.slotCeremonies[oldSlot.id];
309
+ if (!ceremony) continue;
310
+ const result = await ceremony({ newKek, newDeks: deks, oldSlot });
311
+ if (result.id !== oldSlot.id) {
312
+ throw new ValidationError(
313
+ `slotCeremonies['${oldSlot.id}'] returned id="${result.id}". The id must match the rotated slot \u2014 a ceremony cannot change a slot's identity.`
314
+ );
315
+ }
316
+ if (result.method !== oldSlot.method) {
317
+ throw new ValidationError(
318
+ `slotCeremonies['${oldSlot.id}'] returned method="${result.method}", expected "${oldSlot.method}". The method must match the rotated slot \u2014 a ceremony cannot change the auth method (e.g. webauthn \u2192 password) under cover of rotation.`
319
+ );
320
+ }
321
+ const oldWrapKind = oldSlot.wrapKind ?? "kek";
322
+ const newWrapKind = result.wrapKind ?? "kek";
323
+ if (oldWrapKind !== newWrapKind) {
324
+ throw new ValidationError(
325
+ `slotCeremonies['${oldSlot.id}'] returned wrapKind="${newWrapKind}", expected "${oldWrapKind}". The wrap format must match the rotated slot \u2014 a ceremony cannot change the wrap shape (e.g. wrap-KEK \u2192 wrap-DEKs) under cover of rotation, since that would silently change the session tier produced at unlock.`
326
+ );
327
+ }
328
+ const baseFields = {
329
+ id: result.id,
330
+ method: result.method,
331
+ // Preserve original enrolled_at — rotation is rewrapping, not
332
+ // re-enrollment. The slot's enrolment timestamp tracks when
333
+ // the user originally added the slot, not when it was last
334
+ // rewrapped. Forensics consumers reading enrolled_at are
335
+ // tracking the slot's ORIGIN, not its CURRENT wrapping.
336
+ enrolled_at: oldSlot.enrolled_at,
337
+ enrolled_via_tier: result.enrolled_via_tier ?? oldSlot.enrolled_via_tier,
338
+ meta: result.meta
339
+ };
340
+ const newSlot = result.wrapKind === "deks" ? {
341
+ ...baseFields,
342
+ wrapKind: "deks",
343
+ wrapped_deks: result.wrapped_deks,
344
+ iv: result.iv
345
+ } : {
346
+ ...baseFields,
347
+ wrapped_kek: result.wrapped_kek
348
+ };
349
+ newSlots.push(newSlot);
350
+ }
351
+ }
352
+ const canary = await mintKeyringCanary(newKek);
353
+ const next = {
354
+ ...file,
355
+ _noydb_keyring: NOYDB_KEYRING_VERSION,
356
+ deks: wrappedDeks,
357
+ salt: bufferToBase64(newSalt),
358
+ authenticators: newSlots,
359
+ canary
360
+ };
361
+ await writeKeyringFile(store, vault, userId, next);
362
+ return {
363
+ userId: file.user_id,
364
+ displayName: file.display_name,
365
+ role: file.role,
366
+ permissions: file.permissions,
367
+ deks,
368
+ kek: newKek,
369
+ salt: newSalt,
370
+ authenticators: newSlots,
371
+ ...file.export_capability !== void 0 && { exportCapability: file.export_capability },
372
+ ...file.import_capability !== void 0 && { importCapability: file.import_capability }
373
+ };
374
+ }
375
+ async function recoverPassphrase(store, vault, userId, input) {
376
+ if (!input.allowWeakPassphrase) {
377
+ assertStrongPassphrase(input.newPassphrase, input.passphrasePolicy);
378
+ }
379
+ const profile = input.recoveryProof.profile;
380
+ if (profile !== "paper") {
381
+ throw new RecoveryProfileNotImplementedError(
382
+ profile,
383
+ "https://github.com/vLannaAi/noy-db/issues/10"
384
+ );
385
+ }
386
+ return recoverViaPaperCode(store, vault, userId, input);
387
+ }
388
+ async function recoverViaPaperCode(store, vault, userId, input) {
389
+ if (input.recoveryProof.profile !== "paper") throw new Error("unreachable");
390
+ const { code } = input.recoveryProof.payload;
391
+ const env = await store.get(vault, "_keyring", userId);
392
+ if (!env) {
393
+ throw new NoAccessError(`No keyring found for user "${userId}" in vault "${vault}".`);
394
+ }
395
+ const file = JSON.parse(env._data);
396
+ const entries = await loadPaperRecoveryEntries(store, vault);
397
+ if (entries.length === 0) {
398
+ throw new NoAccessError(
399
+ `No paper-recovery entries enrolled for vault "${vault}". Enroll via \`db.enrollRecovery({ profile: "paper", entries })\` before relying on recovery.`
400
+ );
401
+ }
402
+ const normalized = normalizePaperCode(code);
403
+ let recovered;
404
+ for (const entry of entries) {
405
+ try {
406
+ const deks2 = await unwrapDeksFromPaperEntry(entry, normalized);
407
+ recovered = { deks: deks2, entry };
408
+ break;
409
+ } catch {
410
+ }
411
+ }
412
+ if (!recovered) {
413
+ throw new InvalidKeyError(
414
+ "Recovery code does not match any enrolled paper entry. The code may have been previously used (single-use) or typed incorrectly."
415
+ );
416
+ }
417
+ const deks = recovered.deks;
418
+ const newSalt = generateSalt();
419
+ const newKek = await deriveKey(input.newPassphrase, newSalt);
420
+ const wrappedDeks = {};
421
+ for (const [coll, dek] of deks) {
422
+ wrappedDeks[coll] = await wrapKey(dek, newKek);
423
+ }
424
+ const canary = await mintKeyringCanary(newKek);
425
+ const next = {
426
+ ...file,
427
+ _noydb_keyring: NOYDB_KEYRING_VERSION,
428
+ deks: wrappedDeks,
429
+ salt: bufferToBase64(newSalt),
430
+ authenticators: [],
431
+ // tier-2 slots wrap old KEK, drop them
432
+ canary
433
+ };
434
+ await burnPaperRecoveryEntry(store, vault, recovered.entry.codeId);
435
+ await writeKeyringFile(store, vault, userId, next);
436
+ return {
437
+ userId: file.user_id,
438
+ displayName: file.display_name,
439
+ role: file.role,
440
+ permissions: file.permissions,
441
+ deks,
442
+ kek: newKek,
443
+ salt: newSalt,
444
+ authenticators: [],
445
+ ...file.export_capability !== void 0 && { exportCapability: file.export_capability },
446
+ ...file.import_capability !== void 0 && { importCapability: file.import_capability }
447
+ };
448
+ }
449
+ function normalizePaperCode(input) {
450
+ return input.toUpperCase().replace(/[\s\-_]/g, "");
451
+ }
452
+ async function writeKeyringFile(store, vault, userId, file) {
453
+ const envelope = {
454
+ _noydb: 1,
455
+ _v: 1,
456
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
457
+ _iv: "",
458
+ _data: JSON.stringify(file)
459
+ };
460
+ await store.put(vault, "_keyring", userId, envelope);
461
+ }
462
+
463
+ // src/team/peer-recover.ts
464
+ var ADMIN_RECOVERABLE_TARGETS = ["operator", "viewer", "client", "admin"];
465
+ function canRecover(callerRole, targetRole) {
466
+ if (callerRole === "owner") return true;
467
+ if (callerRole === "admin") return ADMIN_RECOVERABLE_TARGETS.includes(targetRole);
468
+ return false;
469
+ }
470
+ async function recoverUser(store, vault, callerKeyring, options) {
471
+ const env = await store.get(vault, "_keyring", options.userId);
472
+ if (!env) {
473
+ throw new NoAccessError(
474
+ `recoverUser: user "${options.userId}" has no keyring in vault "${vault}".`
475
+ );
476
+ }
477
+ const target = JSON.parse(env._data);
478
+ const targetRole = options.role ?? target.role;
479
+ if (!canRecover(callerKeyring.role, targetRole)) {
480
+ throw new PermissionDeniedError(
481
+ `Role "${callerKeyring.role}" cannot recover role "${targetRole}"`
482
+ );
483
+ }
484
+ if (!canRecover(callerKeyring.role, target.role)) {
485
+ throw new PermissionDeniedError(
486
+ `Role "${callerKeyring.role}" cannot recover role "${target.role}"`
487
+ );
488
+ }
489
+ for (const coll of Object.keys(target.deks)) {
490
+ if (!callerKeyring.deks.has(coll)) {
491
+ throw new PrivilegeEscalationError(coll);
492
+ }
493
+ }
494
+ if (options.validatePassphrase && !options.allowWeakPassphrase) {
495
+ assertStrongPassphrase(options.passphrase, options.passphrasePolicy);
496
+ }
497
+ const newSalt = generateSalt();
498
+ const newKek = await deriveKey(options.passphrase, newSalt);
499
+ const wrappedDeks = {};
500
+ for (const coll of Object.keys(target.deks)) {
501
+ const callerDek = callerKeyring.deks.get(coll);
502
+ if (!callerDek) {
503
+ throw new PrivilegeEscalationError(coll);
504
+ }
505
+ wrappedDeks[coll] = await wrapKey(callerDek, newKek);
506
+ }
507
+ const canary = await mintKeyringCanary(newKek);
508
+ const next = {
509
+ ...target,
510
+ _noydb_keyring: NOYDB_KEYRING_VERSION,
511
+ role: targetRole,
512
+ display_name: options.displayName ?? target.display_name,
513
+ deks: wrappedDeks,
514
+ salt: bufferToBase64(newSalt),
515
+ granted_by: callerKeyring.userId,
516
+ authenticators: [],
517
+ canary
518
+ };
519
+ const envelope = {
520
+ _noydb: 1,
521
+ _v: 1,
522
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
523
+ _iv: "",
524
+ _data: JSON.stringify(next)
525
+ };
526
+ await store.put(vault, "_keyring", options.userId, envelope);
527
+ }
528
+
529
+ // src/team/magic-link-grant.ts
530
+ var MAGIC_LINK_GRANTS_COLLECTION = "_magic_link_grants";
531
+ var MAGIC_LINK_CONTENT_INFO_PREFIX = "noydb-magic-link-content-v1:";
532
+ var MAGIC_LINK_KEK_INFO_PREFIX = "noydb-magic-link-v1:";
533
+ async function deriveMagicLinkContentKey(serverSecret, token, vault) {
534
+ const subtle2 = globalThis.crypto.subtle;
535
+ const ikmBytes = serverSecret instanceof Uint8Array ? serverSecret : new TextEncoder().encode(serverSecret);
536
+ const tokenBytes = new TextEncoder().encode(token);
537
+ const saltBuffer = await subtle2.digest("SHA-256", tokenBytes);
538
+ const info = new TextEncoder().encode(MAGIC_LINK_CONTENT_INFO_PREFIX + vault);
539
+ const ikm = await subtle2.importKey("raw", ikmBytes, "HKDF", false, ["deriveKey"]);
540
+ return subtle2.deriveKey(
541
+ { name: "HKDF", hash: "SHA-256", salt: saltBuffer, info },
542
+ ikm,
543
+ { name: "AES-GCM", length: 256 },
544
+ false,
545
+ ["encrypt", "decrypt"]
546
+ );
547
+ }
548
+ async function writeMagicLinkGrant(store, vault, grantor, contentKey, grantKek, recordId, opts) {
549
+ const collectionName = opts.collection ?? null;
550
+ const sourceKey = collectionName ? dekKey(collectionName, opts.tier) : `__any#${opts.tier}`;
551
+ const sourceDek = grantor.deks.get(sourceKey);
552
+ if (!sourceDek) {
553
+ throw new DelegationTargetMissingError(
554
+ `grantor cannot find tier ${opts.tier} DEK for ${collectionName ?? "(any)"}`
555
+ );
556
+ }
557
+ const wrappedDek = await wrapKey(sourceDek, grantKek);
558
+ const until = typeof opts.until === "string" ? opts.until : opts.until.toISOString();
559
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
560
+ const payload = {
561
+ id: recordId,
562
+ toUser: opts.toUser,
563
+ fromUser: grantor.userId,
564
+ tier: opts.tier,
565
+ collection: collectionName,
566
+ ...opts.record && { record: opts.record },
567
+ until,
568
+ wrappedDek,
569
+ createdAt,
570
+ ...opts.note && { note: opts.note }
571
+ };
572
+ const { iv, data } = await encrypt(JSON.stringify(payload), contentKey);
573
+ const envelope = {
574
+ _noydb: 1,
575
+ _v: 1,
576
+ _ts: createdAt,
577
+ _iv: iv,
578
+ _data: data,
579
+ _by: grantor.userId
580
+ };
581
+ await store.put(vault, MAGIC_LINK_GRANTS_COLLECTION, recordId, envelope);
582
+ return { recordId, payload };
583
+ }
584
+ async function readMagicLinkGrantRecord(store, vault, contentKey, recordId) {
585
+ const env = await store.get(vault, MAGIC_LINK_GRANTS_COLLECTION, recordId);
586
+ if (!env) return null;
587
+ try {
588
+ const json = await decrypt(env._iv, env._data, contentKey);
589
+ return JSON.parse(json);
590
+ } catch {
591
+ return null;
592
+ }
593
+ }
594
+ async function listMagicLinkGrants(store, vault, contentKey, token) {
595
+ const ids = await store.list(vault, MAGIC_LINK_GRANTS_COLLECTION);
596
+ const matching = ids.filter((id) => id === token || id.startsWith(`${token}:`));
597
+ const out = [];
598
+ for (const id of matching) {
599
+ const payload = await readMagicLinkGrantRecord(store, vault, contentKey, id);
600
+ if (payload) out.push(payload);
601
+ }
602
+ return out;
603
+ }
604
+ async function unwrapMagicLinkGrant(payload, grantKek) {
605
+ return unwrapKey(payload.wrappedDek, grantKek);
606
+ }
607
+ async function revokeMagicLinkGrant(store, vault, token) {
608
+ const ids = await store.list(vault, MAGIC_LINK_GRANTS_COLLECTION);
609
+ const matching = ids.filter((id) => id === token || id.startsWith(`${token}:`));
610
+ for (const id of matching) {
611
+ await store.delete(vault, MAGIC_LINK_GRANTS_COLLECTION, id);
612
+ }
613
+ return matching.length;
614
+ }
615
+ function magicLinkGrantRecordId(token, index) {
616
+ return index === 0 ? token : `${token}:${index}`;
617
+ }
618
+ function isMagicLinkGrantExpired(payload, now = /* @__PURE__ */ new Date()) {
619
+ return payload.until <= now.toISOString();
620
+ }
621
+
622
+ // src/team/sync-credentials.ts
623
+ var SYNC_CREDENTIALS_COLLECTION = "_sync_credentials";
624
+ function requireAdminAccess(keyring) {
625
+ if (keyring.role !== "owner" && keyring.role !== "admin") {
626
+ throw new PermissionDeniedError(
627
+ `Sync credentials require owner or admin role. Current role: "${keyring.role}"`
628
+ );
629
+ }
630
+ }
631
+ async function putCredential(adapter, vault, keyring, credential) {
632
+ requireAdminAccess(keyring);
633
+ const getDek = await ensureCollectionDEK(adapter, vault, keyring);
634
+ const dek = await getDek(SYNC_CREDENTIALS_COLLECTION);
635
+ const { iv, data } = await encrypt(JSON.stringify(credential), dek);
636
+ const existing = await adapter.get(vault, SYNC_CREDENTIALS_COLLECTION, credential.adapterId);
637
+ const version = existing ? existing._v + 1 : 1;
638
+ const envelope = {
639
+ _noydb: NOYDB_FORMAT_VERSION,
640
+ _v: version,
641
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
642
+ _iv: iv,
643
+ _data: data,
644
+ _by: keyring.userId
645
+ };
646
+ await adapter.put(
647
+ vault,
648
+ SYNC_CREDENTIALS_COLLECTION,
649
+ credential.adapterId,
650
+ envelope,
651
+ existing ? existing._v : void 0
652
+ );
653
+ }
654
+ async function getCredential(adapter, vault, keyring, adapterId) {
655
+ requireAdminAccess(keyring);
656
+ const getDek = await ensureCollectionDEK(adapter, vault, keyring);
657
+ const dek = await getDek(SYNC_CREDENTIALS_COLLECTION);
658
+ const envelope = await adapter.get(vault, SYNC_CREDENTIALS_COLLECTION, adapterId);
659
+ if (!envelope) return null;
660
+ const plaintext = await decrypt(envelope._iv, envelope._data, dek);
661
+ return JSON.parse(plaintext);
662
+ }
663
+ async function deleteCredential(adapter, vault, keyring, adapterId) {
664
+ requireAdminAccess(keyring);
665
+ await adapter.delete(vault, SYNC_CREDENTIALS_COLLECTION, adapterId);
666
+ }
667
+ async function listCredentials(adapter, vault, keyring) {
668
+ requireAdminAccess(keyring);
669
+ return adapter.list(vault, SYNC_CREDENTIALS_COLLECTION);
670
+ }
671
+ async function credentialStatus(adapter, vault, keyring, adapterId) {
672
+ const credential = await getCredential(adapter, vault, keyring, adapterId);
673
+ if (!credential) return { exists: false };
674
+ const expired = credential.expiresAt ? Date.now() > new Date(credential.expiresAt).getTime() : false;
675
+ return { exists: true, expired };
676
+ }
677
+
678
+ export {
679
+ enrollAuthenticator,
680
+ updateAuthenticator,
681
+ removeAuthenticator,
682
+ findAuthenticator,
683
+ PolicyDeniedError,
684
+ RecoveryNotEnrolledError,
685
+ RecoveryProfileNotImplementedError,
686
+ mintWrappedDeksBlob,
687
+ unwrapDeksFromBlob,
688
+ loadPaperRecoveryEntries,
689
+ savePaperRecoveryEntries,
690
+ burnPaperRecoveryEntry,
691
+ hasRecoveryEnrolled,
692
+ mintPaperRecoveryEntry,
693
+ unwrapDeksFromPaperEntry,
694
+ rotatePassphrase,
695
+ recoverPassphrase,
696
+ recoverUser,
697
+ MAGIC_LINK_GRANTS_COLLECTION,
698
+ MAGIC_LINK_CONTENT_INFO_PREFIX,
699
+ MAGIC_LINK_KEK_INFO_PREFIX,
700
+ deriveMagicLinkContentKey,
701
+ writeMagicLinkGrant,
702
+ readMagicLinkGrantRecord,
703
+ listMagicLinkGrants,
704
+ unwrapMagicLinkGrant,
705
+ revokeMagicLinkGrant,
706
+ magicLinkGrantRecordId,
707
+ isMagicLinkGrantExpired,
708
+ SYNC_CREDENTIALS_COLLECTION,
709
+ putCredential,
710
+ getCredential,
711
+ deleteCredential,
712
+ listCredentials,
713
+ credentialStatus
714
+ };
715
+ //# sourceMappingURL=chunk-YWKJZZGV.js.map