@noy-db/hub 0.1.0-pre.9 → 0.2.0-pre.1

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 (253) hide show
  1. package/dist/aggregate/index.cjs +91 -36
  2. package/dist/aggregate/index.cjs.map +1 -1
  3. package/dist/aggregate/index.d.cts +2 -2
  4. package/dist/aggregate/index.d.ts +2 -2
  5. package/dist/aggregate/index.js +16 -9
  6. package/dist/aggregate/index.js.map +1 -1
  7. package/dist/blobs/index.cjs.map +1 -1
  8. package/dist/blobs/index.d.cts +6 -6
  9. package/dist/blobs/index.d.ts +6 -6
  10. package/dist/blobs/index.js +4 -4
  11. package/dist/bundle/index.cjs +298 -7
  12. package/dist/bundle/index.cjs.map +1 -1
  13. package/dist/bundle/index.d.cts +6 -6
  14. package/dist/bundle/index.d.ts +6 -6
  15. package/dist/bundle/index.js +15 -4
  16. package/dist/{chunk-GOUT6DND.js → chunk-23TTQXVO.js} +173 -91
  17. package/dist/chunk-23TTQXVO.js.map +1 -0
  18. package/dist/{chunk-CIMZBAZB.js → chunk-2AXFIYHT.js} +1 -1
  19. package/dist/chunk-2AXFIYHT.js.map +1 -0
  20. package/dist/chunk-34YSDCDP.js +73 -0
  21. package/dist/chunk-34YSDCDP.js.map +1 -0
  22. package/dist/{chunk-AVVPZ4BC.js → chunk-4TFSM22V.js} +4 -4
  23. package/dist/{chunk-QGZRWRSL.js → chunk-537VFZTR.js} +4 -4
  24. package/dist/{chunk-M62XNWRA.js → chunk-5DWL3JBF.js} +2 -2
  25. package/dist/{chunk-PTVMYYON.js → chunk-5SCJ5UEF.js} +3 -3
  26. package/dist/chunk-5ZGZ6HIZ.js +100 -0
  27. package/dist/chunk-5ZGZ6HIZ.js.map +1 -0
  28. package/dist/chunk-6HPZY4ON.js +291 -0
  29. package/dist/chunk-6HPZY4ON.js.map +1 -0
  30. package/dist/{chunk-EXHNQEV4.js → chunk-7H6DOO3E.js} +239 -11
  31. package/dist/chunk-7H6DOO3E.js.map +1 -0
  32. package/dist/{chunk-ACLDOTNQ.js → chunk-ADQ5MQ54.js} +275 -3
  33. package/dist/chunk-ADQ5MQ54.js.map +1 -0
  34. package/dist/chunk-CBAHB2BF.js +893 -0
  35. package/dist/chunk-CBAHB2BF.js.map +1 -0
  36. package/dist/chunk-DPMFBCV6.js +296 -0
  37. package/dist/chunk-DPMFBCV6.js.map +1 -0
  38. package/dist/chunk-DYBQG5PQ.js +34 -0
  39. package/dist/chunk-DYBQG5PQ.js.map +1 -0
  40. package/dist/{chunk-ZFKD4QMV.js → chunk-DYECX3IX.js} +3 -3
  41. package/dist/chunk-EGQYGYIU.js +51 -0
  42. package/dist/chunk-EGQYGYIU.js.map +1 -0
  43. package/dist/chunk-FCXOFQAJ.js +79 -0
  44. package/dist/chunk-FCXOFQAJ.js.map +1 -0
  45. package/dist/chunk-HB3Z2GCR.js +124 -0
  46. package/dist/chunk-HB3Z2GCR.js.map +1 -0
  47. package/dist/{chunk-SCZXXXU4.js → chunk-I6MX32UC.js} +7 -32
  48. package/dist/chunk-I6MX32UC.js.map +1 -0
  49. package/dist/{chunk-VQBTTTUN.js → chunk-KESP7GOK.js} +4 -4
  50. package/dist/{chunk-VQBTTTUN.js.map → chunk-KESP7GOK.js.map} +1 -1
  51. package/dist/{chunk-NXFEYLVG.js → chunk-MIQHZESA.js} +4 -3
  52. package/dist/{chunk-NXFEYLVG.js.map → chunk-MIQHZESA.js.map} +1 -1
  53. package/dist/chunk-MKSA2V7A.js +19 -0
  54. package/dist/chunk-MKSA2V7A.js.map +1 -0
  55. package/dist/{chunk-M5INGEFC.js → chunk-MRIBLZL3.js} +3 -1
  56. package/dist/chunk-MRIBLZL3.js.map +1 -0
  57. package/dist/{chunk-MDDTIZUO.js → chunk-NIOHFJPJ.js} +6 -6
  58. package/dist/chunk-OMLIZL2P.js +61 -0
  59. package/dist/chunk-OMLIZL2P.js.map +1 -0
  60. package/dist/{chunk-USKYUS74.js → chunk-P7EQ2S5O.js} +2 -2
  61. package/dist/{chunk-WDM5XGGS.js → chunk-PA6R5ZCI.js} +181 -11
  62. package/dist/chunk-PA6R5ZCI.js.map +1 -0
  63. package/dist/chunk-PEULZC6M.js +118 -0
  64. package/dist/chunk-PEULZC6M.js.map +1 -0
  65. package/dist/chunk-RD5LYKD6.js +82 -0
  66. package/dist/chunk-RD5LYKD6.js.map +1 -0
  67. package/dist/chunk-SIZWEV2Y.js +145 -0
  68. package/dist/chunk-SIZWEV2Y.js.map +1 -0
  69. package/dist/{chunk-QAVUREFT.js → chunk-UA4RI7OT.js} +12 -6
  70. package/dist/chunk-UA4RI7OT.js.map +1 -0
  71. package/dist/chunk-UMLVJTYV.js +20 -0
  72. package/dist/chunk-UMLVJTYV.js.map +1 -0
  73. package/dist/chunk-UZXLQCHP.js +53 -0
  74. package/dist/chunk-UZXLQCHP.js.map +1 -0
  75. package/dist/{chunk-2CSJGFCB.js → chunk-VMIO4IXG.js} +5 -5
  76. package/dist/{chunk-MR4424N3.js → chunk-WCA2NROQ.js} +2 -2
  77. package/dist/{chunk-TDR6T5CJ.js → chunk-XGSOTWYX.js} +91 -132
  78. package/dist/chunk-XGSOTWYX.js.map +1 -0
  79. package/dist/{chunk-NPC4LFV5.js → chunk-YMYK7US4.js} +2 -2
  80. package/dist/{chunk-RKJ6OL7K.js → chunk-YS3POABP.js} +1 -1
  81. package/dist/chunk-YS3POABP.js.map +1 -0
  82. package/dist/chunk-Z72JH4KG.js +209 -0
  83. package/dist/chunk-Z72JH4KG.js.map +1 -0
  84. package/dist/{chunk-R36SIKES.js → chunk-ZNOEIM6Y.js} +2 -2
  85. package/dist/consent/index.cjs.map +1 -1
  86. package/dist/consent/index.d.cts +6 -6
  87. package/dist/consent/index.d.ts +6 -6
  88. package/dist/consent/index.js +3 -3
  89. package/dist/{crypto-IVKU7YTT.js → crypto-A7FRXYHC.js} +3 -3
  90. package/dist/{delegation-2DBS2EOH.js → delegation-YBA4X4JN.js} +5 -4
  91. package/dist/derivations/index.cjs +351 -0
  92. package/dist/derivations/index.cjs.map +1 -0
  93. package/dist/derivations/index.d.cts +71 -0
  94. package/dist/derivations/index.d.ts +71 -0
  95. package/dist/derivations/index.js +27 -0
  96. package/dist/{dev-unlock-BdPp68qn.d.ts → dev-unlock-D9s-loPr.d.ts} +1 -1
  97. package/dist/{dev-unlock-Da1B0TIK.d.cts → dev-unlock-DRwVSy2S.d.cts} +1 -1
  98. package/dist/executor-7E3VFGW7.js +11 -0
  99. package/dist/executor-CEWX2FQI.js +8 -0
  100. package/dist/executor-CEWX2FQI.js.map +1 -0
  101. package/dist/executor-X4SQ3ZLC.js +8 -0
  102. package/dist/executor-X4SQ3ZLC.js.map +1 -0
  103. package/dist/fanout-sidecar-VJ52RIEY.js +51 -0
  104. package/dist/fanout-sidecar-VJ52RIEY.js.map +1 -0
  105. package/dist/guards/index.cjs +315 -0
  106. package/dist/guards/index.cjs.map +1 -0
  107. package/dist/guards/index.d.cts +30 -0
  108. package/dist/guards/index.d.ts +30 -0
  109. package/dist/guards/index.js +29 -0
  110. package/dist/guards/index.js.map +1 -0
  111. package/dist/{hash-lsoL3eEW.d.ts → hash-DXXXusyk.d.ts} +1 -1
  112. package/dist/{hash-BEfzPKwo.d.cts → hash-DtRih9MQ.d.cts} +1 -1
  113. package/dist/history/index.cjs +8 -1
  114. package/dist/history/index.cjs.map +1 -1
  115. package/dist/history/index.d.cts +7 -7
  116. package/dist/history/index.d.ts +7 -7
  117. package/dist/history/index.js +6 -6
  118. package/dist/i18n/index.cjs +81 -0
  119. package/dist/i18n/index.cjs.map +1 -1
  120. package/dist/i18n/index.d.cts +6 -6
  121. package/dist/i18n/index.d.ts +6 -6
  122. package/dist/i18n/index.js +19 -6
  123. package/dist/i18n/index.js.map +1 -1
  124. package/dist/{index-8QDuznDr.d.ts → index-4agOpzqd.d.ts} +174 -3
  125. package/dist/{index-6xNpPsxR.d.cts → index-CNwA-B6-.d.ts} +303 -5
  126. package/dist/{index-DJTf9yxn.d.ts → index-CmVgTkqk.d.cts} +303 -5
  127. package/dist/{index-CywCC1qZ.d.cts → index-hdFvZkBP.d.cts} +174 -3
  128. package/dist/index.cjs +5615 -979
  129. package/dist/index.cjs.map +1 -1
  130. package/dist/index.d.cts +207 -16
  131. package/dist/index.d.ts +207 -16
  132. package/dist/index.js +2302 -741
  133. package/dist/index.js.map +1 -1
  134. package/dist/indexing/index.cjs +2 -0
  135. package/dist/indexing/index.cjs.map +1 -1
  136. package/dist/indexing/index.d.cts +3 -3
  137. package/dist/indexing/index.d.ts +3 -3
  138. package/dist/indexing/index.js +4 -4
  139. package/dist/{lazy-builder-CZVLKh0Z.d.cts → lazy-builder-C-rPfWG0.d.cts} +1 -1
  140. package/dist/{lazy-builder-BwEoBQZ9.d.ts → lazy-builder-Rpd-V3jP.d.ts} +1 -1
  141. package/dist/{ledger-QZTTHQAQ.js → ledger-3TXNP47J.js} +6 -6
  142. package/dist/ledger-3TXNP47J.js.map +1 -0
  143. package/dist/materialized-views/index.cjs +837 -0
  144. package/dist/materialized-views/index.cjs.map +1 -0
  145. package/dist/materialized-views/index.d.cts +183 -0
  146. package/dist/materialized-views/index.d.ts +183 -0
  147. package/dist/materialized-views/index.js +45 -0
  148. package/dist/materialized-views/index.js.map +1 -0
  149. package/dist/overlay-views/index.cjs +359 -0
  150. package/dist/overlay-views/index.cjs.map +1 -0
  151. package/dist/overlay-views/index.d.cts +81 -0
  152. package/dist/overlay-views/index.d.ts +81 -0
  153. package/dist/overlay-views/index.js +23 -0
  154. package/dist/overlay-views/index.js.map +1 -0
  155. package/dist/periods/index.cjs +7 -1
  156. package/dist/periods/index.cjs.map +1 -1
  157. package/dist/periods/index.d.cts +6 -6
  158. package/dist/periods/index.d.ts +6 -6
  159. package/dist/periods/index.js +6 -6
  160. package/dist/{predicate-SBHmi6D0.d.cts → predicate-Dnu81tsS.d.cts} +25 -1
  161. package/dist/{predicate-SBHmi6D0.d.ts → predicate-Dnu81tsS.d.ts} +25 -1
  162. package/dist/{public-envelope-6JTACYJV.js → public-envelope-PY6NKFLI.js} +4 -4
  163. package/dist/public-envelope-PY6NKFLI.js.map +1 -0
  164. package/dist/query/index.cjs +302 -124
  165. package/dist/query/index.cjs.map +1 -1
  166. package/dist/query/index.d.cts +3 -3
  167. package/dist/query/index.d.ts +3 -3
  168. package/dist/query/index.js +26 -11
  169. package/dist/read-only-facade-ITU6L7BL.js +7 -0
  170. package/dist/read-only-facade-ITU6L7BL.js.map +1 -0
  171. package/dist/registry-3L3N3PTG.js +10 -0
  172. package/dist/registry-3L3N3PTG.js.map +1 -0
  173. package/dist/registry-O47PUPSY.js +8 -0
  174. package/dist/registry-O47PUPSY.js.map +1 -0
  175. package/dist/registry-RFGGMVNJ.js +7 -0
  176. package/dist/registry-RFGGMVNJ.js.map +1 -0
  177. package/dist/registry-WLLMODKN.js +8 -0
  178. package/dist/registry-WLLMODKN.js.map +1 -0
  179. package/dist/session/index.cjs +7 -1
  180. package/dist/session/index.cjs.map +1 -1
  181. package/dist/session/index.d.cts +7 -7
  182. package/dist/session/index.d.ts +7 -7
  183. package/dist/session/index.js +10 -3
  184. package/dist/session/index.js.map +1 -1
  185. package/dist/shadow/index.cjs.map +1 -1
  186. package/dist/shadow/index.d.cts +6 -6
  187. package/dist/shadow/index.d.ts +6 -6
  188. package/dist/shadow/index.js +2 -2
  189. package/dist/stale-HSC5YO2O.js +13 -0
  190. package/dist/stale-HSC5YO2O.js.map +1 -0
  191. package/dist/store/index.cjs +14 -0
  192. package/dist/store/index.cjs.map +1 -1
  193. package/dist/store/index.d.cts +6 -6
  194. package/dist/store/index.d.ts +6 -6
  195. package/dist/store/index.js +5 -2
  196. package/dist/{strategy-D-SrOLCl.d.cts → strategy-DSTrsZ8t.d.cts} +72 -19
  197. package/dist/{strategy-D-SrOLCl.d.ts → strategy-DSTrsZ8t.d.ts} +72 -19
  198. package/dist/sync/index.cjs.map +1 -1
  199. package/dist/sync/index.d.cts +5 -5
  200. package/dist/sync/index.d.ts +5 -5
  201. package/dist/sync/index.js +4 -4
  202. package/dist/team/index.cjs +1554 -2
  203. package/dist/team/index.cjs.map +1 -1
  204. package/dist/team/index.d.cts +6 -6
  205. package/dist/team/index.d.ts +6 -6
  206. package/dist/team/index.js +76 -9
  207. package/dist/tx/index.cjs +296 -44
  208. package/dist/tx/index.cjs.map +1 -1
  209. package/dist/tx/index.d.cts +6 -6
  210. package/dist/tx/index.d.ts +6 -6
  211. package/dist/tx/index.js +2 -2
  212. package/dist/{types-Bnb82f5R.d.cts → types-C4lwMKKF.d.cts} +2605 -328
  213. package/dist/{types-Bo7NSXJr.d.ts → types-DW9RGSSs.d.ts} +2605 -328
  214. package/dist/util/index.cjs.map +1 -1
  215. package/dist/util/index.js +1 -1
  216. package/dist/with-derivation-C8LDlV7t.d.cts +13 -0
  217. package/dist/with-derivation-g-pGoMzL.d.ts +13 -0
  218. package/dist/with-guard-DWOCK4Ca.d.ts +18 -0
  219. package/dist/with-guard-jI1x9Z3k.d.cts +18 -0
  220. package/dist/with-materialized-view-DaKR-N6J.d.ts +27 -0
  221. package/dist/with-materialized-view-DcTx4H3j.d.cts +27 -0
  222. package/dist/with-overlayed-view-D-6oWAgM.d.cts +13 -0
  223. package/dist/with-overlayed-view-N7jYuNOS.d.ts +13 -0
  224. package/package.json +53 -2
  225. package/dist/chunk-4PWAI7Q4.js +0 -79
  226. package/dist/chunk-4PWAI7Q4.js.map +0 -1
  227. package/dist/chunk-ACLDOTNQ.js.map +0 -1
  228. package/dist/chunk-BTDCBVJW.js +0 -160
  229. package/dist/chunk-BTDCBVJW.js.map +0 -1
  230. package/dist/chunk-CIMZBAZB.js.map +0 -1
  231. package/dist/chunk-EXHNQEV4.js.map +0 -1
  232. package/dist/chunk-GOUT6DND.js.map +0 -1
  233. package/dist/chunk-M5INGEFC.js.map +0 -1
  234. package/dist/chunk-QAVUREFT.js.map +0 -1
  235. package/dist/chunk-RKJ6OL7K.js.map +0 -1
  236. package/dist/chunk-SCZXXXU4.js.map +0 -1
  237. package/dist/chunk-TDR6T5CJ.js.map +0 -1
  238. package/dist/chunk-WDM5XGGS.js.map +0 -1
  239. /package/dist/{chunk-AVVPZ4BC.js.map → chunk-4TFSM22V.js.map} +0 -0
  240. /package/dist/{chunk-QGZRWRSL.js.map → chunk-537VFZTR.js.map} +0 -0
  241. /package/dist/{chunk-M62XNWRA.js.map → chunk-5DWL3JBF.js.map} +0 -0
  242. /package/dist/{chunk-PTVMYYON.js.map → chunk-5SCJ5UEF.js.map} +0 -0
  243. /package/dist/{chunk-ZFKD4QMV.js.map → chunk-DYECX3IX.js.map} +0 -0
  244. /package/dist/{chunk-MDDTIZUO.js.map → chunk-NIOHFJPJ.js.map} +0 -0
  245. /package/dist/{chunk-USKYUS74.js.map → chunk-P7EQ2S5O.js.map} +0 -0
  246. /package/dist/{chunk-2CSJGFCB.js.map → chunk-VMIO4IXG.js.map} +0 -0
  247. /package/dist/{chunk-MR4424N3.js.map → chunk-WCA2NROQ.js.map} +0 -0
  248. /package/dist/{chunk-NPC4LFV5.js.map → chunk-YMYK7US4.js.map} +0 -0
  249. /package/dist/{chunk-R36SIKES.js.map → chunk-ZNOEIM6Y.js.map} +0 -0
  250. /package/dist/{crypto-IVKU7YTT.js.map → crypto-A7FRXYHC.js.map} +0 -0
  251. /package/dist/{delegation-2DBS2EOH.js.map → delegation-YBA4X4JN.js.map} +0 -0
  252. /package/dist/{ledger-QZTTHQAQ.js.map → derivations/index.js.map} +0 -0
  253. /package/dist/{public-envelope-6JTACYJV.js.map → executor-7E3VFGW7.js.map} +0 -0
package/dist/index.js CHANGED
@@ -1,22 +1,42 @@
1
+ import {
2
+ DELEGATIONS_COLLECTION,
3
+ issueDelegation,
4
+ loadActiveDelegations,
5
+ revokeDelegation
6
+ } from "./chunk-I6MX32UC.js";
1
7
  import {
2
8
  DEFAULT_PUBLIC_ENVELOPE_SCHEMA,
3
9
  PUBLIC_ENVELOPE_FIELDS,
4
10
  resolveSchema
5
11
  } from "./chunk-EMIGCR7X.js";
6
12
  import {
7
- DELEGATIONS_COLLECTION,
8
- assertTierAccess,
9
- dekKey,
10
- effectiveClearance,
11
- issueDelegation,
12
- loadActiveDelegations,
13
- revokeDelegation
14
- } from "./chunk-SCZXXXU4.js";
13
+ TxCollection,
14
+ TxContext,
15
+ TxVault,
16
+ revertExecuted,
17
+ runTransaction
18
+ } from "./chunk-6HPZY4ON.js";
19
+ import {
20
+ withDerivation
21
+ } from "./chunk-EGQYGYIU.js";
22
+ import "./chunk-FCXOFQAJ.js";
23
+ import "./chunk-HB3Z2GCR.js";
24
+ import {
25
+ withMaterializedView
26
+ } from "./chunk-RD5LYKD6.js";
27
+ import "./chunk-SIZWEV2Y.js";
28
+ import "./chunk-DPMFBCV6.js";
29
+ import "./chunk-UZXLQCHP.js";
30
+ import {
31
+ OverlayedCollection,
32
+ withOverlayedView
33
+ } from "./chunk-Z72JH4KG.js";
34
+ import "./chunk-OMLIZL2P.js";
15
35
  import {
16
36
  LazyQuery,
17
37
  decodeIdxId,
18
38
  encodeIdxId
19
- } from "./chunk-ZFKD4QMV.js";
39
+ } from "./chunk-DYECX3IX.js";
20
40
  import {
21
41
  mergeCrdtStates,
22
42
  resolveCrdtSnapshot
@@ -31,7 +51,7 @@ import {
31
51
  readNoydbBundlePublicEnvelope,
32
52
  resetBrotliSupportCache,
33
53
  writeNoydbBundle
34
- } from "./chunk-EXHNQEV4.js";
54
+ } from "./chunk-7H6DOO3E.js";
35
55
  import {
36
56
  PUBLIC_ENVELOPE_RECORD_ID,
37
57
  isPublicEnvelope,
@@ -39,24 +59,24 @@ import {
39
59
  readPublicEnvelope,
40
60
  savePublicEnvelope,
41
61
  validatePublicEnvelopeInput
42
- } from "./chunk-PTVMYYON.js";
62
+ } from "./chunk-5SCJ5UEF.js";
43
63
  import {
44
64
  CONSENT_AUDIT_COLLECTION
45
- } from "./chunk-M62XNWRA.js";
65
+ } from "./chunk-5DWL3JBF.js";
46
66
  import {
47
67
  PERIODS_COLLECTION
48
- } from "./chunk-QGZRWRSL.js";
68
+ } from "./chunk-537VFZTR.js";
49
69
  import "./chunk-UF3BUNQZ.js";
70
+ import {
71
+ withGuard
72
+ } from "./chunk-MKSA2V7A.js";
73
+ import "./chunk-PEULZC6M.js";
74
+ import "./chunk-UMLVJTYV.js";
75
+ import "./chunk-34YSDCDP.js";
50
76
  import {
51
77
  CollectionFrame,
52
78
  VaultFrame
53
- } from "./chunk-R36SIKES.js";
54
- import {
55
- TxCollection,
56
- TxContext,
57
- TxVault,
58
- runTransaction
59
- } from "./chunk-BTDCBVJW.js";
79
+ } from "./chunk-ZNOEIM6Y.js";
60
80
  import {
61
81
  DICT_COLLECTION_PREFIX,
62
82
  DictionaryHandle,
@@ -69,7 +89,7 @@ import {
69
89
  isI18nTextDescriptor,
70
90
  resolveI18nText,
71
91
  validateI18nTextValue
72
- } from "./chunk-MDDTIZUO.js";
92
+ } from "./chunk-NIOHFJPJ.js";
73
93
  import {
74
94
  createBundleStore,
75
95
  routeStore,
@@ -81,30 +101,73 @@ import {
81
101
  withRetry,
82
102
  wrapBundleStore,
83
103
  wrapStore
84
- } from "./chunk-USKYUS74.js";
104
+ } from "./chunk-P7EQ2S5O.js";
85
105
  import {
106
+ MAGIC_LINK_CONTENT_INFO_PREFIX,
107
+ MAGIC_LINK_GRANTS_COLLECTION,
108
+ MAGIC_LINK_KEK_INFO_PREFIX,
109
+ ManagedRecoveryNotEnrolledError,
110
+ PolicyDeniedError,
111
+ RecoveryNotEnrolledError,
112
+ RecoveryProfileNotImplementedError,
86
113
  SYNC_CREDENTIALS_COLLECTION,
114
+ burnPaperRecoveryEntry,
87
115
  credentialStatus,
88
116
  deleteCredential,
117
+ deriveMagicLinkContentKey,
118
+ enrollAuthenticator,
119
+ findAuthenticator,
89
120
  getCredential,
121
+ hasRecoveryEnrolled,
122
+ hasStrongRecoveryEnrolled,
123
+ isMagicLinkGrantExpired,
90
124
  listCredentials,
91
- putCredential
92
- } from "./chunk-4PWAI7Q4.js";
125
+ listMagicLinkGrants,
126
+ loadPaperRecoveryEntries,
127
+ loadShamirRecoveryEntries,
128
+ magicLinkGrantRecordId,
129
+ mintPaperRecoveryEntry,
130
+ mintShamirRecoveryEntry,
131
+ mintWrappedDeksBlob,
132
+ putCredential,
133
+ readMagicLinkGrantRecord,
134
+ recoverPassphrase,
135
+ recoverUser,
136
+ removeAuthenticator,
137
+ revokeMagicLinkGrant,
138
+ rotatePassphrase,
139
+ savePaperRecoveryEntries,
140
+ saveShamirRecoveryEntries,
141
+ unwrapDeksFromBlob,
142
+ unwrapDeksFromPaperEntry,
143
+ unwrapDeksFromShamirEntry,
144
+ unwrapMagicLinkGrant,
145
+ updateAuthenticator,
146
+ writeMagicLinkGrant
147
+ } from "./chunk-CBAHB2BF.js";
148
+ import {
149
+ assertTierAccess,
150
+ dekKey,
151
+ effectiveClearance
152
+ } from "./chunk-DYBQG5PQ.js";
93
153
  import {
94
154
  PresenceHandle,
95
155
  SyncEngine,
96
156
  SyncTransaction
97
- } from "./chunk-AVVPZ4BC.js";
157
+ } from "./chunk-4TFSM22V.js";
98
158
  import {
159
+ DIRECTORY_RECORD_ID,
99
160
  USER_ENVELOPE_COLLECTION,
100
161
  USER_ENVELOPE_MAX_BYTES,
101
162
  UserEnvelopeOversizedError,
163
+ VISIBILITY_RECORD_PREFIX,
102
164
  WeakPassphraseError,
103
165
  assertStrongPassphrase,
104
166
  buildRecipientKeyringFile,
105
167
  changeSecret,
106
168
  createOwnerKeyring,
107
169
  deleteUserEnvelope,
170
+ deleteUserVisibility,
108
171
  ensureCollectionDEK,
109
172
  estimateEntropy,
110
173
  evaluateExportCapability,
@@ -119,13 +182,17 @@ import {
119
182
  listUsersWithEnvelopes,
120
183
  loadKeyring,
121
184
  loadUserEnvelope,
122
- persistKeyring,
185
+ persistDirectoryConfig,
186
+ persistUserVisibility,
187
+ readDirectoryConfig,
188
+ readUserVisibility,
123
189
  revoke,
124
190
  rotateKeys,
125
191
  saveUserEnvelope,
126
192
  updateKeyringIdentity,
127
- validatePassphrase
128
- } from "./chunk-WDM5XGGS.js";
193
+ validatePassphrase,
194
+ visibilityRecordId
195
+ } from "./chunk-PA6R5ZCI.js";
129
196
  import {
130
197
  BUNDLE_STORE_POLICY,
131
198
  INDEXED_STORE_POLICY,
@@ -145,7 +212,7 @@ import {
145
212
  revokeAllSessions,
146
213
  revokeSession,
147
214
  validateSessionPolicy
148
- } from "./chunk-VQBTTTUN.js";
215
+ } from "./chunk-KESP7GOK.js";
149
216
  import {
150
217
  generateULID,
151
218
  isULID
@@ -155,22 +222,22 @@ import {
155
222
  VaultInstant,
156
223
  diff,
157
224
  formatDiff
158
- } from "./chunk-NXFEYLVG.js";
225
+ } from "./chunk-MIQHZESA.js";
159
226
  import {
160
227
  LEDGER_COLLECTION,
161
228
  LEDGER_DELTAS_COLLECTION,
162
229
  LedgerStore,
163
230
  applyPatch,
164
231
  computePatch
165
- } from "./chunk-QAVUREFT.js";
232
+ } from "./chunk-UA4RI7OT.js";
166
233
  import {
167
234
  canonicalJson,
168
235
  envelopePayloadHash,
169
236
  hashEntry,
170
237
  paddedIndex,
171
238
  parseIndex,
172
- sha256Hex
173
- } from "./chunk-CIMZBAZB.js";
239
+ sha256Hex as sha256Hex2
240
+ } from "./chunk-2AXFIYHT.js";
174
241
  import {
175
242
  DEFAULT_JOIN_MAX_ROWS,
176
243
  NO_AGGREGATE,
@@ -180,29 +247,32 @@ import {
180
247
  buildLiveQuery,
181
248
  executePlan,
182
249
  resetJoinWarnings
183
- } from "./chunk-GOUT6DND.js";
250
+ } from "./chunk-23TTQXVO.js";
184
251
  import {
185
252
  CollectionIndexes
186
- } from "./chunk-NPC4LFV5.js";
253
+ } from "./chunk-YMYK7US4.js";
254
+ import {
255
+ avg,
256
+ count,
257
+ max,
258
+ min,
259
+ sum
260
+ } from "./chunk-5ZGZ6HIZ.js";
187
261
  import {
188
262
  Aggregation,
189
263
  GROUPBY_MAX_CARDINALITY,
190
264
  GROUPBY_WARN_CARDINALITY,
191
265
  GroupedAggregation,
192
266
  GroupedQuery,
193
- avg,
194
- count,
267
+ GroupedQueryN,
195
268
  groupAndReduce,
196
- max,
197
- min,
198
- reduceRecords,
199
- sum
200
- } from "./chunk-TDR6T5CJ.js";
269
+ reduceRecords
270
+ } from "./chunk-XGSOTWYX.js";
201
271
  import {
202
272
  evaluateClause,
203
273
  evaluateFieldClause,
204
274
  readPath
205
- } from "./chunk-M5INGEFC.js";
275
+ } from "./chunk-MRIBLZL3.js";
206
276
  import {
207
277
  BLOB_CHUNKS_COLLECTION,
208
278
  BLOB_COLLECTION,
@@ -217,58 +287,74 @@ import {
217
287
  detectMimeType,
218
288
  isPreCompressed,
219
289
  runCompaction
220
- } from "./chunk-2CSJGFCB.js";
290
+ } from "./chunk-VMIO4IXG.js";
221
291
  import {
222
292
  NOYDB_BACKUP_VERSION,
223
293
  NOYDB_FORMAT_VERSION,
224
294
  NOYDB_KEYRING_VERSION,
225
295
  NOYDB_SYNC_VERSION,
226
296
  createStore
227
- } from "./chunk-RKJ6OL7K.js";
297
+ } from "./chunk-YS3POABP.js";
228
298
  import {
229
299
  base64ToBuffer,
230
300
  bufferToBase64,
231
301
  decrypt,
232
302
  decryptBytes,
233
303
  decryptDeterministic,
234
- deriveKey,
235
304
  derivePresenceKey,
236
305
  encrypt,
237
306
  encryptBytes,
238
307
  encryptDeterministic,
239
- generateSalt,
240
- unwrapKey,
241
- wrapKey
242
- } from "./chunk-MR4424N3.js";
308
+ sha256Hex
309
+ } from "./chunk-WCA2NROQ.js";
243
310
  import {
244
311
  AlreadyElevatedError,
312
+ AmendmentForbiddenError,
245
313
  BackupCorruptedError,
246
314
  BackupLedgerError,
247
315
  BundleIntegrityError,
316
+ BundleSealMismatchError,
248
317
  BundleVersionConflictError,
249
318
  ConflictError,
250
319
  DanglingReferenceError,
251
320
  DecryptionError,
252
321
  DelegationTargetMissingError,
322
+ DerivationCapExceededError,
323
+ DerivationCycleError,
324
+ DerivationDepthError,
325
+ DerivationOutputShapeError,
326
+ DerivationOutputUnknownError,
253
327
  DictKeyInUseError,
254
328
  DictKeyMissingError,
329
+ DirectoryDisabledError,
255
330
  ElevationExpiredError,
256
331
  ExportCapabilityError,
332
+ FieldFrozenError,
257
333
  FilenameSanitizationError,
258
334
  GroupCardinalityError,
259
335
  ImportCapabilityError,
260
336
  IndexRequiredError,
261
337
  IndexWriteFailureError,
262
338
  InvalidKeyError,
339
+ InvariantError,
263
340
  JoinTooLargeError,
341
+ KeyringCorruptError,
264
342
  KeyringExpiredError,
265
343
  LedgerContentionError,
266
344
  LocaleNotSpecifiedError,
345
+ MaterializedViewConfigError,
346
+ MaterializedViewCycleError,
347
+ MaterializedViewSourceUnknownError,
348
+ MaterializedViewTooLargeError,
267
349
  MissingTranslationError,
268
350
  NetworkError,
269
351
  NoAccessError,
270
352
  NotFoundError,
271
353
  NoydbError,
354
+ OverlayBaseIsVirtualError,
355
+ OverlayCollectionUnavailableError,
356
+ OverlayIdMismatchError,
357
+ OverlayNameCollisionError,
272
358
  PathEscapeError,
273
359
  PeriodClosedError,
274
360
  PermissionDeniedError,
@@ -276,6 +362,7 @@ import {
276
362
  ReadOnlyAtInstantError,
277
363
  ReadOnlyError,
278
364
  ReadOnlyFrameError,
365
+ RecordLockedError,
279
366
  ReservedCollectionNameError,
280
367
  SchemaValidationError,
281
368
  SessionExpiredError,
@@ -287,7 +374,7 @@ import {
287
374
  TierNotGrantedError,
288
375
  TranslatorNotConfiguredError,
289
376
  ValidationError
290
- } from "./chunk-ACLDOTNQ.js";
377
+ } from "./chunk-ADQ5MQ54.js";
291
378
 
292
379
  // src/schema.ts
293
380
  async function validateSchemaInput(schema, value, context) {
@@ -327,6 +414,109 @@ function formatPath(path) {
327
414
  ).join(".");
328
415
  }
329
416
 
417
+ // src/persisted-schemas/canonicalize.ts
418
+ function canonicalize(value) {
419
+ if (value === null || typeof value !== "object") {
420
+ return JSON.stringify(value);
421
+ }
422
+ if (Array.isArray(value)) {
423
+ return "[" + value.map(canonicalize).join(",") + "]";
424
+ }
425
+ const obj = value;
426
+ const keys = Object.keys(obj).sort();
427
+ const parts = keys.map((k) => JSON.stringify(k) + ":" + canonicalize(obj[k]));
428
+ return "{" + parts.join(",") + "}";
429
+ }
430
+
431
+ // src/persisted-schemas/derive.ts
432
+ function isZodSchema(value) {
433
+ if (value === null || typeof value !== "object") return false;
434
+ const def = value._def;
435
+ if (!def || typeof def !== "object") return false;
436
+ return typeof def.typeName === "string" && def.typeName.startsWith("Zod");
437
+ }
438
+ function detectKind(validator) {
439
+ if (isZodSchema(validator)) return "Zod";
440
+ return "Unknown";
441
+ }
442
+ async function loadZodConverter() {
443
+ try {
444
+ const mod = await import("zod-to-json-schema");
445
+ if (!mod.zodToJsonSchema) {
446
+ throw new Error("zod-to-json-schema export missing");
447
+ }
448
+ return mod.zodToJsonSchema;
449
+ } catch (err) {
450
+ throw new Error(
451
+ `persistJsonSchema requires the optional peer-dep \`zod-to-json-schema\`. Install it: \`pnpm add zod-to-json-schema\` (or npm/yarn equivalent). Original error: ${err instanceof Error ? err.message : String(err)}`
452
+ );
453
+ }
454
+ }
455
+ async function derivePersistedSchema(validator) {
456
+ const kind = detectKind(validator);
457
+ const derivedAt = (/* @__PURE__ */ new Date()).toISOString();
458
+ if (kind === "Zod") {
459
+ const convert = await loadZodConverter();
460
+ const jsonSchema = convert(validator);
461
+ const canonical = canonicalize(jsonSchema);
462
+ const hash = await sha256Hex(new TextEncoder().encode(canonical));
463
+ return { _noydb_schema: 1, kind, jsonSchema, hash, derivedAt };
464
+ }
465
+ return {
466
+ _noydb_schema: 1,
467
+ kind,
468
+ jsonSchema: null,
469
+ hash: null,
470
+ reason: `derivation not yet supported for kind=${kind} (v0 supports Zod only)`,
471
+ derivedAt
472
+ };
473
+ }
474
+
475
+ // src/persisted-schemas/storage.ts
476
+ var SCHEMAS_COLLECTION = "_schemas";
477
+ async function loadPersistedSchema(store, vault, collection, dek) {
478
+ const envelope = await store.get(vault, SCHEMAS_COLLECTION, collection);
479
+ if (!envelope) return void 0;
480
+ try {
481
+ const plaintext = await decrypt(envelope._iv, envelope._data, dek);
482
+ const parsed = JSON.parse(plaintext);
483
+ if (parsed._noydb_schema !== 1) return void 0;
484
+ return parsed;
485
+ } catch {
486
+ return void 0;
487
+ }
488
+ }
489
+ async function savePersistedSchema(store, vault, collection, dek, payload) {
490
+ const json = JSON.stringify(payload);
491
+ const { iv, data } = await encrypt(json, dek);
492
+ const prior = await store.get(vault, SCHEMAS_COLLECTION, collection);
493
+ const env = {
494
+ _noydb: NOYDB_FORMAT_VERSION,
495
+ _v: (prior?._v ?? 0) + 1,
496
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
497
+ _iv: iv,
498
+ _data: data
499
+ };
500
+ await store.put(vault, SCHEMAS_COLLECTION, collection, env);
501
+ }
502
+
503
+ // src/persisted-schemas/register.ts
504
+ async function persistSchemaIfNeeded(opts) {
505
+ const fresh = await derivePersistedSchema(opts.validator);
506
+ const stored = await loadPersistedSchema(opts.store, opts.vault, opts.collectionName, opts.dek);
507
+ if (stored && isEquivalent(stored, fresh)) {
508
+ return { written: false, skipped: true, envelope: stored };
509
+ }
510
+ await savePersistedSchema(opts.store, opts.vault, opts.collectionName, opts.dek, fresh);
511
+ return { written: true, skipped: false, envelope: fresh };
512
+ }
513
+ function isEquivalent(a, b) {
514
+ if (a.kind !== b.kind) return false;
515
+ if (a.hash && b.hash) return a.hash === b.hash;
516
+ if (a.hash === null && b.hash === null) return true;
517
+ return false;
518
+ }
519
+
330
520
  // src/refs.ts
331
521
  var RefIntegrityError = class extends NoydbError {
332
522
  collection;
@@ -427,88 +617,6 @@ var RefRegistry = class {
427
617
  }
428
618
  };
429
619
 
430
- // src/team/authenticators.ts
431
- async function enrollAuthenticator(store, vault, keyring, options) {
432
- const existing = keyring.authenticators.find((a) => a.id === options.id);
433
- if (existing) {
434
- throw new ValidationError(
435
- `enrollAuthenticator: slot id "${options.id}" already exists in vault "${vault}". Remove the slot first or pick a unique id.`
436
- );
437
- }
438
- const base = {
439
- id: options.id,
440
- method: options.method,
441
- enrolled_at: (/* @__PURE__ */ new Date()).toISOString(),
442
- enrolled_via_tier: options.enrolled_via_tier ?? 1,
443
- meta: options.meta
444
- };
445
- const slot = options.wrapKind === "deks" ? {
446
- ...base,
447
- wrapKind: "deks",
448
- wrapped_deks: options.wrapped_deks,
449
- iv: options.iv
450
- } : {
451
- ...base,
452
- wrapped_kek: options.wrapped_kek
453
- };
454
- const next = appendSlot(keyring, slot);
455
- await persistKeyring(store, vault, next);
456
- return next;
457
- }
458
- async function updateAuthenticator(store, vault, keyring, slotId, options) {
459
- if (options.meta === void 0) {
460
- throw new ValidationError(
461
- `updateAuthenticator: at least one of meta must be provided (slotId: "${slotId}").`
462
- );
463
- }
464
- const idx = keyring.authenticators.findIndex((a) => a.id === slotId);
465
- if (idx === -1) {
466
- throw new NoAccessError(
467
- `updateAuthenticator: slot "${slotId}" not found in vault "${vault}".`
468
- );
469
- }
470
- const existing = keyring.authenticators[idx];
471
- const mergedMeta = { ...existing.meta };
472
- for (const [k, v] of Object.entries(options.meta)) {
473
- if (v === void 0) continue;
474
- if (v === null) {
475
- delete mergedMeta[k];
476
- continue;
477
- }
478
- mergedMeta[k] = v;
479
- }
480
- const next = { ...existing, meta: mergedMeta };
481
- const nextSlots = [...keyring.authenticators];
482
- nextSlots[idx] = next;
483
- const nextKeyring = {
484
- ...keyring,
485
- authenticators: nextSlots
486
- };
487
- await persistKeyring(store, vault, nextKeyring);
488
- return nextKeyring;
489
- }
490
- async function removeAuthenticator(store, vault, keyring, slotId) {
491
- const filtered = keyring.authenticators.filter((a) => a.id !== slotId);
492
- if (filtered.length === keyring.authenticators.length) {
493
- return keyring;
494
- }
495
- const next = {
496
- ...keyring,
497
- authenticators: filtered
498
- };
499
- await persistKeyring(store, vault, next);
500
- return next;
501
- }
502
- function findAuthenticator(keyring, slotId) {
503
- return keyring.authenticators.find((a) => a.id === slotId);
504
- }
505
- function appendSlot(keyring, slot) {
506
- return {
507
- ...keyring,
508
- authenticators: [...keyring.authenticators, slot]
509
- };
510
- }
511
-
512
620
  // src/session/unlock-state.ts
513
621
  var QuickUnlockStore = class {
514
622
  states = /* @__PURE__ */ new Map();
@@ -550,357 +658,6 @@ var QuickUnlockStore = class {
550
658
  }
551
659
  };
552
660
 
553
- // src/policy/errors.ts
554
- var PolicyDeniedError = class extends NoydbError {
555
- gate;
556
- reason;
557
- required;
558
- constructor(gate, reason, required, message) {
559
- super(
560
- "POLICY_DENIED",
561
- message ?? `Gate "${gate}" denied: ${reason}.`
562
- );
563
- this.name = "PolicyDeniedError";
564
- this.gate = gate;
565
- this.reason = reason;
566
- this.required = required;
567
- }
568
- };
569
- var RecoveryNotEnrolledError = class extends NoydbError {
570
- 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.') {
571
- super("RECOVERY_NOT_ENROLLED", message);
572
- this.name = "RecoveryNotEnrolledError";
573
- }
574
- };
575
- var RecoveryProfileNotImplementedError = class extends NoydbError {
576
- profile;
577
- tracking;
578
- constructor(profile, tracking) {
579
- super(
580
- "RECOVERY_PROFILE_NOT_IMPLEMENTED",
581
- `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.`
582
- );
583
- this.name = "RecoveryProfileNotImplementedError";
584
- this.profile = profile;
585
- this.tracking = tracking;
586
- }
587
- };
588
-
589
- // src/team/wrapped-deks.ts
590
- var PBKDF2_ITERATIONS = 6e5;
591
- var SALT_BYTES = 32;
592
- var IV_BYTES = 12;
593
- var subtle = globalThis.crypto.subtle;
594
- async function mintWrappedDeksBlob(deks, credential) {
595
- const salt = crypto.getRandomValues(new Uint8Array(SALT_BYTES));
596
- const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
597
- const wrappingKey = await deriveWrappingKey(credential, salt);
598
- const exported = {};
599
- for (const [coll, dek] of deks) {
600
- const raw = await subtle.exportKey("raw", dek);
601
- exported[coll] = bytesToBase64(new Uint8Array(raw));
602
- }
603
- const plaintext = new TextEncoder().encode(JSON.stringify({ deks: exported }));
604
- const ciphertext = await subtle.encrypt(
605
- { name: "AES-GCM", iv },
606
- wrappingKey,
607
- plaintext
608
- );
609
- return {
610
- salt: bytesToBase64(salt),
611
- iv: bytesToBase64(iv),
612
- wrappedDeks: bytesToBase64(new Uint8Array(ciphertext))
613
- };
614
- }
615
- async function unwrapDeksFromBlob(blob, credential) {
616
- const wrappingKey = await deriveWrappingKey(credential, base64ToBytes(blob.salt));
617
- const plaintext = await subtle.decrypt(
618
- { name: "AES-GCM", iv: base64ToBytes(blob.iv) },
619
- wrappingKey,
620
- base64ToBytes(blob.wrappedDeks)
621
- );
622
- const parsed = JSON.parse(new TextDecoder().decode(plaintext));
623
- const deks = /* @__PURE__ */ new Map();
624
- for (const [coll, b64] of Object.entries(parsed.deks)) {
625
- const raw = base64ToBytes(b64);
626
- const key = await subtle.importKey(
627
- "raw",
628
- raw,
629
- { name: "AES-GCM", length: 256 },
630
- true,
631
- ["encrypt", "decrypt"]
632
- );
633
- deks.set(coll, key);
634
- }
635
- return deks;
636
- }
637
- async function deriveWrappingKey(credential, salt) {
638
- const ikm = await subtle.importKey(
639
- "raw",
640
- new TextEncoder().encode(credential),
641
- "PBKDF2",
642
- false,
643
- ["deriveKey"]
644
- );
645
- return subtle.deriveKey(
646
- {
647
- name: "PBKDF2",
648
- salt,
649
- iterations: PBKDF2_ITERATIONS,
650
- hash: "SHA-256"
651
- },
652
- ikm,
653
- { name: "AES-GCM", length: 256 },
654
- false,
655
- ["encrypt", "decrypt"]
656
- );
657
- }
658
- function bytesToBase64(b) {
659
- let s = "";
660
- for (const x of b) s += String.fromCharCode(x);
661
- return btoa(s);
662
- }
663
- function base64ToBytes(b64) {
664
- const s = atob(b64);
665
- const out = new Uint8Array(s.length);
666
- for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
667
- return out;
668
- }
669
-
670
- // src/team/recovery.ts
671
- var PAPER_DOC_ID = "recovery-paper";
672
- async function loadPaperRecoveryEntries(store, vault) {
673
- const env = await store.get(vault, "_meta", PAPER_DOC_ID);
674
- if (!env) return [];
675
- try {
676
- const doc = JSON.parse(env._data);
677
- if (doc.profile !== "paper" || !Array.isArray(doc.entries)) return [];
678
- return doc.entries;
679
- } catch {
680
- return [];
681
- }
682
- }
683
- async function savePaperRecoveryEntries(store, vault, entries) {
684
- const doc = {
685
- _noydb_recovery: 1,
686
- profile: "paper",
687
- entries
688
- };
689
- const envelope = {
690
- _noydb: NOYDB_FORMAT_VERSION,
691
- _v: 1,
692
- _ts: (/* @__PURE__ */ new Date()).toISOString(),
693
- _iv: "",
694
- _data: JSON.stringify(doc)
695
- };
696
- await store.put(vault, "_meta", PAPER_DOC_ID, envelope);
697
- }
698
- async function burnPaperRecoveryEntry(store, vault, codeId) {
699
- const entries = await loadPaperRecoveryEntries(store, vault);
700
- const remaining = entries.filter((e) => e.codeId !== codeId);
701
- await savePaperRecoveryEntries(store, vault, remaining);
702
- }
703
- async function hasRecoveryEnrolled(store, vault) {
704
- const paper = await loadPaperRecoveryEntries(store, vault);
705
- return paper.length > 0;
706
- }
707
- async function mintPaperRecoveryEntry(deks, code, codeId) {
708
- const blob = await mintWrappedDeksBlob(deks, code);
709
- return {
710
- ...blob,
711
- codeId,
712
- enrolledAt: (/* @__PURE__ */ new Date()).toISOString()
713
- };
714
- }
715
- async function unwrapDeksFromPaperEntry(entry, code) {
716
- return unwrapDeksFromBlob(entry, code);
717
- }
718
-
719
- // src/team/rotate-recover.ts
720
- async function rotatePassphrase(store, vault, userId, input) {
721
- if (!input.allowWeakPassphrase) {
722
- assertStrongPassphrase(input.newPassphrase, input.passphrasePolicy);
723
- }
724
- const env = await store.get(vault, "_keyring", userId);
725
- if (!env) {
726
- throw new NoAccessError(`No keyring found for user "${userId}" in vault "${vault}".`);
727
- }
728
- const file = JSON.parse(env._data);
729
- const oldSalt = base64ToBuffer(file.salt);
730
- const oldKek = await deriveKey(input.oldPassphrase, oldSalt);
731
- const deks = /* @__PURE__ */ new Map();
732
- for (const [coll, wrapped] of Object.entries(file.deks)) {
733
- deks.set(coll, await unwrapKey(wrapped, oldKek));
734
- }
735
- const newSalt = generateSalt();
736
- const newKek = await deriveKey(input.newPassphrase, newSalt);
737
- const wrappedDeks = {};
738
- for (const [coll, dek] of deks) {
739
- wrappedDeks[coll] = await wrapKey(dek, newKek);
740
- }
741
- const oldSlots = file.authenticators ?? [];
742
- const newSlots = [];
743
- if (input.slotCeremonies && oldSlots.length > 0) {
744
- for (const oldSlot of oldSlots) {
745
- const ceremony = input.slotCeremonies[oldSlot.id];
746
- if (!ceremony) continue;
747
- const result = await ceremony({ newKek, newDeks: deks, oldSlot });
748
- if (result.id !== oldSlot.id) {
749
- throw new ValidationError(
750
- `slotCeremonies['${oldSlot.id}'] returned id="${result.id}". The id must match the rotated slot \u2014 a ceremony cannot change a slot's identity.`
751
- );
752
- }
753
- if (result.method !== oldSlot.method) {
754
- throw new ValidationError(
755
- `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.`
756
- );
757
- }
758
- const baseFields = {
759
- id: result.id,
760
- method: result.method,
761
- // Preserve original enrolled_at — rotation is rewrapping, not
762
- // re-enrollment. The slot's enrolment timestamp tracks when
763
- // the user originally added the slot, not when it was last
764
- // rewrapped. Forensics consumers reading enrolled_at are
765
- // tracking the slot's ORIGIN, not its CURRENT wrapping.
766
- enrolled_at: oldSlot.enrolled_at,
767
- enrolled_via_tier: result.enrolled_via_tier ?? oldSlot.enrolled_via_tier,
768
- meta: result.meta
769
- };
770
- const newSlot = result.wrapKind === "deks" ? {
771
- ...baseFields,
772
- wrapKind: "deks",
773
- wrapped_deks: result.wrapped_deks,
774
- iv: result.iv
775
- } : {
776
- ...baseFields,
777
- wrapped_kek: result.wrapped_kek
778
- };
779
- newSlots.push(newSlot);
780
- }
781
- }
782
- const next = {
783
- ...file,
784
- _noydb_keyring: NOYDB_KEYRING_VERSION,
785
- deks: wrappedDeks,
786
- salt: bufferToBase64(newSalt),
787
- authenticators: newSlots
788
- };
789
- await writeKeyringFile(store, vault, userId, next);
790
- return {
791
- userId: file.user_id,
792
- displayName: file.display_name,
793
- role: file.role,
794
- permissions: file.permissions,
795
- deks,
796
- kek: newKek,
797
- salt: newSalt,
798
- authenticators: newSlots,
799
- ...file.export_capability !== void 0 && { exportCapability: file.export_capability },
800
- ...file.import_capability !== void 0 && { importCapability: file.import_capability }
801
- };
802
- }
803
- async function recoverPassphrase(store, vault, userId, input) {
804
- if (!input.allowWeakPassphrase) {
805
- assertStrongPassphrase(input.newPassphrase, input.passphrasePolicy);
806
- }
807
- switch (input.recoveryProof.profile) {
808
- case "paper":
809
- return recoverViaPaperCode(store, vault, userId, input);
810
- case "shamir":
811
- throw new RecoveryProfileNotImplementedError(
812
- "shamir",
813
- "https://github.com/vLannaAi/noy-db/issues/10"
814
- );
815
- case "multi-channel":
816
- throw new RecoveryProfileNotImplementedError(
817
- "multi-channel",
818
- "https://github.com/vLannaAi/noy-db/issues/10"
819
- );
820
- case "admin-mediated":
821
- throw new RecoveryProfileNotImplementedError(
822
- "admin-mediated",
823
- "https://github.com/vLannaAi/noy-db/issues/10"
824
- );
825
- default: {
826
- const _exhaustive = input.recoveryProof;
827
- throw new Error(`Unknown recovery profile: ${String(_exhaustive)}`);
828
- }
829
- }
830
- }
831
- async function recoverViaPaperCode(store, vault, userId, input) {
832
- if (input.recoveryProof.profile !== "paper") throw new Error("unreachable");
833
- const { code } = input.recoveryProof.payload;
834
- const env = await store.get(vault, "_keyring", userId);
835
- if (!env) {
836
- throw new NoAccessError(`No keyring found for user "${userId}" in vault "${vault}".`);
837
- }
838
- const file = JSON.parse(env._data);
839
- const entries = await loadPaperRecoveryEntries(store, vault);
840
- if (entries.length === 0) {
841
- throw new NoAccessError(
842
- `No paper-recovery entries enrolled for vault "${vault}". Enroll via \`db.enrollRecovery({ profile: "paper", entries })\` before relying on recovery.`
843
- );
844
- }
845
- const normalized = normalizePaperCode(code);
846
- let recovered;
847
- for (const entry of entries) {
848
- try {
849
- const deks2 = await unwrapDeksFromPaperEntry(entry, normalized);
850
- recovered = { deks: deks2, entry };
851
- break;
852
- } catch {
853
- }
854
- }
855
- if (!recovered) {
856
- throw new InvalidKeyError(
857
- "Recovery code does not match any enrolled paper entry. The code may have been previously used (single-use) or typed incorrectly."
858
- );
859
- }
860
- const deks = recovered.deks;
861
- const newSalt = generateSalt();
862
- const newKek = await deriveKey(input.newPassphrase, newSalt);
863
- const wrappedDeks = {};
864
- for (const [coll, dek] of deks) {
865
- wrappedDeks[coll] = await wrapKey(dek, newKek);
866
- }
867
- const next = {
868
- ...file,
869
- _noydb_keyring: NOYDB_KEYRING_VERSION,
870
- deks: wrappedDeks,
871
- salt: bufferToBase64(newSalt),
872
- authenticators: []
873
- // tier-2 slots wrap old KEK, drop them
874
- };
875
- await writeKeyringFile(store, vault, userId, next);
876
- await burnPaperRecoveryEntry(store, vault, recovered.entry.codeId);
877
- return {
878
- userId: file.user_id,
879
- displayName: file.display_name,
880
- role: file.role,
881
- permissions: file.permissions,
882
- deks,
883
- kek: newKek,
884
- salt: newSalt,
885
- authenticators: [],
886
- ...file.export_capability !== void 0 && { exportCapability: file.export_capability },
887
- ...file.import_capability !== void 0 && { importCapability: file.import_capability }
888
- };
889
- }
890
- function normalizePaperCode(input) {
891
- return input.toUpperCase().replace(/[\s\-_]/g, "");
892
- }
893
- async function writeKeyringFile(store, vault, userId, file) {
894
- const envelope = {
895
- _noydb: 1,
896
- _v: 1,
897
- _ts: (/* @__PURE__ */ new Date()).toISOString(),
898
- _iv: "",
899
- _data: JSON.stringify(file)
900
- };
901
- await store.put(vault, "_keyring", userId, envelope);
902
- }
903
-
904
661
  // src/meta/user-envelope/api.ts
905
662
  var UserApi = class {
906
663
  constructor(adapter, vaultName, writerKeyringId, getDek, checkGate2) {
@@ -984,6 +741,41 @@ var UserApi = class {
984
741
  this.fireChange(this.writerKeyringId, written);
985
742
  return written;
986
743
  }
744
+ // ─── Visibility (#122) ───────────────────────────────────────────────
745
+ /**
746
+ * Read the current user's visibility flag from
747
+ * `_meta/visibility/<keyringId>`. Returns `{ hidden: false }` when no
748
+ * document has been persisted (the default-visible case).
749
+ */
750
+ async getMyVisibility() {
751
+ const persisted = await readUserVisibility(this.adapter, this.vaultName, this.writerKeyringId);
752
+ return persisted ?? { hidden: false };
753
+ }
754
+ /**
755
+ * Update the current user's visibility in the team directory.
756
+ *
757
+ * - `hidden: true` — opt out of the default `listUsersWithEnvelopes`
758
+ * listing. `owner`/`admin` callers can still see the user by passing
759
+ * `{ includeHidden: true }`.
760
+ * - `hidden: false` — opt back in.
761
+ *
762
+ * Own-only by construction: the keyringId argument doesn't exist on
763
+ * this method, so no caller can hide or unhide another principal.
764
+ *
765
+ * Honest caveat: this is a UX flag, not a privacy guarantee. The
766
+ * envelope ciphertext at `_users/<keyringId>` and the keyring file at
767
+ * `_keyring/<userId>` are both still observable to anyone with direct
768
+ * store read access. See `docs/subsystems/user-envelope.md` →
769
+ * "Directory visibility".
770
+ */
771
+ async setMyVisibility(visibility) {
772
+ await persistUserVisibility(
773
+ this.adapter,
774
+ this.vaultName,
775
+ this.writerKeyringId,
776
+ { hidden: visibility.hidden }
777
+ );
778
+ }
987
779
  // ─── Read-anyone ─────────────────────────────────────────────────────
988
780
  /**
989
781
  * Read another principal's envelope by their keyringId. Returns null
@@ -1176,7 +968,7 @@ async function describeAuthConfig(store, vault) {
1176
968
  lines.push(` Phrase format: ${policy.passphrase?.minWords ?? 6}+ words, lowercase letters, \u2265${policy.passphrase?.minWordLength ?? 3} chars/word`);
1177
969
  lines.push(" Strength validator: enforced (override available for tests only)");
1178
970
  lines.push("");
1179
- lines.push("Tier 2 \u2014 Authenticate (daily login)");
971
+ lines.push("Tier 2 \u2014 Authenticate (routine login)");
1180
972
  lines.push(" Allowed methods: WebAuthn (passkey), OIDC, Password");
1181
973
  lines.push(" Slots per user: unlimited");
1182
974
  lines.push("");
@@ -1288,68 +1080,131 @@ function sanitizeId(s) {
1288
1080
  return s.replace(/[^a-zA-Z0-9]/g, "_");
1289
1081
  }
1290
1082
 
1291
- // src/team/peer-recover.ts
1292
- var ADMIN_RECOVERABLE_TARGETS = ["operator", "viewer", "client", "admin"];
1293
- function canRecover(callerRole, targetRole) {
1294
- if (callerRole === "owner") return true;
1295
- if (callerRole === "admin") return ADMIN_RECOVERABLE_TARGETS.includes(targetRole);
1296
- return false;
1297
- }
1298
- async function recoverUser(store, vault, callerKeyring, options) {
1299
- const env = await store.get(vault, "_keyring", options.userId);
1300
- if (!env) {
1301
- throw new NoAccessError(
1302
- `recoverUser: user "${options.userId}" has no keyring in vault "${vault}".`
1303
- );
1304
- }
1305
- const target = JSON.parse(env._data);
1306
- const targetRole = options.role ?? target.role;
1307
- if (!canRecover(callerKeyring.role, targetRole)) {
1308
- throw new PermissionDeniedError(
1309
- `Role "${callerKeyring.role}" cannot recover role "${targetRole}"`
1310
- );
1083
+ // src/team/managed-passphrase.ts
1084
+ var MemorySealingKeyProvider = class {
1085
+ id;
1086
+ fingerprint;
1087
+ keyBytes;
1088
+ constructor(opts) {
1089
+ this.id = opts.id;
1090
+ const encoded = new TextEncoder().encode(opts.id);
1091
+ let h = 0;
1092
+ for (let i = 0; i < encoded.length; i++) {
1093
+ h = h * 31 + encoded[i] >>> 0;
1094
+ }
1095
+ this.fingerprint = new Uint8Array([
1096
+ h >>> 24 & 255,
1097
+ h >>> 16 & 255,
1098
+ h >>> 8 & 255,
1099
+ h & 255
1100
+ ]);
1101
+ this.keyBytes = new Uint8Array(16);
1102
+ for (let i = 0; i < 16; i++) {
1103
+ this.keyBytes[i] = this.fingerprint[i % 4] ^ i * 17;
1104
+ }
1311
1105
  }
1312
- if (!canRecover(callerKeyring.role, target.role)) {
1313
- throw new PermissionDeniedError(
1314
- `Role "${callerKeyring.role}" cannot recover role "${target.role}"`
1315
- );
1106
+ async seal(passphrase) {
1107
+ const out = new Uint8Array(4 + passphrase.length);
1108
+ out.set(this.fingerprint, 0);
1109
+ for (let i = 0; i < passphrase.length; i++) {
1110
+ out[4 + i] = passphrase[i] ^ this.keyBytes[i % 16];
1111
+ }
1112
+ return out;
1316
1113
  }
1317
- for (const coll of Object.keys(target.deks)) {
1318
- if (!callerKeyring.deks.has(coll)) {
1319
- throw new PrivilegeEscalationError(coll);
1114
+ async unseal(sealed) {
1115
+ if (sealed.length < 4) {
1116
+ throw new Error("MemorySealingKeyProvider: sealed input too short");
1320
1117
  }
1118
+ for (let i = 0; i < 4; i++) {
1119
+ if (sealed[i] !== this.fingerprint[i]) {
1120
+ throw new Error(
1121
+ `MemorySealingKeyProvider("${this.id}"): provider-id mismatch on unseal (sealed bytes were produced by a different provider)`
1122
+ );
1123
+ }
1124
+ }
1125
+ const body = sealed.subarray(4);
1126
+ const out = new Uint8Array(body.length);
1127
+ for (let i = 0; i < body.length; i++) {
1128
+ out[i] = body[i] ^ this.keyBytes[i % 16];
1129
+ }
1130
+ return out;
1321
1131
  }
1322
- if (options.validatePassphrase && !options.allowWeakPassphrase) {
1323
- assertStrongPassphrase(options.passphrase, options.passphrasePolicy);
1132
+ };
1133
+ var SEALED_PASSPHRASE_RECORD_ID = "sealed-passphrase";
1134
+ function bytesToBase64(bytes) {
1135
+ let binary = "";
1136
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
1137
+ return btoa(binary);
1138
+ }
1139
+ function base64ToBytes(b64) {
1140
+ const binary = atob(b64);
1141
+ const out = new Uint8Array(binary.length);
1142
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
1143
+ return out;
1144
+ }
1145
+ function parseSealedEnvelope(raw) {
1146
+ if (typeof raw !== "object" || raw === null) return void 0;
1147
+ const r = raw;
1148
+ if (r._noydb_sealed !== 1) return void 0;
1149
+ if (r.v === 1 && typeof r.pid === "string" && typeof r.payload === "string") {
1150
+ return {
1151
+ _noydb_sealed: 1,
1152
+ providerId: r.pid,
1153
+ sealed: base64ToBytes(r.payload)
1154
+ };
1324
1155
  }
1325
- const newSalt = generateSalt();
1326
- const newKek = await deriveKey(options.passphrase, newSalt);
1327
- const wrappedDeks = {};
1328
- for (const coll of Object.keys(target.deks)) {
1329
- const callerDek = callerKeyring.deks.get(coll);
1330
- if (!callerDek) {
1331
- throw new PrivilegeEscalationError(coll);
1332
- }
1333
- wrappedDeks[coll] = await wrapKey(callerDek, newKek);
1156
+ if (typeof r.providerId === "string" && typeof r.sealed === "string") {
1157
+ return {
1158
+ _noydb_sealed: 1,
1159
+ providerId: r.providerId,
1160
+ sealed: base64ToBytes(r.sealed)
1161
+ };
1334
1162
  }
1335
- const next = {
1336
- ...target,
1337
- _noydb_keyring: NOYDB_KEYRING_VERSION,
1338
- role: targetRole,
1339
- display_name: options.displayName ?? target.display_name,
1340
- deks: wrappedDeks,
1341
- salt: bufferToBase64(newSalt),
1342
- granted_by: callerKeyring.userId,
1343
- authenticators: []
1163
+ return void 0;
1164
+ }
1165
+ async function saveSealedPassphrase(store, vault, payload) {
1166
+ const persisted = {
1167
+ v: 1,
1168
+ _noydb_sealed: 1,
1169
+ pid: payload.providerId,
1170
+ payload: bytesToBase64(payload.sealed)
1344
1171
  };
1345
- const envelope = {
1346
- _noydb: 1,
1347
- _v: 1,
1172
+ const prior = await store.get(vault, "_meta", SEALED_PASSPHRASE_RECORD_ID);
1173
+ const env = {
1174
+ _noydb: NOYDB_FORMAT_VERSION,
1175
+ _v: (prior?._v ?? 0) + 1,
1348
1176
  _ts: (/* @__PURE__ */ new Date()).toISOString(),
1177
+ // AES-GCM bypassed — the sealing layer is the security boundary.
1349
1178
  _iv: "",
1350
- _data: JSON.stringify(next)
1179
+ _data: JSON.stringify(persisted)
1351
1180
  };
1352
- await store.put(vault, "_keyring", options.userId, envelope);
1181
+ await store.put(vault, "_meta", SEALED_PASSPHRASE_RECORD_ID, env);
1182
+ }
1183
+ async function loadSealedPassphrase(store, vault) {
1184
+ const envelope = await store.get(vault, "_meta", SEALED_PASSPHRASE_RECORD_ID);
1185
+ if (!envelope) return void 0;
1186
+ try {
1187
+ return parseSealedEnvelope(JSON.parse(envelope._data));
1188
+ } catch {
1189
+ return void 0;
1190
+ }
1191
+ }
1192
+ async function resolveManagedSecret(store, vault, provider) {
1193
+ const existing = await loadSealedPassphrase(store, vault);
1194
+ if (existing) {
1195
+ if (existing.providerId !== provider.id) {
1196
+ throw new Error(
1197
+ `Managed-mode vault "${vault}" was sealed under provider id "${existing.providerId}" but the current SealingKeyProvider is "${provider.id}". Pass the same provider that originally enrolled the vault, or treat this as a fresh enrollment and clear \`_meta/sealed-passphrase\` first.`
1198
+ );
1199
+ }
1200
+ const plaintext = await provider.unseal(existing.sealed);
1201
+ return bytesToBase64(plaintext);
1202
+ }
1203
+ const random = new Uint8Array(32);
1204
+ globalThis.crypto.getRandomValues(random);
1205
+ const sealed = await provider.seal(random);
1206
+ await saveSealedPassphrase(store, vault, { providerId: provider.id, sealed });
1207
+ return bytesToBase64(random);
1353
1208
  }
1354
1209
 
1355
1210
  // src/crdt/strategy.ts
@@ -1625,6 +1480,79 @@ var NO_BLOBS = {
1625
1480
  }
1626
1481
  };
1627
1482
 
1483
+ // src/derivations/stale.ts
1484
+ var _staleByRegistry = /* @__PURE__ */ new WeakMap();
1485
+ var keyFor = (source, sourceId) => `${source}/${sourceId}`;
1486
+ async function markStale(registry, strategy, sourceId) {
1487
+ let map = _staleByRegistry.get(registry);
1488
+ if (!map) {
1489
+ map = /* @__PURE__ */ new Map();
1490
+ _staleByRegistry.set(registry, map);
1491
+ }
1492
+ const k = keyFor(strategy.source, sourceId);
1493
+ let set = map.get(k);
1494
+ if (!set) {
1495
+ set = /* @__PURE__ */ new Set();
1496
+ map.set(k, set);
1497
+ }
1498
+ set.add(strategy);
1499
+ }
1500
+ async function resolveStaleOnRead(accessor, outputCollection, id) {
1501
+ const registry = accessor.registry();
1502
+ const producers = registry.strategiesProducingOutput(outputCollection);
1503
+ if (producers.length === 0) return;
1504
+ const map = _staleByRegistry.get(registry);
1505
+ if (!map) return;
1506
+ let DerivationExecutor = null;
1507
+ for (const { spec, strategyHash } of producers) {
1508
+ const k = keyFor(spec.source, id);
1509
+ const pending = map.get(k);
1510
+ if (!pending || !pending.has(spec)) continue;
1511
+ const sourceColl = accessor.getCollection(spec.source);
1512
+ const source = await sourceColl.get(id);
1513
+ if (!source) {
1514
+ pending.delete(spec);
1515
+ continue;
1516
+ }
1517
+ const sourceWithId = { ...source, id };
1518
+ if (DerivationExecutor === null) {
1519
+ ({ DerivationExecutor } = await import("./executor-X4SQ3ZLC.js"));
1520
+ }
1521
+ const ctx = { vault: accessor.getReadOnlyFacade() };
1522
+ const result = await DerivationExecutor.run(spec, sourceWithId, 0, strategyHash, ctx);
1523
+ for (const key of Object.keys(spec.outputs)) {
1524
+ const out = result.outputs[key];
1525
+ if (!out) continue;
1526
+ if (out.kind === "failed") {
1527
+ const err = out.error;
1528
+ if (spec.strict) {
1529
+ throw err;
1530
+ }
1531
+ console.warn(
1532
+ `[derivation] lazy output "${key}" for source "${spec.source}" id="${id}" failed:`,
1533
+ err
1534
+ );
1535
+ continue;
1536
+ }
1537
+ if (out.kind === "array") {
1538
+ console.warn(
1539
+ `[derivation] unexpected array-shape output "${key}" in lazy resolve path; array-shape derivations require lifecycle: "eager" (#200 slice 1).`
1540
+ );
1541
+ continue;
1542
+ }
1543
+ const outSpec = spec.outputs[key];
1544
+ if (!outSpec) continue;
1545
+ const outputColl = accessor.getCollection(outSpec.collection);
1546
+ if (out.skipped === true) {
1547
+ await outputColl._internalDelete(id, accessor.getActiveTxContext());
1548
+ continue;
1549
+ }
1550
+ await outputColl.put(id, out.value);
1551
+ }
1552
+ pending.delete(spec);
1553
+ }
1554
+ }
1555
+
1628
1556
  // src/collection.ts
1629
1557
  var fallbackWarned = /* @__PURE__ */ new Set();
1630
1558
  function warnOnceFallback(adapterName) {
@@ -1632,7 +1560,7 @@ function warnOnceFallback(adapterName) {
1632
1560
  fallbackWarned.add(adapterName);
1633
1561
  if (typeof process !== "undefined" && process.env["NODE_ENV"] === "test") return;
1634
1562
  console.warn(
1635
- `[noy-db] Adapter "${adapterName}" does not implement listPage(); Collection.scan()/listPage() are using a synthetic fallback (slower). Add a listPage method to opt into the streaming fast path.`
1563
+ `[noy-db] Store "${adapterName}" does not implement listPage(); Collection.scan()/listPage() are using a synthetic fallback (slower). Add a listPage method to opt into the streaming fast path.`
1636
1564
  );
1637
1565
  }
1638
1566
  var Collection = class {
@@ -1842,6 +1770,34 @@ var Collection = class {
1842
1770
  * adapter on first use.
1843
1771
  */
1844
1772
  periodGuard;
1773
+ /**
1774
+ * Optional back-reference to the owning vault's guard registry + a
1775
+ * read-only vault facade. When present, `Collection.put` and
1776
+ * `Collection.delete` consult the registry for guards declared
1777
+ * against this collection and run their `check` + `frozenFields`
1778
+ * before the adapter write. Absent in unit tests that construct
1779
+ * a Collection directly; production code always sets it via
1780
+ * `Vault.collection()`.
1781
+ *
1782
+ * Typed structurally rather than as `Vault` to avoid a circular
1783
+ * import (mirrors the `refEnforcer` / `joinResolver` pattern).
1784
+ */
1785
+ guardSource;
1786
+ /**
1787
+ * Vault-internal hook for derivation dispatch. When set,
1788
+ * `Collection.put` consults the registry after the source-write
1789
+ * commits and writes derived outputs through `getCollection(name).put`.
1790
+ * Same structural-interface pattern as `guardSource` to avoid a
1791
+ * circular Vault import.
1792
+ */
1793
+ derivationSource;
1794
+ /**
1795
+ * Vault-internal hook for materialized-view dispatch (#143/#150).
1796
+ * Parallel to `derivationSource` — when set, `Collection.put` fires
1797
+ * `MaterializedViewRegistry.onSourceWrite` after the source-write
1798
+ * commits + after `dispatchDerivations` has run.
1799
+ */
1800
+ materializedViewSource;
1845
1801
  /**
1846
1802
  * Optional back-reference to the owning compartment's ref
1847
1803
  * enforcer. When present, `Collection.put` calls
@@ -1912,6 +1868,9 @@ var Collection = class {
1912
1868
  this.syncAdapter = opts.syncAdapter;
1913
1869
  this.onAccess = opts.onAccess;
1914
1870
  this.periodGuard = opts.periodGuard;
1871
+ this.guardSource = opts.guardSource;
1872
+ this.derivationSource = opts.derivationSource;
1873
+ this.materializedViewSource = opts.materializedViewSource;
1915
1874
  this.tiers = opts.tiers && opts.tiers.length > 0 ? new Set(opts.tiers) : null;
1916
1875
  this.tierMode = opts.tierMode ?? "invisibility";
1917
1876
  this.onCrossTierAccess = opts.onCrossTierAccess;
@@ -2036,6 +1995,16 @@ var Collection = class {
2036
1995
  * `null` if not found.
2037
1996
  */
2038
1997
  async get(id, locale) {
1998
+ if (this.derivationSource !== void 0) {
1999
+ const registry = this.derivationSource.registry();
2000
+ if (registry.strategiesProducingOutput(this.name).length > 0) {
2001
+ await resolveStaleOnRead(this.derivationSource, this.name, id);
2002
+ }
2003
+ }
2004
+ if (this.materializedViewSource !== void 0) {
2005
+ const { resolveStaleMVOnRead } = await import("./stale-HSC5YO2O.js");
2006
+ await resolveStaleMVOnRead(this.materializedViewSource, this.name);
2007
+ }
2039
2008
  let record;
2040
2009
  if (this.lazy && this.lru) {
2041
2010
  const cached = this.lru.get(id);
@@ -2099,11 +2068,53 @@ var Collection = class {
2099
2068
  if (opts?.pollIntervalMs !== void 0) presenceOpts.pollIntervalMs = opts.pollIntervalMs;
2100
2069
  return this.syncStrategy.buildPresence(presenceOpts);
2101
2070
  }
2102
- /** Create or update a record. */
2103
- async put(id, record) {
2071
+ /**
2072
+ * Create or update a record.
2073
+ *
2074
+ * @param id Record identifier.
2075
+ * @param record The record body (validated by the collection's schema
2076
+ * if one was attached at `vault.collection(...)` time).
2077
+ * @param options Optional metadata for audit + import workflows.
2078
+ * `reason` is stamped onto the resulting ledger entry
2079
+ * (see #1) so audit consumers can filter via
2080
+ * `entries.filter(e => e.reason?.startsWith('import:'))`.
2081
+ */
2082
+ async put(id, record, options) {
2104
2083
  if (!hasWritePermission(this.keyring, this.name)) {
2105
2084
  throw new ReadOnlyError();
2106
2085
  }
2086
+ if (this.guardSource) {
2087
+ const registry = this.guardSource.registry();
2088
+ const guards = registry.guardsFor(this.name);
2089
+ if (guards.length > 0) {
2090
+ const existingEnv = await this.adapter.get(this.vault, this.name, id);
2091
+ let existingRecord = null;
2092
+ if (existingEnv) {
2093
+ try {
2094
+ existingRecord = await this.decryptRecord(existingEnv, { skipValidation: true });
2095
+ } catch {
2096
+ existingRecord = null;
2097
+ }
2098
+ }
2099
+ const incomingRecord = record;
2100
+ const ctx = {
2101
+ existing: existingRecord,
2102
+ vault: this.guardSource.readOnlyVault(),
2103
+ userId: this.keyring.userId,
2104
+ role: this.keyring.role
2105
+ };
2106
+ if (registry.isAmendmentActive()) {
2107
+ const vBefore = existingEnv?._v ?? 0;
2108
+ registry.collectChange(this.name, id, existingRecord, incomingRecord, vBefore, vBefore + 1);
2109
+ } else {
2110
+ await registry.runChecks(this.name, incomingRecord, ctx);
2111
+ const { GuardExecutor } = await import("./executor-CEWX2FQI.js");
2112
+ for (const g of guards) {
2113
+ await GuardExecutor.checkFrozenFields(g, id, existingRecord, incomingRecord);
2114
+ }
2115
+ }
2116
+ }
2117
+ }
2107
2118
  if (this.periodGuard !== void 0) {
2108
2119
  const existingEnv = await this.adapter.get(this.vault, this.name, id);
2109
2120
  let priorRecord = null;
@@ -2212,6 +2223,7 @@ var Collection = class {
2212
2223
  payloadHash: await this.historyStrategy.envelopePayloadHash(envelope2)
2213
2224
  };
2214
2225
  if (existingResolved) appendInput.delta = this.historyStrategy.computePatch(resolvedRecord, existingResolved.record);
2226
+ if (options?.reason !== void 0) appendInput.reason = options.reason;
2215
2227
  await this.ledger.append(appendInput);
2216
2228
  }
2217
2229
  if (this.lazy && this.lru) {
@@ -2229,6 +2241,8 @@ var Collection = class {
2229
2241
  await this.onDirty?.(this.name, id, "put", version2);
2230
2242
  this.emitter.emit("change", { vault: this.vault, collection: this.name, id, action: "put" });
2231
2243
  await this.onAccess?.("put", id);
2244
+ await this.dispatchDerivations(id, record, version2);
2245
+ await this.dispatchMaterializedViews(id, record);
2232
2246
  return;
2233
2247
  }
2234
2248
  let existing;
@@ -2275,6 +2289,7 @@ var Collection = class {
2275
2289
  if (existing) {
2276
2290
  appendInput.delta = this.historyStrategy.computePatch(record, existing.record);
2277
2291
  }
2292
+ if (options?.reason !== void 0) appendInput.reason = options.reason;
2278
2293
  await this.ledger.append(appendInput);
2279
2294
  }
2280
2295
  if (this.lazy && this.lru) {
@@ -2292,13 +2307,256 @@ var Collection = class {
2292
2307
  action: "put"
2293
2308
  });
2294
2309
  await this.onAccess?.("put", id);
2310
+ await this.dispatchDerivations(id, record, version);
2311
+ await this.dispatchMaterializedViews(id, record);
2312
+ }
2313
+ /**
2314
+ * Fire registered MV strategies whose dependency set includes this
2315
+ * collection. Eager-mode MVs re-materialize inline via
2316
+ * `MaterializedViewExecutor.refresh`; lazy / manual modes are
2317
+ * no-ops in the foundation (subtask #150) — wired in #151.
2318
+ *
2319
+ * Skips entirely when the record being written is itself an
2320
+ * MV-emitted row (carries `_materializedFrom`) — defensive guard
2321
+ * against missed cycle detection.
2322
+ *
2323
+ * @internal
2324
+ */
2325
+ async dispatchMaterializedViews(id, record) {
2326
+ void id;
2327
+ if (this.materializedViewSource === void 0) return;
2328
+ const incoming = record;
2329
+ if (incoming && typeof incoming === "object" && "_materializedFrom" in incoming) return;
2330
+ const registry = this.materializedViewSource.registry();
2331
+ const mvs = registry.mvsForSource(this.name);
2332
+ if (mvs.length === 0) return;
2333
+ let executor = null;
2334
+ let staleHelpers = null;
2335
+ for (const reg of mvs) {
2336
+ const mode = reg.spec.refresh;
2337
+ if (mode === "eager") {
2338
+ if (executor === null) {
2339
+ ;
2340
+ ({ MaterializedViewExecutor: executor } = await import("./executor-7E3VFGW7.js"));
2341
+ }
2342
+ await executor.refresh(reg, {
2343
+ getCollection: (name) => this.materializedViewSource.getCollection(name),
2344
+ getActiveTxContext: () => this.materializedViewSource.getActiveTxContext(),
2345
+ getQueryContext: () => this.materializedViewSource.getQueryContext()
2346
+ });
2347
+ } else if (mode === "lazy") {
2348
+ if (staleHelpers === null) {
2349
+ staleHelpers = await import("./stale-HSC5YO2O.js");
2350
+ }
2351
+ staleHelpers.markMVStale(registry, reg.spec.name);
2352
+ }
2353
+ }
2354
+ }
2355
+ /**
2356
+ * Fire registered derivation strategies for this source collection.
2357
+ * Eager mode runs `derive` inline and writes each output via the
2358
+ * sibling `Collection.put`; lazy mode marks dependent outputs stale
2359
+ * (D11 stub today). Errors in non-strict mode are logged and
2360
+ * skipped; strict mode propagates the first failing output's error.
2361
+ *
2362
+ * Skips entirely when the record being written is itself a derived
2363
+ * output (carries `_derivedFrom`) — defensive guard against missed
2364
+ * cycle detection.
2365
+ */
2366
+ async dispatchDerivations(id, record, version) {
2367
+ if (this.derivationSource === void 0) return;
2368
+ const incoming = record;
2369
+ if (incoming && typeof incoming === "object" && "_derivedFrom" in incoming) return;
2370
+ const registry = this.derivationSource.registry();
2371
+ const strategies = registry.strategiesForSource(this.name);
2372
+ if (strategies.length === 0) return;
2373
+ let DerivationExecutor = null;
2374
+ for (const { spec, strategyHash } of strategies) {
2375
+ const mode = typeof spec.lifecycle === "string" ? spec.lifecycle : spec.lifecycle.mode;
2376
+ if (mode === "eager") {
2377
+ if (DerivationExecutor === null) {
2378
+ ({ DerivationExecutor } = await import("./executor-X4SQ3ZLC.js"));
2379
+ }
2380
+ const sourceWithId = { ...incoming, id };
2381
+ const ctx = { vault: this.derivationSource.getReadOnlyFacade() };
2382
+ const result = await DerivationExecutor.run(spec, sourceWithId, version, strategyHash, ctx);
2383
+ for (const key of Object.keys(spec.outputs)) {
2384
+ const out = result.outputs[key];
2385
+ if (!out) continue;
2386
+ if (out.kind === "failed") {
2387
+ const err = out.error;
2388
+ if (spec.strict) throw err;
2389
+ console.warn(`[derivation] output "${key}" for source "${spec.source}" id="${id}" failed:`, err);
2390
+ continue;
2391
+ }
2392
+ const outSpec = spec.outputs[key];
2393
+ if (!outSpec) continue;
2394
+ const outputCollection = this.derivationSource.getCollection(outSpec.collection);
2395
+ const txCtx = this.derivationSource.getActiveTxContext();
2396
+ if (out.kind === "array") {
2397
+ const { loadFanoutSidecar, saveFanoutSidecar } = await import("./fanout-sidecar-VJ52RIEY.js");
2398
+ const prior = await loadFanoutSidecar(
2399
+ this.adapter,
2400
+ this.vault,
2401
+ spec.source,
2402
+ id,
2403
+ key
2404
+ );
2405
+ const prevKeys = new Set(prior?.keys ?? []);
2406
+ const newKeysList = out.entries.map((e) => e.key);
2407
+ const newKeysSet = new Set(newKeysList);
2408
+ for (const k of prevKeys) {
2409
+ if (newKeysSet.has(k)) continue;
2410
+ await outputCollection._internalDelete(k, txCtx);
2411
+ }
2412
+ for (const entry of out.entries) {
2413
+ if (txCtx !== null) {
2414
+ const priorEnvelope = await this.adapter.get(this.vault, outSpec.collection, entry.key);
2415
+ txCtx._executed.push({
2416
+ op: {
2417
+ type: "put",
2418
+ vaultName: this.vault,
2419
+ collectionName: outSpec.collection,
2420
+ id: entry.key
2421
+ },
2422
+ priorEnvelope
2423
+ });
2424
+ }
2425
+ await outputCollection.put(entry.key, entry.value);
2426
+ }
2427
+ await saveFanoutSidecar(this.adapter, this.vault, {
2428
+ source: spec.source,
2429
+ sourceId: id,
2430
+ outputKey: key,
2431
+ outputCollection: outSpec.collection,
2432
+ keys: newKeysList
2433
+ });
2434
+ continue;
2435
+ }
2436
+ if (out.skipped === true) {
2437
+ await outputCollection._internalDelete(id, txCtx);
2438
+ continue;
2439
+ }
2440
+ if (txCtx !== null) {
2441
+ const prior = await this.adapter.get(this.vault, outSpec.collection, id);
2442
+ txCtx._executed.push({
2443
+ op: {
2444
+ type: "put",
2445
+ vaultName: this.vault,
2446
+ collectionName: outSpec.collection,
2447
+ id
2448
+ },
2449
+ priorEnvelope: prior
2450
+ });
2451
+ }
2452
+ await outputCollection.put(id, out.value);
2453
+ }
2454
+ } else {
2455
+ await markStale(registry, spec, id);
2456
+ }
2457
+ }
2295
2458
  }
2296
2459
  /** Delete a record by ID. */
2297
2460
  async delete(id) {
2461
+ await this._doDelete(id, false);
2462
+ }
2463
+ /**
2464
+ * @internal — system-internal delete that bypasses user-facing
2465
+ * delete hooks (`onDelete`, accounting-period guard, FK ref
2466
+ * enforcer). Used by derivation tombstones (#144) and MV refresh
2467
+ * (Dim 14 v2) — system housekeeping shouldn't trip user invariants
2468
+ * registered against the output collection. The ledger entry and
2469
+ * history snapshot still fire so backup integrity and time-travel
2470
+ * reconstruction stay consistent.
2471
+ *
2472
+ * Returns silently for delete-of-absent (idempotent contract — both
2473
+ * paths honour this: the `txCtx === null` path also reads the prior
2474
+ * envelope and short-circuits before the ledger/event side-effects).
2475
+ *
2476
+ * When a `txCtx` is supplied, the prior envelope is captured and
2477
+ * pushed onto `txCtx._executed` BEFORE the delete fires — mirrors
2478
+ * the #133 rollback hardening for puts. Callers outside a
2479
+ * multi-record transaction pass `null` and skip the tracking.
2480
+ *
2481
+ * Amendment composition: if `_internalDelete` runs while a vault's
2482
+ * `GuardRegistry` has an amendment window open, the `{before, after:
2483
+ * null}` change pair is pushed onto the amendment change-set the
2484
+ * same way a user-initiated delete would. The `onDelete` user-hook
2485
+ * is still skipped (housekeeping must not trip user invariants in
2486
+ * normal mode), but the amendment's invariant DOES see the change
2487
+ * — so a `RCT-CANCEL-001`-style invariant pairing can reject a
2488
+ * derivation-driven tombstone fired during an admin amendment.
2489
+ *
2490
+ * Constraint to surface to consumers: output collections of
2491
+ * derivations with `optional: true` outputs should not be the
2492
+ * targets of `strict` or `cascade` inbound foreign-key refs —
2493
+ * `_internalDelete` bypasses the ref enforcer by design (the
2494
+ * `onDelete` bypass primitive). Treat the housekeeping path as
2495
+ * "system can tombstone its own emissions regardless of FK shape."
2496
+ *
2497
+ * Permission handling is unchanged: the caller must still hold
2498
+ * write permission on the collection (derivations run under the
2499
+ * user's keyring).
2500
+ */
2501
+ async _internalDelete(id, txCtx = null) {
2502
+ const prior = await this.adapter.get(this.vault, this.name, id);
2503
+ if (prior === null) return;
2504
+ if (txCtx !== null) {
2505
+ txCtx._executed.push({
2506
+ op: {
2507
+ type: "delete",
2508
+ vaultName: this.vault,
2509
+ collectionName: this.name,
2510
+ id
2511
+ },
2512
+ priorEnvelope: prior
2513
+ });
2514
+ }
2515
+ await this._doDelete(id, true);
2516
+ }
2517
+ async _doDelete(id, internal) {
2298
2518
  if (!hasWritePermission(this.keyring, this.name)) {
2299
2519
  throw new ReadOnlyError();
2300
2520
  }
2301
- if (this.periodGuard !== void 0) {
2521
+ if (this.guardSource) {
2522
+ const registry = this.guardSource.registry();
2523
+ const guards = registry.guardsFor(this.name);
2524
+ if (guards.length > 0) {
2525
+ const existingEnv = await this.adapter.get(this.vault, this.name, id);
2526
+ if (existingEnv) {
2527
+ let existingRecord = null;
2528
+ try {
2529
+ existingRecord = await this.decryptRecord(existingEnv, { skipValidation: true });
2530
+ } catch {
2531
+ existingRecord = null;
2532
+ }
2533
+ if (registry.isAmendmentActive()) {
2534
+ const vBefore = existingEnv._v;
2535
+ registry.collectChange(
2536
+ this.name,
2537
+ id,
2538
+ existingRecord,
2539
+ null,
2540
+ vBefore,
2541
+ vBefore
2542
+ );
2543
+ } else if (!internal) {
2544
+ const ctx = {
2545
+ existing: existingRecord,
2546
+ vault: this.guardSource.readOnlyVault(),
2547
+ userId: this.keyring.userId,
2548
+ role: this.keyring.role
2549
+ };
2550
+ await registry.runOnDelete(
2551
+ this.name,
2552
+ existingRecord ?? {},
2553
+ ctx
2554
+ );
2555
+ }
2556
+ }
2557
+ }
2558
+ }
2559
+ if (!internal && this.periodGuard !== void 0) {
2302
2560
  const existingEnv = await this.adapter.get(this.vault, this.name, id);
2303
2561
  let priorRecord = null;
2304
2562
  if (existingEnv) {
@@ -2313,7 +2571,7 @@ var Collection = class {
2313
2571
  null
2314
2572
  );
2315
2573
  }
2316
- if (this.refEnforcer !== void 0) {
2574
+ if (!internal && this.refEnforcer !== void 0) {
2317
2575
  await this.refEnforcer.enforceRefsOnDelete(this.name, id);
2318
2576
  }
2319
2577
  let existing;
@@ -2365,6 +2623,87 @@ var Collection = class {
2365
2623
  action: "delete"
2366
2624
  });
2367
2625
  await this.onAccess?.("delete", id);
2626
+ if (!internal) {
2627
+ await this.dispatchMaterializedViewsOnDelete(id);
2628
+ await this.dispatchArrayDerivationsOnDelete(id);
2629
+ }
2630
+ }
2631
+ /**
2632
+ * Cascade deletes of array-shape derived rows when a source row is
2633
+ * deleted (#200). Reads each registered strategy's fanout sidecar
2634
+ * for this source id, deletes every listed derived row, then
2635
+ * deletes the sidecar itself.
2636
+ *
2637
+ * Record-shape derivations are skipped — see _doDelete's comment
2638
+ * for why the asymmetry is correct.
2639
+ *
2640
+ * @internal
2641
+ */
2642
+ async dispatchArrayDerivationsOnDelete(id) {
2643
+ if (this.derivationSource === void 0) return;
2644
+ const registry = this.derivationSource.registry();
2645
+ const strategies = registry.strategiesForSource(this.name);
2646
+ if (strategies.length === 0) return;
2647
+ let helpers = null;
2648
+ const txCtx = this.derivationSource.getActiveTxContext();
2649
+ for (const { spec } of strategies) {
2650
+ for (const [outputKey, outSpec] of Object.entries(spec.outputs)) {
2651
+ if (outSpec.shape !== "array") continue;
2652
+ if (helpers === null) {
2653
+ helpers = await import("./fanout-sidecar-VJ52RIEY.js");
2654
+ }
2655
+ const sidecar = await helpers.loadFanoutSidecar(
2656
+ this.adapter,
2657
+ this.vault,
2658
+ spec.source,
2659
+ id,
2660
+ outputKey
2661
+ );
2662
+ if (!sidecar) continue;
2663
+ const outputCollection = this.derivationSource.getCollection(outSpec.collection);
2664
+ for (const derivedId of sidecar.keys) {
2665
+ await outputCollection._internalDelete(derivedId, txCtx);
2666
+ }
2667
+ await helpers.deleteFanoutSidecar(this.adapter, this.vault, spec.source, id, outputKey);
2668
+ }
2669
+ }
2670
+ }
2671
+ /**
2672
+ * Mirror of {@link dispatchMaterializedViews} for the delete path
2673
+ * (#181). No record content is available (it's gone), so the
2674
+ * `_materializedFrom` skip used by the put-side dispatch doesn't
2675
+ * apply here — instead, the recursion guard is the `internal` gate
2676
+ * at the `_doDelete` call site above.
2677
+ *
2678
+ * @internal
2679
+ */
2680
+ async dispatchMaterializedViewsOnDelete(id) {
2681
+ void id;
2682
+ if (this.materializedViewSource === void 0) return;
2683
+ const registry = this.materializedViewSource.registry();
2684
+ const mvs = registry.mvsForSource(this.name);
2685
+ if (mvs.length === 0) return;
2686
+ let executor = null;
2687
+ let staleHelpers = null;
2688
+ for (const reg of mvs) {
2689
+ const mode = reg.spec.refresh;
2690
+ if (mode === "eager") {
2691
+ if (executor === null) {
2692
+ ;
2693
+ ({ MaterializedViewExecutor: executor } = await import("./executor-7E3VFGW7.js"));
2694
+ }
2695
+ await executor.refresh(reg, {
2696
+ getCollection: (name) => this.materializedViewSource.getCollection(name),
2697
+ getActiveTxContext: () => this.materializedViewSource.getActiveTxContext(),
2698
+ getQueryContext: () => this.materializedViewSource.getQueryContext()
2699
+ });
2700
+ } else if (mode === "lazy") {
2701
+ if (staleHelpers === null) {
2702
+ staleHelpers = await import("./stale-HSC5YO2O.js");
2703
+ }
2704
+ staleHelpers.markMVStale(registry, reg.spec.name);
2705
+ }
2706
+ }
2368
2707
  }
2369
2708
  /**
2370
2709
  * List all records in the collection.
@@ -2382,6 +2721,10 @@ var Collection = class {
2382
2721
  `Collection "${this.name}": list() is not available in lazy mode (prefetch: false). Use collection.scan({ pageSize }) to iterate over the full collection.`
2383
2722
  );
2384
2723
  }
2724
+ if (this.materializedViewSource !== void 0) {
2725
+ const { resolveStaleMVOnRead } = await import("./stale-HSC5YO2O.js");
2726
+ await resolveStaleMVOnRead(this.materializedViewSource, this.name);
2727
+ }
2385
2728
  await this.ensureHydrated();
2386
2729
  const records = [...this.cache.values()].map((e) => e.record);
2387
2730
  if (!locale) return records;
@@ -2456,22 +2799,42 @@ var Collection = class {
2456
2799
  }
2457
2800
  }
2458
2801
  }
2459
- const executed = [];
2802
+ const txCtx = this.derivationSource?.createTxContext() ?? null;
2803
+ if (txCtx !== null && this.derivationSource) {
2804
+ this.derivationSource.setActiveTxContext(txCtx);
2805
+ }
2806
+ const localExecuted = [];
2460
2807
  try {
2461
2808
  for (const [id, record] of entries) {
2809
+ const entry = {
2810
+ op: { type: "put", vaultName: this.vault, collectionName: this.name, id },
2811
+ priorEnvelope: priors.get(id) ?? null
2812
+ };
2813
+ if (txCtx !== null) txCtx._executed.push(entry);
2814
+ else localExecuted.push(entry);
2462
2815
  await this.put(id, record);
2463
- executed.push({ id, prior: priors.get(id) ?? null });
2464
2816
  }
2465
- return { ok: true, success: executed.map((e) => e.id), failures: [] };
2817
+ return { ok: true, success: entries.map(([id]) => id), failures: [] };
2466
2818
  } catch (err) {
2467
- for (const { id, prior } of executed.slice().reverse()) {
2819
+ const executedForRevert = txCtx !== null ? txCtx._executed : localExecuted;
2820
+ await revertExecuted(executedForRevert, this.adapter);
2821
+ for (const { op } of [...executedForRevert].reverse()) {
2822
+ if (op.vaultName !== this.vault) continue;
2468
2823
  try {
2469
- if (prior) await this.adapter.put(this.vault, this.name, id, prior);
2470
- else await this.adapter.delete(this.vault, this.name, id);
2824
+ if (op.collectionName === this.name) {
2825
+ await this._invalidateCacheEntry(op.id);
2826
+ } else if (this.derivationSource) {
2827
+ const sibling = this.derivationSource.getCollection(op.collectionName);
2828
+ await sibling._invalidateCacheEntry(op.id);
2829
+ }
2471
2830
  } catch {
2472
2831
  }
2473
2832
  }
2474
2833
  throw err;
2834
+ } finally {
2835
+ if (txCtx !== null && this.derivationSource) {
2836
+ this.derivationSource.clearActiveTxContext(txCtx);
2837
+ }
2475
2838
  }
2476
2839
  }
2477
2840
  /**
@@ -2837,6 +3200,11 @@ var Collection = class {
2837
3200
  * .aggregate({ total: sum('amount'), n: count() })
2838
3201
  * ```
2839
3202
  *
3203
+ * **Lazy-MV gap (#157):** `scan()` is synchronous-build and does
3204
+ * NOT trigger lazy materialized-view resolve-on-read. For lazy
3205
+ * MVs, call `list()` (which DOES resolve) or `vault.refreshView(name)`
3206
+ * before scanning. Same shape as the `query()` limitation.
3207
+ *
2840
3208
  * Returns a `ScanBuilder<T>` instead of the raw async iterator
2841
3209
  * that previous versions used. The builder implements
2842
3210
  * `AsyncIterable<T>`, so every existing `for await … of` call
@@ -2880,6 +3248,38 @@ var Collection = class {
2880
3248
  }
2881
3249
  // ─── Internal ──────────────────────────────────────────────────
2882
3250
  /** Load all records from adapter into memory cache. */
3251
+ /**
3252
+ * @internal — refresh the in-memory cache entry for a single id by
3253
+ * re-reading from the adapter. Used by the transaction executor's
3254
+ * Phase-3 revert path: that path writes the prior envelope directly
3255
+ * via the raw store (to avoid re-firing Collection-level side
3256
+ * effects), which would otherwise leave this Collection's eager
3257
+ * cache holding the rolled-back value. After revert, the executor
3258
+ * calls this hook so subsequent `get` / `query` reads see the
3259
+ * actual on-disk state.
3260
+ *
3261
+ * Lazy mode: drops the LRU entry; the next `get` repopulates from
3262
+ * the adapter. Eager mode: re-reads the envelope and either sets
3263
+ * the cache entry (record still present) or deletes it (record was
3264
+ * gone before the tx and the revert deleted it again).
3265
+ */
3266
+ async _invalidateCacheEntry(id) {
3267
+ if (this.lazy && this.lru) {
3268
+ this.lru.remove(id);
3269
+ return;
3270
+ }
3271
+ if (!this.hydrated) return;
3272
+ const previous = this.cache.get(id);
3273
+ const envelope = await this.adapter.get(this.vault, this.name, id);
3274
+ if (!envelope) {
3275
+ this.cache.delete(id);
3276
+ if (previous) this.indexes?.remove(id, previous.record);
3277
+ return;
3278
+ }
3279
+ const record = await this.decryptRecord(envelope);
3280
+ this.cache.set(id, { record, version: envelope._v });
3281
+ this.indexes?.upsert(id, record, previous ? previous.record : null);
3282
+ }
2883
3283
  async ensureHydrated() {
2884
3284
  if (this.hydrated) return;
2885
3285
  const ids = await this.adapter.list(this.vault, this.name);
@@ -3851,97 +4251,246 @@ var NO_PERIODS = {
3851
4251
  }
3852
4252
  };
3853
4253
 
3854
- // src/team/magic-link-grant.ts
3855
- var MAGIC_LINK_GRANTS_COLLECTION = "_magic_link_grants";
3856
- var MAGIC_LINK_CONTENT_INFO_PREFIX = "noydb-magic-link-content-v1:";
3857
- var MAGIC_LINK_KEK_INFO_PREFIX = "noydb-magic-link-v1:";
3858
- async function deriveMagicLinkContentKey(serverSecret, token, vault) {
3859
- const subtle2 = globalThis.crypto.subtle;
3860
- const ikmBytes = serverSecret instanceof Uint8Array ? serverSecret : new TextEncoder().encode(serverSecret);
3861
- const tokenBytes = new TextEncoder().encode(token);
3862
- const saltBuffer = await subtle2.digest("SHA-256", tokenBytes);
3863
- const info = new TextEncoder().encode(MAGIC_LINK_CONTENT_INFO_PREFIX + vault);
3864
- const ikm = await subtle2.importKey("raw", ikmBytes, "HKDF", false, ["deriveKey"]);
3865
- return subtle2.deriveKey(
3866
- { name: "HKDF", hash: "SHA-256", salt: saltBuffer, info },
3867
- ikm,
3868
- { name: "AES-GCM", length: 256 },
3869
- false,
3870
- ["encrypt", "decrypt"]
3871
- );
4254
+ // src/introspection/fields.ts
4255
+ function jsonSchemaType(node) {
4256
+ if (Array.isArray(node.type)) {
4257
+ const non = node.type.filter((t) => t !== "null");
4258
+ return non[0] ?? "opaque";
4259
+ }
4260
+ if (node.enum && Array.isArray(node.enum)) return "enum";
4261
+ if (typeof node.type === "string") return node.type;
4262
+ return "opaque";
3872
4263
  }
3873
- async function writeMagicLinkGrant(store, vault, grantor, contentKey, grantKek, recordId, opts) {
3874
- const collectionName = opts.collection ?? null;
3875
- const sourceKey = collectionName ? dekKey(collectionName, opts.tier) : `__any#${opts.tier}`;
3876
- const sourceDek = grantor.deks.get(sourceKey);
3877
- if (!sourceDek) {
3878
- throw new DelegationTargetMissingError(
3879
- `grantor cannot find tier ${opts.tier} DEK for ${collectionName ?? "(any)"}`
3880
- );
4264
+ function constraintsFor(node) {
4265
+ const out = {};
4266
+ if (node.enum) out.values = node.enum;
4267
+ if (node.minLength !== void 0) out.minLength = node.minLength;
4268
+ if (node.maxLength !== void 0) out.maxLength = node.maxLength;
4269
+ if (node.pattern !== void 0) out.pattern = node.pattern;
4270
+ if (node.format !== void 0) out.format = node.format;
4271
+ if (node.minimum !== void 0) out.minimum = node.minimum;
4272
+ if (node.maximum !== void 0) out.maximum = node.maximum;
4273
+ if (node.exclusiveMinimum !== void 0) out.gt = node.exclusiveMinimum;
4274
+ if (node.exclusiveMaximum !== void 0) out.lt = node.exclusiveMaximum;
4275
+ return Object.keys(out).length === 0 ? void 0 : out;
4276
+ }
4277
+ function jsonSchemaToFields(jsonSchema, source, refs) {
4278
+ if (!jsonSchema || typeof jsonSchema !== "object") return {};
4279
+ const root = jsonSchema;
4280
+ if (!root.properties || typeof root.properties !== "object") return {};
4281
+ const required = new Set(Array.isArray(root.required) ? root.required : []);
4282
+ const out = {};
4283
+ for (const [name, node] of Object.entries(root.properties)) {
4284
+ const descriptor = {
4285
+ type: jsonSchemaType(node),
4286
+ source,
4287
+ ...required.has(name) ? {} : { optional: true },
4288
+ ...refs?.[name] ? { references: `${refs[name].target}.id` } : {}
4289
+ };
4290
+ const constraints = constraintsFor(node);
4291
+ if (constraints) descriptor.constraints = constraints;
4292
+ out[name] = descriptor;
3881
4293
  }
3882
- const wrappedDek = await wrapKey(sourceDek, grantKek);
3883
- const until = typeof opts.until === "string" ? opts.until : opts.until.toISOString();
3884
- const createdAt = (/* @__PURE__ */ new Date()).toISOString();
3885
- const payload = {
3886
- id: recordId,
3887
- toUser: opts.toUser,
3888
- fromUser: grantor.userId,
3889
- tier: opts.tier,
3890
- collection: collectionName,
3891
- ...opts.record && { record: opts.record },
3892
- until,
3893
- wrappedDek,
3894
- createdAt,
3895
- ...opts.note && { note: opts.note }
3896
- };
3897
- const { iv, data } = await encrypt(JSON.stringify(payload), contentKey);
3898
- const envelope = {
3899
- _noydb: 1,
3900
- _v: 1,
3901
- _ts: createdAt,
3902
- _iv: iv,
3903
- _data: data,
3904
- _by: grantor.userId
4294
+ return out;
4295
+ }
4296
+
4297
+ // src/introspection/walk.ts
4298
+ var INTERNAL_PREFIX = "_";
4299
+ var KNOWN_INTERNAL_NAMES = ["_keyring", "_ledger", "_meta", "_schemas", "_deltas"];
4300
+ async function dumpVaultSchema(vault, opts) {
4301
+ const state = vault._introspectState();
4302
+ const sampleSize = opts.sampleSize ?? 50;
4303
+ const withStats = opts.withStats === true;
4304
+ const cacheNames = [...state.collectionCache.keys()];
4305
+ const storageNames = (await safeListAllCollections(state.adapter, state.name)).filter((n) => !n.startsWith(INTERNAL_PREFIX));
4306
+ const allNames = Array.from(/* @__PURE__ */ new Set([...cacheNames, ...storageNames])).sort();
4307
+ const collections = {};
4308
+ for (const name of allNames) {
4309
+ collections[name] = await describeCollection(state, name, sampleSize, withStats);
4310
+ }
4311
+ const materializedViews = describeMVs(state.mvRegistry);
4312
+ const overlayViews = describeOverlays(state.overlayRegistry);
4313
+ const derivations = describeDerivations(state.derivationRegistry);
4314
+ let internal;
4315
+ if (withStats) {
4316
+ internal = {};
4317
+ for (const name of KNOWN_INTERNAL_NAMES) {
4318
+ const stats = await statsForCollection(state.adapter, state.name, name);
4319
+ if (stats.records > 0) {
4320
+ internal[name] = { records: stats.records, bytes: stats.bytes };
4321
+ }
4322
+ }
4323
+ }
4324
+ const snap = {
4325
+ _noydb_snapshot: 1,
4326
+ vault: state.name,
4327
+ emittedAt: (/* @__PURE__ */ new Date()).toISOString(),
4328
+ subsystems: state.subsystems,
4329
+ collections,
4330
+ materializedViews,
4331
+ overlayViews,
4332
+ derivations,
4333
+ ...internal !== void 0 ? { internal } : {}
3905
4334
  };
3906
- await store.put(vault, MAGIC_LINK_GRANTS_COLLECTION, recordId, envelope);
3907
- return { recordId, payload };
4335
+ return snap;
3908
4336
  }
3909
- async function readMagicLinkGrantRecord(store, vault, contentKey, recordId) {
3910
- const env = await store.get(vault, MAGIC_LINK_GRANTS_COLLECTION, recordId);
3911
- if (!env) return null;
4337
+ async function safeListAllCollections(adapter, vault) {
3912
4338
  try {
3913
- const json = await decrypt(env._iv, env._data, contentKey);
3914
- return JSON.parse(json);
4339
+ const snap = await adapter.loadAll(vault);
4340
+ return Object.keys(snap);
3915
4341
  } catch {
3916
- return null;
4342
+ return [];
4343
+ }
4344
+ }
4345
+ async function describeCollection(state, collectionName, sampleSize, withStats) {
4346
+ let fields = {};
4347
+ let validator;
4348
+ const refsRaw = state.refRegistry.getOutbound(collectionName);
4349
+ const refs = {};
4350
+ for (const [name, desc] of Object.entries(refsRaw)) {
4351
+ refs[name] = { target: desc.target, mode: desc.mode };
4352
+ }
4353
+ try {
4354
+ const dek = await state.getDEK(collectionName);
4355
+ const persisted = await loadPersistedSchema(state.adapter, state.name, collectionName, dek);
4356
+ if (persisted) {
4357
+ validator = { kind: persisted.kind, source: "persisted" };
4358
+ if (persisted.jsonSchema) {
4359
+ fields = jsonSchemaToFields(persisted.jsonSchema, "persisted", refsRaw);
4360
+ }
4361
+ }
4362
+ } catch {
4363
+ }
4364
+ if (!validator) {
4365
+ const coll = state.collectionCache.get(collectionName);
4366
+ const schema = coll?.getSchema();
4367
+ if (schema) {
4368
+ try {
4369
+ const derived = await derivePersistedSchema(schema);
4370
+ validator = { kind: derived.kind, source: "live-validator" };
4371
+ if (derived.jsonSchema) {
4372
+ fields = jsonSchemaToFields(derived.jsonSchema, "live-validator", refsRaw);
4373
+ }
4374
+ } catch {
4375
+ }
4376
+ }
4377
+ }
4378
+ if (Object.keys(fields).length === 0 && sampleSize > 0) {
4379
+ }
4380
+ const descriptor = {
4381
+ fields,
4382
+ indexes: [],
4383
+ refs,
4384
+ ...validator ? { validator } : {}
4385
+ };
4386
+ if (withStats) {
4387
+ const stats = await statsForCollection(state.adapter, state.name, collectionName);
4388
+ descriptor.stats = stats;
3917
4389
  }
4390
+ return descriptor;
4391
+ }
4392
+ async function statsForCollection(adapter, vault, collection) {
4393
+ const ids = await adapter.list(vault, collection);
4394
+ if (ids.length === 0) {
4395
+ return { records: 0, bytes: 0, bytesAvg: 0, bytesMin: 0, bytesMax: 0, oldest: "", newest: "" };
4396
+ }
4397
+ let total = 0;
4398
+ let min2 = Number.POSITIVE_INFINITY;
4399
+ let max2 = 0;
4400
+ let oldest = "\uFFFF";
4401
+ let newest = "";
4402
+ for (const id of ids) {
4403
+ const env = await adapter.get(vault, collection, id);
4404
+ if (!env) continue;
4405
+ const size = env._data.length;
4406
+ total += size;
4407
+ if (size < min2) min2 = size;
4408
+ if (size > max2) max2 = size;
4409
+ if (env._ts < oldest) oldest = env._ts;
4410
+ if (env._ts > newest) newest = env._ts;
4411
+ }
4412
+ return {
4413
+ records: ids.length,
4414
+ bytes: total,
4415
+ bytesAvg: Math.round(total / ids.length),
4416
+ bytesMin: min2 === Number.POSITIVE_INFINITY ? 0 : min2,
4417
+ bytesMax: max2,
4418
+ oldest: oldest === "\uFFFF" ? "" : oldest,
4419
+ newest
4420
+ };
3918
4421
  }
3919
- async function listMagicLinkGrants(store, vault, contentKey, token) {
3920
- const ids = await store.list(vault, MAGIC_LINK_GRANTS_COLLECTION);
3921
- const matching = ids.filter((id) => id === token || id.startsWith(`${token}:`));
3922
- const out = [];
3923
- for (const id of matching) {
3924
- const payload = await readMagicLinkGrantRecord(store, vault, contentKey, id);
3925
- if (payload) out.push(payload);
4422
+ function describeMVs(registry) {
4423
+ if (!registry || typeof registry !== "object") return {};
4424
+ const items = listFromRegistry(registry);
4425
+ const out = {};
4426
+ for (const item of items) {
4427
+ const reg = item;
4428
+ const spec = reg.spec;
4429
+ if (!spec?.name) continue;
4430
+ const sources = spec.unionSources ? spec.unionSources.map((u) => u.collection) : reg.dependencies ? [...reg.dependencies].sort() : [];
4431
+ const groupBy = spec.groupBy ? Array.isArray(spec.groupBy) ? [...spec.groupBy] : [spec.groupBy] : void 0;
4432
+ const aggregate = spec.aggregate ? Object.fromEntries(
4433
+ Object.entries(spec.aggregate).map(([k, v]) => [k, summariseAggregateOp(v)])
4434
+ ) : void 0;
4435
+ out[spec.name] = {
4436
+ sources,
4437
+ ...groupBy ? { groupBy } : {},
4438
+ ...aggregate ? { aggregate } : {},
4439
+ refresh: spec.refresh ?? "eager"
4440
+ };
3926
4441
  }
3927
4442
  return out;
3928
4443
  }
3929
- async function unwrapMagicLinkGrant(payload, grantKek) {
3930
- return unwrapKey(payload.wrappedDek, grantKek);
4444
+ function describeOverlays(registry) {
4445
+ if (!registry || typeof registry !== "object") return {};
4446
+ const specs = listFromRegistry(registry);
4447
+ const out = {};
4448
+ for (const spec of specs) {
4449
+ const s = spec;
4450
+ if (!s.name || !s.base || !s.overlay) continue;
4451
+ out[s.name] = { base: s.base, overlay: s.overlay };
4452
+ }
4453
+ return out;
3931
4454
  }
3932
- async function revokeMagicLinkGrant(store, vault, token) {
3933
- const ids = await store.list(vault, MAGIC_LINK_GRANTS_COLLECTION);
3934
- const matching = ids.filter((id) => id === token || id.startsWith(`${token}:`));
3935
- for (const id of matching) {
3936
- await store.delete(vault, MAGIC_LINK_GRANTS_COLLECTION, id);
4455
+ function describeDerivations(registry) {
4456
+ if (!registry || typeof registry !== "object") return {};
4457
+ const specs = listFromRegistry(registry);
4458
+ const out = {};
4459
+ for (const spec of specs) {
4460
+ const s = spec;
4461
+ if (!s.name) continue;
4462
+ out[s.name] = {
4463
+ source: s.source ?? "",
4464
+ outputs: s.outputs ?? []
4465
+ };
3937
4466
  }
3938
- return matching.length;
4467
+ return out;
3939
4468
  }
3940
- function magicLinkGrantRecordId(token, index) {
3941
- return index === 0 ? token : `${token}:${index}`;
4469
+ function listFromRegistry(reg) {
4470
+ for (const method of ["all", "list", "specs", "values"]) {
4471
+ const fn = reg[method];
4472
+ if (typeof fn === "function") {
4473
+ try {
4474
+ const out = fn.call(reg);
4475
+ if (Array.isArray(out)) return out;
4476
+ if (out && typeof out.values === "function") {
4477
+ return [...out.values()];
4478
+ }
4479
+ } catch {
4480
+ continue;
4481
+ }
4482
+ }
4483
+ }
4484
+ return [];
3942
4485
  }
3943
- function isMagicLinkGrantExpired(payload, now = /* @__PURE__ */ new Date()) {
3944
- return payload.until <= now.toISOString();
4486
+ function summariseAggregateOp(value) {
4487
+ if (value && typeof value === "object") {
4488
+ const op = value.op ?? value.kind;
4489
+ const field = value.field;
4490
+ if (op && field) return `${op}(${field})`;
4491
+ if (op) return op;
4492
+ }
4493
+ return String(value);
3945
4494
  }
3946
4495
 
3947
4496
  // src/vault.ts
@@ -3986,6 +4535,40 @@ var Vault = class {
3986
4535
  historyStrategy;
3987
4536
  i18nStrategy;
3988
4537
  syncStrategy;
4538
+ /**
4539
+ * Per-vault guard registry. `null` until `_initGuards()` runs; stays
4540
+ * `null` for vaults that never register any guard strategy. The
4541
+ * runtime class is dynamic-imported on demand so consumers that
4542
+ * never use guards don't pull `GuardRegistry`/`GuardExecutor` into
4543
+ * their bundle (#130).
4544
+ */
4545
+ guardRegistry = null;
4546
+ /**
4547
+ * Per-vault derivation registry. Same lazy-load contract as
4548
+ * `guardRegistry` — `null` until `_initDerivations()` runs with at
4549
+ * least one strategy handle. See #130 for the bundle motivation.
4550
+ */
4551
+ derivationRegistry = null;
4552
+ /**
4553
+ * Per-vault materialized-view registry (#143/#150). Same lazy-load
4554
+ * contract as `derivationRegistry` — `null` until
4555
+ * `_initMaterializedViews()` runs with at least one MV handle.
4556
+ */
4557
+ materializedViewRegistry = null;
4558
+ /**
4559
+ * Per-vault overlay registry (#154). Same lazy-load contract as
4560
+ * `materializedViewRegistry` — `null` until `_initOverlayedViews()`
4561
+ * runs with at least one handle.
4562
+ */
4563
+ overlayedViewRegistry = null;
4564
+ /**
4565
+ * Cached read-only facade handed to guard callbacks via `ctx.vault`,
4566
+ * and to derivation callbacks via `derive(source, ctx)`. Allocated
4567
+ * eagerly inside `_initGuards()` and/or `_initDerivations()` so read
4568
+ * accessors stay synchronous (callers in `tx/transaction.ts` rely on
4569
+ * that). Stays `null` for vaults with neither subsystem configured.
4570
+ */
4571
+ readOnlyFacade = null;
3989
4572
  getDEK;
3990
4573
  /**
3991
4574
  * Per-principal user envelope API.
@@ -4033,6 +4616,16 @@ var Vault = class {
4033
4616
  * docstring.
4034
4617
  */
4035
4618
  ledgerStore = null;
4619
+ /**
4620
+ * Background writes for persisted-schema envelopes (#schema-dump v0
4621
+ * slice 1). One promise per `collection({ persistJsonSchema: true })`
4622
+ * registration that actually fired a derive call. Fire-and-forget
4623
+ * from the collection factory; tests await
4624
+ * {@link _drainPendingSchemaWrites} before asserting on storage.
4625
+ * Production code does not need to drain — the writes are
4626
+ * idempotent fingerprints, not correctness invariants.
4627
+ */
4628
+ _pendingSchemaWrites = [];
4036
4629
  /**
4037
4630
  * Per-vault foreign-key reference registry. Collections
4038
4631
  * register their `refs` option here on construction; the
@@ -4127,6 +4720,7 @@ var Vault = class {
4127
4720
  this.historyStrategy = opts.historyStrategy ?? NO_HISTORY;
4128
4721
  this.i18nStrategy = opts.i18nStrategy ?? NO_I18N;
4129
4722
  this.syncStrategy = opts.syncStrategy ?? NO_SYNC;
4723
+ void opts.guardStrategies;
4130
4724
  this.historyConfig = opts.historyConfig ?? { enabled: true };
4131
4725
  this.reloadKeyring = opts.reloadKeyring;
4132
4726
  this.locale = opts.locale;
@@ -4186,6 +4780,16 @@ var Vault = class {
4186
4780
  * Collection constructor for the rationale.
4187
4781
  */
4188
4782
  collection(collectionName, options) {
4783
+ const overlayRegistry = this.overlayedViewRegistry;
4784
+ if (overlayRegistry !== null && overlayRegistry.isOverlay(collectionName)) {
4785
+ const spec = overlayRegistry.byName(collectionName);
4786
+ if (spec) {
4787
+ const base = this.collection(spec.base);
4788
+ const overlay = this.collection(spec.overlay);
4789
+ const baseRowKey = overlayRegistry.resolveBaseRowKey(collectionName, this.materializedViewRegistry);
4790
+ return new OverlayedCollection(spec, base, overlay, baseRowKey);
4791
+ }
4792
+ }
4189
4793
  if (isDictCollectionName(collectionName)) {
4190
4794
  throw new ReservedCollectionNameError(collectionName);
4191
4795
  }
@@ -4233,7 +4837,38 @@ var Vault = class {
4233
4837
  defaultLocale: this.locale,
4234
4838
  onRegisterConflictResolver: this.onRegisterConflictResolver,
4235
4839
  onAccess: (op, id) => this._logConsent(op, collectionName, id),
4236
- periodGuard: (existing, incoming) => this._assertTsWritable(existing, incoming)
4840
+ periodGuard: (existing, incoming) => this._assertTsWritable(existing, incoming),
4841
+ // Guard / derivation sources are only wired when the
4842
+ // corresponding registry has been initialised. Vaults without
4843
+ // guards/derivations skip this entirely so `Collection.put`'s
4844
+ // `if (this.guardSource)` / `if (this.derivationSource)`
4845
+ // branches no-op without ever touching the subsystem code.
4846
+ ...this.guardRegistry !== null ? {
4847
+ guardSource: {
4848
+ registry: () => this.guardRegistry,
4849
+ readOnlyVault: () => this._ensureReadOnlyFacade()
4850
+ }
4851
+ } : {},
4852
+ ...this.derivationRegistry !== null ? {
4853
+ derivationSource: {
4854
+ registry: () => this.derivationRegistry,
4855
+ getCollection: (name) => this.collection(name),
4856
+ getReadOnlyFacade: () => this._ensureReadOnlyFacade(),
4857
+ getActiveTxContext: () => this.noydb._activeTxContextOrNull,
4858
+ createTxContext: () => this.noydb._createTxContext(),
4859
+ setActiveTxContext: (ctx) => this.noydb._setActiveTxContext(ctx),
4860
+ clearActiveTxContext: (ctx) => this.noydb._clearActiveTxContext(ctx)
4861
+ }
4862
+ } : {},
4863
+ ...this.materializedViewRegistry !== null ? {
4864
+ materializedViewSource: {
4865
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
4866
+ registry: () => this.materializedViewRegistry,
4867
+ getCollection: (name) => this.collection(name),
4868
+ getActiveTxContext: () => this.noydb._activeTxContextOrNull,
4869
+ getQueryContext: () => this
4870
+ }
4871
+ } : {}
4237
4872
  };
4238
4873
  if (options?.indexes !== void 0) collOpts.indexes = options.indexes;
4239
4874
  if (options?.reconcileOnOpen !== void 0) collOpts.reconcileOnOpen = options.reconcileOnOpen;
@@ -4270,9 +4905,39 @@ var Vault = class {
4270
4905
  }
4271
4906
  coll = new Collection(collOpts);
4272
4907
  this.collectionCache.set(collectionName, coll);
4908
+ if (options?.persistJsonSchema === true && options.schema !== void 0) {
4909
+ const validator = options.schema;
4910
+ const work = (async () => {
4911
+ try {
4912
+ const dek = await this.getDEK(collectionName);
4913
+ await persistSchemaIfNeeded({
4914
+ store: this.adapter,
4915
+ vault: this.name,
4916
+ collectionName,
4917
+ validator,
4918
+ dek
4919
+ });
4920
+ } catch (err) {
4921
+ console.warn(
4922
+ `[noy-db] persisted-schema write failed for "${collectionName}": ` + (err instanceof Error ? err.message : String(err))
4923
+ );
4924
+ }
4925
+ })();
4926
+ this._pendingSchemaWrites.push(work);
4927
+ }
4273
4928
  }
4274
4929
  return coll;
4275
4930
  }
4931
+ /**
4932
+ * Await all background persisted-schema writes triggered by
4933
+ * `collection({ persistJsonSchema: true })` calls on this vault.
4934
+ * Used in tests; production code does not need to call this.
4935
+ */
4936
+ async _drainPendingSchemaWrites() {
4937
+ const pending = this._pendingSchemaWrites;
4938
+ this._pendingSchemaWrites = [];
4939
+ await Promise.allSettled(pending);
4940
+ }
4276
4941
  /**
4277
4942
  * Validate i18nText fields on a `put()`. Called by Collection just
4278
4943
  * before the adapter write, after schema validation. Throws
@@ -4816,45 +5481,309 @@ var Vault = class {
4816
5481
  return { violations };
4817
5482
  }
4818
5483
  /**
4819
- * Return this compartment's hash-chained audit log.
4820
- *
4821
- * The ledger is lazy-initialized on first access and cached for the
4822
- * lifetime of the Vault instance. Every LedgerStore instance
4823
- * shares the same adapter and DEK resolver, so `vault.ledger()`
4824
- * can be called repeatedly without performance cost.
4825
- *
4826
- * The LedgerStore itself is the public API: consumers call
4827
- * `.append()` (via Collection internals), `.head()`, `.verify()`,
4828
- * and `.entries({ from, to })`. See the LedgerStore docstring for
4829
- * the full surface and the concurrency caveats.
5484
+ * Return this compartment's hash-chained audit log.
5485
+ *
5486
+ * The ledger is lazy-initialized on first access and cached for the
5487
+ * lifetime of the Vault instance. Every LedgerStore instance
5488
+ * shares the same adapter and DEK resolver, so `vault.ledger()`
5489
+ * can be called repeatedly without performance cost.
5490
+ *
5491
+ * The LedgerStore itself is the public API: consumers call
5492
+ * `.append()` (via Collection internals), `.head()`, `.verify()`,
5493
+ * and `.entries({ from, to })`. See the LedgerStore docstring for
5494
+ * the full surface and the concurrency caveats.
5495
+ */
5496
+ ledger() {
5497
+ const store = this.getLedgerOrNull();
5498
+ if (!store) {
5499
+ throw new Error(
5500
+ 'vault.ledger() requires the history strategy. Import `{ withHistory }` from "@noy-db/hub/history" and pass it to `createNoydb({ historyStrategy: withHistory() })`.'
5501
+ );
5502
+ }
5503
+ return store;
5504
+ }
5505
+ /**
5506
+ * Internal accessor — returns the LedgerStore if the history
5507
+ * strategy is opted in, or `null` otherwise. Used by dump/restore/
5508
+ * verifyBackupIntegrity and by Collection write paths that already
5509
+ * gate on `if (this.ledger)`. The public `ledger()` accessor above
5510
+ * throws on null; this one stays silent so the off-path no-ops.
5511
+ */
5512
+ getLedgerOrNull() {
5513
+ if (!this.ledgerStore) {
5514
+ this.ledgerStore = this.historyStrategy.buildLedger({
5515
+ adapter: this.adapter,
5516
+ vault: this.name,
5517
+ encrypted: this.encrypted,
5518
+ getDEK: this.getDEK,
5519
+ actor: this.keyring.userId
5520
+ });
5521
+ }
5522
+ return this.ledgerStore;
5523
+ }
5524
+ /**
5525
+ * @internal — called by `Noydb.openVault` after construction.
5526
+ * Dynamic-imports `GuardRegistry` + `ReadOnlyVaultFacade` and seeds
5527
+ * the registry with the supplied strategy handles. No-op when the
5528
+ * handles array is empty — keeps the guard subsystem out of the
5529
+ * floor bundle for consumers that don't use guards (#130).
5530
+ *
5531
+ * The read-only facade is eagerly instantiated here so the sync
5532
+ * accessor `_getReadOnlyFacade()` (called from the tx amendment
5533
+ * runner) stays synchronous.
5534
+ */
5535
+ async _initGuards(handles) {
5536
+ if (handles.length === 0) return;
5537
+ const [{ GuardRegistry }, { ReadOnlyVaultFacade }] = await Promise.all([
5538
+ import("./registry-RFGGMVNJ.js"),
5539
+ import("./read-only-facade-ITU6L7BL.js")
5540
+ ]);
5541
+ const registry = new GuardRegistry();
5542
+ for (const h of handles) registry.register(h.spec);
5543
+ this.guardRegistry = registry;
5544
+ this.readOnlyFacade = new ReadOnlyVaultFacade(this);
5545
+ }
5546
+ /**
5547
+ * @internal — Collection.put calls into this. Returns `null` for
5548
+ * vaults that never registered any guard strategy. Callers MUST
5549
+ * gate on null (the existing `if (this.guardSource)` branches in
5550
+ * `Collection` already do this transitively).
5551
+ */
5552
+ _getGuardRegistry() {
5553
+ return this.guardRegistry;
5554
+ }
5555
+ /**
5556
+ * @internal — called by `Noydb.openVault` after construction.
5557
+ * Dynamic-imports `DerivationRegistry` and registers the supplied
5558
+ * derivation strategies (async because `strategyHash` computation
5559
+ * goes through `crypto.subtle.digest`). No-op when the handles
5560
+ * array is empty — keeps the derivation subsystem out of the floor
5561
+ * bundle for consumers that don't use derivations (#130). Throws
5562
+ * `DerivationCycleError` if a cycle is detected after registration.
5563
+ */
5564
+ async _initDerivations(handles) {
5565
+ if (handles.length === 0) return;
5566
+ const [{ DerivationRegistry }, { ReadOnlyVaultFacade }] = await Promise.all([
5567
+ import("./registry-WLLMODKN.js"),
5568
+ import("./read-only-facade-ITU6L7BL.js")
5569
+ ]);
5570
+ const registry = new DerivationRegistry();
5571
+ for (const h of handles) {
5572
+ await registry.register(h.spec);
5573
+ }
5574
+ registry.validate();
5575
+ this.derivationRegistry = registry;
5576
+ if (this.readOnlyFacade === null) {
5577
+ this.readOnlyFacade = new ReadOnlyVaultFacade(this);
5578
+ }
5579
+ }
5580
+ /**
5581
+ * @internal — consumed by `Collection.put` at write-time. Returns
5582
+ * `null` for vaults that never registered any derivation strategy.
5583
+ */
5584
+ _getDerivationRegistry() {
5585
+ return this.derivationRegistry;
5586
+ }
5587
+ /**
5588
+ * @internal — called by `Noydb.openVault` after collections are
5589
+ * wired. Dynamic-imports `MaterializedViewRegistry`, registers each
5590
+ * MV spec (which invokes its `query()` once for dependency
5591
+ * analysis), then runs the unified cycle detection across the MV +
5592
+ * derivation graphs. No-op when the handles array is empty — keeps
5593
+ * the MV subsystem out of the floor bundle (mirrors v1 #130).
5594
+ * Throws `MaterializedViewCycleError` if a cycle is detected.
5595
+ */
5596
+ async _initMaterializedViews(handles) {
5597
+ if (handles.length === 0) return;
5598
+ const { MaterializedViewRegistry } = await import("./registry-3L3N3PTG.js");
5599
+ const registry = new MaterializedViewRegistry();
5600
+ this.materializedViewRegistry = registry;
5601
+ const db = this;
5602
+ for (const h of handles) {
5603
+ await registry.register(h.spec, db);
5604
+ }
5605
+ registry.validate(this.derivationRegistry);
5606
+ }
5607
+ /**
5608
+ * @internal — consumed by `Collection.put` at write-time. Returns
5609
+ * `null` for vaults that never registered any MV strategy.
5610
+ */
5611
+ _getMaterializedViewRegistry() {
5612
+ return this.materializedViewRegistry;
5613
+ }
5614
+ /**
5615
+ * @internal — called by `Noydb.openVault` after MVs are wired.
5616
+ * Dynamic-imports `OverlayedViewRegistry`, registers each spec,
5617
+ * validates against the MV registry for name/base/overlay collisions.
5618
+ * Throws on validation failure.
5619
+ */
5620
+ async _initOverlayedViews(handles) {
5621
+ if (handles.length === 0) return;
5622
+ const { OverlayedViewRegistry } = await import("./registry-O47PUPSY.js");
5623
+ const registry = new OverlayedViewRegistry();
5624
+ const mvRegistry = this.materializedViewRegistry;
5625
+ const overlayNames = /* @__PURE__ */ new Set();
5626
+ for (const h of handles) overlayNames.add(h.spec.name);
5627
+ const isMVOutput = (name) => {
5628
+ if (!mvRegistry) return false;
5629
+ for (const reg of mvRegistry.all()) {
5630
+ if (reg.outputCollection === name) return true;
5631
+ }
5632
+ return false;
5633
+ };
5634
+ for (const h of handles) {
5635
+ registry.register(h.spec, {
5636
+ isOverlayName: (n) => overlayNames.has(n) && n !== h.spec.name,
5637
+ isMVOutput
5638
+ });
5639
+ }
5640
+ this.overlayedViewRegistry = registry;
5641
+ }
5642
+ /**
5643
+ * @internal — consumed by `Vault.collection()`. Returns `null` for
5644
+ * vaults with no overlays registered.
5645
+ */
5646
+ _getOverlayedViewRegistry() {
5647
+ return this.overlayedViewRegistry;
5648
+ }
5649
+ /**
5650
+ * Manual re-materialize for a single registered MV (#151). Useful
5651
+ * for `refresh: 'manual'` MVs (whose consumer drives refreshes
5652
+ * externally), for stale-bit recovery on vault re-open, and as the
5653
+ * explicit bulk-recompute escape hatch after a strategy change.
5654
+ *
5655
+ * Returns `{ written, deleted, failed }`. `deleted` is always 0 in
5656
+ * foundation + this sub-issue — tombstoning lands in #152.
5657
+ *
5658
+ * Throws if `name` is not a registered MV.
5659
+ */
5660
+ async refreshView(name) {
5661
+ const registry = this.materializedViewRegistry;
5662
+ if (registry === null) {
5663
+ return { written: 0, deleted: 0, failed: 0 };
5664
+ }
5665
+ const reg = registry.byName(name);
5666
+ if (!reg) {
5667
+ throw new Error(`refreshView: no MV registered with name "${name}"`);
5668
+ }
5669
+ const { MaterializedViewExecutor } = await import("./executor-7E3VFGW7.js");
5670
+ const result = await MaterializedViewExecutor.refresh(reg, {
5671
+ getCollection: (n) => this.collection(n),
5672
+ getActiveTxContext: () => this.noydb._activeTxContextOrNull,
5673
+ getQueryContext: () => this
5674
+ });
5675
+ const { clearMVStale } = await import("./stale-HSC5YO2O.js");
5676
+ clearMVStale(registry, name);
5677
+ return result;
5678
+ }
5679
+ /**
5680
+ * Re-derive every record in the named source collection. Useful
5681
+ * after a strategy change to bring previously-derived records
5682
+ * up-to-date.
5683
+ *
5684
+ * Sequential in v1; parallelisation deferred to v2.
5685
+ */
5686
+ async deriveAll(sourceCollection) {
5687
+ const registry = this._getDerivationRegistry();
5688
+ if (registry === null) return { derived: 0, failed: 0 };
5689
+ const strategies = registry.strategiesForSource(sourceCollection);
5690
+ if (strategies.length === 0) return { derived: 0, failed: 0 };
5691
+ const { DerivationExecutor } = await import("./executor-X4SQ3ZLC.js");
5692
+ const sourceColl = this.collection(sourceCollection);
5693
+ const records = await sourceColl.list();
5694
+ const ctx = { vault: this.readOnlyFacade ?? new (await import("./read-only-facade-ITU6L7BL.js")).ReadOnlyVaultFacade(this) };
5695
+ let derived = 0;
5696
+ let failed = 0;
5697
+ for (const record of records) {
5698
+ if (typeof record !== "object" || record === null) continue;
5699
+ const id = record.id;
5700
+ if (typeof id !== "string") continue;
5701
+ for (const { spec, strategyHash } of strategies) {
5702
+ const sourceWithId = { ...record, id };
5703
+ const result = await DerivationExecutor.run(spec, sourceWithId, 0, strategyHash, ctx);
5704
+ let anyFailed = false;
5705
+ for (const key of Object.keys(spec.outputs)) {
5706
+ const out = result.outputs[key];
5707
+ if (!out) continue;
5708
+ if (out.kind === "failed") {
5709
+ anyFailed = true;
5710
+ continue;
5711
+ }
5712
+ const outSpec = spec.outputs[key];
5713
+ if (!outSpec) continue;
5714
+ const outputColl = this.collection(outSpec.collection);
5715
+ if (out.kind === "array") {
5716
+ const { loadFanoutSidecar, saveFanoutSidecar } = await import("./fanout-sidecar-VJ52RIEY.js");
5717
+ const prior = await loadFanoutSidecar(this.adapter, this.name, spec.source, id, key);
5718
+ const prevKeys = new Set(prior?.keys ?? []);
5719
+ const newKeysList = out.entries.map((e) => e.key);
5720
+ const newKeysSet = new Set(newKeysList);
5721
+ for (const k of prevKeys) {
5722
+ if (newKeysSet.has(k)) continue;
5723
+ await outputColl._internalDelete(k);
5724
+ }
5725
+ for (const entry of out.entries) {
5726
+ await outputColl.put(entry.key, entry.value);
5727
+ }
5728
+ await saveFanoutSidecar(this.adapter, this.name, {
5729
+ source: spec.source,
5730
+ sourceId: id,
5731
+ outputKey: key,
5732
+ outputCollection: outSpec.collection,
5733
+ keys: newKeysList
5734
+ });
5735
+ continue;
5736
+ }
5737
+ if (out.skipped === true) {
5738
+ await outputColl._internalDelete(id);
5739
+ continue;
5740
+ }
5741
+ await outputColl.put(id, out.value);
5742
+ }
5743
+ if (anyFailed) failed++;
5744
+ else derived++;
5745
+ }
5746
+ }
5747
+ return { derived, failed };
5748
+ }
5749
+ /**
5750
+ * @internal — exposed for `runTransaction({ amendment: true })` so
5751
+ * the amendment invariant runner can pass the SAME read-only vault
5752
+ * facade that the per-record `Collection.put` guard hook uses
5753
+ * (`guardSource.readOnlyVault()` above). Eagerly instantiated by
5754
+ * `_initGuards()` so this accessor stays synchronous; returns
5755
+ * `null` for vaults that never registered any guard (amendments
5756
+ * require at least one guard, so the caller should never see null).
5757
+ */
5758
+ _getReadOnlyFacade() {
5759
+ return this.readOnlyFacade;
5760
+ }
5761
+ /**
5762
+ * Internal lazy-allocator for the read-only facade. Used by the
5763
+ * per-collection `guardSource.readOnlyVault` callback when guards
5764
+ * ARE configured but `_initGuards()` raced with the first guard
5765
+ * invocation (theoretically impossible — `Noydb.openVault` awaits
5766
+ * `_initGuards` before returning — but we keep the defensive lazy
5767
+ * path so the closure's contract stays "always returns a facade").
4830
5768
  */
4831
- ledger() {
4832
- const store = this.getLedgerOrNull();
4833
- if (!store) {
4834
- throw new Error(
4835
- 'vault.ledger() requires the history strategy. Import `{ withHistory }` from "@noy-db/hub/history" and pass it to `createNoydb({ historyStrategy: withHistory() })`.'
4836
- );
4837
- }
4838
- return store;
5769
+ _ensureReadOnlyFacade() {
5770
+ if (this.readOnlyFacade !== null) return this.readOnlyFacade;
5771
+ throw new Error(
5772
+ "Vault: guard hook fired before _initGuards() completed. This typically means the vault was opened via the sync fallback path (Noydb.vault(name)) without first calling await db.openVault(name). See issue #132."
5773
+ );
4839
5774
  }
4840
5775
  /**
4841
- * Internal accessor returns the LedgerStore if the history
4842
- * strategy is opted in, or `null` otherwise. Used by dump/restore/
4843
- * verifyBackupIntegrity and by Collection write paths that already
4844
- * gate on `if (this.ledger)`. The public `ledger()` accessor above
4845
- * throws on null; this one stays silent so the off-path no-ops.
5776
+ * @internalexposed for `runTransaction({ amendment: true })`
5777
+ * to append the structured `op: 'amendment'` audit entry without
5778
+ * dragging this private accessor onto the public surface or
5779
+ * forcing the tx executor to depend on the history-strategy
5780
+ * shape directly. Returns `null` when no history strategy is
5781
+ * configured, in which case the amendment commits silently
5782
+ * (the records still write through; only the multi-record
5783
+ * audit summary is skipped).
4846
5784
  */
4847
- getLedgerOrNull() {
4848
- if (!this.ledgerStore) {
4849
- this.ledgerStore = this.historyStrategy.buildLedger({
4850
- adapter: this.adapter,
4851
- vault: this.name,
4852
- encrypted: this.encrypted,
4853
- getDEK: this.getDEK,
4854
- actor: this.keyring.userId
4855
- });
4856
- }
4857
- return this.ledgerStore;
5785
+ _getLedgerOrNull() {
5786
+ return this.getLedgerOrNull();
4858
5787
  }
4859
5788
  /**
4860
5789
  * Return a read-only view of this vault as it existed at
@@ -5008,7 +5937,7 @@ var Vault = class {
5008
5937
  * collection.
5009
5938
  */
5010
5939
  async delegate(opts) {
5011
- const { issueDelegation: issueDelegation2, DELEGATIONS_COLLECTION: DELEGATIONS_COLLECTION2 } = await import("./delegation-2DBS2EOH.js");
5940
+ const { issueDelegation: issueDelegation2, DELEGATIONS_COLLECTION: DELEGATIONS_COLLECTION2 } = await import("./delegation-YBA4X4JN.js");
5012
5941
  if (!this.keyring.kek) {
5013
5942
  throw new ValidationError(
5014
5943
  "issueDelegation: keyring.kek is null \u2014 issuing a delegation requires a tier-1 unlock. Re-authenticate at tier 1 (passphrase) first."
@@ -5030,7 +5959,7 @@ var Vault = class {
5030
5959
  * if the id does not exist.
5031
5960
  */
5032
5961
  async revokeDelegation(id) {
5033
- const { revokeDelegation: revokeDelegation2, DELEGATIONS_COLLECTION: DELEGATIONS_COLLECTION2 } = await import("./delegation-2DBS2EOH.js");
5962
+ const { revokeDelegation: revokeDelegation2, DELEGATIONS_COLLECTION: DELEGATIONS_COLLECTION2 } = await import("./delegation-YBA4X4JN.js");
5034
5963
  await revokeDelegation2(this.adapter, this.name, id);
5035
5964
  void DELEGATIONS_COLLECTION2;
5036
5965
  }
@@ -5355,6 +6284,54 @@ var Vault = class {
5355
6284
  const snapshot = await this.adapter.loadAll(this.name);
5356
6285
  return Object.keys(snapshot);
5357
6286
  }
6287
+ /**
6288
+ * Emit a structured introspection snapshot of this vault — vault name,
6289
+ * subsystem opt-in matrix, collections + their fields, materialized
6290
+ * views, overlay views, derivations. With `withStats: true`, walks
6291
+ * every collection's envelopes to compute record counts, byte totals,
6292
+ * and oldest/newest timestamps.
6293
+ *
6294
+ * Consumed by the `noydb describe` CLI to produce human-readable
6295
+ * audit YAML/JSON from a `.noydb` bundle.
6296
+ *
6297
+ * Field provenance:
6298
+ * - `persisted`: read from `_schemas/<col>` envelope (Route B opt-in)
6299
+ * - `live-validator`: derived in-process from a Zod schema attached
6300
+ * to the live `Collection`
6301
+ * - `sampled`: inferred from decrypted records (deferred to a follow-up)
6302
+ * - `unknown`: no schema info available
6303
+ *
6304
+ * @see docs/superpowers/specs/2026-05-22-schema-dump-design.md
6305
+ */
6306
+ async dumpSchema(opts = {}) {
6307
+ return dumpVaultSchema(this, opts);
6308
+ }
6309
+ /**
6310
+ * Internal accessor for {@link dumpVaultSchema}. Exposes the structural
6311
+ * state the walker needs (collection cache, registries, ref registry,
6312
+ * adapter) without widening the public Vault surface.
6313
+ *
6314
+ * @internal
6315
+ */
6316
+ _introspectState() {
6317
+ return {
6318
+ name: this.name,
6319
+ adapter: this.adapter,
6320
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6321
+ collectionCache: this.collectionCache,
6322
+ refRegistry: this.refRegistry,
6323
+ getDEK: this.getDEK,
6324
+ subsystems: {
6325
+ guards: this.guardRegistry !== null,
6326
+ derivations: this.derivationRegistry !== null,
6327
+ materializedViews: this.materializedViewRegistry !== null,
6328
+ overlayViews: this.overlayedViewRegistry !== null
6329
+ },
6330
+ mvRegistry: this.materializedViewRegistry,
6331
+ overlayRegistry: this.overlayedViewRegistry,
6332
+ derivationRegistry: this.derivationRegistry
6333
+ };
6334
+ }
5358
6335
  /**
5359
6336
  * Return the stable opaque bundle handle for this vault,
5360
6337
  * generating and persisting a fresh ULID on first call.
@@ -5430,7 +6407,7 @@ var Vault = class {
5430
6407
  * @see docs/subsystems/public-envelope.md
5431
6408
  */
5432
6409
  async getPublicEnvelope(opts = {}) {
5433
- const { readPublicEnvelope: readPublicEnvelope2 } = await import("./public-envelope-6JTACYJV.js");
6410
+ const { readPublicEnvelope: readPublicEnvelope2 } = await import("./public-envelope-PY6NKFLI.js");
5434
6411
  return readPublicEnvelope2(this.adapter, this.name, opts);
5435
6412
  }
5436
6413
  /**
@@ -5455,7 +6432,7 @@ var Vault = class {
5455
6432
  }
5456
6433
  }
5457
6434
  const internalSnapshot = {};
5458
- for (const internalName of [LEDGER_COLLECTION, LEDGER_DELTAS_COLLECTION]) {
6435
+ for (const internalName of [LEDGER_COLLECTION, LEDGER_DELTAS_COLLECTION, SCHEMAS_COLLECTION]) {
5459
6436
  const ids = await this.adapter.list(this.name, internalName);
5460
6437
  if (ids.length === 0) continue;
5461
6438
  const records = {};
@@ -5604,6 +6581,7 @@ var Vault = class {
5604
6581
  for (let i = allEntries.length - 1; i >= 0; i--) {
5605
6582
  const entry = allEntries[i];
5606
6583
  if (!entry) continue;
6584
+ if (entry.op === "amendment") continue;
5607
6585
  const key = `${entry.collection}/${entry.id}`;
5608
6586
  if (seen.has(key)) continue;
5609
6587
  seen.add(key);
@@ -5625,7 +6603,7 @@ var Vault = class {
5625
6603
  message: `Ledger expects data record "${collection}/${id}" to exist, but the adapter has no envelope for it.`
5626
6604
  };
5627
6605
  }
5628
- const actualHash = await sha256Hex(envelope._data);
6606
+ const actualHash = await sha256Hex2(envelope._data);
5629
6607
  if (actualHash !== expectedHash) {
5630
6608
  return {
5631
6609
  ok: false,
@@ -6026,6 +7004,10 @@ var PERSONAL_POLICY = Object.freeze({
6026
7004
  minTier: 1,
6027
7005
  enabled: true
6028
7006
  },
7007
+ // rotate-recovery (#121): deliberate paper-sheet regeneration
7008
+ // when the user remembers their passphrase. PERSONAL matches the
7009
+ // pre-#121 low-level flow's bar — knowing the passphrase is enough.
7010
+ "rotate-recovery": { minTier: 1 },
6029
7011
  "enroll-authenticator": { minTier: 1 },
6030
7012
  "remove-authenticator": { minTier: 1 },
6031
7013
  // update-authenticator: meta-only mutation (slot rename, label
@@ -6085,6 +7067,14 @@ var STRICT_POLICY = Object.freeze({
6085
7067
  minTier: 1,
6086
7068
  enabled: true
6087
7069
  },
7070
+ // rotate-recovery (#121): STRICT requires an off-device factor —
7071
+ // rotating recovery is an off-site-trust event; a stolen unlocked
7072
+ // laptop must not be able to silently mint a new sheet for the
7073
+ // attacker. Matches the `peer-recover-user` STRICT default.
7074
+ "rotate-recovery": {
7075
+ minTier: 1,
7076
+ factors: [{ anyOf: ["totp", "email-otp", "webauthn-roaming"] }]
7077
+ },
6088
7078
  "enroll-authenticator": {
6089
7079
  minTier: 1,
6090
7080
  factors: [{ anyOf: ["totp", "email-otp"] }]
@@ -6273,6 +7263,14 @@ var Noydb = class {
6273
7263
  * `_meta/policy` load; replaced by `db.updatePolicy()`.
6274
7264
  */
6275
7265
  policyCache = /* @__PURE__ */ new Map();
7266
+ /**
7267
+ * One-shot bypass for the managed-mode strong-recovery check (#195).
7268
+ * Set true by {@link openVaultAndEnrollRecovery} for the duration of
7269
+ * the bootstrap window so the keyring can be created before the
7270
+ * strong recovery is enrolled. Always cleared (try/finally).
7271
+ * @internal
7272
+ */
7273
+ _skipNextManagedRecoveryCheck = false;
6276
7274
  /** Per-vault tier-3 (PIN / quick-resume) state — issue #11. */
6277
7275
  quickUnlock = new QuickUnlockStore();
6278
7276
  /**
@@ -6288,6 +7286,17 @@ var Noydb = class {
6288
7286
  txStrategy;
6289
7287
  sessionStrategy;
6290
7288
  syncStrategy;
7289
+ /**
7290
+ * Currently-running multi-record transaction, set by
7291
+ * `runTransaction` at the start of Phase 2 (commit) and cleared in
7292
+ * the same function's `finally` block. Side-effect writes triggered
7293
+ * during a staged op's `Collection.put` (today: eager derivation
7294
+ * outputs) register their pre-write envelope on `_executed` here so
7295
+ * a mid-batch failure rolls them back alongside the main staged ops
7296
+ * (#133). `null` outside of Phase 2.
7297
+ * @internal
7298
+ */
7299
+ _activeTxContext = null;
6291
7300
  // ─── plaintextTranslator state ─────────────────────────
6292
7301
  /**
6293
7302
  * In-process translation cache. Key is `"${field}\x00${collection}\x00${from}\x00${to}\x00${text}"`.
@@ -6372,7 +7381,7 @@ var Noydb = class {
6372
7381
  }
6373
7382
  return comp;
6374
7383
  }
6375
- const keyring = await this.getKeyring(name);
7384
+ const keyring = await this.getKeyringInternal(name);
6376
7385
  if (!this.activeTier.has(name)) {
6377
7386
  this.activeTier.set(name, 1);
6378
7387
  }
@@ -6439,6 +7448,7 @@ var Noydb = class {
6439
7448
  ...this.options.historyStrategy !== void 0 ? { historyStrategy: this.options.historyStrategy } : {},
6440
7449
  ...this.options.i18nStrategy !== void 0 ? { i18nStrategy: this.options.i18nStrategy } : {},
6441
7450
  ...this.options.syncStrategy !== void 0 ? { syncStrategy: this.options.syncStrategy } : {},
7451
+ ...this.options.guardStrategies !== void 0 ? { guardStrategies: this.options.guardStrategies } : {},
6442
7452
  locale: opts?.locale,
6443
7453
  // Thread the translator hook so Collection.put() can invoke it
6444
7454
  plaintextTranslator: this.options.plaintextTranslator ? (text, from, to, field, collection) => this.invokeTranslator(text, from, to, field, collection) : void 0,
@@ -6459,6 +7469,10 @@ var Noydb = class {
6459
7469
  return refreshed;
6460
7470
  } : void 0
6461
7471
  });
7472
+ await comp._initGuards(this.options.guardStrategies ?? []);
7473
+ await comp._initDerivations(this.options.derivationStrategies ?? []);
7474
+ await comp._initMaterializedViews(this.options.materializedViewStrategies ?? []);
7475
+ await comp._initOverlayedViews(this.options.overlayedViewStrategies ?? []);
6462
7476
  this.vaultCache.set(name, comp);
6463
7477
  return comp;
6464
7478
  }
@@ -6485,7 +7499,8 @@ var Noydb = class {
6485
7499
  ...this.options.shadowStrategy !== void 0 ? { shadowStrategy: this.options.shadowStrategy } : {},
6486
7500
  ...this.options.historyStrategy !== void 0 ? { historyStrategy: this.options.historyStrategy } : {},
6487
7501
  ...this.options.i18nStrategy !== void 0 ? { i18nStrategy: this.options.i18nStrategy } : {},
6488
- ...this.options.syncStrategy !== void 0 ? { syncStrategy: this.options.syncStrategy } : {}
7502
+ ...this.options.syncStrategy !== void 0 ? { syncStrategy: this.options.syncStrategy } : {},
7503
+ ...this.options.guardStrategies !== void 0 ? { guardStrategies: this.options.guardStrategies } : {}
6489
7504
  });
6490
7505
  this.vaultCache.set(name, comp2);
6491
7506
  return comp2;
@@ -6513,21 +7528,41 @@ var Noydb = class {
6513
7528
  ...this.options.historyStrategy !== void 0 ? { historyStrategy: this.options.historyStrategy } : {},
6514
7529
  ...this.options.i18nStrategy !== void 0 ? { i18nStrategy: this.options.i18nStrategy } : {},
6515
7530
  ...this.options.syncStrategy !== void 0 ? { syncStrategy: this.options.syncStrategy } : {},
7531
+ ...this.options.guardStrategies !== void 0 ? { guardStrategies: this.options.guardStrategies } : {},
6516
7532
  emitter: this.emitter
6517
7533
  });
6518
7534
  this.vaultCache.set(name, comp);
6519
7535
  return comp;
6520
7536
  }
6521
- /** Grant access to a user for a vault. */
6522
- async grant(vault, options) {
7537
+ /**
7538
+ * Grant access to a user for a vault.
7539
+ *
7540
+ * Gated by `enroll-user`. `STRICT_POLICY` requires a TOTP / email-OTP
7541
+ * factor proof so the operator affirmatively re-asserts identity at
7542
+ * the moment of grant; `PERSONAL_POLICY` accepts a tier-1 unlock alone.
7543
+ *
7544
+ * The legacy `requireReAuthFor: ['grant']` session-policy check still
7545
+ * fires on top — both are independent opt-ins.
7546
+ */
7547
+ async grant(vault, options, factors) {
6523
7548
  this.checkPolicyOperation(vault, "grant");
6524
- const keyring = await this.getKeyring(vault);
7549
+ await this.checkGate(vault, "enroll-user", factors);
7550
+ const keyring = await this.getKeyringInternal(vault);
6525
7551
  await grant(this.options.store, vault, keyring, options);
6526
7552
  }
6527
- /** Revoke a user's access to a vault. */
6528
- async revoke(vault, options) {
7553
+ /**
7554
+ * Revoke a user's access to a vault.
7555
+ *
7556
+ * Gated by `revoke-user`. `STRICT_POLICY` requires a TOTP / email-OTP
7557
+ * factor proof; `PERSONAL_POLICY` accepts a tier-1 unlock alone.
7558
+ *
7559
+ * The legacy `requireReAuthFor: ['revoke']` session-policy check still
7560
+ * fires on top — both are independent opt-ins.
7561
+ */
7562
+ async revoke(vault, options, factors) {
6529
7563
  this.checkPolicyOperation(vault, "revoke");
6530
- const keyring = await this.getKeyring(vault);
7564
+ await this.checkGate(vault, "revoke-user", factors);
7565
+ const keyring = await this.getKeyringInternal(vault);
6531
7566
  await revoke(this.options.store, vault, keyring, options);
6532
7567
  }
6533
7568
  /**
@@ -6574,7 +7609,7 @@ var Noydb = class {
6574
7609
  */
6575
7610
  async updateUser(vault, options, factors) {
6576
7611
  await this.checkGate(vault, "update-user", factors);
6577
- const keyring = await this.getKeyring(vault);
7612
+ const keyring = await this.getKeyringInternal(vault);
6578
7613
  await updateKeyringIdentity(this.options.store, vault, keyring, options);
6579
7614
  if (options.userId === this.options.user) {
6580
7615
  this.keyringCache.delete(vault);
@@ -6600,7 +7635,7 @@ var Noydb = class {
6600
7635
  */
6601
7636
  async rotate(vault, collections) {
6602
7637
  this.checkPolicyOperation(vault, "rotate");
6603
- const keyring = await this.getKeyring(vault);
7638
+ const keyring = await this.getKeyringInternal(vault);
6604
7639
  await rotateKeys(this.options.store, vault, keyring, collections);
6605
7640
  this.keyringCache.set(vault, keyring);
6606
7641
  }
@@ -6703,7 +7738,7 @@ var Noydb = class {
6703
7738
  this.options.secret
6704
7739
  );
6705
7740
  } catch (err) {
6706
- if (err instanceof NoAccessError || err instanceof InvalidKeyError) {
7741
+ if (err instanceof NoAccessError || err instanceof InvalidKeyError || err instanceof KeyringCorruptError) {
6707
7742
  continue;
6708
7743
  }
6709
7744
  throw err;
@@ -6800,15 +7835,23 @@ var Noydb = class {
6800
7835
  }
6801
7836
  return results;
6802
7837
  }
6803
- /** Change the current user's passphrase for a vault. */
6804
- async changeSecret(vault, newPassphrase) {
7838
+ /**
7839
+ * Change the current user's passphrase for a vault.
7840
+ *
7841
+ * Validates the new passphrase against the strength rules. Pass
7842
+ * `{ allowWeakPassphrase: true }` to skip — typically only useful for
7843
+ * fixtures and migrations. Pass a `PassphrasePolicy` to override the
7844
+ * default rules (e.g. consumer-tunable `pattern` / `customValidator`).
7845
+ */
7846
+ async changeSecret(vault, newPassphrase, options) {
6805
7847
  this.checkPolicyOperation(vault, "changeSecret");
6806
- const keyring = await this.getKeyring(vault);
7848
+ const keyring = await this.getKeyringInternal(vault);
6807
7849
  const updated = await changeSecret(
6808
7850
  this.options.store,
6809
7851
  vault,
6810
7852
  keyring,
6811
- newPassphrase
7853
+ newPassphrase,
7854
+ options
6812
7855
  );
6813
7856
  this.keyringCache.set(vault, updated);
6814
7857
  }
@@ -6853,10 +7896,18 @@ var Noydb = class {
6853
7896
  }
6854
7897
  return result;
6855
7898
  }
6856
- transaction(arg) {
7899
+ transaction(arg, maybeFn) {
6857
7900
  if (typeof arg === "function") {
6858
7901
  return this.txStrategy.runTransaction(this, arg);
6859
7902
  }
7903
+ if (typeof arg === "object" && arg !== null && arg.amendment === true) {
7904
+ if (typeof maybeFn !== "function") {
7905
+ throw new ValidationError(
7906
+ "db.transaction({ amendment: true }, fn) requires the callback as the second argument."
7907
+ );
7908
+ }
7909
+ return this.txStrategy.runTransaction(this, maybeFn, arg);
7910
+ }
6860
7911
  const vault = arg;
6861
7912
  const comp = this.vaultCache.get(vault);
6862
7913
  if (!comp) {
@@ -6877,6 +7928,59 @@ var Noydb = class {
6877
7928
  get _store() {
6878
7929
  return this.options.store;
6879
7930
  }
7931
+ /**
7932
+ * Currently-running multi-record transaction, or `null` outside
7933
+ * Phase 2. `Collection.dispatchDerivations` consults this so a
7934
+ * recursive derived-output write inside `Collection.put` can register
7935
+ * its envelope onto `ctx._executed` and roll back with the main
7936
+ * staged ops on mid-batch failure (#133).
7937
+ *
7938
+ * @internal
7939
+ */
7940
+ get _activeTxContextOrNull() {
7941
+ return this._activeTxContext;
7942
+ }
7943
+ /**
7944
+ * Called by `runTransaction` at Phase 2 start, and by
7945
+ * `Collection.putManyAtomic` (via `derivationSource.setActiveTxContext`)
7946
+ * for its own Phase 2 loop. Nested or concurrent (non-nested)
7947
+ * transactions on the same Noydb instance are NOT supported —
7948
+ * overwriting an active context means another transaction is still
7949
+ * running and its `_executed` list would be cross-contaminated by
7950
+ * the nested writes. We tolerate the overwrite (best-effort, no
7951
+ * throw) to keep the rare interleaving from breaking consumers who
7952
+ * currently get lucky with timing, but applications should ensure
7953
+ * their multi-record commits are serialised on a single Noydb.
7954
+ *
7955
+ * @internal
7956
+ */
7957
+ _setActiveTxContext(ctx) {
7958
+ this._activeTxContext = ctx;
7959
+ }
7960
+ /**
7961
+ * Factory for a transient `TxContext` bound to this Noydb. Used by
7962
+ * `Collection.putManyAtomic` (via `derivationSource.createTxContext`)
7963
+ * to publish an active context for the duration of its bulk-atomic
7964
+ * Phase 2 loop, so recursive derivation-output writes register on
7965
+ * `ctx._executed` and roll back together with the source ops (#133).
7966
+ *
7967
+ * @internal
7968
+ */
7969
+ _createTxContext() {
7970
+ return new TxContext(this);
7971
+ }
7972
+ /**
7973
+ * Called by `runTransaction` in its `finally`. Only clears when the
7974
+ * passed ctx matches the active one — a defensive no-op if some
7975
+ * other code path already cleared it.
7976
+ *
7977
+ * @internal
7978
+ */
7979
+ _clearActiveTxContext(ctx) {
7980
+ if (this._activeTxContext === ctx) {
7981
+ this._activeTxContext = null;
7982
+ }
7983
+ }
6880
7984
  /** Get sync status for a vault. */
6881
7985
  syncStatus(vault) {
6882
7986
  const engine = this.syncEngines.get(vault);
@@ -6885,6 +7989,15 @@ var Noydb = class {
6885
7989
  }
6886
7990
  return engine.status();
6887
7991
  }
7992
+ requireShamirProvider() {
7993
+ const p = this.options.shamirRecovery;
7994
+ if (!p) {
7995
+ throw new Error(
7996
+ "shamir recovery requires a ShamirRecoveryProvider \u2014 pass shamirRecovery: shamirRecoveryProvider() from '@noy-db/on-shamir' to createNoydb()"
7997
+ );
7998
+ }
7999
+ return p;
8000
+ }
6888
8001
  getSyncEngine(vault) {
6889
8002
  const engine = this.syncEngines.get(vault);
6890
8003
  if (!engine) {
@@ -7029,6 +8142,40 @@ var Noydb = class {
7029
8142
  this.policyCache.set(vault, merged);
7030
8143
  return merged;
7031
8144
  }
8145
+ /**
8146
+ * Read the current vault-level user-directory toggle (#122). Returns
8147
+ * the default-on shape (`{ enabled: true }`) when no `_meta/directory`
8148
+ * document has been persisted yet.
8149
+ *
8150
+ * No role gate — anyone who can open the vault can read the toggle.
8151
+ */
8152
+ async getDirectoryEnabled(vault) {
8153
+ if (this.closed) throw new ValidationError("Instance is closed");
8154
+ const persisted = await readDirectoryConfig(this.options.store, vault);
8155
+ return persisted?.enabled ?? true;
8156
+ }
8157
+ /**
8158
+ * Toggle the vault's user-directory listing on or off (#122).
8159
+ * Owner-only. When disabled, `listUsersWithEnvelopes()` throws
8160
+ * {@link import('./errors.js').DirectoryDisabledError} for callers
8161
+ * whose role is neither `owner` nor `admin`.
8162
+ *
8163
+ * Honest caveat: this is a UX flag, not a privacy guarantee. The
8164
+ * keyring file at `_keyring/<userId>` and the envelope ciphertext at
8165
+ * `_users/<keyringId>` remain observable to anyone with direct store
8166
+ * read access — only the hub-level enumeration is gated. See
8167
+ * `docs/subsystems/user-envelope.md` → "Directory visibility".
8168
+ */
8169
+ async setDirectoryEnabled(vault, enabled) {
8170
+ if (this.closed) throw new ValidationError("Instance is closed");
8171
+ const keyring = await this.getKeyringInternal(vault);
8172
+ if (keyring.role !== "owner") {
8173
+ throw new PermissionDeniedError(
8174
+ `setDirectoryEnabled requires owner role; caller has role "${keyring.role}"`
8175
+ );
8176
+ }
8177
+ await persistDirectoryConfig(this.options.store, vault, { enabled });
8178
+ }
7032
8179
  /**
7033
8180
  * Evaluate a policy gate against the active session tier and the
7034
8181
  * presented factor proofs. Throws {@link PolicyDeniedError} on
@@ -7039,40 +8186,61 @@ var Noydb = class {
7039
8186
  * or app-defined (`app:*`).
7040
8187
  * @param presented Caller-supplied factor proofs.
7041
8188
  */
7042
- async checkGate(vault, gate, presented) {
8189
+ async checkGate(vault, gate, factors) {
7043
8190
  const policy = await this.getPolicy(vault);
7044
8191
  const tier = this.activeTier.get(vault) ?? 1;
7045
8192
  await checkGate(policy, gate, {
7046
8193
  activeTier: tier,
7047
- ...presented?.factors !== void 0 ? { factors: presented.factors } : {},
7048
- ...presented?.sharedDevice !== void 0 ? { sharedDevice: presented.sharedDevice } : {}
8194
+ ...factors?.factors !== void 0 ? { factors: factors.factors } : {},
8195
+ ...factors?.sharedDevice !== void 0 ? { sharedDevice: factors.sharedDevice } : {}
7049
8196
  });
7050
8197
  }
7051
8198
  /** Read or persist the vault policy at `_meta/policy` on first open. */
7052
- async bootstrapPolicy(vault) {
8199
+ async bootstrapPolicy(vault, opts) {
7053
8200
  const onDisk = await loadVaultPolicy(this.options.store, vault);
7054
8201
  if (onDisk) {
7055
8202
  this.policyCache.set(vault, onDisk);
7056
- await this.assertRecoveryEnrolled(vault, onDisk);
8203
+ await this.assertRecoveryEnrolled(vault, onDisk, opts);
7057
8204
  return;
7058
8205
  }
7059
8206
  const initial = this.options.policy ? mergePolicy(PERSONAL_POLICY, this.options.policy) : PERSONAL_POLICY;
7060
8207
  await saveVaultPolicy(this.options.store, vault, initial);
7061
8208
  this.policyCache.set(vault, initial);
7062
- await this.assertRecoveryEnrolled(vault, initial);
7063
- }
7064
- /**
7065
- * Throw {@link RecoveryNotEnrolledError} when the developer
7066
- * explicitly opts into strict mandatory-recovery enforcement
7067
- * (`createNoydb({ requireRecovery: true })`) and no recovery
7068
- * entries are persisted.
7069
- *
7070
- * The default behavior is lenient — `recover-passphrase` is enabled
7071
- * in `PERSONAL_POLICY` but the hub does not block vault open on
7072
- * missing enrollment. v1.0 will flip the default to strict; for now,
7073
- * apps that want the spec-mandated check turn it on per-vault.
7074
- */
7075
- async assertRecoveryEnrolled(vault, policy) {
8209
+ await this.assertRecoveryEnrolled(vault, initial, opts);
8210
+ }
8211
+ /**
8212
+ * Throw {@link RecoveryNotEnrolledError} or
8213
+ * {@link ManagedRecoveryNotEnrolledError} when recovery enrollment
8214
+ * is missing.
8215
+ *
8216
+ * Two enforcement modes:
8217
+ *
8218
+ * 1. **Managed-mode mandatory strong-recovery (#195).** When
8219
+ * `passphraseMode === 'managed'`, the vault MUST have at least
8220
+ * one **strong** recovery profile (Shamir today). Paper alone is
8221
+ * rejected because under managed mode the user has no memorized
8222
+ * passphrase, so losing the paper sheet = losing every record.
8223
+ * This check is unconditional — independent of `requireRecovery`
8224
+ * and the `recover-passphrase` gate.
8225
+ *
8226
+ * 2. **Opt-in strict mandatory-recovery.** When
8227
+ * `requireRecovery: true` is set on createNoydb (and the gate is
8228
+ * not explicitly disabled), require ANY recovery profile (paper
8229
+ * or shamir). This is the v0.x default-off behavior; v1.0 may
8230
+ * flip it default-on.
8231
+ *
8232
+ * The managed-mode check fires from {@link bootstrapPolicy} unless
8233
+ * the `skipManagedCheck` flag is set (used by
8234
+ * {@link openVaultAndEnrollRecovery} to allow atomic create-and-enroll).
8235
+ */
8236
+ async assertRecoveryEnrolled(vault, policy, opts) {
8237
+ const skipManaged = (opts?.skipManagedCheck ?? false) || this._skipNextManagedRecoveryCheck;
8238
+ if (this.options.passphraseMode === "managed" && !skipManaged) {
8239
+ const enrolled2 = await hasStrongRecoveryEnrolled(this.options.store, vault);
8240
+ if (!enrolled2) {
8241
+ throw new ManagedRecoveryNotEnrolledError(vault);
8242
+ }
8243
+ }
7076
8244
  if (this.options.requireRecovery !== true) return;
7077
8245
  const gate = policy.gates["recover-passphrase"];
7078
8246
  if (gate?.enabled === false) return;
@@ -7101,9 +8269,9 @@ var Noydb = class {
7101
8269
  * Gated by `enroll-authenticator`; `presented` carries any factor
7102
8270
  * proofs the active policy demands.
7103
8271
  */
7104
- async enrollAuthenticator(vault, options, presented) {
7105
- await this.checkGate(vault, "enroll-authenticator", presented);
7106
- const keyring = await this.getKeyring(vault);
8272
+ async enrollAuthenticator(vault, options, factors) {
8273
+ await this.checkGate(vault, "enroll-authenticator", factors);
8274
+ const keyring = await this.getKeyringInternal(vault);
7107
8275
  const next = await enrollAuthenticator(this.options.store, vault, keyring, options);
7108
8276
  this.keyringCache.set(vault, next);
7109
8277
  }
@@ -7112,15 +8280,15 @@ var Noydb = class {
7112
8280
  * non-existent slot is a successful no-op. Gated by
7113
8281
  * `remove-authenticator`.
7114
8282
  */
7115
- async removeAuthenticator(vault, slotId, presented) {
7116
- await this.checkGate(vault, "remove-authenticator", presented);
7117
- const keyring = await this.getKeyring(vault);
8283
+ async removeAuthenticator(vault, slotId, factors) {
8284
+ await this.checkGate(vault, "remove-authenticator", factors);
8285
+ const keyring = await this.getKeyringInternal(vault);
7118
8286
  const next = await removeAuthenticator(this.options.store, vault, keyring, slotId);
7119
8287
  this.keyringCache.set(vault, next);
7120
8288
  }
7121
8289
  /** Read the slot list for a vault. Internal — `describeAuthConfig` (#13) consumes this. */
7122
8290
  async listAuthenticators(vault) {
7123
- const keyring = await this.getKeyring(vault);
8291
+ const keyring = await this.getKeyringInternal(vault);
7124
8292
  return keyring.authenticators;
7125
8293
  }
7126
8294
  /**
@@ -7151,9 +8319,9 @@ var Noydb = class {
7151
8319
  *
7152
8320
  * @see #55
7153
8321
  */
7154
- async updateAuthenticator(vault, slotId, options, presented) {
7155
- await this.checkGate(vault, "update-authenticator", presented);
7156
- const keyring = await this.getKeyring(vault);
8322
+ async updateAuthenticator(vault, slotId, options, factors) {
8323
+ await this.checkGate(vault, "update-authenticator", factors);
8324
+ const keyring = await this.getKeyringInternal(vault);
7157
8325
  const next = await updateAuthenticator(this.options.store, vault, keyring, slotId, options);
7158
8326
  this.keyringCache.set(vault, next);
7159
8327
  }
@@ -7204,9 +8372,9 @@ var Noydb = class {
7204
8372
  *
7205
8373
  * @see #16
7206
8374
  */
7207
- async enrollWebAuthn(vault, ceremony, presented) {
7208
- await this.checkGate(vault, "enroll-authenticator", presented);
7209
- const keyring = await this.getKeyring(vault);
8375
+ async enrollWebAuthn(vault, ceremony, factors) {
8376
+ await this.checkGate(vault, "enroll-authenticator", factors);
8377
+ const keyring = await this.getKeyringInternal(vault);
7210
8378
  const slotOptions = await ceremony(keyring);
7211
8379
  if (slotOptions.method !== "webauthn") {
7212
8380
  throw new ValidationError(
@@ -7233,7 +8401,7 @@ var Noydb = class {
7233
8401
  * @see #16
7234
8402
  */
7235
8403
  async listWebAuthnSlots(vault) {
7236
- const keyring = await this.getKeyring(vault);
8404
+ const keyring = await this.getKeyringInternal(vault);
7237
8405
  return keyring.authenticators.filter((a) => a.method === "webauthn").map((a) => {
7238
8406
  const credentialId = a.meta.credentialId;
7239
8407
  return {
@@ -7253,7 +8421,7 @@ var Noydb = class {
7253
8421
  * `checkGate` calls see a tier-2 unlock.
7254
8422
  */
7255
8423
  async unlockViaAuthenticator(vault, slotId, verify) {
7256
- const keyring = await this.getKeyring(vault);
8424
+ const keyring = await this.getKeyringInternal(vault);
7257
8425
  const slot = findAuthenticator(keyring, slotId);
7258
8426
  if (!slot) {
7259
8427
  throw new ValidationError(
@@ -7351,6 +8519,14 @@ var Noydb = class {
7351
8519
  * @throws `InvalidKeyError` when `oldPassphrase` is wrong.
7352
8520
  */
7353
8521
  async rotatePassphrase(vault, input, factors) {
8522
+ if (this.options.passphraseMode === "managed") {
8523
+ throw new PolicyDeniedError(
8524
+ "rotate-passphrase",
8525
+ "disabled",
8526
+ { minTier: 1, enabled: false },
8527
+ "Managed-passphrase mode (#14): the passphrase is hub-generated and sealed under the SealingKeyProvider \u2014 there is no plaintext to rotate. Use the recovery flow (follow-up issue) to mint a fresh sealed passphrase."
8528
+ );
8529
+ }
7354
8530
  await this.checkGate(vault, "rotate-passphrase", factors);
7355
8531
  const userId = this.options.user;
7356
8532
  const next = await rotatePassphrase(this.options.store, vault, userId, input);
@@ -7367,7 +8543,7 @@ var Noydb = class {
7367
8543
  await this.checkGate(vault, "recover-passphrase", factors);
7368
8544
  const userId = this.options.user;
7369
8545
  const entriesBeforeRecovery = await loadPaperRecoveryEntries(this.options.store, vault);
7370
- const next = await recoverPassphrase(this.options.store, vault, userId, input);
8546
+ const next = await recoverPassphrase(this.options.shamirRecovery, this.options.store, vault, userId, input);
7371
8547
  this.keyringCache.set(vault, next);
7372
8548
  const rotateRemaining = input.rotateRemainingCodes ?? true;
7373
8549
  const remainingAfterBurn = Math.max(0, entriesBeforeRecovery.length - 1);
@@ -7387,6 +8563,256 @@ var Noydb = class {
7387
8563
  await savePaperRecoveryEntries(this.options.store, vault, newEntries);
7388
8564
  return { newCodes: codes };
7389
8565
  }
8566
+ /**
8567
+ * Deliberate paper-recovery-code regeneration (#121). User knows their
8568
+ * passphrase but wants a fresh sheet — they lost the printout or
8569
+ * suspect compromise of the off-site copy.
8570
+ *
8571
+ * Symmetric to {@link rotatePassphrase} for the recovery profile:
8572
+ * gated, audit-trackable, ergonomic. Replaces (not appends) the
8573
+ * paper sheet under `_meta/recovery-paper` in a single envelope `put`.
8574
+ *
8575
+ * Gated by the `rotate-recovery` policy gate:
8576
+ * - PERSONAL_POLICY: `{ minTier: 1 }` — knowing the passphrase
8577
+ * suffices, matching the pre-#121 low-level flow's bar.
8578
+ * - STRICT_POLICY: `{ minTier: 1, factors: [{ anyOf: ['totp',
8579
+ * 'email-otp', 'webauthn-roaming'] }] }` — rotation is an
8580
+ * off-site-trust event; require an off-device factor so a
8581
+ * stolen unlocked laptop cannot silently mint a sheet for the
8582
+ * attacker.
8583
+ *
8584
+ * Defaults `count` to the existing sheet size so consumers aren't
8585
+ * surprised by a different code count. Explicit `count` overrides.
8586
+ *
8587
+ * @throws {@link RecoveryProfileNotImplementedError} when `profile`
8588
+ * is anything other than `'paper'` (v1 dispatch limit).
8589
+ * @throws {@link PolicyDeniedError} when the gate denies (missing
8590
+ * factor, tier mismatch, ...).
8591
+ * @throws on missing paper sheet — "nothing to rotate" surfaces as
8592
+ * an error rather than silently minting an entire new sheet.
8593
+ *
8594
+ * @example Default count + show-once UI
8595
+ * ```ts
8596
+ * const { newCodes } = await db.rotateRecovery('acme', { profile: 'paper' })
8597
+ * showCodesToUser(newCodes)
8598
+ * ```
8599
+ *
8600
+ * @example STRICT-policy site with TOTP factor proof
8601
+ * ```ts
8602
+ * await db.rotateRecovery(
8603
+ * 'acme',
8604
+ * { profile: 'paper', count: 10 },
8605
+ * { factors: [{ kind: 'totp', proof: '123456' }] },
8606
+ * )
8607
+ * ```
8608
+ */
8609
+ async rotateRecovery(vault, options, factors) {
8610
+ if (options.profile === "paper") {
8611
+ return this.rotateRecoveryPaper(vault, options, factors);
8612
+ }
8613
+ if (options.profile === "shamir") {
8614
+ return this.rotateRecoveryShamir(vault, options, factors);
8615
+ }
8616
+ throw new RecoveryProfileNotImplementedError(
8617
+ options.profile,
8618
+ "#196"
8619
+ );
8620
+ }
8621
+ async rotateRecoveryPaper(vault, options, factors) {
8622
+ await this.checkGate(vault, "rotate-recovery", factors);
8623
+ const existing = await loadPaperRecoveryEntries(this.options.store, vault);
8624
+ if (existing.length === 0) {
8625
+ throw new Error(
8626
+ `db.rotateRecovery: no recovery codes are enrolled for vault "${vault}". Call db.enrollRecovery({ profile: 'paper', entries }) first; rotateRecovery replaces an existing sheet rather than minting one from scratch.`
8627
+ );
8628
+ }
8629
+ const keyring = await this.getKeyring(vault);
8630
+ const codeGen = options.codeGenerator ?? generateULID;
8631
+ const count2 = options.count ?? existing.length;
8632
+ const codes = [];
8633
+ const newEntries = [];
8634
+ for (let i = 0; i < count2; i++) {
8635
+ const rawCode = codeGen();
8636
+ const entry = await mintPaperRecoveryEntry(keyring.deks, rawCode, generateULID());
8637
+ codes.push(rawCode);
8638
+ newEntries.push(entry);
8639
+ }
8640
+ await savePaperRecoveryEntries(this.options.store, vault, newEntries);
8641
+ return { newCodes: codes, entryId: "paper-batch" };
8642
+ }
8643
+ async rotateRecoveryShamir(vault, options, factors) {
8644
+ await this.checkGate(vault, "rotate-recovery", factors);
8645
+ const existing = await loadShamirRecoveryEntries(this.options.store, vault);
8646
+ if (existing.length === 0) {
8647
+ throw new Error(
8648
+ `db.rotateRecovery: no Shamir recovery entry is enrolled for vault "${vault}". Call db.enrollRecovery({ profile: 'shamir', k, n }) first; rotateRecovery replaces an existing entry rather than minting one from scratch.`
8649
+ );
8650
+ }
8651
+ let targetEntryId;
8652
+ if (options.entryId !== void 0) {
8653
+ const found = existing.find((e) => e.entryId === options.entryId);
8654
+ if (!found) {
8655
+ throw new Error(
8656
+ `db.rotateRecovery: no Shamir entry with entryId="${options.entryId}" found in vault "${vault}". Available: ${existing.map((e) => `"${e.entryId}"`).join(", ")}.`
8657
+ );
8658
+ }
8659
+ targetEntryId = options.entryId;
8660
+ } else {
8661
+ if (existing.length > 1) {
8662
+ throw new Error(
8663
+ `db.rotateRecovery: vault "${vault}" has ${existing.length} Shamir entries enrolled (${existing.map((e) => `"${e.entryId}"`).join(", ")}). Pass \`entryId\` to disambiguate which one to rotate; ambiguous rotation would risk replacing the wrong entry.`
8664
+ );
8665
+ }
8666
+ targetEntryId = existing[0].entryId;
8667
+ }
8668
+ const keyring = await this.getKeyring(vault);
8669
+ const { entry, shareStrings } = await mintShamirRecoveryEntry(
8670
+ this.requireShamirProvider(),
8671
+ keyring.deks,
8672
+ targetEntryId,
8673
+ options.k,
8674
+ options.n,
8675
+ options.label
8676
+ );
8677
+ const next = existing.filter((e) => e.entryId !== targetEntryId).concat(entry);
8678
+ await saveShamirRecoveryEntries(this.options.store, vault, next);
8679
+ return { newShares: shareStrings, entryId: targetEntryId };
8680
+ }
8681
+ /**
8682
+ * **Atomic create-and-enroll for managed-mode vaults (#195).**
8683
+ *
8684
+ * Bootstraps a managed-mode vault and enrolls strong recovery in
8685
+ * a single ceremony. Under `passphraseMode: 'managed'`, every
8686
+ * `openVault` call requires a strong recovery profile (Shamir
8687
+ * today) to be enrolled — otherwise it throws
8688
+ * {@link ManagedRecoveryNotEnrolledError}. This method bypasses
8689
+ * the check temporarily so the keyring can be created, enrolls
8690
+ * the supplied recovery profile(s), then returns the vault.
8691
+ *
8692
+ * For Shamir enrollments, the show-once share strings come back
8693
+ * in `recoveryEnrollments[i].shares`. The hub never retains them
8694
+ * — the caller MUST display them to the user (once) before any
8695
+ * subsequent operation.
8696
+ *
8697
+ * Paper alone is NOT a strong profile under managed mode; passing
8698
+ * `{ profile: 'paper', ... }` without an accompanying shamir entry
8699
+ * is rejected at validation time.
8700
+ *
8701
+ * ```ts
8702
+ * const db = await createNoydb({
8703
+ * store, user: 'alice',
8704
+ * passphraseMode: 'managed',
8705
+ * sealingKey: macosKeychainSealingProvider({ ... }),
8706
+ * })
8707
+ *
8708
+ * const { vault, recoveryEnrollments } = await db.openVaultAndEnrollRecovery('acme', {
8709
+ * recovery: [{ profile: 'shamir', k: 2, n: 3 }],
8710
+ * })
8711
+ * for (const r of recoveryEnrollments) {
8712
+ * if (r.shares) showSharesToUser(r.shares) // ONCE
8713
+ * }
8714
+ * ```
8715
+ *
8716
+ * @throws ValidationError if recovery is empty, or contains no
8717
+ * strong profile under managed mode.
8718
+ */
8719
+ async openVaultAndEnrollRecovery(vault, opts) {
8720
+ if (opts.recovery.length === 0) {
8721
+ throw new ValidationError(
8722
+ "openVaultAndEnrollRecovery: at least one recovery enrollment is required."
8723
+ );
8724
+ }
8725
+ if (this.options.passphraseMode === "managed") {
8726
+ const hasStrong = opts.recovery.some((r) => r.profile === "shamir");
8727
+ if (!hasStrong) {
8728
+ throw new ValidationError(
8729
+ 'openVaultAndEnrollRecovery: managed-mode vaults require at least one strong recovery profile in the `recovery` array. Paper alone is not strong under managed mode (no user passphrase to fall back on). Include { profile: "shamir", k, n } in `recovery`.'
8730
+ );
8731
+ }
8732
+ }
8733
+ this._skipNextManagedRecoveryCheck = true;
8734
+ let vaultHandle;
8735
+ try {
8736
+ vaultHandle = await this.openVault(vault, opts.locale !== void 0 ? { locale: opts.locale } : void 0);
8737
+ } finally {
8738
+ this._skipNextManagedRecoveryCheck = false;
8739
+ }
8740
+ const recoveryEnrollments = [];
8741
+ for (const enrollment of opts.recovery) {
8742
+ recoveryEnrollments.push(await this.enrollRecovery(vault, enrollment));
8743
+ }
8744
+ if (this.options.passphraseMode === "managed") {
8745
+ const policy = this.policyCache.get(vault);
8746
+ if (policy) {
8747
+ await this.assertRecoveryEnrolled(vault, policy);
8748
+ }
8749
+ }
8750
+ return { vault: vaultHandle, recoveryEnrollments };
8751
+ }
8752
+ /**
8753
+ * **Recovery flow under managed-passphrase mode (#195).**
8754
+ *
8755
+ * Replaces the sealed passphrase of a managed-mode vault with a
8756
+ * fresh 256-bit random, sealed under the configured
8757
+ * `SealingKeyProvider`. The user never sees the new passphrase.
8758
+ *
8759
+ * Internally:
8760
+ * 1. Verify the recovery proof (Shamir today) and unwrap the
8761
+ * DEK set.
8762
+ * 2. Mint a fresh 256-bit random as the new effective passphrase.
8763
+ * 3. Rewrap the DEK set under a fresh KEK derived from the new
8764
+ * passphrase (via the existing `recoverPassphrase` path).
8765
+ * 4. Seal the random bytes under the provider and overwrite
8766
+ * `_meta/sealed-passphrase`.
8767
+ * 5. Drop the keyring cache so the next operation re-derives.
8768
+ *
8769
+ * The vault's strong-recovery enrollment is preserved across
8770
+ * recovery (Shamir entries are not burned on use — see #196).
8771
+ *
8772
+ * @throws ValidationError if the Noydb instance is not in managed mode.
8773
+ */
8774
+ async recoverManagedPassphrase(vault, options) {
8775
+ if (this.options.passphraseMode !== "managed") {
8776
+ throw new ValidationError(
8777
+ "recoverManagedPassphrase: this method only applies to vaults opened in managed-passphrase mode. For standard mode, use db.recoverPassphrase."
8778
+ );
8779
+ }
8780
+ const provider = this.options.sealingKey;
8781
+ if (!provider) {
8782
+ throw new ValidationError(
8783
+ 'recoverManagedPassphrase: createNoydb({ passphraseMode: "managed" }) requires `sealingKey` to be supplied; without it the new sealed passphrase cannot be persisted.'
8784
+ );
8785
+ }
8786
+ const randomBytes = new Uint8Array(32);
8787
+ globalThis.crypto.getRandomValues(randomBytes);
8788
+ let binary = "";
8789
+ for (let i = 0; i < randomBytes.length; i++) binary += String.fromCharCode(randomBytes[i]);
8790
+ const newPassphrase = btoa(binary);
8791
+ try {
8792
+ const sealed = await provider.seal(randomBytes);
8793
+ await recoverPassphrase(
8794
+ this.options.shamirRecovery,
8795
+ this.options.store,
8796
+ vault,
8797
+ this.options.user,
8798
+ {
8799
+ newPassphrase,
8800
+ recoveryProof: options.recoveryProof,
8801
+ // The new passphrase IS 256 bits of random; policy gates on
8802
+ // length/entropy don't apply.
8803
+ allowWeakPassphrase: true,
8804
+ ...options.passphrasePolicy !== void 0 ? { passphrasePolicy: options.passphrasePolicy } : {}
8805
+ }
8806
+ );
8807
+ await saveSealedPassphrase(this.options.store, vault, {
8808
+ providerId: provider.id,
8809
+ sealed
8810
+ });
8811
+ } finally {
8812
+ randomBytes.fill(0);
8813
+ }
8814
+ this.keyringCache.delete(vault);
8815
+ }
7390
8816
  /**
7391
8817
  * Atomic peer-recovery — re-wraps an EXISTING user's keyring under
7392
8818
  * a fresh temp passphrase in a single store write. Closes #34's
@@ -7432,7 +8858,7 @@ var Noydb = class {
7432
8858
  */
7433
8859
  async recoverUser(vault, options, factors) {
7434
8860
  await this.checkGate(vault, "peer-recover-user", factors);
7435
- const callerKeyring = await this.getKeyring(vault);
8861
+ const callerKeyring = await this.getKeyringInternal(vault);
7436
8862
  await recoverUser(this.options.store, vault, callerKeyring, options);
7437
8863
  if (options.userId === this.options.user) {
7438
8864
  this.keyringCache.delete(vault);
@@ -7470,21 +8896,40 @@ var Noydb = class {
7470
8896
  * ```
7471
8897
  */
7472
8898
  async enrollRecovery(vault, enrollment) {
7473
- if (enrollment.profile !== "paper") {
7474
- throw new ValidationError(
7475
- `enrollRecovery: only 'paper' is implemented in v0.1.0-pre.5. Profile '${enrollment.profile}' is tracked under issue #10.`
8899
+ if (enrollment.profile === "paper") {
8900
+ const existing = await loadPaperRecoveryEntries(this.options.store, vault);
8901
+ await savePaperRecoveryEntries(this.options.store, vault, [
8902
+ ...existing,
8903
+ ...enrollment.entries
8904
+ ]);
8905
+ return { entryId: "paper-batch" };
8906
+ }
8907
+ if (enrollment.profile === "shamir") {
8908
+ const keyring = await this.getKeyring(vault);
8909
+ const entryId = enrollment.entryId ?? generateULID();
8910
+ const { entry, shareStrings } = await mintShamirRecoveryEntry(
8911
+ this.requireShamirProvider(),
8912
+ keyring.deks,
8913
+ entryId,
8914
+ enrollment.k,
8915
+ enrollment.n,
8916
+ enrollment.label
7476
8917
  );
7477
- }
7478
- const existing = await loadPaperRecoveryEntries(this.options.store, vault);
7479
- await savePaperRecoveryEntries(this.options.store, vault, [
7480
- ...existing,
7481
- ...enrollment.entries
7482
- ]);
8918
+ const existing = await loadShamirRecoveryEntries(this.options.store, vault);
8919
+ const next = existing.filter((e) => e.entryId !== entryId).concat(entry);
8920
+ await saveShamirRecoveryEntries(this.options.store, vault, next);
8921
+ return { entryId, shares: shareStrings };
8922
+ }
8923
+ throw new RecoveryProfileNotImplementedError(
8924
+ enrollment.profile,
8925
+ "#196"
8926
+ );
7483
8927
  }
7484
- /** Read the persisted paper-recovery entries. Used by `describeAuthConfig` (#13). */
8928
+ /** Read the persisted recovery entries (paper + Shamir). Used by `describeAuthConfig` (#13). */
7485
8929
  async listRecoveryEntries(vault) {
7486
8930
  const paper = await loadPaperRecoveryEntries(this.options.store, vault);
7487
- return { paper };
8931
+ const shamir = await loadShamirRecoveryEntries(this.options.store, vault);
8932
+ return { paper, shamir };
7488
8933
  }
7489
8934
  // ─── Tier-3 enroll / unlock (issue #11) ────────────────────────
7490
8935
  /**
@@ -7496,8 +8941,8 @@ var Noydb = class {
7496
8941
  * Gated by `rotate-unlock` (the same gate covers "set" and "rotate"
7497
8942
  * because tier-3 is a single-slot rolling secret).
7498
8943
  */
7499
- async enrollUnlock(vault, state, presented) {
7500
- await this.checkGate(vault, "rotate-unlock", presented);
8944
+ async enrollUnlock(vault, state, factors) {
8945
+ await this.checkGate(vault, "rotate-unlock", factors);
7501
8946
  this.quickUnlock.set(vault, state);
7502
8947
  }
7503
8948
  /**
@@ -7524,8 +8969,17 @@ var Noydb = class {
7524
8969
  /**
7525
8970
  * Public accessor for the unlocked keyring of a vault — issue #28.
7526
8971
  *
7527
- * Returns the cached `UnlockedKeyring` (already in memory after
7528
- * `createNoydb` + first vault touch); loads it on demand if absent.
8972
+ * Returns a **defensive shallow copy** so consumers can read the DEK
8973
+ * map and authenticator list without the risk of mutating the hub's
8974
+ * internal cache (#88). Internal hub code paths use a live reference
8975
+ * via `getKeyringInternal`; ceremonies and external consumers always
8976
+ * get a snapshot.
8977
+ *
8978
+ * The CryptoKey values inside `deks` are not cloned — Web Crypto
8979
+ * keys are opaque handles, and a shared handle is intentional
8980
+ * (encrypt / decrypt go through the same key the cache holds).
8981
+ * Only the container Map / authenticator array is fresh.
8982
+ *
7529
8983
  * Used by `@noy-db/on-*` ceremonies that need the live DEK set
7530
8984
  * (paper recovery via {@link mintPaperRecoveryEntry}, tier-3 PIN
7531
8985
  * enrolment via on-pin's `enrollPin`, custom on-* ceremonies that
@@ -7540,11 +8994,33 @@ var Noydb = class {
7540
8994
  * ```ts
7541
8995
  * const keyring = await db.getKeyring('acme')
7542
8996
  * // keyring.deks: Map<collection, CryptoKey>
7543
- * // keyring.kek: CryptoKey (non-extractable; null for tier-3 sessions)
8997
+ * // keyring.kek: CryptoKey | null (null for tier-3 / wrap-DEKs sessions)
7544
8998
  * // keyring.role / .permissions / .authenticators
7545
8999
  * ```
7546
9000
  */
7547
9001
  async getKeyring(vault) {
9002
+ const live = await this.getKeyringInternal(vault);
9003
+ return {
9004
+ ...live,
9005
+ deks: new Map(live.deks),
9006
+ permissions: { ...live.permissions },
9007
+ authenticators: live.authenticators.map((a) => ({
9008
+ ...a,
9009
+ meta: { ...a.meta }
9010
+ })),
9011
+ ...live.policy !== void 0 ? { policy: { ...live.policy } } : {},
9012
+ ...live.exportCapability !== void 0 ? { exportCapability: { ...live.exportCapability } } : {},
9013
+ ...live.importCapability !== void 0 ? { importCapability: { ...live.importCapability } } : {}
9014
+ };
9015
+ }
9016
+ /**
9017
+ * Live-reference variant used by the hub's own code paths. Internal
9018
+ * mutations on `deks` (e.g. {@link ensureCollectionDEK} adding a
9019
+ * collection key) need to land on the cached keyring so subsequent
9020
+ * accesses see them. Not exposed publicly — callers outside hub
9021
+ * should use {@link getKeyring}, which returns a defensive copy.
9022
+ */
9023
+ async getKeyringInternal(vault) {
7548
9024
  if (this.options.encrypt === false) {
7549
9025
  return createPlaintextKeyring(this.options.user);
7550
9026
  }
@@ -7555,20 +9031,36 @@ var Noydb = class {
7555
9031
  this.keyringCache.set(vault, keyring2);
7556
9032
  return keyring2;
7557
9033
  }
7558
- if (!this.options.secret) {
9034
+ let effectiveSecret;
9035
+ if (this.options.passphraseMode === "managed") {
9036
+ effectiveSecret = await resolveManagedSecret(
9037
+ this.options.store,
9038
+ vault,
9039
+ this.options.sealingKey
9040
+ );
9041
+ } else {
9042
+ effectiveSecret = this.options.secret;
9043
+ }
9044
+ if (!effectiveSecret) {
7559
9045
  throw new ValidationError("A secret (passphrase) or getKeyring callback is required when encryption is enabled");
7560
9046
  }
7561
9047
  let keyring;
7562
9048
  try {
7563
- keyring = await loadKeyring(this.options.store, vault, this.options.user, this.options.secret);
9049
+ keyring = await loadKeyring(this.options.store, vault, this.options.user, effectiveSecret);
7564
9050
  } catch (err) {
7565
9051
  if (err instanceof NoAccessError) {
7566
9052
  keyring = await createOwnerKeyring(
7567
9053
  this.options.store,
7568
9054
  vault,
7569
9055
  this.options.user,
7570
- this.options.secret,
7571
- { validate: this.options.validatePassphrase === true }
9056
+ effectiveSecret,
9057
+ {
9058
+ // Managed mode generates 256-bit base64 strings that don't satisfy
9059
+ // the human-passphrase strength rules (no spaces, no "words").
9060
+ // Skip validation in managed mode — the entropy floor is already
9061
+ // 256 bits by construction.
9062
+ validate: this.options.passphraseMode === "managed" ? false : this.options.validatePassphrase === true
9063
+ }
7572
9064
  );
7573
9065
  } else if (err instanceof InvalidKeyError && this.options.onInvalidKey === "reset") {
7574
9066
  await this.options.store.delete(vault, "_keyring", this.options.user);
@@ -7576,8 +9068,10 @@ var Noydb = class {
7576
9068
  this.options.store,
7577
9069
  vault,
7578
9070
  this.options.user,
7579
- this.options.secret,
7580
- { validate: this.options.validatePassphrase === true }
9071
+ effectiveSecret,
9072
+ {
9073
+ validate: this.options.passphraseMode === "managed" ? false : this.options.validatePassphrase === true
9074
+ }
7581
9075
  );
7582
9076
  } else {
7583
9077
  throw err;
@@ -7589,10 +9083,28 @@ var Noydb = class {
7589
9083
  };
7590
9084
  async function createNoydb(options) {
7591
9085
  const encrypted = options.encrypt !== false;
9086
+ const managed = options.passphraseMode === "managed";
7592
9087
  if (options.secret && options.getKeyring) {
7593
9088
  throw new ValidationError("Provide either `secret` or `getKeyring`, not both");
7594
9089
  }
7595
- if (encrypted && !options.secret && !options.getKeyring) {
9090
+ if (managed) {
9091
+ if (options.secret) {
9092
+ throw new ValidationError(
9093
+ '`passphraseMode: "managed"` is mutually exclusive with `secret` \u2014 managed mode generates the passphrase itself. Drop `secret`.'
9094
+ );
9095
+ }
9096
+ if (options.getKeyring) {
9097
+ throw new ValidationError(
9098
+ '`passphraseMode: "managed"` is mutually exclusive with `getKeyring` \u2014 a custom unlock callback would bypass the sealing flow. Drop `getKeyring`.'
9099
+ );
9100
+ }
9101
+ if (!options.sealingKey) {
9102
+ throw new ValidationError(
9103
+ '`passphraseMode: "managed"` requires `sealingKey: SealingKeyProvider` (see @noy-db/seal-macos-keychain / @noy-db/seal-aws-kms / etc.).'
9104
+ );
9105
+ }
9106
+ }
9107
+ if (encrypted && !managed && !options.secret && !options.getKeyring) {
7596
9108
  throw new ValidationError("A secret (passphrase) or getKeyring callback is required when encryption is enabled");
7597
9109
  }
7598
9110
  return new Noydb(options);
@@ -7760,6 +9272,7 @@ function shortJSON(value) {
7760
9272
  export {
7761
9273
  Aggregation,
7762
9274
  AlreadyElevatedError,
9275
+ AmendmentForbiddenError,
7763
9276
  BLOB_CHUNKS_COLLECTION,
7764
9277
  BLOB_COLLECTION,
7765
9278
  BLOB_INDEX_COLLECTION,
@@ -7770,6 +9283,7 @@ export {
7770
9283
  BackupLedgerError,
7771
9284
  BlobSet,
7772
9285
  BundleIntegrityError,
9286
+ BundleSealMismatchError,
7773
9287
  BundleVersionConflictError,
7774
9288
  CONSENT_AUDIT_COLLECTION,
7775
9289
  Collection,
@@ -7783,28 +9297,39 @@ export {
7783
9297
  DEFAULT_PUBLIC_ENVELOPE_SCHEMA,
7784
9298
  DELEGATIONS_COLLECTION,
7785
9299
  DICT_COLLECTION_PREFIX,
9300
+ DIRECTORY_RECORD_ID,
7786
9301
  DanglingReferenceError,
7787
9302
  DecryptionError,
7788
9303
  DelegationTargetMissingError,
9304
+ DerivationCapExceededError,
9305
+ DerivationCycleError,
9306
+ DerivationDepthError,
9307
+ DerivationOutputShapeError,
9308
+ DerivationOutputUnknownError,
7789
9309
  DictKeyInUseError,
7790
9310
  DictKeyMissingError,
7791
9311
  DictionaryHandle,
9312
+ DirectoryDisabledError,
7792
9313
  ELEVATION_AUDIT_COLLECTION,
7793
9314
  ElevatedHandle,
7794
9315
  ElevationExpiredError,
7795
9316
  ExportCapabilityError,
9317
+ FieldFrozenError,
7796
9318
  FilenameSanitizationError,
7797
9319
  GROUPBY_MAX_CARDINALITY,
7798
9320
  GROUPBY_WARN_CARDINALITY,
7799
9321
  GroupCardinalityError,
7800
9322
  GroupedAggregation,
7801
9323
  GroupedQuery,
9324
+ GroupedQueryN,
7802
9325
  INDEXED_STORE_POLICY,
7803
9326
  ImportCapabilityError,
7804
9327
  IndexRequiredError,
7805
9328
  IndexWriteFailureError,
7806
9329
  InvalidKeyError,
9330
+ InvariantError,
7807
9331
  JoinTooLargeError,
9332
+ KeyringCorruptError,
7808
9333
  KeyringExpiredError,
7809
9334
  LEDGER_COLLECTION,
7810
9335
  LEDGER_DELTAS_COLLECTION,
@@ -7816,6 +9341,12 @@ export {
7816
9341
  MAGIC_LINK_GRANTS_COLLECTION,
7817
9342
  MAGIC_LINK_KEK_INFO_PREFIX,
7818
9343
  META_COLLECTION,
9344
+ ManagedRecoveryNotEnrolledError,
9345
+ MaterializedViewConfigError,
9346
+ MaterializedViewCycleError,
9347
+ MaterializedViewSourceUnknownError,
9348
+ MaterializedViewTooLargeError,
9349
+ MemorySealingKeyProvider,
7819
9350
  MissingTranslationError,
7820
9351
  NOYDB_BACKUP_VERSION,
7821
9352
  NOYDB_BUNDLE_FORMAT_VERSION,
@@ -7829,6 +9360,10 @@ export {
7829
9360
  NotFoundError,
7830
9361
  Noydb,
7831
9362
  NoydbError,
9363
+ OverlayBaseIsVirtualError,
9364
+ OverlayCollectionUnavailableError,
9365
+ OverlayIdMismatchError,
9366
+ OverlayNameCollisionError,
7832
9367
  PERIODS_COLLECTION,
7833
9368
  PERSONAL_POLICY,
7834
9369
  POLICY_RECORD_ID,
@@ -7846,12 +9381,15 @@ export {
7846
9381
  ReadOnlyAtInstantError,
7847
9382
  ReadOnlyError,
7848
9383
  ReadOnlyFrameError,
9384
+ RecordLockedError,
7849
9385
  RecoveryNotEnrolledError,
7850
9386
  RecoveryProfileNotImplementedError,
7851
9387
  RefIntegrityError,
7852
9388
  RefRegistry,
7853
9389
  RefScopeError,
7854
9390
  ReservedCollectionNameError,
9391
+ SCHEMAS_COLLECTION,
9392
+ SEALED_PASSPHRASE_RECORD_ID,
7855
9393
  STRICT_POLICY,
7856
9394
  SYNC_CREDENTIALS_COLLECTION,
7857
9395
  ScanBuilder,
@@ -7874,6 +9412,7 @@ export {
7874
9412
  USER_ENVELOPE_MAX_BYTES,
7875
9413
  UserApi,
7876
9414
  UserEnvelopeOversizedError,
9415
+ VISIBILITY_RECORD_PREFIX,
7877
9416
  ValidationError,
7878
9417
  Vault,
7879
9418
  VaultFrame,
@@ -7907,7 +9446,9 @@ export {
7907
9446
  dekKey,
7908
9447
  deleteCredential,
7909
9448
  deleteUserEnvelope,
9449
+ deleteUserVisibility,
7910
9450
  deriveMagicLinkContentKey,
9451
+ derivePersistedSchema,
7911
9452
  derivePresenceKey,
7912
9453
  describeAllUsersAuth,
7913
9454
  describeAuthConfig,
@@ -7953,6 +9494,7 @@ export {
7953
9494
  isPublicEnvelope,
7954
9495
  isSessionAlive,
7955
9496
  isULID,
9497
+ isZodSchema,
7956
9498
  issueDelegation,
7957
9499
  recoverPassphrase as keyringRecoverPassphrase,
7958
9500
  rotatePassphrase as keyringRotatePassphrase,
@@ -7964,7 +9506,10 @@ export {
7964
9506
  loadActiveDelegations,
7965
9507
  loadDevUnlock,
7966
9508
  loadPaperRecoveryEntries,
9509
+ loadPersistedSchema,
7967
9510
  loadPublicEnvelope,
9511
+ loadSealedPassphrase,
9512
+ loadShamirRecoveryEntries,
7968
9513
  loadUserEnvelope,
7969
9514
  loadVaultPolicy,
7970
9515
  magicLinkGrantRecordId,
@@ -7973,17 +9518,24 @@ export {
7973
9518
  mergePolicy,
7974
9519
  min,
7975
9520
  mintPaperRecoveryEntry,
9521
+ mintShamirRecoveryEntry,
7976
9522
  mintWrappedDeksBlob,
7977
9523
  paddedIndex,
7978
9524
  parseBytes,
7979
9525
  parseIndex,
9526
+ parseSealedEnvelope,
9527
+ persistDirectoryConfig,
9528
+ persistSchemaIfNeeded,
9529
+ persistUserVisibility,
7980
9530
  putCredential,
9531
+ readDirectoryConfig,
7981
9532
  readMagicLinkGrantRecord,
7982
9533
  readNoydbBundle,
7983
9534
  readNoydbBundleHeader,
7984
9535
  readNoydbBundlePublicEnvelope,
7985
9536
  readPath,
7986
9537
  readPublicEnvelope,
9538
+ readUserVisibility,
7987
9539
  recoverUser,
7988
9540
  reduceRecords,
7989
9541
  ref,
@@ -8001,13 +9553,17 @@ export {
8001
9553
  routeStore,
8002
9554
  runTransaction,
8003
9555
  savePaperRecoveryEntries,
9556
+ savePersistedSchema,
8004
9557
  savePublicEnvelope,
9558
+ saveSealedPassphrase,
9559
+ saveShamirRecoveryEntries,
8005
9560
  saveUserEnvelope,
8006
9561
  saveVaultPolicy,
8007
- sha256Hex,
9562
+ sha256Hex2 as sha256Hex,
8008
9563
  sum,
8009
9564
  unwrapDeksFromBlob,
8010
9565
  unwrapDeksFromPaperEntry,
9566
+ unwrapDeksFromShamirEntry,
8011
9567
  unwrapMagicLinkGrant,
8012
9568
  validateI18nTextValue,
8013
9569
  validatePassphrase,
@@ -8015,11 +9571,16 @@ export {
8015
9571
  validateSchemaInput,
8016
9572
  validateSchemaOutput,
8017
9573
  validateSessionPolicy,
9574
+ visibilityRecordId,
8018
9575
  withCache,
8019
9576
  withCircuitBreaker,
9577
+ withDerivation,
9578
+ withGuard,
8020
9579
  withHealthCheck,
8021
9580
  withLogging,
9581
+ withMaterializedView,
8022
9582
  withMetrics,
9583
+ withOverlayedView,
8023
9584
  withRetry,
8024
9585
  wrapBundleStore,
8025
9586
  wrapStore,