@noy-db/hub 0.2.0-pre.16 → 0.2.0-pre.18

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 (306) hide show
  1. package/dist/aggregate/index.cjs.map +1 -1
  2. package/dist/aggregate/index.d.cts +3 -2
  3. package/dist/aggregate/index.d.ts +3 -2
  4. package/dist/aggregate/index.js +4 -4
  5. package/dist/attestation/index.cjs.map +1 -1
  6. package/dist/attestation/index.d.cts +5 -3
  7. package/dist/attestation/index.d.ts +5 -3
  8. package/dist/attestation/index.js +6 -6
  9. package/dist/blobs/index.cjs +226 -11
  10. package/dist/blobs/index.cjs.map +1 -1
  11. package/dist/blobs/index.d.cts +6 -4
  12. package/dist/blobs/index.d.ts +6 -4
  13. package/dist/blobs/index.js +6 -5
  14. package/dist/blobs/index.js.map +1 -1
  15. package/dist/bundle/index.cjs +2065 -352
  16. package/dist/bundle/index.cjs.map +1 -1
  17. package/dist/bundle/index.d.cts +7 -5
  18. package/dist/bundle/index.d.ts +7 -5
  19. package/dist/bundle/index.js +21 -10
  20. package/dist/bundle/index.js.map +1 -1
  21. package/dist/{chunk-6RR3MNMG.js → chunk-2U226RDC.js} +3 -3
  22. package/dist/{chunk-L2BNJ6HM.js → chunk-32XVU2LT.js} +3 -3
  23. package/dist/{chunk-X73VS74Y.js → chunk-33DAO2XG.js} +2 -2
  24. package/dist/chunk-45643PAU.js +151 -0
  25. package/dist/chunk-45643PAU.js.map +1 -0
  26. package/dist/{chunk-QSUK7YWK.js → chunk-4UI5T3K7.js} +4 -4
  27. package/dist/{chunk-G4SCICH5.js → chunk-5KKNBDCT.js} +2 -2
  28. package/dist/{chunk-DUREQF5W.js → chunk-647TFNYL.js} +34 -8
  29. package/dist/chunk-647TFNYL.js.map +1 -0
  30. package/dist/{chunk-E2CDVKMH.js → chunk-6FHCU3QO.js} +5 -5
  31. package/dist/{chunk-F4OJZIWQ.js → chunk-6Q5XRLKG.js} +4 -4
  32. package/dist/{chunk-HOR4R722.js → chunk-6XEGHIBA.js} +30 -4
  33. package/dist/chunk-6XEGHIBA.js.map +1 -0
  34. package/dist/{chunk-4TBBMHVC.js → chunk-6YEC7LLO.js} +2 -2
  35. package/dist/{chunk-ZNQYHJXX.js → chunk-AB7JF2KF.js} +2 -2
  36. package/dist/{chunk-UMLVJTYV.js → chunk-ADB7GPM3.js} +7 -4
  37. package/dist/chunk-ADB7GPM3.js.map +1 -0
  38. package/dist/{chunk-XL35NSEN.js → chunk-BUBJYIZ7.js} +3 -3
  39. package/dist/chunk-C2OYWD5S.js +125 -0
  40. package/dist/chunk-C2OYWD5S.js.map +1 -0
  41. package/dist/{chunk-KABJXG2F.js → chunk-CMISAJAE.js} +195 -17
  42. package/dist/chunk-CMISAJAE.js.map +1 -0
  43. package/dist/{chunk-3YWP3WBP.js → chunk-DKMPR76W.js} +5 -5
  44. package/dist/{chunk-BI6ETQPF.js → chunk-DR5I7Q6N.js} +4 -4
  45. package/dist/{chunk-667MB6AH.js → chunk-F2IJ2HGD.js} +1370 -232
  46. package/dist/chunk-F2IJ2HGD.js.map +1 -0
  47. package/dist/{chunk-6H2ZUNR7.js → chunk-FQRAYDS4.js} +4 -4
  48. package/dist/{chunk-535SSHBS.js → chunk-HMFC6M2G.js} +99 -2
  49. package/dist/chunk-HMFC6M2G.js.map +1 -0
  50. package/dist/{chunk-TS26M2SB.js → chunk-HOO5I3VG.js} +2 -2
  51. package/dist/{chunk-OMAMZKKD.js → chunk-HWK75CYX.js} +2 -2
  52. package/dist/{chunk-TKIY625R.js → chunk-HZOEBM67.js} +2 -2
  53. package/dist/{chunk-DLZ2ONOD.js → chunk-IQ4GMEYZ.js} +6 -6
  54. package/dist/{chunk-XWH4MXIU.js → chunk-K3NYRK7U.js} +2 -2
  55. package/dist/{chunk-7BQ4QWYX.js → chunk-KOURQXIU.js} +23 -6
  56. package/dist/chunk-KOURQXIU.js.map +1 -0
  57. package/dist/{chunk-7Z7KSVA5.js → chunk-KQ523X3A.js} +15 -2
  58. package/dist/chunk-KQ523X3A.js.map +1 -0
  59. package/dist/{chunk-JD3OZAI4.js → chunk-KTZ2MHQK.js} +2 -2
  60. package/dist/{chunk-F3BPIPLS.js → chunk-LGPSCKWZ.js} +1 -1
  61. package/dist/chunk-LGPSCKWZ.js.map +1 -0
  62. package/dist/{chunk-SCJPI4Z5.js → chunk-LQ3GD5LL.js} +5 -5
  63. package/dist/{chunk-AAVWKNZW.js → chunk-M3H7VSRV.js} +2 -2
  64. package/dist/{chunk-BR3AMFGS.js → chunk-MGB67HKX.js} +5 -5
  65. package/dist/{chunk-GNI5STXQ.js → chunk-P57D4KBG.js} +52 -38
  66. package/dist/chunk-P57D4KBG.js.map +1 -0
  67. package/dist/{chunk-Z6FNBOTC.js → chunk-PDVP3C2I.js} +1 -1
  68. package/dist/{chunk-Z6FNBOTC.js.map → chunk-PDVP3C2I.js.map} +1 -1
  69. package/dist/{chunk-OB2ZJQ2D.js → chunk-PGVEL5IZ.js} +3 -3
  70. package/dist/{chunk-YULZKK4F.js → chunk-QJKZ5WUP.js} +37 -2
  71. package/dist/chunk-QJKZ5WUP.js.map +1 -0
  72. package/dist/{chunk-BQ65SS5A.js → chunk-QPJ7Z4L3.js} +2 -2
  73. package/dist/{chunk-CZI2A4MQ.js → chunk-RQFG2YSV.js} +3 -3
  74. package/dist/{chunk-CJORTUJ2.js → chunk-RZWQNMMP.js} +2 -2
  75. package/dist/{chunk-FFXM3ZIF.js → chunk-T4T5I5L6.js} +3 -3
  76. package/dist/{chunk-QVIEAYTP.js → chunk-TFAN3NFD.js} +3 -3
  77. package/dist/{chunk-Z4DO7YSI.js → chunk-TPOHMOGX.js} +2 -2
  78. package/dist/{chunk-VLMPU56Q.js → chunk-TTS3RWL5.js} +2 -2
  79. package/dist/{chunk-IXBIFDEW.js → chunk-VVDSDOVV.js} +4 -4
  80. package/dist/{chunk-FWPKCXTN.js → chunk-WZCG3EZ6.js} +2 -2
  81. package/dist/{chunk-HBXJ37ZY.js → chunk-Y5XVB75E.js} +4 -4
  82. package/dist/chunk-YWYW2YNO.js +129 -0
  83. package/dist/chunk-YWYW2YNO.js.map +1 -0
  84. package/dist/{chunk-IQLVUT37.js → chunk-Z3BE5BRK.js} +2 -2
  85. package/dist/{chunk-42FEUPZQ.js → chunk-Z3I2WNGF.js} +58 -3
  86. package/dist/chunk-Z3I2WNGF.js.map +1 -0
  87. package/dist/{state-vault-TMXZRTY5.js → chunk-ZJ67TB4S.js} +24 -7
  88. package/dist/chunk-ZJ67TB4S.js.map +1 -0
  89. package/dist/consent/index.cjs.map +1 -1
  90. package/dist/consent/index.d.cts +6 -4
  91. package/dist/consent/index.d.ts +6 -4
  92. package/dist/consent/index.js +3 -3
  93. package/dist/{crypto-QXQOHMHF.js → crypto-FNK3XPCS.js} +7 -3
  94. package/dist/{delegation-NIQ43IPU.js → delegation-FMXNUWE6.js} +5 -5
  95. package/dist/derivations/index.cjs +82 -2
  96. package/dist/derivations/index.cjs.map +1 -1
  97. package/dist/derivations/index.d.cts +7 -5
  98. package/dist/derivations/index.d.ts +7 -5
  99. package/dist/derivations/index.js +8 -6
  100. package/dist/{dev-unlock-8XzcD2Z4.d.cts → dev-unlock-3_2b_vo6.d.cts} +1 -1
  101. package/dist/{dev-unlock-DR3upLd1.d.ts → dev-unlock-BMvwPr_E.d.ts} +1 -1
  102. package/dist/{strategy-BtW8fAjz.d.cts → errors-DUTlAt3Y.d.cts} +113 -727
  103. package/dist/{strategy-BtW8fAjz.d.ts → errors-DUTlAt3Y.d.ts} +113 -727
  104. package/dist/executor-IZ2NVXCY.js +11 -0
  105. package/dist/executor-THSEYEJG.js +8 -0
  106. package/dist/executor-WLFDUTOM.js +8 -0
  107. package/dist/{fanout-sidecar-67CMI3UT.js → fanout-sidecar-JGHXAJO5.js} +2 -2
  108. package/dist/forget/index.cjs +43 -0
  109. package/dist/forget/index.cjs.map +1 -0
  110. package/dist/forget/index.d.cts +1 -0
  111. package/dist/forget/index.d.ts +1 -0
  112. package/dist/forget/index.js +14 -0
  113. package/dist/guards/index.cjs +80 -3
  114. package/dist/guards/index.cjs.map +1 -1
  115. package/dist/guards/index.d.cts +7 -5
  116. package/dist/guards/index.d.ts +7 -5
  117. package/dist/guards/index.js +10 -6
  118. package/dist/{hash-CDjye9KV.d.ts → hash-BThBJFO1.d.ts} +1 -1
  119. package/dist/{hash-DuQ88_5W.d.cts → hash-BnWnL9bQ.d.cts} +1 -1
  120. package/dist/history/index.cjs +27 -4
  121. package/dist/history/index.cjs.map +1 -1
  122. package/dist/history/index.d.cts +7 -5
  123. package/dist/history/index.d.ts +7 -5
  124. package/dist/history/index.js +9 -7
  125. package/dist/history/index.js.map +1 -1
  126. package/dist/i18n/index.cjs +53 -0
  127. package/dist/i18n/index.cjs.map +1 -1
  128. package/dist/i18n/index.d.cts +6 -4
  129. package/dist/i18n/index.d.ts +6 -4
  130. package/dist/i18n/index.js +16 -8
  131. package/dist/i18n/index.js.map +1 -1
  132. package/dist/index-C-SSRIxP.d.cts +348 -0
  133. package/dist/index-C-SSRIxP.d.ts +348 -0
  134. package/dist/{index-C8Bk3-VF.d.cts → index-C6lgoUhK.d.cts} +47 -2
  135. package/dist/{index-nP99bXLg.d.ts → index-DP1JTWHZ.d.ts} +47 -2
  136. package/dist/index.cjs +3280 -1208
  137. package/dist/index.cjs.map +1 -1
  138. package/dist/index.d.cts +15 -12
  139. package/dist/index.d.ts +15 -12
  140. package/dist/index.js +149 -107
  141. package/dist/index.js.map +1 -1
  142. package/dist/indexing/index.cjs.map +1 -1
  143. package/dist/indexing/index.js +4 -4
  144. package/dist/issue-R2MWQO6K.js +12 -0
  145. package/dist/{ledger-A3LL253R.js → ledger-GXC2YA3A.js} +6 -6
  146. package/dist/materialized-views/index.cjs.map +1 -1
  147. package/dist/materialized-views/index.d.cts +7 -5
  148. package/dist/materialized-views/index.d.ts +7 -5
  149. package/dist/materialized-views/index.js +12 -12
  150. package/dist/noydb-RJL6FQ4B.js +37 -0
  151. package/dist/overlay-views/index.cjs.map +1 -1
  152. package/dist/overlay-views/index.d.cts +7 -5
  153. package/dist/overlay-views/index.d.ts +7 -5
  154. package/dist/overlay-views/index.js +4 -4
  155. package/dist/periods/index.cjs.map +1 -1
  156. package/dist/periods/index.d.cts +6 -4
  157. package/dist/periods/index.d.ts +6 -4
  158. package/dist/periods/index.js +6 -6
  159. package/dist/{public-envelope-YP2UWMLG.js → public-envelope-HXOFHY4N.js} +4 -4
  160. package/dist/query/index.cjs +30 -4
  161. package/dist/query/index.cjs.map +1 -1
  162. package/dist/query/index.d.cts +3 -2
  163. package/dist/query/index.d.ts +3 -2
  164. package/dist/query/index.js +6 -6
  165. package/dist/read-only-facade-EX6WZZBP.js +7 -0
  166. package/dist/registry-3T2RZC5A.js +8 -0
  167. package/dist/registry-DMS7OKBM.js +8 -0
  168. package/dist/{registry-UTA4CLQS.js → registry-WVXO6NH5.js} +3 -3
  169. package/dist/{revoke-HNMQZSCL.js → revoke-7LCWE2AH.js} +6 -6
  170. package/dist/sealed-record/index.cjs +139 -0
  171. package/dist/sealed-record/index.cjs.map +1 -0
  172. package/dist/sealed-record/index.d.cts +123 -0
  173. package/dist/sealed-record/index.d.ts +123 -0
  174. package/dist/sealed-record/index.js +42 -0
  175. package/dist/sealed-record/index.js.map +1 -0
  176. package/dist/session/index.cjs.map +1 -1
  177. package/dist/session/index.d.cts +7 -5
  178. package/dist/session/index.d.ts +7 -5
  179. package/dist/session/index.js +3 -3
  180. package/dist/shadow/index.cjs.map +1 -1
  181. package/dist/shadow/index.d.cts +6 -4
  182. package/dist/shadow/index.d.ts +6 -4
  183. package/dist/shadow/index.js +2 -2
  184. package/dist/{signer-DCMNKXSF.js → signer-HAVDLGOK.js} +5 -5
  185. package/dist/snapshots/index.cjs.map +1 -1
  186. package/dist/snapshots/index.d.cts +6 -4
  187. package/dist/snapshots/index.d.ts +6 -4
  188. package/dist/snapshots/index.js +4 -4
  189. package/dist/{stale-W5PQTRYH.js → stale-PGTEGJDI.js} +2 -2
  190. package/dist/stale-PGTEGJDI.js.map +1 -0
  191. package/dist/state-vault-QKQKN3H3.js +14 -0
  192. package/dist/state-vault-QKQKN3H3.js.map +1 -0
  193. package/dist/store/index.cjs.map +1 -1
  194. package/dist/store/index.d.cts +6 -4
  195. package/dist/store/index.d.ts +6 -4
  196. package/dist/store/index.js +2 -2
  197. package/dist/strategy-Diwh5lzS.d.ts +739 -0
  198. package/dist/strategy-nuyN8K5N.d.cts +739 -0
  199. package/dist/sync/index.cjs.map +1 -1
  200. package/dist/sync/index.d.cts +5 -3
  201. package/dist/sync/index.d.ts +5 -3
  202. package/dist/sync/index.js +4 -4
  203. package/dist/team/index.cjs.map +1 -1
  204. package/dist/team/index.d.cts +6 -4
  205. package/dist/team/index.d.ts +6 -4
  206. package/dist/team/index.js +8 -8
  207. package/dist/transition-guard--t3exQHF.d.cts +165 -0
  208. package/dist/transition-guard-BlI9Oy5K.d.ts +165 -0
  209. package/dist/tx/index.cjs.map +1 -1
  210. package/dist/tx/index.d.cts +6 -4
  211. package/dist/tx/index.d.ts +6 -4
  212. package/dist/tx/index.js +3 -3
  213. package/dist/{types-Bze6vkwm.d.cts → types-BpLPqyaO.d.cts} +1264 -513
  214. package/dist/{types-DrmBTscX.d.ts → types-Diqc2caK.d.ts} +1264 -513
  215. package/dist/{ulid-DbBVrNSt.d.ts → ulid-B1zNV8r9.d.ts} +1 -1
  216. package/dist/{ulid-DfZlAh0u.d.cts → ulid-DNiRB4Mx.d.cts} +1 -1
  217. package/dist/util/index.cjs.map +1 -1
  218. package/dist/util/index.js +1 -1
  219. package/dist/{vault-group-DX2HFQMX.js → vault-group-DPZVFRI5.js} +182 -6
  220. package/dist/vault-group-DPZVFRI5.js.map +1 -0
  221. package/dist/{with-materialized-view--4PsvMDu.d.cts → with-materialized-view-BdH_A_r6.d.cts} +1 -1
  222. package/dist/{with-materialized-view-QT1Tp7NO.d.ts → with-materialized-view-CzAgp_HJ.d.ts} +1 -1
  223. package/dist/{with-overlayed-view-BEXfpzSb.d.ts → with-overlayed-view-BJbqQnsR.d.ts} +1 -1
  224. package/dist/{with-overlayed-view-DlH5qmeB.d.cts → with-overlayed-view-C40rDPlu.d.cts} +1 -1
  225. package/dist/with-rollup-Bopu5UDZ.d.cts +47 -0
  226. package/dist/with-rollup-DrlGkxiE.d.ts +47 -0
  227. package/package.json +23 -3
  228. package/dist/chunk-42FEUPZQ.js.map +0 -1
  229. package/dist/chunk-535SSHBS.js.map +0 -1
  230. package/dist/chunk-667MB6AH.js.map +0 -1
  231. package/dist/chunk-7BQ4QWYX.js.map +0 -1
  232. package/dist/chunk-7Z7KSVA5.js.map +0 -1
  233. package/dist/chunk-DUREQF5W.js.map +0 -1
  234. package/dist/chunk-F3BPIPLS.js.map +0 -1
  235. package/dist/chunk-GNI5STXQ.js.map +0 -1
  236. package/dist/chunk-HOR4R722.js.map +0 -1
  237. package/dist/chunk-KABJXG2F.js.map +0 -1
  238. package/dist/chunk-OQSRJG6A.js +0 -63
  239. package/dist/chunk-OQSRJG6A.js.map +0 -1
  240. package/dist/chunk-UMLVJTYV.js.map +0 -1
  241. package/dist/chunk-YULZKK4F.js.map +0 -1
  242. package/dist/executor-6ZDSDZ6V.js +0 -8
  243. package/dist/executor-AZLS3KBK.js +0 -11
  244. package/dist/executor-IDZDAFNH.js +0 -8
  245. package/dist/immutable-guard-CRPvu24K.d.cts +0 -82
  246. package/dist/immutable-guard-Dov3WvwF.d.ts +0 -82
  247. package/dist/issue-RZP3VI6O.js +0 -12
  248. package/dist/noydb-WCMY2ZOW.js +0 -35
  249. package/dist/read-only-facade-ITU6L7BL.js +0 -7
  250. package/dist/registry-EB6SISTA.js +0 -8
  251. package/dist/registry-IUZQVVBB.js +0 -8
  252. package/dist/state-vault-TMXZRTY5.js.map +0 -1
  253. package/dist/vault-group-DX2HFQMX.js.map +0 -1
  254. package/dist/with-derivation-CCqAchD5.d.cts +0 -13
  255. package/dist/with-derivation-_lySGdlm.d.ts +0 -13
  256. /package/dist/{chunk-6RR3MNMG.js.map → chunk-2U226RDC.js.map} +0 -0
  257. /package/dist/{chunk-L2BNJ6HM.js.map → chunk-32XVU2LT.js.map} +0 -0
  258. /package/dist/{chunk-X73VS74Y.js.map → chunk-33DAO2XG.js.map} +0 -0
  259. /package/dist/{chunk-QSUK7YWK.js.map → chunk-4UI5T3K7.js.map} +0 -0
  260. /package/dist/{chunk-G4SCICH5.js.map → chunk-5KKNBDCT.js.map} +0 -0
  261. /package/dist/{chunk-E2CDVKMH.js.map → chunk-6FHCU3QO.js.map} +0 -0
  262. /package/dist/{chunk-F4OJZIWQ.js.map → chunk-6Q5XRLKG.js.map} +0 -0
  263. /package/dist/{chunk-4TBBMHVC.js.map → chunk-6YEC7LLO.js.map} +0 -0
  264. /package/dist/{chunk-ZNQYHJXX.js.map → chunk-AB7JF2KF.js.map} +0 -0
  265. /package/dist/{chunk-XL35NSEN.js.map → chunk-BUBJYIZ7.js.map} +0 -0
  266. /package/dist/{chunk-3YWP3WBP.js.map → chunk-DKMPR76W.js.map} +0 -0
  267. /package/dist/{chunk-BI6ETQPF.js.map → chunk-DR5I7Q6N.js.map} +0 -0
  268. /package/dist/{chunk-6H2ZUNR7.js.map → chunk-FQRAYDS4.js.map} +0 -0
  269. /package/dist/{chunk-TS26M2SB.js.map → chunk-HOO5I3VG.js.map} +0 -0
  270. /package/dist/{chunk-OMAMZKKD.js.map → chunk-HWK75CYX.js.map} +0 -0
  271. /package/dist/{chunk-TKIY625R.js.map → chunk-HZOEBM67.js.map} +0 -0
  272. /package/dist/{chunk-DLZ2ONOD.js.map → chunk-IQ4GMEYZ.js.map} +0 -0
  273. /package/dist/{chunk-XWH4MXIU.js.map → chunk-K3NYRK7U.js.map} +0 -0
  274. /package/dist/{chunk-JD3OZAI4.js.map → chunk-KTZ2MHQK.js.map} +0 -0
  275. /package/dist/{chunk-SCJPI4Z5.js.map → chunk-LQ3GD5LL.js.map} +0 -0
  276. /package/dist/{chunk-AAVWKNZW.js.map → chunk-M3H7VSRV.js.map} +0 -0
  277. /package/dist/{chunk-BR3AMFGS.js.map → chunk-MGB67HKX.js.map} +0 -0
  278. /package/dist/{chunk-OB2ZJQ2D.js.map → chunk-PGVEL5IZ.js.map} +0 -0
  279. /package/dist/{chunk-BQ65SS5A.js.map → chunk-QPJ7Z4L3.js.map} +0 -0
  280. /package/dist/{chunk-CZI2A4MQ.js.map → chunk-RQFG2YSV.js.map} +0 -0
  281. /package/dist/{chunk-CJORTUJ2.js.map → chunk-RZWQNMMP.js.map} +0 -0
  282. /package/dist/{chunk-FFXM3ZIF.js.map → chunk-T4T5I5L6.js.map} +0 -0
  283. /package/dist/{chunk-QVIEAYTP.js.map → chunk-TFAN3NFD.js.map} +0 -0
  284. /package/dist/{chunk-Z4DO7YSI.js.map → chunk-TPOHMOGX.js.map} +0 -0
  285. /package/dist/{chunk-VLMPU56Q.js.map → chunk-TTS3RWL5.js.map} +0 -0
  286. /package/dist/{chunk-IXBIFDEW.js.map → chunk-VVDSDOVV.js.map} +0 -0
  287. /package/dist/{chunk-FWPKCXTN.js.map → chunk-WZCG3EZ6.js.map} +0 -0
  288. /package/dist/{chunk-HBXJ37ZY.js.map → chunk-Y5XVB75E.js.map} +0 -0
  289. /package/dist/{chunk-IQLVUT37.js.map → chunk-Z3BE5BRK.js.map} +0 -0
  290. /package/dist/{crypto-QXQOHMHF.js.map → crypto-FNK3XPCS.js.map} +0 -0
  291. /package/dist/{delegation-NIQ43IPU.js.map → delegation-FMXNUWE6.js.map} +0 -0
  292. /package/dist/{executor-6ZDSDZ6V.js.map → executor-IZ2NVXCY.js.map} +0 -0
  293. /package/dist/{executor-AZLS3KBK.js.map → executor-THSEYEJG.js.map} +0 -0
  294. /package/dist/{executor-IDZDAFNH.js.map → executor-WLFDUTOM.js.map} +0 -0
  295. /package/dist/{fanout-sidecar-67CMI3UT.js.map → fanout-sidecar-JGHXAJO5.js.map} +0 -0
  296. /package/dist/{issue-RZP3VI6O.js.map → forget/index.js.map} +0 -0
  297. /package/dist/{ledger-A3LL253R.js.map → issue-R2MWQO6K.js.map} +0 -0
  298. /package/dist/{noydb-WCMY2ZOW.js.map → ledger-GXC2YA3A.js.map} +0 -0
  299. /package/dist/{public-envelope-YP2UWMLG.js.map → noydb-RJL6FQ4B.js.map} +0 -0
  300. /package/dist/{read-only-facade-ITU6L7BL.js.map → public-envelope-HXOFHY4N.js.map} +0 -0
  301. /package/dist/{registry-EB6SISTA.js.map → read-only-facade-EX6WZZBP.js.map} +0 -0
  302. /package/dist/{registry-IUZQVVBB.js.map → registry-3T2RZC5A.js.map} +0 -0
  303. /package/dist/{registry-UTA4CLQS.js.map → registry-DMS7OKBM.js.map} +0 -0
  304. /package/dist/{revoke-HNMQZSCL.js.map → registry-WVXO6NH5.js.map} +0 -0
  305. /package/dist/{signer-DCMNKXSF.js.map → revoke-7LCWE2AH.js.map} +0 -0
  306. /package/dist/{stale-W5PQTRYH.js.map → signer-HAVDLGOK.js.map} +0 -0
@@ -7,37 +7,68 @@ import {
7
7
  import {
8
8
  TxContext,
9
9
  revertExecuted
10
- } from "./chunk-QVIEAYTP.js";
10
+ } from "./chunk-TFAN3NFD.js";
11
11
  import {
12
12
  OverlayedCollection
13
- } from "./chunk-VLMPU56Q.js";
13
+ } from "./chunk-TTS3RWL5.js";
14
+ import {
15
+ NO_AGGREGATE,
16
+ Query,
17
+ ScanBuilder,
18
+ canonicalizeIncomingMoney,
19
+ canonicalizeStoredMoney,
20
+ decodeMoneyFields,
21
+ quantizeMoneyFields,
22
+ validateMoneyFieldPaths
23
+ } from "./chunk-647TFNYL.js";
24
+ import {
25
+ EXPORT_AUDIT_COLLECTION,
26
+ createExportBlobsHandle,
27
+ runCompaction
28
+ } from "./chunk-BUBJYIZ7.js";
14
29
  import {
15
30
  LazyQuery,
16
31
  decodeIdxId,
17
32
  encodeIdxId
18
- } from "./chunk-CZI2A4MQ.js";
33
+ } from "./chunk-RQFG2YSV.js";
34
+ import {
35
+ canonicalGroupKey
36
+ } from "./chunk-32XVU2LT.js";
37
+ import {
38
+ readPath
39
+ } from "./chunk-RZWQNMMP.js";
19
40
  import {
20
41
  SCHEMAS_COLLECTION,
21
42
  loadPersistedSchema,
22
43
  resolveManagedSecret,
23
44
  savePersistedSchema,
24
45
  saveSealedPassphrase
25
- } from "./chunk-GNI5STXQ.js";
46
+ } from "./chunk-P57D4KBG.js";
26
47
  import {
27
48
  loadPublicEnvelope,
28
49
  readPublicEnvelope,
29
50
  savePublicEnvelope,
30
51
  validatePublicEnvelopeInput
31
- } from "./chunk-OB2ZJQ2D.js";
52
+ } from "./chunk-PGVEL5IZ.js";
53
+ import {
54
+ buildTombstone,
55
+ isTombstone,
56
+ resolveStableCek,
57
+ revokeSealedRecord,
58
+ rewrapBodyToDek,
59
+ rotateRecordCek,
60
+ sealRecordToHost
61
+ } from "./chunk-45643PAU.js";
32
62
  import {
33
63
  PERIODS_COLLECTION
34
- } from "./chunk-QSUK7YWK.js";
64
+ } from "./chunk-4UI5T3K7.js";
35
65
  import {
36
66
  getAtPath,
37
67
  isDictCollectionName,
68
+ isStaticDictDescriptor,
38
69
  resolvePolicy,
39
70
  setAtPathInPlace
40
- } from "./chunk-7BQ4QWYX.js";
71
+ } from "./chunk-KOURQXIU.js";
41
72
  import {
42
73
  ManagedRecoveryNotEnrolledError,
43
74
  PolicyDeniedError,
@@ -59,11 +90,11 @@ import {
59
90
  saveShamirRecoveryEntries,
60
91
  updateAuthenticator,
61
92
  writeMagicLinkGrant
62
- } from "./chunk-DLZ2ONOD.js";
93
+ } from "./chunk-IQ4GMEYZ.js";
63
94
  import {
64
95
  assertTierAccess,
65
96
  dekKey
66
- } from "./chunk-4TBBMHVC.js";
97
+ } from "./chunk-6YEC7LLO.js";
67
98
  import {
68
99
  USER_ENVELOPE_COLLECTION,
69
100
  assertKeyringOpenAllowed,
@@ -88,7 +119,7 @@ import {
88
119
  rotateKeys,
89
120
  saveUserEnvelope,
90
121
  updateKeyringIdentity
91
- } from "./chunk-6H2ZUNR7.js";
122
+ } from "./chunk-FQRAYDS4.js";
92
123
  import {
93
124
  INDEXED_STORE_POLICY
94
125
  } from "./chunk-2QR2PQTT.js";
@@ -98,49 +129,41 @@ import {
98
129
  import {
99
130
  LEDGER_COLLECTION,
100
131
  LEDGER_DELTAS_COLLECTION
101
- } from "./chunk-BR3AMFGS.js";
132
+ } from "./chunk-MGB67HKX.js";
102
133
  import {
103
134
  sha256Hex as sha256Hex2
104
- } from "./chunk-Z6FNBOTC.js";
105
- import {
106
- NO_AGGREGATE,
107
- Query,
108
- ScanBuilder,
109
- canonicalizeIncomingMoney,
110
- canonicalizeStoredMoney,
111
- decodeMoneyFields,
112
- quantizeMoneyFields,
113
- validateMoneyFieldPaths
114
- } from "./chunk-DUREQF5W.js";
115
- import {
116
- canonicalGroupKey
117
- } from "./chunk-L2BNJ6HM.js";
135
+ } from "./chunk-PDVP3C2I.js";
118
136
  import {
119
- readPath
120
- } from "./chunk-CJORTUJ2.js";
121
- import {
122
- EXPORT_AUDIT_COLLECTION,
123
- createExportBlobsHandle,
124
- runCompaction
125
- } from "./chunk-XL35NSEN.js";
137
+ NO_FORGET,
138
+ addSubjectRef,
139
+ coerceSubjectId,
140
+ lookupSubject,
141
+ readDottedPath,
142
+ rebuildSubjectIndex,
143
+ removeSubjectRef
144
+ } from "./chunk-C2OYWD5S.js";
126
145
  import {
127
146
  NOYDB_BACKUP_VERSION,
128
147
  NOYDB_FORMAT_VERSION
129
- } from "./chunk-F3BPIPLS.js";
148
+ } from "./chunk-LGPSCKWZ.js";
130
149
  import {
131
150
  decrypt,
132
151
  encrypt,
133
152
  encryptDeterministic,
134
- sha256Hex
135
- } from "./chunk-YULZKK4F.js";
153
+ sha256Hex,
154
+ unwrapCek,
155
+ wrapCek
156
+ } from "./chunk-QJKZ5WUP.js";
136
157
  import {
137
158
  AlreadyElevatedError,
138
159
  AttestationError,
139
160
  BackupCorruptedError,
140
161
  BackupLedgerError,
141
162
  ConflictError,
163
+ DerivationCapExceededError,
142
164
  ElevationExpiredError,
143
165
  ExportCapabilityError,
166
+ ForgetStrategyNotConfiguredError,
144
167
  ImportCapabilityError,
145
168
  IndexWriteFailureError,
146
169
  InvalidKeyError,
@@ -159,15 +182,17 @@ import {
159
182
  SchemaValidationError,
160
183
  SequenceContentionError,
161
184
  SequenceOfflineError,
185
+ StaticDictReadonlyError,
162
186
  StoreCapabilityError,
163
187
  TierDemoteDeniedError,
164
188
  TierNotGrantedError,
165
189
  TranslatorNotConfiguredError,
166
190
  UniqueConstraintError,
191
+ UnknownDictCodeError,
167
192
  UnsupportedIndexOptionError,
168
193
  ValidationError,
169
194
  VaultTemplateNotFoundError
170
- } from "./chunk-535SSHBS.js";
195
+ } from "./chunk-HMFC6M2G.js";
171
196
 
172
197
  // src/policy/storage.ts
173
198
  var META_COLLECTION = "_meta";
@@ -450,6 +475,9 @@ var NO_HISTORY = {
450
475
  async clearHistory() {
451
476
  return 0;
452
477
  },
478
+ async tombstoneHistory() {
479
+ return 0;
480
+ },
453
481
  async envelopePayloadHash() {
454
482
  return "";
455
483
  },
@@ -810,7 +838,7 @@ async function resolveStaleOnRead(accessor, outputCollection, id) {
810
838
  }
811
839
  const sourceWithId = { ...source, id };
812
840
  if (DerivationExecutor === null) {
813
- ({ DerivationExecutor } = await import("./executor-6ZDSDZ6V.js"));
841
+ ({ DerivationExecutor } = await import("./executor-WLFDUTOM.js"));
814
842
  }
815
843
  const ctx = { vault: accessor.getReadOnlyFacade() };
816
844
  const result = await DerivationExecutor.run(spec, sourceWithId, 0, strategyHash, ctx);
@@ -848,6 +876,15 @@ async function resolveStaleOnRead(accessor, outputCollection, id) {
848
876
  }
849
877
 
850
878
  // src/collection.ts
879
+ function selfWriteFieldEqual(a, b) {
880
+ if (a === b) return true;
881
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
882
+ try {
883
+ return JSON.stringify(a) === JSON.stringify(b);
884
+ } catch {
885
+ return false;
886
+ }
887
+ }
851
888
  var fallbackWarned = /* @__PURE__ */ new Set();
852
889
  function warnOnceFallback(adapterName) {
853
890
  if (fallbackWarned.has(adapterName)) return;
@@ -1035,6 +1072,25 @@ var Collection = class {
1035
1072
  * is inactive for this collection; a frozen `Set` otherwise.
1036
1073
  */
1037
1074
  deterministicFields;
1075
+ /**
1076
+ * Per-record CEK opt-in (`perRecordKeys: true`). When set, writes mint /
1077
+ * reuse a per-record content-encryption key and stamp `_cek` on the
1078
+ * envelope (see {@link EncryptedEnvelope._cek}). OFF by default — a
1079
+ * non-adopting collection takes the byte-identical legacy path. The READ
1080
+ * path does not consult this flag: `_cek` presence on the envelope is the
1081
+ * format discriminant, so a mixed vault (and a recipient that never set the
1082
+ * flag) still decrypts CEK records.
1083
+ */
1084
+ perRecordCek;
1085
+ /**
1086
+ * Session-scoped `(id) → CEK` cache for this collection. Lets updates
1087
+ * reuse a record's stable CEK and lets repeated reads skip the AES-KW
1088
+ * unwrap. Bounded by LRU; never persisted. Dropped when the owning
1089
+ * collection instance is discarded — `vault.load()` clears the
1090
+ * collectionCache, so a keyring refresh drops every CEK alongside the
1091
+ * DEK cache. `null` unless `perRecordCek` is set.
1092
+ */
1093
+ cekCache;
1038
1094
  /**
1039
1095
  * declared tiers for this collection. `null` when
1040
1096
  * tier-aware methods are disabled. Tier 0 is implicit and never
@@ -1181,19 +1237,24 @@ var Collection = class {
1181
1237
  } else {
1182
1238
  this.deterministicFields = null;
1183
1239
  }
1240
+ this.perRecordCek = opts.perRecordKeys === true;
1241
+ this.cekCache = this.perRecordCek ? new Lru({ maxRecords: 4096 }) : null;
1184
1242
  if (opts.crdt && opts.onRegisterConflictResolver) {
1185
1243
  const crdtMode = opts.crdt;
1186
- const crdtResolver = async (_id, local, remote) => {
1244
+ const crdtResolver = async (id, local, remote) => {
1187
1245
  if (crdtMode === "yjs") {
1188
1246
  return local._v >= remote._v ? local : remote;
1189
1247
  }
1190
- const localJson = await this.decryptJsonString(local);
1191
- const remoteJson = await this.decryptJsonString(remote);
1248
+ const localJson = await this.decryptJsonString(local, id);
1249
+ const remoteJson = await this.decryptJsonString(remote, id);
1250
+ if (localJson === null) return local;
1251
+ if (remoteJson === null) return remote;
1192
1252
  const localState = JSON.parse(localJson);
1193
1253
  const remoteState = JSON.parse(remoteJson);
1194
1254
  const merged = this.crdtStrategy.mergeCrdtStates(localState, remoteState);
1195
1255
  const mergedVersion = Math.max(local._v, remote._v) + 1;
1196
- return this.encryptJsonString(JSON.stringify(merged), mergedVersion);
1256
+ const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
1257
+ return this.encryptJsonString(JSON.stringify(merged), mergedVersion, cek);
1197
1258
  };
1198
1259
  opts.onRegisterConflictResolver(this.name, crdtResolver);
1199
1260
  }
@@ -1233,12 +1294,15 @@ var Collection = class {
1233
1294
  });
1234
1295
  } else {
1235
1296
  const mergeFn = policy;
1236
- resolver = async (_id, local, remote) => {
1237
- const localRecord = await this.decryptRecord(local, { skipValidation: true });
1238
- const remoteRecord = await this.decryptRecord(remote, { skipValidation: true });
1297
+ resolver = async (id, local, remote) => {
1298
+ const localRecord = await this.decryptRecord(local, { skipValidation: true, id });
1299
+ const remoteRecord = await this.decryptRecord(remote, { skipValidation: true, id });
1300
+ if (localRecord === null) return local;
1301
+ if (remoteRecord === null) return remote;
1239
1302
  const merged = mergeFn(localRecord, remoteRecord);
1240
1303
  const mergedVersion = Math.max(local._v, remote._v) + 1;
1241
- return this.encryptRecord(merged, mergedVersion);
1304
+ const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
1305
+ return this.encryptRecord(merged, mergedVersion, cek);
1242
1306
  };
1243
1307
  }
1244
1308
  opts.onRegisterConflictResolver(collectionName, resolver);
@@ -1319,7 +1383,7 @@ var Collection = class {
1319
1383
  }
1320
1384
  }
1321
1385
  if (this.materializedViewSource !== void 0) {
1322
- const { resolveStaleMVOnRead } = await import("./stale-W5PQTRYH.js");
1386
+ const { resolveStaleMVOnRead } = await import("./stale-PGTEGJDI.js");
1323
1387
  await resolveStaleMVOnRead(this.materializedViewSource, this.name);
1324
1388
  }
1325
1389
  let record;
@@ -1330,7 +1394,9 @@ var Collection = class {
1330
1394
  } else {
1331
1395
  const envelope = await this.adapter.get(this.vault, this.name, id);
1332
1396
  if (!envelope) return null;
1333
- record = await this.decryptRecord(envelope);
1397
+ if (isTombstone(envelope, this.encrypted)) return null;
1398
+ record = await this.decryptRecord(envelope, { id });
1399
+ if (record === null) return null;
1334
1400
  this.lru.set(id, { record, version: envelope._v }, estimateRecordBytes(record));
1335
1401
  }
1336
1402
  } else {
@@ -1357,6 +1423,7 @@ var Collection = class {
1357
1423
  const envelope = await this.adapter.get(this.vault, this.name, id);
1358
1424
  if (!envelope) return null;
1359
1425
  const json = await this.decryptJsonString(envelope);
1426
+ if (json === null) return null;
1360
1427
  return JSON.parse(json);
1361
1428
  }
1362
1429
  /**
@@ -1445,7 +1512,7 @@ var Collection = class {
1445
1512
  if (cached2) return { record: cached2.record, version: cached2.version };
1446
1513
  const env = await this.adapter.get(this.vault, this.name, id);
1447
1514
  if (!env) return { record: null, version: 0 };
1448
- return { record: await this.decryptRecord(env, { skipValidation: true }), version: env._v };
1515
+ return { record: await this.decryptRecord(env, { skipValidation: true }) ?? null, version: env._v };
1449
1516
  }
1450
1517
  await this.ensureHydrated();
1451
1518
  const cached = this.cache.get(id);
@@ -1558,9 +1625,11 @@ var Collection = class {
1558
1625
  let existingState;
1559
1626
  if (existingEnvelope) {
1560
1627
  const prevJson = await this.decryptJsonString(existingEnvelope);
1561
- const prevParsed = JSON.parse(prevJson);
1562
- if (prevParsed !== null && typeof prevParsed === "object" && "_crdt" in prevParsed) {
1563
- existingState = prevParsed;
1628
+ if (prevJson !== null) {
1629
+ const prevParsed = JSON.parse(prevJson);
1630
+ if (prevParsed !== null && typeof prevParsed === "object" && "_crdt" in prevParsed) {
1631
+ existingState = prevParsed;
1632
+ }
1564
1633
  }
1565
1634
  }
1566
1635
  crdtState = this.crdtStrategy.buildLwwMapState(record, existingState, now);
@@ -1568,9 +1637,11 @@ var Collection = class {
1568
1637
  let existingState;
1569
1638
  if (existingEnvelope) {
1570
1639
  const prevJson = await this.decryptJsonString(existingEnvelope);
1571
- const prevParsed = JSON.parse(prevJson);
1572
- if (prevParsed !== null && typeof prevParsed === "object" && "_crdt" in prevParsed) {
1573
- existingState = prevParsed;
1640
+ if (prevJson !== null) {
1641
+ const prevParsed = JSON.parse(prevJson);
1642
+ if (prevParsed !== null && typeof prevParsed === "object" && "_crdt" in prevParsed) {
1643
+ existingState = prevParsed;
1644
+ }
1574
1645
  }
1575
1646
  }
1576
1647
  const arr = Array.isArray(record) ? record : [record];
@@ -1579,12 +1650,14 @@ var Collection = class {
1579
1650
  crdtState = { _crdt: "yjs", update: record };
1580
1651
  }
1581
1652
  const version2 = existingVersion + 1;
1582
- const envelope2 = await this.encryptJsonString(JSON.stringify(crdtState), version2);
1653
+ const cek2 = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
1654
+ const envelope2 = await this.encryptJsonString(JSON.stringify(crdtState), version2, cek2);
1583
1655
  await this.adapter.put(this.vault, this.name, id, envelope2);
1584
1656
  const resolvedRecord = this.crdtStrategy.resolveCrdtSnapshot(crdtState);
1585
- const existingResolved = existingEnvelope ? { record: await this.decryptRecord(existingEnvelope, { skipValidation: true }), version: existingVersion } : void 0;
1657
+ const existingResolvedRecord = existingEnvelope ? await this.decryptRecord(existingEnvelope, { skipValidation: true }) : null;
1658
+ const existingResolved = existingResolvedRecord !== null ? { record: existingResolvedRecord, version: existingVersion } : void 0;
1586
1659
  if (existingResolved && this.historyConfig.enabled !== false) {
1587
- const histEnvelope = await this.encryptRecord(existingResolved.record, existingResolved.version);
1660
+ const histEnvelope = await this.encryptRecord(existingResolved.record, existingResolved.version, cek2);
1588
1661
  await this.historyStrategy.saveHistory(this.adapter, this.vault, this.name, id, histEnvelope);
1589
1662
  this.emitter.emit("history:save", { vault: this.vault, collection: this.name, id, version: existingResolved.version });
1590
1663
  if (this.historyConfig.maxVersions) {
@@ -1630,7 +1703,9 @@ var Collection = class {
1630
1703
  const previousEnvelope = await this.adapter.get(this.vault, this.name, id);
1631
1704
  if (previousEnvelope) {
1632
1705
  const previousRecord = await this.decryptRecord(previousEnvelope);
1633
- existing = { record: previousRecord, version: previousEnvelope._v };
1706
+ if (previousRecord !== null) {
1707
+ existing = { record: previousRecord, version: previousEnvelope._v };
1708
+ }
1634
1709
  }
1635
1710
  }
1636
1711
  } else {
@@ -1639,8 +1714,9 @@ var Collection = class {
1639
1714
  }
1640
1715
  const version = existing ? existing.version + 1 : 1;
1641
1716
  this.uniqueConstraints?.check(id, record);
1717
+ const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
1642
1718
  if (existing && this.historyConfig.enabled !== false) {
1643
- const historyEnvelope = await this.encryptRecord(existing.record, existing.version);
1719
+ const historyEnvelope = await this.encryptRecord(existing.record, existing.version, cek);
1644
1720
  await this.historyStrategy.saveHistory(this.adapter, this.vault, this.name, id, historyEnvelope);
1645
1721
  this.emitter.emit("history:save", {
1646
1722
  vault: this.vault,
@@ -1654,7 +1730,7 @@ var Collection = class {
1654
1730
  });
1655
1731
  }
1656
1732
  }
1657
- const envelope = await this.encryptRecord(record, version);
1733
+ const envelope = await this.encryptRecord(record, version, cek);
1658
1734
  await this.adapter.put(this.vault, this.name, id, envelope);
1659
1735
  if (this.ledger) {
1660
1736
  const appendInput = {
@@ -1717,7 +1793,7 @@ var Collection = class {
1717
1793
  if (mode === "eager") {
1718
1794
  if (executor === null) {
1719
1795
  ;
1720
- ({ MaterializedViewExecutor: executor } = await import("./executor-AZLS3KBK.js"));
1796
+ ({ MaterializedViewExecutor: executor } = await import("./executor-IZ2NVXCY.js"));
1721
1797
  }
1722
1798
  await executor.refresh(reg, {
1723
1799
  getCollection: (name) => this.materializedViewSource.getCollection(name),
@@ -1726,7 +1802,7 @@ var Collection = class {
1726
1802
  });
1727
1803
  } else if (mode === "lazy") {
1728
1804
  if (staleHelpers === null) {
1729
- staleHelpers = await import("./stale-W5PQTRYH.js");
1805
+ staleHelpers = await import("./stale-PGTEGJDI.js");
1730
1806
  }
1731
1807
  staleHelpers.markMVStale(registry, reg.spec.name);
1732
1808
  }
@@ -1743,6 +1819,111 @@ var Collection = class {
1743
1819
  * output (carries `_derivedFrom`) — defensive guard against missed
1744
1820
  * cycle detection.
1745
1821
  */
1822
+ /**
1823
+ * @internal #376 — the RAW stored record (canonical-money form, i18n maps
1824
+ * intact), WITHOUT the locale resolution `get()` applies. Used as the
1825
+ * patch base for self-write reverse-denorm so writing back never clobbers
1826
+ * an i18n map or re-quantizes money incorrectly. Returns null for
1827
+ * missing / tombstoned records.
1828
+ */
1829
+ async _getStoredRecord(id) {
1830
+ let raw;
1831
+ if (this.lazy && this.lru) {
1832
+ const cached = this.lru.get(id);
1833
+ if (cached) raw = cached.record;
1834
+ else {
1835
+ const env = await this.adapter.get(this.vault, this.name, id);
1836
+ if (!env || isTombstone(env, this.encrypted)) return null;
1837
+ raw = await this.decryptRecord(env, { id });
1838
+ if (raw === null) return null;
1839
+ this.lru.set(id, { record: raw, version: env._v }, estimateRecordBytes(raw));
1840
+ }
1841
+ } else {
1842
+ await this.ensureHydrated();
1843
+ raw = this.cache.get(id)?.record ?? null;
1844
+ }
1845
+ if (raw === null) return null;
1846
+ return canonicalizeStoredMoney(raw, this.moneyFields);
1847
+ }
1848
+ /**
1849
+ * @internal #376 — ids of records whose top-level `field` equals `value`.
1850
+ * Uses the FK index when the field is indexed (O(matches)); otherwise a
1851
+ * linear scan (O(N) — fine for small child sets; index the FK to scale).
1852
+ */
1853
+ async _findMatchingIds(field, value) {
1854
+ const hit = this.getIndexes()?.lookupEqual(field, value);
1855
+ if (hit) return [...hit];
1856
+ const target = String(value);
1857
+ const matches = (rec) => {
1858
+ const fv = rec[field];
1859
+ return (typeof fv === "string" || typeof fv === "number") && String(fv) === target;
1860
+ };
1861
+ if (!this.lazy) {
1862
+ await this.ensureHydrated();
1863
+ const out2 = [];
1864
+ for (const [rid, e] of this.cache) {
1865
+ if (matches(e.record)) out2.push(rid);
1866
+ }
1867
+ return out2;
1868
+ }
1869
+ const ids = await this.adapter.list(this.vault, this.name);
1870
+ const out = [];
1871
+ for (const rid of ids) {
1872
+ const raw = await this._getStoredRecord(rid);
1873
+ if (raw !== null && matches(raw)) out.push(rid);
1874
+ }
1875
+ return out;
1876
+ }
1877
+ /**
1878
+ * @internal #376 slice 2 — recompute a rollup aggregate onto the parent.
1879
+ * Gathers every child of `parentId`, runs `compute`, and patches only the
1880
+ * rollup `field` onto the parent's raw stored record (value-equality
1881
+ * guarded). No-op when the parent record does not exist.
1882
+ */
1883
+ async recomputeRollup(spec, parentId) {
1884
+ if (this.derivationSource === void 0 || spec.rollup === void 0) return;
1885
+ const { from, key, field, compute } = spec.rollup;
1886
+ const into = spec.source;
1887
+ const intoColl = this.derivationSource.getCollection(into);
1888
+ const base = await intoColl._getStoredRecord(parentId);
1889
+ if (base === null) return;
1890
+ const fromColl = this.derivationSource.getCollection(from);
1891
+ const childIds = await fromColl._findMatchingIds(key, parentId);
1892
+ const children = [];
1893
+ for (const cid of childIds) {
1894
+ const c = await fromColl.get(cid);
1895
+ if (c !== null && c !== void 0) children.push(c);
1896
+ }
1897
+ const newValue = compute(children);
1898
+ if (selfWriteFieldEqual(base[field], newValue)) return;
1899
+ const patched = { ...base, [field]: newValue };
1900
+ const txCtx = this.derivationSource.getActiveTxContext();
1901
+ if (txCtx !== null) {
1902
+ const prior = await this.adapter.get(this.vault, into, parentId);
1903
+ txCtx._executed.push({
1904
+ op: { type: "put", vaultName: this.vault, collectionName: into, id: parentId },
1905
+ priorEnvelope: prior
1906
+ });
1907
+ }
1908
+ await intoColl.put(parentId, patched);
1909
+ }
1910
+ /**
1911
+ * @internal #376 slice 2 — fire any rollups for which THIS collection is the
1912
+ * child `from`, recomputing the affected parent after a child delete. Called
1913
+ * from the delete path with the just-removed record's key value. Other
1914
+ * derivation kinds do not react to deletes (unchanged).
1915
+ */
1916
+ async dispatchRollupsOnDelete(deleted) {
1917
+ if (this.derivationSource === void 0) return;
1918
+ const registry = this.derivationSource.registry();
1919
+ const rec = deleted;
1920
+ for (const { spec } of registry.strategiesForSource(this.name)) {
1921
+ if (!spec.rollup || spec.rollup.from !== this.name) continue;
1922
+ const kv = rec[spec.rollup.key];
1923
+ if (typeof kv !== "string" && typeof kv !== "number") continue;
1924
+ await this.recomputeRollup(spec, String(kv));
1925
+ }
1926
+ }
1746
1927
  async dispatchDerivations(id, record, version) {
1747
1928
  if (this.derivationSource === void 0) return;
1748
1929
  const incoming = canonicalizeStoredMoney(record, this.moneyFields);
@@ -1753,29 +1934,60 @@ var Collection = class {
1753
1934
  let DerivationExecutor = null;
1754
1935
  for (const { spec, strategyHash } of strategies) {
1755
1936
  const mode = typeof spec.lifecycle === "string" ? spec.lifecycle : spec.lifecycle.mode;
1756
- if (mode === "eager") {
1757
- if (DerivationExecutor === null) {
1758
- ({ DerivationExecutor } = await import("./executor-6ZDSDZ6V.js"));
1759
- }
1760
- let sourceWithId;
1761
- let sourceVersion = version;
1762
- if (spec.source === this.name) {
1763
- sourceWithId = { ...incoming, id };
1937
+ if (spec.rollup) {
1938
+ if (mode !== "eager") continue;
1939
+ let parentId;
1940
+ if (this.name === spec.rollup.from) {
1941
+ const kv = incoming[spec.rollup.key];
1942
+ parentId = typeof kv === "string" || typeof kv === "number" ? String(kv) : null;
1764
1943
  } else {
1765
- const primary = await this.derivationSource.getCollection(spec.source).get(id);
1766
- if (primary === null || primary === void 0) continue;
1767
- sourceWithId = { ...primary, id };
1768
- sourceVersion = 0;
1944
+ parentId = id;
1945
+ }
1946
+ if (parentId !== null) await this.recomputeRollup(spec, parentId);
1947
+ continue;
1948
+ }
1949
+ const isSource = spec.source === this.name;
1950
+ const isSibling = !isSource && (spec.sources?.includes(this.name) ?? false);
1951
+ const trigger = !isSource && !isSibling ? spec.triggerBy?.find((t) => t.collection === this.name) : void 0;
1952
+ const runs = [];
1953
+ if (isSource) {
1954
+ runs.push({ input: { ...incoming, id }, base: incoming, runId: id, version });
1955
+ } else if (isSibling) {
1956
+ const p = await this.derivationSource.getCollection(spec.source).get(id);
1957
+ if (p !== null && p !== void 0) {
1958
+ const raw = await this.derivationSource.getCollection(spec.source)._getStoredRecord(id);
1959
+ runs.push({ input: { ...p, id }, base: raw ?? p, runId: id, version: 0 });
1960
+ }
1961
+ } else if (trigger) {
1962
+ const srcColl = this.derivationSource.getCollection(spec.source);
1963
+ const ids = await srcColl._findMatchingIds(trigger.on, id);
1964
+ if (trigger.maxFanout !== void 0 && ids.length > trigger.maxFanout) {
1965
+ throw new DerivationCapExceededError(`triggerBy ${this.name}\u2192${spec.source}`, ids.length, trigger.maxFanout);
1966
+ }
1967
+ for (const sid of ids) {
1968
+ const raw = await srcColl._getStoredRecord(sid);
1969
+ if (raw === null) continue;
1970
+ runs.push({ input: { ...raw, id: sid }, base: raw, runId: sid, version: 0 });
1769
1971
  }
1972
+ }
1973
+ if (runs.length === 0) continue;
1974
+ if (mode !== "eager") {
1975
+ for (const run of runs) await markStale(registry, spec, run.runId);
1976
+ continue;
1977
+ }
1978
+ if (DerivationExecutor === null) {
1979
+ ({ DerivationExecutor } = await import("./executor-WLFDUTOM.js"));
1980
+ }
1981
+ for (const run of runs) {
1770
1982
  const ctx = { vault: this.derivationSource.getReadOnlyFacade() };
1771
- const result = await DerivationExecutor.run(spec, sourceWithId, sourceVersion, strategyHash, ctx);
1983
+ const result = await DerivationExecutor.run(spec, run.input, run.version, strategyHash, ctx);
1772
1984
  for (const key of Object.keys(spec.outputs)) {
1773
1985
  const out = result.outputs[key];
1774
1986
  if (!out) continue;
1775
1987
  if (out.kind === "failed") {
1776
1988
  const err = out.error;
1777
1989
  if (spec.strict) throw err;
1778
- console.warn(`[derivation] output "${key}" for source "${spec.source}" id="${id}" failed:`, err);
1990
+ console.warn(`[derivation] output "${key}" for source "${spec.source}" id="${run.runId}" failed:`, err);
1779
1991
  continue;
1780
1992
  }
1781
1993
  const outSpec = spec.outputs[key];
@@ -1783,12 +1995,12 @@ var Collection = class {
1783
1995
  const outputCollection = this.derivationSource.getCollection(outSpec.collection);
1784
1996
  const txCtx = this.derivationSource.getActiveTxContext();
1785
1997
  if (out.kind === "array") {
1786
- const { loadFanoutSidecar, saveFanoutSidecar } = await import("./fanout-sidecar-67CMI3UT.js");
1998
+ const { loadFanoutSidecar, saveFanoutSidecar } = await import("./fanout-sidecar-JGHXAJO5.js");
1787
1999
  const prior = await loadFanoutSidecar(
1788
2000
  this.adapter,
1789
2001
  this.vault,
1790
2002
  spec.source,
1791
- id,
2003
+ run.runId,
1792
2004
  key
1793
2005
  );
1794
2006
  const prevKeys = new Set(prior?.keys ?? []);
@@ -1815,7 +2027,7 @@ var Collection = class {
1815
2027
  }
1816
2028
  await saveFanoutSidecar(this.adapter, this.vault, {
1817
2029
  source: spec.source,
1818
- sourceId: id,
2030
+ sourceId: run.runId,
1819
2031
  outputKey: key,
1820
2032
  outputCollection: outSpec.collection,
1821
2033
  keys: newKeysList
@@ -1823,25 +2035,44 @@ var Collection = class {
1823
2035
  continue;
1824
2036
  }
1825
2037
  if (out.skipped === true) {
1826
- await outputCollection._internalDelete(id, txCtx);
2038
+ await outputCollection._internalDelete(run.runId, txCtx);
2039
+ continue;
2040
+ }
2041
+ if (outSpec.shape === "record" && outSpec.denorm !== void 0 && outSpec.collection === spec.source) {
2042
+ const value = out.value;
2043
+ const patched = { ...run.base };
2044
+ let changed = false;
2045
+ for (const f of outSpec.denorm) {
2046
+ if (!selfWriteFieldEqual(run.base[f], value[f])) {
2047
+ patched[f] = value[f];
2048
+ changed = true;
2049
+ }
2050
+ }
2051
+ if (!changed) continue;
2052
+ if (txCtx !== null) {
2053
+ const prior = await this.adapter.get(this.vault, outSpec.collection, run.runId);
2054
+ txCtx._executed.push({
2055
+ op: { type: "put", vaultName: this.vault, collectionName: outSpec.collection, id: run.runId },
2056
+ priorEnvelope: prior
2057
+ });
2058
+ }
2059
+ await outputCollection.put(run.runId, patched);
1827
2060
  continue;
1828
2061
  }
1829
2062
  if (txCtx !== null) {
1830
- const prior = await this.adapter.get(this.vault, outSpec.collection, id);
2063
+ const prior = await this.adapter.get(this.vault, outSpec.collection, run.runId);
1831
2064
  txCtx._executed.push({
1832
2065
  op: {
1833
2066
  type: "put",
1834
2067
  vaultName: this.vault,
1835
2068
  collectionName: outSpec.collection,
1836
- id
2069
+ id: run.runId
1837
2070
  },
1838
2071
  priorEnvelope: prior
1839
2072
  });
1840
2073
  }
1841
- await outputCollection.put(id, out.value);
2074
+ await outputCollection.put(run.runId, out.value);
1842
2075
  }
1843
- } else {
1844
- await markStale(registry, spec, id);
1845
2076
  }
1846
2077
  }
1847
2078
  }
@@ -1890,11 +2121,14 @@ var Collection = class {
1890
2121
  let count = 0;
1891
2122
  for (const id of ids) {
1892
2123
  const env = await this.adapter.get(this.vault, this.name, id);
1893
- if (!env) continue;
1894
- const record = await this.decryptRecord(env, { skipValidation: true });
2124
+ if (!env || isTombstone(env, this.encrypted)) continue;
2125
+ const decoded = await this.decryptRecord(env, { skipValidation: true, id });
2126
+ if (decoded === null) continue;
2127
+ const record = decoded;
1895
2128
  const next = transform(record);
1896
2129
  const nextVersion = (env._v ?? 0) + 1;
1897
- const newEnv = await this.encryptRecord(next, nextVersion);
2130
+ const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
2131
+ const newEnv = await this.encryptRecord(next, nextVersion, cek);
1898
2132
  await this.adapter.put(this.vault, this.name, id, newEnv);
1899
2133
  await this._invalidateCacheEntry(id);
1900
2134
  if (this.ledger) {
@@ -2006,14 +2240,17 @@ var Collection = class {
2006
2240
  const previousEnvelope2 = await this.adapter.get(this.vault, this.name, id);
2007
2241
  if (previousEnvelope2) {
2008
2242
  const previousRecord = await this.decryptRecord(previousEnvelope2);
2009
- existing = { record: previousRecord, version: previousEnvelope2._v };
2243
+ if (previousRecord !== null) {
2244
+ existing = { record: previousRecord, version: previousEnvelope2._v };
2245
+ }
2010
2246
  }
2011
2247
  }
2012
2248
  } else {
2013
2249
  existing = this.cache.get(id);
2014
2250
  }
2015
2251
  if (existing && this.historyConfig.enabled !== false) {
2016
- const historyEnvelope = await this.encryptRecord(existing.record, existing.version);
2252
+ const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
2253
+ const historyEnvelope = await this.encryptRecord(existing.record, existing.version, cek);
2017
2254
  await this.historyStrategy.saveHistory(this.adapter, this.vault, this.name, id, historyEnvelope);
2018
2255
  }
2019
2256
  const previousEnvelope = await this.adapter.get(this.vault, this.name, id);
@@ -2052,8 +2289,53 @@ var Collection = class {
2052
2289
  if (!internal) {
2053
2290
  await this.dispatchMaterializedViewsOnDelete(id);
2054
2291
  await this.dispatchArrayDerivationsOnDelete(id);
2292
+ if (existing) await this.dispatchRollupsOnDelete(existing.record);
2293
+ }
2294
+ }
2295
+ /**
2296
+ * @internal — GDPR crypto-shred a LIVE record to a tombstone (#304).
2297
+ *
2298
+ * Rewrites the on-disk envelope to `{ _noydb, _v, _ts, _by, _iv:'', _data:'' }`,
2299
+ * dropping `_iv`/`_data`/`_cek`/`_det`. The wrapped per-record CEK is gone, so
2300
+ * the body — and (via {@link tombstoneHistory}) every history version under
2301
+ * the same CEK — is permanently undecryptable; the collection DEK and every
2302
+ * other record are untouched. `_det` is stripped too, so `findByDet` no
2303
+ * longer matches the shredded record (avoiding a post-shred TamperedError).
2304
+ *
2305
+ * Unlike `delete()`/`_internalDelete`, this:
2306
+ * - does NOT fire onDelete guards / MV / derivation dispatch (a shred is an
2307
+ * erasure, not a domain delete — re-running those would be wrong),
2308
+ * - does NOT append a per-record ledger entry (`vault.forget()` appends a
2309
+ * single `op:'forget'` summary for the whole subject),
2310
+ * - keeps the record KEY present (it's an overwrite, not an adapter delete)
2311
+ * so the version counter + "record existed" survive for audit.
2312
+ *
2313
+ * Idempotent: returns `null` when the record is absent or already a tombstone.
2314
+ * Otherwise returns `{ previousVersion }`. Invalidates the eager cache, the
2315
+ * lazy LRU, and the per-record CEK cache for this id.
2316
+ */
2317
+ /**
2318
+ * @internal — decrypt an envelope to a plain record for subject-index
2319
+ * rebuild (#304). Returns `null` for a tombstone or unreadable envelope.
2320
+ * Skips schema validation — the rebuild only reads the subject field.
2321
+ */
2322
+ async _decodeEnvelope(envelope, id) {
2323
+ try {
2324
+ const rec = await this.decryptRecord(envelope, { skipValidation: true, id });
2325
+ return rec === null ? null : rec;
2326
+ } catch {
2327
+ return null;
2055
2328
  }
2056
2329
  }
2330
+ async _writeTombstone(id, actor) {
2331
+ const live = await this.adapter.get(this.vault, this.name, id);
2332
+ if (!live || isTombstone(live, this.encrypted)) return null;
2333
+ await this.adapter.put(this.vault, this.name, id, buildTombstone(live._v, actor));
2334
+ this.cache.delete(id);
2335
+ this.lru?.remove(id);
2336
+ this.cekCache?.remove(id);
2337
+ return { previousVersion: live._v };
2338
+ }
2057
2339
  /**
2058
2340
  * Cascade deletes of array-shape derived rows when a source row is
2059
2341
  * deleted. Reads each registered strategy's fanout sidecar
@@ -2076,7 +2358,7 @@ var Collection = class {
2076
2358
  for (const [outputKey, outSpec] of Object.entries(spec.outputs)) {
2077
2359
  if (outSpec.shape !== "array") continue;
2078
2360
  if (helpers === null) {
2079
- helpers = await import("./fanout-sidecar-67CMI3UT.js");
2361
+ helpers = await import("./fanout-sidecar-JGHXAJO5.js");
2080
2362
  }
2081
2363
  const sidecar = await helpers.loadFanoutSidecar(
2082
2364
  this.adapter,
@@ -2116,7 +2398,7 @@ var Collection = class {
2116
2398
  if (mode === "eager") {
2117
2399
  if (executor === null) {
2118
2400
  ;
2119
- ({ MaterializedViewExecutor: executor } = await import("./executor-AZLS3KBK.js"));
2401
+ ({ MaterializedViewExecutor: executor } = await import("./executor-IZ2NVXCY.js"));
2120
2402
  }
2121
2403
  await executor.refresh(reg, {
2122
2404
  getCollection: (name) => this.materializedViewSource.getCollection(name),
@@ -2125,7 +2407,7 @@ var Collection = class {
2125
2407
  });
2126
2408
  } else if (mode === "lazy") {
2127
2409
  if (staleHelpers === null) {
2128
- staleHelpers = await import("./stale-W5PQTRYH.js");
2410
+ staleHelpers = await import("./stale-PGTEGJDI.js");
2129
2411
  }
2130
2412
  staleHelpers.markMVStale(registry, reg.spec.name);
2131
2413
  }
@@ -2148,7 +2430,7 @@ var Collection = class {
2148
2430
  );
2149
2431
  }
2150
2432
  if (this.materializedViewSource !== void 0) {
2151
- const { resolveStaleMVOnRead } = await import("./stale-W5PQTRYH.js");
2433
+ const { resolveStaleMVOnRead } = await import("./stale-PGTEGJDI.js");
2152
2434
  await resolveStaleMVOnRead(this.materializedViewSource, this.name);
2153
2435
  }
2154
2436
  await this.ensureHydrated();
@@ -2466,6 +2748,7 @@ var Collection = class {
2466
2748
  const entries = [];
2467
2749
  for (const env of envelopes) {
2468
2750
  const record = await this.decryptRecord(env, { skipValidation: true });
2751
+ if (record === null) continue;
2469
2752
  entries.push({
2470
2753
  version: env._v,
2471
2754
  timestamp: env._ts,
@@ -2602,6 +2885,7 @@ var Collection = class {
2602
2885
  const envelope = await this.adapter.get(this.vault, this.name, id);
2603
2886
  if (envelope) {
2604
2887
  const record = await this.decryptRecord(envelope);
2888
+ if (record === null) continue;
2605
2889
  items.push(record);
2606
2890
  if (!this.lazy && !this.cache.has(id)) {
2607
2891
  this.cache.set(id, { record, version: envelope._v });
@@ -2678,6 +2962,7 @@ var Collection = class {
2678
2962
  const out = [];
2679
2963
  for (const { id, envelope } of items) {
2680
2964
  const record = await this.decryptRecord(envelope);
2965
+ if (record === null) continue;
2681
2966
  out.push({ id, record, version: envelope._v });
2682
2967
  }
2683
2968
  return out;
@@ -2699,6 +2984,18 @@ var Collection = class {
2699
2984
  * the cache entry (record still present) or deletes it (record was
2700
2985
  * gone before the tx and the revert deleted it again).
2701
2986
  */
2987
+ /**
2988
+ * @internal — evict ONLY the per-record CEK cache entry for `id`. Used by
2989
+ * `vault.rotateRecordCek()`: after a hard CEK rotation the cached unwrapped
2990
+ * CEK is stale (it would decrypt the pre-rotation body and fail GCM auth on
2991
+ * the post-rotation body). Eviction must be synchronous with the live-envelope
2992
+ * rewrite so no concurrent read observes the old CEK. Paired with
2993
+ * {@link _invalidateCacheEntry} (which refreshes the decrypted-record cache).
2994
+ * No-op when the collection is not `perRecordKeys`.
2995
+ */
2996
+ _invalidateCekCacheEntry(id) {
2997
+ this.cekCache?.remove(id);
2998
+ }
2702
2999
  async _invalidateCacheEntry(id) {
2703
3000
  if (this.lazy && this.lru) {
2704
3001
  this.lru.remove(id);
@@ -2716,6 +3013,14 @@ var Collection = class {
2716
3013
  return;
2717
3014
  }
2718
3015
  const record = await this.decryptRecord(envelope);
3016
+ if (record === null) {
3017
+ this.cache.delete(id);
3018
+ if (previous) {
3019
+ this.indexes?.remove(id, previous.record);
3020
+ this.uniqueConstraints?.remove(id, previous.record);
3021
+ }
3022
+ return;
3023
+ }
2719
3024
  this.cache.set(id, { record, version: envelope._v });
2720
3025
  this.indexes?.upsert(id, record, previous ? previous.record : null);
2721
3026
  this.uniqueConstraints?.upsert(id, record, previous?.record);
@@ -2740,8 +3045,9 @@ var Collection = class {
2740
3045
  const ids = await this.adapter.list(this.vault, this.name);
2741
3046
  for (const id of ids) {
2742
3047
  const envelope = await this.adapter.get(this.vault, this.name, id);
2743
- if (envelope) {
2744
- const record = await this.decryptRecord(envelope);
3048
+ if (envelope && !isTombstone(envelope, this.encrypted)) {
3049
+ const record = await this.decryptRecord(envelope, { id });
3050
+ if (record === null) continue;
2745
3051
  this.cache.set(id, { record, version: envelope._v });
2746
3052
  }
2747
3053
  }
@@ -2752,7 +3058,9 @@ var Collection = class {
2752
3058
  /** Hydrate from a pre-loaded snapshot (used by Vault). */
2753
3059
  async hydrateFromSnapshot(records) {
2754
3060
  for (const [id, envelope] of Object.entries(records)) {
2755
- const record = await this.decryptRecord(envelope);
3061
+ if (isTombstone(envelope, this.encrypted)) continue;
3062
+ const record = await this.decryptRecord(envelope, { id });
3063
+ if (record === null) continue;
2756
3064
  this.cache.set(id, { record, version: envelope._v });
2757
3065
  }
2758
3066
  this.hydrated = true;
@@ -2840,6 +3148,7 @@ var Collection = class {
2840
3148
  const envelope = await this.adapter.get(this.vault, this.name, recordId);
2841
3149
  if (!envelope) continue;
2842
3150
  const record = await this.decryptRecord(envelope, { skipValidation: true });
3151
+ if (record === null) continue;
2843
3152
  await this.maintainPersistedIndexesOnPut(recordId, record, null, envelope._v);
2844
3153
  }
2845
3154
  this.persistedIndexesLoaded = true;
@@ -2890,8 +3199,13 @@ var Collection = class {
2890
3199
  const env = await this.adapter.get(this.vault, this.name, id);
2891
3200
  if (!env) continue;
2892
3201
  try {
2893
- const body = JSON.parse(await this.decryptJsonString(env));
2894
- sidecar.set(decoded.recordId, body.value);
3202
+ const sidecarJson = await this.decryptJsonString(env);
3203
+ if (sidecarJson === null) {
3204
+ sidecar.set(decoded.recordId, void 0);
3205
+ } else {
3206
+ const body = JSON.parse(sidecarJson);
3207
+ sidecar.set(decoded.recordId, body.value);
3208
+ }
2895
3209
  } catch {
2896
3210
  sidecar.set(decoded.recordId, void 0);
2897
3211
  }
@@ -2905,6 +3219,7 @@ var Collection = class {
2905
3219
  const env = await this.adapter.get(this.vault, this.name, id);
2906
3220
  if (!env) continue;
2907
3221
  const record = await this.decryptRecord(env, { skipValidation: true });
3222
+ if (record === null) continue;
2908
3223
  const live = readPersistedValue(record, field);
2909
3224
  const stored = sidecar.get(id);
2910
3225
  const hasSidecar = sidecarIds.has(id);
@@ -2987,7 +3302,8 @@ var Collection = class {
2987
3302
  recordId: id,
2988
3303
  getDEK: this.getDEK,
2989
3304
  encrypted: this.encrypted,
2990
- userId: this.keyring.userId
3305
+ userId: this.keyring.userId,
3306
+ erasableBlobs: this.perRecordCek
2991
3307
  });
2992
3308
  }
2993
3309
  /** Get all records as encrypted envelopes (for dump). */
@@ -2995,7 +3311,8 @@ var Collection = class {
2995
3311
  await this.ensureHydrated();
2996
3312
  const result = {};
2997
3313
  for (const [id, entry] of this.cache) {
2998
- result[id] = await this.encryptRecord(entry.record, entry.version);
3314
+ const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
3315
+ result[id] = await this.encryptRecord(entry.record, entry.version, cek);
2999
3316
  }
3000
3317
  return result;
3001
3318
  }
@@ -3023,23 +3340,37 @@ var Collection = class {
3023
3340
  if (hasMoney && this.moneyFields) {
3024
3341
  result = decodeMoneyFields(result, this.moneyFields, typeof locale === "string" ? locale : void 0);
3025
3342
  }
3026
- if (!locale) return result;
3027
- if (hasI18n && this.i18nFields) {
3028
- result = this.i18nStrategy.applyI18nLocale(result, this.i18nFields, locale, localeOpts?.fallback);
3343
+ const hasStaticDisplay = hasDict && this.dictKeyFields !== void 0 && Object.values(this.dictKeyFields).some(
3344
+ (d) => isStaticDictDescriptor(d) && d.displayLocale !== void 0
3345
+ );
3346
+ if (!locale && !hasStaticDisplay) return result;
3347
+ const layer = localeOpts?._layer ?? "read";
3348
+ if (locale && hasI18n && this.i18nFields) {
3349
+ result = this.i18nStrategy.applyI18nLocale(result, this.i18nFields, locale, localeOpts?.fallback, layer);
3029
3350
  }
3030
3351
  if (hasDict && this.dictKeyFields && this.dictLabelResolver && locale !== "raw") {
3031
3352
  const withLabels = { ...result };
3032
3353
  const resolver = this.dictLabelResolver;
3033
3354
  for (const [field, desc] of Object.entries(this.dictKeyFields)) {
3034
- const policy = desc.onMissing ? resolvePolicy(desc.onMissing, "read") : "null";
3355
+ const policy = desc.onMissing ? resolvePolicy(desc.onMissing, layer) : "null";
3035
3356
  const fallback = policy === "substitute" ? localeOpts?.fallback ?? desc.substitute : localeOpts?.fallback;
3357
+ const effLocale = locale ?? (isStaticDictDescriptor(desc) ? desc.displayLocale : void 0);
3036
3358
  const resolveKey = async (key) => {
3037
- const label = await resolver(desc.name, key, locale, fallback);
3359
+ if (!effLocale) {
3360
+ if (policy === "throw") {
3361
+ throw new LocaleNotSpecifiedError(
3362
+ field,
3363
+ `dictKey "${field}": no locale active to resolve key "${key}".`
3364
+ );
3365
+ }
3366
+ return null;
3367
+ }
3368
+ const label = await resolver(desc.name, key, effLocale, fallback);
3038
3369
  if (label === void 0) {
3039
3370
  if (policy === "throw") {
3040
3371
  throw new LocaleNotSpecifiedError(
3041
3372
  field,
3042
- `dictKey "${field}": no label for key "${key}" in locale "${locale}".`
3373
+ `dictKey "${field}": no label for key "${key}" in locale "${effLocale}".`
3043
3374
  );
3044
3375
  }
3045
3376
  return null;
@@ -3196,6 +3527,7 @@ var Collection = class {
3196
3527
  if (!envelope) continue;
3197
3528
  try {
3198
3529
  const json = await this.decryptJsonString(envelope);
3530
+ if (json === null) continue;
3199
3531
  const body = JSON.parse(json);
3200
3532
  if (typeof body.recordId !== "string") continue;
3201
3533
  const rows = byField.get(decoded.field) ?? [];
@@ -3305,7 +3637,31 @@ var Collection = class {
3305
3637
  };
3306
3638
  return new LazyQuery(source);
3307
3639
  }
3308
- async encryptJsonString(json, version) {
3640
+ /**
3641
+ * Resolve the stable CEK for a record on the WRITE path — see
3642
+ * {@link resolveStableCek}. Thin delegate that supplies the collection's
3643
+ * CEK cache, live-envelope reader, and DEK resolver.
3644
+ */
3645
+ resolveRecordCek(id) {
3646
+ return resolveStableCek(
3647
+ {
3648
+ cache: this.cekCache,
3649
+ getLive: (rid) => this.adapter.get(this.vault, this.name, rid),
3650
+ getDEK: () => this.getDEK(this.name)
3651
+ },
3652
+ id
3653
+ );
3654
+ }
3655
+ /**
3656
+ * Encrypt a JSON body into an envelope.
3657
+ *
3658
+ * When `cek` is supplied (per-record CEK collections), the body is
3659
+ * encrypted under the CEK and the CEK is AES-KW-wrapped under the
3660
+ * collection DEK and stamped on `_cek`. When `cek` is omitted, the legacy
3661
+ * path encrypts the body directly under the collection DEK — byte-identical
3662
+ * to pre-CEK behaviour, so non-adopting collections pay nothing.
3663
+ */
3664
+ async encryptJsonString(json, version, cek) {
3309
3665
  const by = this.keyring.userId;
3310
3666
  if (!this.encrypted) {
3311
3667
  return {
@@ -3318,6 +3674,19 @@ var Collection = class {
3318
3674
  };
3319
3675
  }
3320
3676
  const dek = await this.getDEK(this.name);
3677
+ if (cek !== void 0) {
3678
+ const { iv: iv2, data: data2 } = await encrypt(json, cek);
3679
+ const wrapped = await wrapCek(cek, dek);
3680
+ return {
3681
+ _noydb: NOYDB_FORMAT_VERSION,
3682
+ _v: version,
3683
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
3684
+ _iv: iv2,
3685
+ _data: data2,
3686
+ _by: by,
3687
+ _cek: wrapped
3688
+ };
3689
+ }
3321
3690
  const { iv, data } = await encrypt(json, dek);
3322
3691
  return {
3323
3692
  _noydb: NOYDB_FORMAT_VERSION,
@@ -3328,8 +3697,8 @@ var Collection = class {
3328
3697
  _by: by
3329
3698
  };
3330
3699
  }
3331
- async encryptRecord(record, version) {
3332
- const base = await this.encryptJsonString(JSON.stringify(record), version);
3700
+ async encryptRecord(record, version, cek) {
3701
+ const base = await this.encryptJsonString(JSON.stringify(record), version, cek);
3333
3702
  if (!this.deterministicFields || !this.encrypted) return base;
3334
3703
  const dek = await this.getDEK(this.name);
3335
3704
  const rec = record;
@@ -3407,7 +3776,8 @@ var Collection = class {
3407
3776
  const env = await this.adapter.get(this.vault, this.name, id);
3408
3777
  if (!env || !env._det) continue;
3409
3778
  if (env._det[field] === target) {
3410
- matches.push(await this.decryptRecord(env));
3779
+ const rec = await this.decryptRecord(env);
3780
+ if (rec !== null) matches.push(rec);
3411
3781
  }
3412
3782
  }
3413
3783
  return matches;
@@ -3508,7 +3878,14 @@ var Collection = class {
3508
3878
  return null;
3509
3879
  }
3510
3880
  const dek = await this.getDEK(key);
3511
- const plaintext = await decrypt(envelope._iv, envelope._data, dek);
3881
+ let plaintext;
3882
+ if (envelope._cek !== void 0) {
3883
+ const cek = await unwrapCek(envelope._cek, dek);
3884
+ this.cekCache?.set(id, cek, 1);
3885
+ plaintext = await decrypt(envelope._iv, envelope._data, cek);
3886
+ } else {
3887
+ plaintext = await decrypt(envelope._iv, envelope._data, dek);
3888
+ }
3512
3889
  const record = JSON.parse(plaintext);
3513
3890
  this.emitCrossTierEvent({
3514
3891
  actor: this.keyring.userId,
@@ -3564,18 +3941,19 @@ var Collection = class {
3564
3941
  const toKey = dekKey(this.name, toTier);
3565
3942
  const fromDek = await this.getDEK(fromKey);
3566
3943
  const toDek = await this.getDEK(toKey);
3567
- const plaintext = await decrypt(envelope._iv, envelope._data, fromDek);
3568
- const { iv, data } = await encrypt(plaintext, toDek);
3569
3944
  const now = (/* @__PURE__ */ new Date()).toISOString();
3945
+ const body = await rewrapBodyToDek(envelope, fromDek, toDek);
3946
+ if (body.cek) this.cekCache?.set(id, body.cek, 1);
3570
3947
  const next = {
3571
3948
  _noydb: NOYDB_FORMAT_VERSION,
3572
3949
  _v: envelope._v + 1,
3573
3950
  _ts: now,
3574
- _iv: iv,
3575
- _data: data,
3951
+ _iv: body._iv,
3952
+ _data: body._data,
3576
3953
  _by: this.keyring.userId,
3577
3954
  _tier: toTier,
3578
- _elevatedBy: this.keyring.userId
3955
+ _elevatedBy: this.keyring.userId,
3956
+ ...body._cek !== void 0 ? { _cek: body._cek } : {}
3579
3957
  };
3580
3958
  await this.adapter.put(this.vault, this.name, id, next);
3581
3959
  this.emitCrossTierEvent({
@@ -3611,17 +3989,18 @@ var Collection = class {
3611
3989
  if (toTier > 0) this.assertDeclaredTier(toTier);
3612
3990
  const fromDek = await this.getDEK(dekKey(this.name, fromTier));
3613
3991
  const toDek = await this.getDEK(dekKey(this.name, toTier));
3614
- const plaintext = await decrypt(envelope._iv, envelope._data, fromDek);
3615
- const { iv, data } = await encrypt(plaintext, toDek);
3616
3992
  const now = (/* @__PURE__ */ new Date()).toISOString();
3993
+ const body = await rewrapBodyToDek(envelope, fromDek, toDek);
3994
+ if (body.cek) this.cekCache?.set(id, body.cek, 1);
3617
3995
  const next = {
3618
3996
  _noydb: NOYDB_FORMAT_VERSION,
3619
3997
  _v: envelope._v + 1,
3620
3998
  _ts: now,
3621
- _iv: iv,
3622
- _data: data,
3999
+ _iv: body._iv,
4000
+ _data: body._data,
3623
4001
  _by: this.keyring.userId,
3624
- ...toTier > 0 && { _tier: toTier }
4002
+ ...toTier > 0 && { _tier: toTier },
4003
+ ...body._cek !== void 0 ? { _cek: body._cek } : {}
3625
4004
  };
3626
4005
  await this.adapter.put(this.vault, this.name, id, next);
3627
4006
  this.emitCrossTierEvent({
@@ -3643,10 +4022,30 @@ var Collection = class {
3643
4022
  } catch {
3644
4023
  }
3645
4024
  }
3646
- /** Low-level: decrypt an envelope and return the raw JSON string. */
3647
- async decryptJsonString(envelope) {
4025
+ /**
4026
+ * Low-level: decrypt an envelope and return the raw JSON string.
4027
+ *
4028
+ * `_cek` presence is the format discriminant (NOT `this.perRecordCek`),
4029
+ * so a mixed vault — and a recipient that never opted into
4030
+ * `perRecordKeys` — decrypts both legacy and CEK records:
4031
+ * - `_cek` present → unwrap the CEK under the collection DEK, decrypt the
4032
+ * body under the CEK (cache the unwrapped CEK so repeated reads skip it).
4033
+ * - `_cek` absent → legacy path, body decrypts directly under the
4034
+ * collection DEK.
4035
+ *
4036
+ * The optional `id` lets reads populate the CEK cache; it is omitted by
4037
+ * callers (history, conflict merge) that have only the envelope.
4038
+ */
4039
+ async decryptJsonString(envelope, id) {
4040
+ if (isTombstone(envelope, this.encrypted)) return null;
3648
4041
  if (!this.encrypted) return envelope._data;
3649
4042
  const dek = await this.getDEK(this.name);
4043
+ if (envelope._cek !== void 0) {
4044
+ const cached = id !== void 0 ? this.cekCache?.get(id) : void 0;
4045
+ const cek = cached ?? await unwrapCek(envelope._cek, dek);
4046
+ if (cached === void 0 && id !== void 0) this.cekCache?.set(id, cek, 1);
4047
+ return decrypt(envelope._iv, envelope._data, cek);
4048
+ }
3650
4049
  return decrypt(envelope._iv, envelope._data, dek);
3651
4050
  }
3652
4051
  /**
@@ -3665,7 +4064,8 @@ var Collection = class {
3665
4064
  * false positive. Every non-history read leaves this flag `false`.
3666
4065
  */
3667
4066
  async decryptRecord(envelope, opts = {}) {
3668
- const json = await this.decryptJsonString(envelope);
4067
+ const json = await this.decryptJsonString(envelope, opts.id);
4068
+ if (json === null) return null;
3669
4069
  let parsed = JSON.parse(json);
3670
4070
  if (this.crdtMode && parsed !== null && typeof parsed === "object" && "_crdt" in parsed) {
3671
4071
  parsed = this.crdtStrategy.resolveCrdtSnapshot(parsed);
@@ -3793,6 +4193,38 @@ function withArchive(opts) {
3793
4193
  // src/sequence/index.ts
3794
4194
  var SEQUENCE_COLLECTION = "_sequences";
3795
4195
  var MAX_NEXT_ATTEMPTS = 16;
4196
+ var SEQ_FORMAT_TOKEN = /\{([^{}]*)\}/g;
4197
+ var SEQ_PAD_TOKEN = /^seq:0(\d+)$/;
4198
+ var SEQ_PARTITION_TOKEN = /^partition\.(\d+)$/;
4199
+ function compileSequenceFormat(format, series, partition) {
4200
+ const parts = partition ?? [];
4201
+ for (const m of format.matchAll(SEQ_FORMAT_TOKEN)) {
4202
+ const token = m[1] ?? "";
4203
+ if (token === "seq") continue;
4204
+ if (SEQ_PAD_TOKEN.test(token)) continue;
4205
+ const partMatch = SEQ_PARTITION_TOKEN.exec(token);
4206
+ if (partMatch) {
4207
+ const idx = Number(partMatch[1]);
4208
+ if (idx >= parts.length) {
4209
+ throw new ValidationError(
4210
+ `sequence("${series}"): format token "{${token}}" references partition index ${idx}, but only ${parts.length} partition component(s) were supplied.`
4211
+ );
4212
+ }
4213
+ continue;
4214
+ }
4215
+ throw new ValidationError(
4216
+ `sequence("${series}"): format contains unknown token "{${token}}". Accepted tokens: {seq}, {seq:0N}, {partition.i}.`
4217
+ );
4218
+ }
4219
+ return (serial) => format.replace(SEQ_FORMAT_TOKEN, (full, token) => {
4220
+ if (token === "seq") return String(serial);
4221
+ const padMatch = SEQ_PAD_TOKEN.exec(token);
4222
+ if (padMatch) return String(serial).padStart(Number(padMatch[1]), "0");
4223
+ const partMatch = SEQ_PARTITION_TOKEN.exec(token);
4224
+ if (partMatch) return String(parts[Number(partMatch[1])]);
4225
+ return full;
4226
+ });
4227
+ }
3796
4228
  function resolveSequenceKey(series, opts) {
3797
4229
  const partition = opts?.partition;
3798
4230
  if (!partition || partition.length === 0) return series;
@@ -4112,6 +4544,9 @@ var NO_PERIODS = {
4112
4544
  };
4113
4545
 
4114
4546
  // src/refs.ts
4547
+ function isRefArray(desc) {
4548
+ return desc.isArray === true;
4549
+ }
4115
4550
  var RefIntegrityError = class extends NoydbError {
4116
4551
  collection;
4117
4552
  id;
@@ -4148,6 +4583,17 @@ function ref(target, mode = "strict") {
4148
4583
  }
4149
4584
  return { target, mode };
4150
4585
  }
4586
+ function refArray(target, mode = "strict") {
4587
+ if (target.includes("/")) {
4588
+ throw new RefScopeError(target);
4589
+ }
4590
+ if (!target || target.startsWith("_")) {
4591
+ throw new Error(
4592
+ `refArray(): target collection name must be non-empty and cannot start with '_' (reserved for internal collections). Got "${target}".`
4593
+ );
4594
+ }
4595
+ return { target, mode, isArray: true };
4596
+ }
4151
4597
  var RefRegistry = class {
4152
4598
  outbound = /* @__PURE__ */ new Map();
4153
4599
  inbound = /* @__PURE__ */ new Map();
@@ -4172,7 +4618,7 @@ var RefRegistry = class {
4172
4618
  for (const k of existingKeys) {
4173
4619
  const a = existing[k];
4174
4620
  const b = refs[k];
4175
- if (!a || !b || a.target !== b.target || a.mode !== b.mode) {
4621
+ if (!a || !b || a.target !== b.target || a.mode !== b.mode || a.isArray !== b.isArray) {
4176
4622
  throw new Error(
4177
4623
  `RefRegistry: conflicting ref declarations for collection "${collection}" field "${k}"`
4178
4624
  );
@@ -4183,7 +4629,7 @@ var RefRegistry = class {
4183
4629
  this.outbound.set(collection, { ...refs });
4184
4630
  for (const [field, desc] of Object.entries(refs)) {
4185
4631
  const list = this.inbound.get(desc.target) ?? [];
4186
- list.push({ collection, field, mode: desc.mode });
4632
+ list.push({ collection, field, mode: desc.mode, ...desc.isArray ? { isArray: true } : {} });
4187
4633
  this.inbound.set(desc.target, list);
4188
4634
  }
4189
4635
  }
@@ -4211,6 +4657,141 @@ var RefRegistry = class {
4211
4657
  }
4212
4658
  };
4213
4659
 
4660
+ // src/links/link-set.ts
4661
+ var LINK_COLLECTION_PREFIX = "_links_";
4662
+ function linkCollectionName(name) {
4663
+ return `${LINK_COLLECTION_PREFIX}${name}`;
4664
+ }
4665
+ function isLinkCollectionName(name) {
4666
+ return name.startsWith(LINK_COLLECTION_PREFIX);
4667
+ }
4668
+ function linkRowKey(aId, bId) {
4669
+ return `${encodeURIComponent(aId)}|${encodeURIComponent(bId)}`;
4670
+ }
4671
+ var LinkSet = class {
4672
+ constructor(adapter, vault, name, spec, encrypted, getDEK, actor, emitter, endpointExists) {
4673
+ this.adapter = adapter;
4674
+ this.vault = vault;
4675
+ this.name = name;
4676
+ this.spec = spec;
4677
+ this.encrypted = encrypted;
4678
+ this.getDEK = getDEK;
4679
+ this.actor = actor;
4680
+ this.emitter = emitter;
4681
+ this.endpointExists = endpointExists;
4682
+ this.collName = linkCollectionName(name);
4683
+ }
4684
+ adapter;
4685
+ vault;
4686
+ name;
4687
+ spec;
4688
+ encrypted;
4689
+ getDEK;
4690
+ actor;
4691
+ emitter;
4692
+ endpointExists;
4693
+ collName;
4694
+ dekPromise = null;
4695
+ dek() {
4696
+ if (!this.dekPromise) this.dekPromise = this.getDEK(this.collName);
4697
+ return this.dekPromise;
4698
+ }
4699
+ async encryptEntry(entry, version) {
4700
+ const json = JSON.stringify(entry);
4701
+ const base = { _noydb: NOYDB_FORMAT_VERSION, _v: version, _ts: (/* @__PURE__ */ new Date()).toISOString(), _by: this.actor };
4702
+ if (!this.encrypted) return { ...base, _iv: "", _data: json };
4703
+ const { iv, data } = await encrypt(json, await this.dek());
4704
+ return { ...base, _iv: iv, _data: data };
4705
+ }
4706
+ async decryptEntry(env) {
4707
+ const json = this.encrypted ? await decrypt(env._iv, env._data, await this.dek()) : env._data;
4708
+ return JSON.parse(json);
4709
+ }
4710
+ async connect(aId, bId, meta) {
4711
+ if (!await this.endpointExists(this.spec.a, aId)) {
4712
+ throw new LinkEndpointError(this.name, this.spec.a, aId);
4713
+ }
4714
+ if (!await this.endpointExists(this.spec.b, bId)) {
4715
+ throw new LinkEndpointError(this.name, this.spec.b, bId);
4716
+ }
4717
+ const key = linkRowKey(aId, bId);
4718
+ const entry = meta !== void 0 ? { a: aId, b: bId, meta } : { a: aId, b: bId };
4719
+ const existing = await this.adapter.get(this.vault, this.collName, key);
4720
+ const env = await this.encryptEntry(entry, (existing?._v ?? 0) + 1);
4721
+ await this.adapter.put(this.vault, this.collName, key, env, existing?._v);
4722
+ this.emitter.emit("change", { vault: this.vault, collection: this.collName, id: key, action: "put" });
4723
+ }
4724
+ async disconnect(aId, bId) {
4725
+ const key = linkRowKey(aId, bId);
4726
+ const existing = await this.adapter.get(this.vault, this.collName, key);
4727
+ if (!existing) return;
4728
+ await this.adapter.delete(this.vault, this.collName, key);
4729
+ this.emitter.emit("change", { vault: this.vault, collection: this.collName, id: key, action: "delete" });
4730
+ }
4731
+ async has(aId, bId) {
4732
+ return await this.adapter.get(this.vault, this.collName, linkRowKey(aId, bId)) !== null;
4733
+ }
4734
+ async of(id) {
4735
+ const rows = await this.list();
4736
+ return rows.filter((r) => r.a === id || r.b === id);
4737
+ }
4738
+ async list() {
4739
+ const keys = await this.adapter.list(this.vault, this.collName);
4740
+ const out = [];
4741
+ for (const key of keys) {
4742
+ const env = await this.adapter.get(this.vault, this.collName, key);
4743
+ if (!env) continue;
4744
+ const e = await this.decryptEntry(env);
4745
+ out.push(e.meta !== void 0 ? { a: e.a, b: e.b, meta: e.meta } : { a: e.a, b: e.b });
4746
+ }
4747
+ return out;
4748
+ }
4749
+ // ── Vault-internal cascade helpers ──────────────────────────────────
4750
+ /** @internal — rows where the deleted endpoint id matches the relevant slot. */
4751
+ async _rowsTouchingEndpoint(collection, id) {
4752
+ const rows = await this.list();
4753
+ return rows.filter(
4754
+ (r) => this.spec.a === collection && r.a === id || this.spec.b === collection && r.b === id
4755
+ );
4756
+ }
4757
+ /** @internal — the storage collection name (for tx pre-image capture). */
4758
+ get _collectionName() {
4759
+ return this.collName;
4760
+ }
4761
+ };
4762
+ var LinkEndpointError = class extends NoydbError {
4763
+ link;
4764
+ endpoint;
4765
+ missingId;
4766
+ constructor(link, endpoint, missingId) {
4767
+ super(
4768
+ "LINK_ENDPOINT",
4769
+ `link("${link}").connect: endpoint "${endpoint}" has no record "${missingId}".`
4770
+ );
4771
+ this.name = "LinkEndpointError";
4772
+ this.link = link;
4773
+ this.endpoint = endpoint;
4774
+ this.missingId = missingId;
4775
+ }
4776
+ };
4777
+ var LinkIntegrityError = class extends NoydbError {
4778
+ link;
4779
+ endpoint;
4780
+ id;
4781
+ count;
4782
+ constructor(link, endpoint, id, count) {
4783
+ super(
4784
+ "LINK_INTEGRITY",
4785
+ `Cannot delete "${endpoint}"/"${id}": ${count} link(s) in "${link}" still reference it (onDelete: 'strict').`
4786
+ );
4787
+ this.name = "LinkIntegrityError";
4788
+ this.link = link;
4789
+ this.endpoint = endpoint;
4790
+ this.id = id;
4791
+ this.count = count;
4792
+ }
4793
+ };
4794
+
4214
4795
  // src/meta/user-envelope/api.ts
4215
4796
  var UserApi = class {
4216
4797
  constructor(adapter, vaultName, writerKeyringId, getDek, checkGate2) {
@@ -5149,6 +5730,19 @@ function summariseAggregateOp(value) {
5149
5730
  }
5150
5731
 
5151
5732
  // src/vault.ts
5733
+ function resolveLabelFromMap(labels, locale, fallback) {
5734
+ if (labels[locale] !== void 0) return labels[locale];
5735
+ const chain = Array.isArray(fallback) ? fallback : fallback ? [fallback] : [];
5736
+ for (const fb of chain) {
5737
+ if (fb === "any") {
5738
+ const any = Object.values(labels)[0];
5739
+ if (any !== void 0) return any;
5740
+ } else if (labels[fb] !== void 0) {
5741
+ return labels[fb];
5742
+ }
5743
+ }
5744
+ return void 0;
5745
+ }
5152
5746
  var Vault = class {
5153
5747
  adapter;
5154
5748
  /** The vault's name as passed to `openVault()`. Stable for the instance lifetime. */
@@ -5192,6 +5786,7 @@ var Vault = class {
5192
5786
  periodsStrategy;
5193
5787
  shadowStrategy;
5194
5788
  historyStrategy;
5789
+ forgetStrategy;
5195
5790
  i18nStrategy;
5196
5791
  syncStrategy;
5197
5792
  /**
@@ -5221,13 +5816,17 @@ var Vault = class {
5221
5816
  */
5222
5817
  overlayedViewRegistry = null;
5223
5818
  /**
5224
- * Cached read-only facade handed to guard callbacks via `ctx.vault`,
5225
- * and to derivation callbacks via `derive(source, ctx)`. Allocated
5226
- * eagerly inside `_initGuards()` and/or `_initDerivations()` so read
5819
+ * Cached read-only facades handed to guard callbacks via `ctx.vault`
5820
+ * and to derivation callbacks via `derive(source, ctx)`. Split by
5821
+ * resolution layer (#285): the guard facade reads at `layer:'guard'`,
5822
+ * the derivation facade at `layer:'derivation'`, so i18nText / dictKey
5823
+ * fields resolve under that layer's `onMissing` policy. Allocated
5824
+ * eagerly inside `_initGuards()` / `_initDerivations()` so read
5227
5825
  * accessors stay synchronous (callers in `tx/transaction.ts` rely on
5228
- * that). Stays `null` for vaults with neither subsystem configured.
5826
+ * that). Each stays `null` for vaults without that subsystem.
5229
5827
  */
5230
- readOnlyFacade = null;
5828
+ guardFacade = null;
5829
+ derivationFacade = null;
5231
5830
  getDEK;
5232
5831
  /**
5233
5832
  * Per-principal user envelope API.
@@ -5358,6 +5957,27 @@ var Vault = class {
5358
5957
  * Populated by `collection()` when the `dictKeyFields` option is passed.
5359
5958
  */
5360
5959
  dictKeyFieldRegistry = /* @__PURE__ */ new Map();
5960
+ /**
5961
+ * Names of dictionaries backed by a `staticDict()` descriptor (#291).
5962
+ * A static dict skips the `dictKeyFieldRegistry` rename machinery, but the
5963
+ * vault must still *know* a name is static so `vault.dictionary(name)` can
5964
+ * refuse mutation (`StaticDictReadonlyError`). Populated at `collection()`
5965
+ * config time whenever a `StaticDictDescriptor` is seen.
5966
+ */
5967
+ staticDictNames = /* @__PURE__ */ new Set();
5968
+ /**
5969
+ * Static-dict descriptors keyed by dictionary name (#291). Backs the
5970
+ * read-path label resolver (resolve from the in-memory table) and the
5971
+ * query-seam `resolveDictSource` snapshot. Last writer wins when the same
5972
+ * name is registered by multiple collections (identical-across-vaults by
5973
+ * construction, so the tables match).
5974
+ */
5975
+ staticByName = /* @__PURE__ */ new Map();
5976
+ /**
5977
+ * Per-collection map of field name → StaticDictDescriptor (#291). Used by
5978
+ * `enforceStaticDictOnPut` to validate stored codes against `desc.keys`.
5979
+ */
5980
+ staticDescriptorByField = /* @__PURE__ */ new Map();
5361
5981
  /**
5362
5982
  * Registry of i18nText fields declared across all collections. Keyed
5363
5983
  * by collection name → field name → I18nTextDescriptor. Used by
@@ -5368,6 +5988,10 @@ var Vault = class {
5368
5988
  i18nFieldRegistry = /* @__PURE__ */ new Map();
5369
5989
  /** Cache of DictionaryHandle instances, one per dictionary name. */
5370
5990
  dictionaryCache = /* @__PURE__ */ new Map();
5991
+ /** Registered link specs (#377-B), keyed by link name; set by `vault.link()`. */
5992
+ linkRegistry = /* @__PURE__ */ new Map();
5993
+ /** Cache of LinkSet handles, one per link name. */
5994
+ linkSetCache = /* @__PURE__ */ new Map();
5371
5995
  /** — subscribers for cross-tier access events. */
5372
5996
  crossTierSubs = /* @__PURE__ */ new Set();
5373
5997
  /** — currently-active elevation, or null. One per vault. */
@@ -5404,6 +6028,7 @@ var Vault = class {
5404
6028
  this.periodsStrategy = opts.periodsStrategy ?? NO_PERIODS;
5405
6029
  this.shadowStrategy = opts.shadowStrategy ?? NO_SHADOW;
5406
6030
  this.historyStrategy = opts.historyStrategy ?? NO_HISTORY;
6031
+ this.forgetStrategy = opts.forgetStrategy ?? NO_FORGET;
5407
6032
  this.i18nStrategy = opts.i18nStrategy ?? NO_I18N;
5408
6033
  this.syncStrategy = opts.syncStrategy ?? NO_SYNC;
5409
6034
  void opts.guardStrategies;
@@ -5483,6 +6108,9 @@ var Vault = class {
5483
6108
  if (collectionName === SEQUENCE_COLLECTION) {
5484
6109
  throw new ReservedCollectionNameError(collectionName);
5485
6110
  }
6111
+ if (isLinkCollectionName(collectionName)) {
6112
+ throw new ReservedCollectionNameError(collectionName);
6113
+ }
5486
6114
  let coll = this.collectionCache.get(collectionName);
5487
6115
  if (coll && options?.moneyFields) {
5488
6116
  coll._applyMoneyFields(options.moneyFields);
@@ -5508,10 +6136,22 @@ var Vault = class {
5508
6136
  }
5509
6137
  if (options?.dictKeyFields) {
5510
6138
  const dictFieldMap = {};
6139
+ const staticFieldMap = {};
5511
6140
  for (const [field, desc] of Object.entries(options.dictKeyFields)) {
5512
- dictFieldMap[field] = desc.name;
6141
+ if (isStaticDictDescriptor(desc)) {
6142
+ staticFieldMap[field] = desc;
6143
+ this.staticDictNames.add(desc.name);
6144
+ this.staticByName.set(desc.name, desc);
6145
+ } else {
6146
+ dictFieldMap[field] = desc.name;
6147
+ }
6148
+ }
6149
+ if (Object.keys(dictFieldMap).length > 0) {
6150
+ this.dictKeyFieldRegistry.set(collectionName, dictFieldMap);
6151
+ }
6152
+ if (Object.keys(staticFieldMap).length > 0) {
6153
+ this.staticDescriptorByField.set(collectionName, staticFieldMap);
5513
6154
  }
5514
- this.dictKeyFieldRegistry.set(collectionName, dictFieldMap);
5515
6155
  }
5516
6156
  if ((options?.schemaUpdate?.length ?? 0) > 0) {
5517
6157
  this.#schemaUpdateNames.set(collectionName, (options.schemaUpdate ?? []).map((s) => s.name));
@@ -5542,6 +6182,7 @@ var Vault = class {
5542
6182
  }));
5543
6183
  schemaUpdateGate = new SchemaUpdateGate(work);
5544
6184
  }
6185
+ const effectiveHistoryConfig = options?.historyConfig ?? this.historyConfig;
5545
6186
  const collOpts = {
5546
6187
  adapter: this.adapter,
5547
6188
  vault: this.name,
@@ -5557,7 +6198,7 @@ var Vault = class {
5557
6198
  schemaFence: this.schemaFence,
5558
6199
  getDEK: this.getDEK,
5559
6200
  onDirty: this.onDirty,
5560
- historyConfig: this.historyConfig,
6201
+ historyConfig: effectiveHistoryConfig,
5561
6202
  // thread the vault-wide blob strategy into every
5562
6203
  // collection. `undefined` is intentionally preserved so the
5563
6204
  // Collection constructor uses its NO_BLOBS default.
@@ -5568,7 +6209,11 @@ var Vault = class {
5568
6209
  historyStrategy: this.historyStrategy,
5569
6210
  i18nStrategy: this.i18nStrategy,
5570
6211
  syncStrategy: this.syncStrategy,
5571
- ledger: this.getLedgerOrNull() ?? void 0,
6212
+ // Per-collection ledger opt-out (#361): when this collection sets
6213
+ // `historyConfig.ledger: false`, withhold the ledger reference so all
6214
+ // four `if (this.ledger)` append sites in Collection no-op. The chain
6215
+ // stays valid — it simply never receives this collection's entries.
6216
+ ledger: effectiveHistoryConfig.ledger === false ? void 0 : this.getLedgerOrNull() ?? void 0,
5572
6217
  refEnforcer: this,
5573
6218
  joinResolver: this,
5574
6219
  defaultLocale: this.locale,
@@ -5613,6 +6258,17 @@ var Vault = class {
5613
6258
  if (options?.acknowledgeDeterministicRisk !== void 0) {
5614
6259
  collOpts.acknowledgeDeterministicRisk = options.acknowledgeDeterministicRisk;
5615
6260
  }
6261
+ if (options?.perRecordKeys !== void 0) {
6262
+ collOpts.perRecordKeys = options.perRecordKeys;
6263
+ }
6264
+ if (this.forgetStrategy.subjects[collectionName] !== void 0) {
6265
+ if (options?.perRecordKeys === false) {
6266
+ console.warn(
6267
+ `[noy-db] Collection "${collectionName}" is declared in withForgetCascade but opened with perRecordKeys: false. Forcing perRecordKeys: true \u2014 GDPR crypto-shred requires per-record CEKs.`
6268
+ );
6269
+ }
6270
+ collOpts.perRecordKeys = true;
6271
+ }
5616
6272
  if (options?.tiers !== void 0) collOpts.tiers = options.tiers;
5617
6273
  if (options?.tierMode !== void 0) collOpts.tierMode = options.tierMode;
5618
6274
  collOpts.onCrossTierAccess = (event) => this.emitCrossTier(event);
@@ -5622,6 +6278,11 @@ var Vault = class {
5622
6278
  if (options?.computed !== void 0) collOpts.computed = options.computed;
5623
6279
  if (options?.dictKeyFields !== void 0) {
5624
6280
  collOpts.dictLabelResolver = async (dictName, key, locale, fallback) => {
6281
+ const stat = this.staticByName.get(dictName);
6282
+ if (stat) {
6283
+ const labels = stat.table[key];
6284
+ return labels ? resolveLabelFromMap(labels, locale, fallback) : void 0;
6285
+ }
5625
6286
  const handle = this.dictionary(dictName);
5626
6287
  return handle.resolveLabel(key, locale, fallback);
5627
6288
  };
@@ -5630,6 +6291,7 @@ var Vault = class {
5630
6291
  if (options?.i18nFields !== void 0 || options?.dictKeyFields !== void 0) {
5631
6292
  collOpts.i18nPutValidator = (record) => {
5632
6293
  this.enforceI18nOnPut(collectionName, record);
6294
+ this.enforceStaticDictOnPut(collectionName, record);
5633
6295
  };
5634
6296
  }
5635
6297
  if (options?.i18nFields !== void 0 && this.translateText) {
@@ -5769,6 +6431,34 @@ var Vault = class {
5769
6431
  }
5770
6432
  }
5771
6433
  }
6434
+ /**
6435
+ * Validate staticDict codes on a `put()` (#291). For each `staticDict()`
6436
+ * field, every stored code must be a declared key of the descriptor's
6437
+ * table, else `UnknownDictCodeError`. Opt out per descriptor with
6438
+ * `{ validateCodes: false }`. Supports scalar, dotted, and `[].`-wildcard
6439
+ * field paths via `getAtPath` (same path support as i18n validation).
6440
+ */
6441
+ enforceStaticDictOnPut(collectionName, record) {
6442
+ const staticFields = this.staticDescriptorByField.get(collectionName);
6443
+ if (!staticFields || Object.keys(staticFields).length === 0) return;
6444
+ if (!record || typeof record !== "object") return;
6445
+ const obj = record;
6446
+ for (const [field, desc] of Object.entries(staticFields)) {
6447
+ if (desc.validateCodes === false) continue;
6448
+ const known = new Set(desc.keys);
6449
+ const values = getAtPath(obj, field);
6450
+ for (const value of values) {
6451
+ if (value === void 0 || value === null) continue;
6452
+ const codes = Array.isArray(value) ? value : [value];
6453
+ for (const code of codes) {
6454
+ if (typeof code !== "string") continue;
6455
+ if (!known.has(code)) {
6456
+ throw new UnknownDictCodeError(desc.name, field, code);
6457
+ }
6458
+ }
6459
+ }
6460
+ }
6461
+ }
5772
6462
  /**
5773
6463
  * Apply locale resolution to a record for the given collection.
5774
6464
  *
@@ -5777,14 +6467,18 @@ var Vault = class {
5777
6467
  */
5778
6468
  async applyLocale(collectionName, record, localeOpts) {
5779
6469
  const locale = localeOpts.locale ?? this.locale;
5780
- if (!locale) return record;
6470
+ const staticFields = this.staticDescriptorByField.get(collectionName);
6471
+ const hasStaticDisplay = staticFields !== void 0 && Object.values(staticFields).some((d) => d.displayLocale !== void 0);
6472
+ if (!locale && !hasStaticDisplay) return record;
5781
6473
  let result = record;
5782
- const i18nFields = this.i18nFieldRegistry.get(collectionName);
5783
- if (i18nFields && Object.keys(i18nFields).length > 0) {
5784
- result = this.i18nStrategy.applyI18nLocale(result, i18nFields, locale, localeOpts.fallback);
6474
+ if (locale) {
6475
+ const i18nFields = this.i18nFieldRegistry.get(collectionName);
6476
+ if (i18nFields && Object.keys(i18nFields).length > 0) {
6477
+ result = this.i18nStrategy.applyI18nLocale(result, i18nFields, locale, localeOpts.fallback);
6478
+ }
5785
6479
  }
5786
6480
  const dictFields = this.dictKeyFieldRegistry.get(collectionName);
5787
- if (dictFields && Object.keys(dictFields).length > 0 && locale !== "raw") {
6481
+ if (locale && dictFields && Object.keys(dictFields).length > 0 && locale !== "raw") {
5788
6482
  const withLabels = { ...result };
5789
6483
  for (const [field, dictName] of Object.entries(dictFields)) {
5790
6484
  const key = result[field];
@@ -5797,6 +6491,22 @@ var Vault = class {
5797
6491
  }
5798
6492
  result = withLabels;
5799
6493
  }
6494
+ if (staticFields && Object.keys(staticFields).length > 0 && locale !== "raw") {
6495
+ const withLabels = { ...result };
6496
+ for (const [field, desc] of Object.entries(staticFields)) {
6497
+ const effLocale = locale ?? desc.displayLocale;
6498
+ if (!effLocale) continue;
6499
+ const key = result[field];
6500
+ if (typeof key !== "string") continue;
6501
+ const labels = desc.table[key];
6502
+ if (!labels) continue;
6503
+ const label = resolveLabelFromMap(labels, effLocale, localeOpts.fallback ?? desc.substitute);
6504
+ if (label !== void 0) {
6505
+ withLabels[`${field}Label`] = label;
6506
+ }
6507
+ }
6508
+ result = withLabels;
6509
+ }
5800
6510
  return result;
5801
6511
  }
5802
6512
  /**
@@ -5818,6 +6528,9 @@ var Vault = class {
5818
6528
  * ```
5819
6529
  */
5820
6530
  dictionary(name, options = {}) {
6531
+ if (this.staticDictNames.has(name)) {
6532
+ throw new StaticDictReadonlyError(name);
6533
+ }
5821
6534
  let handle = this.dictionaryCache.get(name);
5822
6535
  if (!handle) {
5823
6536
  handle = this.i18nStrategy.buildDictionaryHandle({
@@ -5861,6 +6574,68 @@ var Vault = class {
5861
6574
  }
5862
6575
  return handle;
5863
6576
  }
6577
+ /**
6578
+ * Declare a managed many-to-many link set (#377-B). Registers a
6579
+ * `_links_<name>` junction between two endpoint collections; access its
6580
+ * rows via `vault.links(name)`. Idempotent for an identical re-declaration;
6581
+ * a conflicting one throws. See {@link links}.
6582
+ *
6583
+ * ```ts
6584
+ * vault.link('saleLineLinks', { a: ref('saleLines'), b: ref('purchaseLines'), onDelete: 'cascade' })
6585
+ * ```
6586
+ *
6587
+ * `a` / `b` accept either a collection name or a `ref(target)` descriptor
6588
+ * (only its `target` is used — links manage their own integrity). `onDelete`
6589
+ * governs what happens to link rows when an endpoint record is deleted
6590
+ * (`'cascade'` default, `'strict'`, `'warn'`).
6591
+ */
6592
+ link(name, spec) {
6593
+ const a = typeof spec.a === "string" ? spec.a : spec.a.target;
6594
+ const b = typeof spec.b === "string" ? spec.b : spec.b.target;
6595
+ for (const [slot, target] of [["a", a], ["b", b]]) {
6596
+ if (!target || target.startsWith("_") || target.includes("/")) {
6597
+ throw new ValidationError(
6598
+ `vault.link("${name}"): endpoint "${slot}" must be a simple collection name, got "${target}".`
6599
+ );
6600
+ }
6601
+ }
6602
+ const resolved = { a, b, ...spec.onDelete ? { onDelete: spec.onDelete } : {} };
6603
+ const existing = this.linkRegistry.get(name);
6604
+ if (existing) {
6605
+ if (existing.a !== resolved.a || existing.b !== resolved.b || (existing.onDelete ?? "cascade") !== (resolved.onDelete ?? "cascade")) {
6606
+ throw new ValidationError(`vault.link("${name}"): conflicting re-declaration.`);
6607
+ }
6608
+ return;
6609
+ }
6610
+ this.linkRegistry.set(name, resolved);
6611
+ }
6612
+ /**
6613
+ * Access a declared link set (#377-B). Throws if `name` was not first
6614
+ * declared via {@link link}. Returns a cached {@link LinkSetHandle}:
6615
+ * `connect(a, b, meta?)`, `disconnect(a, b)`, `has(a, b)`, `of(id)`, `list()`.
6616
+ */
6617
+ links(name) {
6618
+ let handle = this.linkSetCache.get(name);
6619
+ if (!handle) {
6620
+ const spec = this.linkRegistry.get(name);
6621
+ if (!spec) {
6622
+ throw new ValidationError(`vault.links("${name}"): not declared. Call vault.link("${name}", { a, b }) first.`);
6623
+ }
6624
+ handle = new LinkSet(
6625
+ this.adapter,
6626
+ this.name,
6627
+ name,
6628
+ spec,
6629
+ this.encrypted,
6630
+ this.getDEK,
6631
+ this.keyring.userId,
6632
+ this.emitter,
6633
+ async (collection, id) => await this.collection(collection).get(id) !== null
6634
+ );
6635
+ this.linkSetCache.set(name, handle);
6636
+ }
6637
+ return handle;
6638
+ }
5864
6639
  /**
5865
6640
  * Build a `JoinableSource` for a dictKey field, for use in dict joins
5866
6641
  *. Returns a source whose snapshot contains `{ key, ...labels }`
@@ -5887,6 +6662,26 @@ var Vault = class {
5887
6662
  * Returns `null` when `field` is not a dictKey in `leftCollection`.
5888
6663
  */
5889
6664
  resolveDictSource(leftCollection, field) {
6665
+ const staticFields = this.staticDescriptorByField.get(leftCollection);
6666
+ if (staticFields && field in staticFields) {
6667
+ const desc = staticFields[field];
6668
+ const rows = Object.entries(desc.table).map(
6669
+ ([key, labels]) => ({ key, labels, ...labels })
6670
+ );
6671
+ const source = {
6672
+ snapshot() {
6673
+ return rows;
6674
+ },
6675
+ lookupById(id) {
6676
+ return rows.find((e) => e["key"] === id);
6677
+ }
6678
+ };
6679
+ if (desc.displayLocale !== void 0) {
6680
+ ;
6681
+ source.displayLocale = desc.displayLocale;
6682
+ }
6683
+ return source;
6684
+ }
5890
6685
  const dictFields = this.dictKeyFieldRegistry.get(leftCollection);
5891
6686
  if (!dictFields || !(field in dictFields)) return null;
5892
6687
  const dictName = dictFields[field];
@@ -6000,65 +6795,16 @@ var Vault = class {
6000
6795
  });
6001
6796
  }
6002
6797
  }
6003
- /**
6004
- * Bulk blob extraction primitive.
6005
- *
6006
- * Returns an async-iterable handle over every blob attached to
6007
- * records in the vault. Single capability check (`plaintext/blob`)
6008
- * at handle creation; single audit entry to `_export_audit` before
6009
- * the first yield. Per-blob decryption happens lazily as the
6010
- * consumer pulls tuples.
6011
- *
6012
- * ```ts
6013
- * const handle = vault.exportBlobs({
6014
- * collections: ['invoiceScans'],
6015
- * where: (rec) => (rec as { clientId?: string }).clientId === 'c-123',
6016
- * })
6017
- * for await (const { bytes, meta, recordRef } of handle) {
6018
- * await uploadToColdStorage(bytes, recordRef)
6019
- * }
6020
- * ```
6021
- *
6022
- * @see `@noy-db/hub/store/export-blobs` for the full option surface.
6023
- */
6024
- /**
6025
- * Evict blob slots per the per-collection `blobFields` retention
6026
- * policy.
6027
- *
6028
- * Iterates every collection declared with `{ blobFields: {...} }`.
6029
- * For each record, checks every configured slot against its
6030
- * policy — `retainDays` (age-based TTL) and/or `evictWhen(record)`
6031
- * (predicate) — and evicts matching slots. Every eviction writes
6032
- * one entry to `_blob_eviction_audit` (actor + eTag + reason +
6033
- * timestamp, no plaintext). Consumer-scheduled; noy-db never runs
6034
- * this on its own.
6035
- *
6036
- * ```ts
6037
- * await vault.compact() // run full pass
6038
- * await vault.compact({ dryRun: true }) // preview counts
6039
- * await vault.compact({ maxEvictions: 1000 }) // cap batch
6040
- * ```
6041
- */
6042
- /**
6043
- * Atomic, gap-free numbering. `vault.sequence('invoice-2026').next()`
6044
- * returns 1, 2, 3, … with no gaps or duplicates under concurrency, via
6045
- * an optimistic-CAS counter at `_sequences/<name>`. Each name is an
6046
- * independent sequence.
6047
- *
6048
- * **Online-only:** `next()` throws `SequenceOfflineError` unless the
6049
- * store advertises `capabilities.casAtomic` — gap-free numbering cannot
6050
- * be serialized by an offline / non-CAS writer.
6051
- *
6052
- * ```ts
6053
- * const n = await vault.sequence('invoice-2026').next() // 1, then 2, …
6054
- * const cur = await vault.sequence('invoice-2026').peek() // current value, no allocation
6055
- * ```
6056
- */
6057
6798
  sequence(series, opts) {
6058
6799
  if (series.includes("\0")) {
6059
6800
  throw new ValidationError(`sequence("${series}"): series name must not contain a null byte (\\x00).`);
6060
6801
  }
6061
6802
  if (this.numberingConfigs.has(series)) {
6803
+ if (opts?.format !== void 0) {
6804
+ throw new ValidationError(
6805
+ `sequence("${series}") is a deferred-numbering series; the format option applies to CAS sequences only.`
6806
+ );
6807
+ }
6062
6808
  const eng = this.deferred();
6063
6809
  return {
6064
6810
  next: async (nextOpts) => {
@@ -6082,7 +6828,17 @@ var Vault = class {
6082
6828
  actor: this.keyring.userId
6083
6829
  });
6084
6830
  }
6085
- return this.sequenceStore.handle(resolveSequenceKey(series, opts));
6831
+ const handle = this.sequenceStore.handle(resolveSequenceKey(series, opts));
6832
+ if (opts?.format === void 0) return handle;
6833
+ const render = compileSequenceFormat(opts.format, series, opts.partition);
6834
+ return {
6835
+ next: async (nextOpts) => {
6836
+ const serial = await handle.next(nextOpts);
6837
+ return { serial, formatted: render(serial) };
6838
+ },
6839
+ peek: () => handle.peek(),
6840
+ seedTo: (n) => handle.seedTo(n)
6841
+ };
6086
6842
  }
6087
6843
  /** @internal — lazily build the deferred-numbering engine with a cache-coherent stamp. */
6088
6844
  deferred() {
@@ -6197,12 +6953,12 @@ var Vault = class {
6197
6953
  if (!fieldSchema) {
6198
6954
  throw new AttestationError(`issueAttestation: collection '${collectionName}' has no attestation field-schema. Declare it via vault.collection('${collectionName}', { attestation: { fields: [...] } }).`);
6199
6955
  }
6200
- const { issueAttestationCore } = await import("./issue-RZP3VI6O.js");
6956
+ const { issueAttestationCore } = await import("./issue-R2MWQO6K.js");
6201
6957
  const out = await issueAttestationCore(this.makeIssueContext(), { collection: collectionName, id, fieldSchema });
6202
6958
  return { docId: out.docId, qr: out.qr, keyId: out.keyId, publicKeyB64: out.publicKeyB64 };
6203
6959
  }
6204
6960
  async getDocumentSigningPublicKey() {
6205
- const { loadSigner, loadOrCreateSigner } = await import("./signer-DCMNKXSF.js");
6961
+ const { loadSigner, loadOrCreateSigner } = await import("./signer-HAVDLGOK.js");
6206
6962
  const existing = await loadSigner(this.adapter, this.name, this.getDEK);
6207
6963
  if (existing) return { keyId: existing.keyId, publicKeyB64: existing.publicKeyB64 };
6208
6964
  if (this.keyring.role !== "owner") {
@@ -6228,19 +6984,19 @@ var Vault = class {
6228
6984
  };
6229
6985
  }
6230
6986
  async revokeAttestation(docId) {
6231
- const { revokeDocCore } = await import("./revoke-HNMQZSCL.js");
6987
+ const { revokeDocCore } = await import("./revoke-7LCWE2AH.js");
6232
6988
  await revokeDocCore(this.makeRevokeContext(), docId);
6233
6989
  }
6234
6990
  async unrevokeAttestation(docId) {
6235
- const { unrevokeDocCore } = await import("./revoke-HNMQZSCL.js");
6991
+ const { unrevokeDocCore } = await import("./revoke-7LCWE2AH.js");
6236
6992
  await unrevokeDocCore(this.makeRevokeContext(), docId);
6237
6993
  }
6238
6994
  async getRevokedDocIds() {
6239
- const { getRevokedDocIdsCore } = await import("./revoke-HNMQZSCL.js");
6995
+ const { getRevokedDocIdsCore } = await import("./revoke-7LCWE2AH.js");
6240
6996
  return getRevokedDocIdsCore(this.makeRevokeContext());
6241
6997
  }
6242
6998
  async publishRevocationList() {
6243
- const { publishRevocationListCore } = await import("./revoke-HNMQZSCL.js");
6999
+ const { publishRevocationListCore } = await import("./revoke-7LCWE2AH.js");
6244
7000
  return publishRevocationListCore(this.makeRevokeContext());
6245
7001
  }
6246
7002
  makeRevokeContext() {
@@ -6309,6 +7065,43 @@ var Vault = class {
6309
7065
  if (descriptor.mode !== "strict") continue;
6310
7066
  const rawId = obj[field];
6311
7067
  if (rawId === null || rawId === void 0) continue;
7068
+ if (isRefArray(descriptor)) {
7069
+ if (!Array.isArray(rawId)) {
7070
+ throw new RefIntegrityError({
7071
+ collection: collectionName,
7072
+ id: obj["id"] ?? "<unknown>",
7073
+ field,
7074
+ refTo: descriptor.target,
7075
+ refId: null,
7076
+ message: `Array ref field "${collectionName}.${field}" must be an array, got ${typeof rawId}.`
7077
+ });
7078
+ }
7079
+ const arrTarget = this.collection(descriptor.target);
7080
+ for (const el of rawId) {
7081
+ if (typeof el !== "string" && typeof el !== "number") {
7082
+ throw new RefIntegrityError({
7083
+ collection: collectionName,
7084
+ id: obj["id"] ?? "<unknown>",
7085
+ field,
7086
+ refTo: descriptor.target,
7087
+ refId: null,
7088
+ message: `Array ref "${collectionName}.${field}" elements must be strings or numbers, got ${typeof el}.`
7089
+ });
7090
+ }
7091
+ const elId = String(el);
7092
+ if (!await arrTarget.get(elId)) {
7093
+ throw new RefIntegrityError({
7094
+ collection: collectionName,
7095
+ id: obj["id"] ?? "<unknown>",
7096
+ field,
7097
+ refTo: descriptor.target,
7098
+ refId: elId,
7099
+ message: `Strict array ref "${collectionName}.${field}" \u2192 "${descriptor.target}" cannot be satisfied: element id "${elId}" not found in "${descriptor.target}".`
7100
+ });
7101
+ }
7102
+ }
7103
+ continue;
7104
+ }
6312
7105
  if (typeof rawId !== "string" && typeof rawId !== "number") {
6313
7106
  throw new RefIntegrityError({
6314
7107
  collection: collectionName,
@@ -6358,6 +7151,11 @@ var Vault = class {
6358
7151
  const allRecords = await fromCollection.list();
6359
7152
  const matches = allRecords.filter((rec) => {
6360
7153
  const raw = rec[rule.field];
7154
+ if (rule.isArray) {
7155
+ return Array.isArray(raw) && raw.some(
7156
+ (el) => (typeof el === "string" || typeof el === "number") && String(el) === id
7157
+ );
7158
+ }
6361
7159
  if (typeof raw !== "string" && typeof raw !== "number") return false;
6362
7160
  return String(raw) === id;
6363
7161
  });
@@ -6396,10 +7194,45 @@ var Vault = class {
6396
7194
  }
6397
7195
  }
6398
7196
  }
7197
+ await this.enforceLinksOnDelete(collectionName, id);
6399
7198
  } finally {
6400
7199
  this.cascadeInProgress.delete(key);
6401
7200
  }
6402
7201
  }
7202
+ /**
7203
+ * @internal — apply link `onDelete` policy when an endpoint record is
7204
+ * deleted (#377-B). `'strict'` throws (blocks the delete), `'cascade'`
7205
+ * removes the touching link rows (tx-atomic when a transaction is active),
7206
+ * `'warn'` leaves orphans for `checkIntegrity()`.
7207
+ */
7208
+ async enforceLinksOnDelete(collectionName, id) {
7209
+ for (const [name, spec] of this.linkRegistry) {
7210
+ if (spec.a !== collectionName && spec.b !== collectionName) continue;
7211
+ const handle = this.links(name);
7212
+ const touching = await handle._rowsTouchingEndpoint(collectionName, id);
7213
+ if (touching.length === 0) continue;
7214
+ const mode = spec.onDelete ?? "cascade";
7215
+ if (mode === "warn") continue;
7216
+ if (mode === "strict") {
7217
+ throw new LinkIntegrityError(name, collectionName, id, touching.length);
7218
+ }
7219
+ const linkColl = handle._collectionName;
7220
+ const txCtx = this.noydb._activeTxContextOrNull;
7221
+ for (const row of touching) {
7222
+ const rowKey = linkRowKey(row.a, row.b);
7223
+ if (txCtx !== null) {
7224
+ const prior = await this.adapter.get(this.name, linkColl, rowKey);
7225
+ if (prior !== null) {
7226
+ txCtx._executed.push({
7227
+ op: { type: "delete", vaultName: this.name, collectionName: linkColl, id: rowKey },
7228
+ priorEnvelope: prior
7229
+ });
7230
+ }
7231
+ }
7232
+ await handle.disconnect(row.a, row.b);
7233
+ }
7234
+ }
7235
+ }
6403
7236
  // ─── Join resolver) ────────────────────
6404
7237
  /**
6405
7238
  * Look up the `RefDescriptor` the left collection declared for a
@@ -6460,6 +7293,23 @@ var Vault = class {
6460
7293
  for (const [field, descriptor] of Object.entries(refs)) {
6461
7294
  const rawId = record[field];
6462
7295
  if (rawId === null || rawId === void 0) continue;
7296
+ const target = this.collection(descriptor.target);
7297
+ if (isRefArray(descriptor)) {
7298
+ if (!Array.isArray(rawId)) {
7299
+ violations.push({ collection: collectionName, id: recId, field, refTo: descriptor.target, refId: rawId, mode: descriptor.mode });
7300
+ continue;
7301
+ }
7302
+ for (const el of rawId) {
7303
+ if (typeof el !== "string" && typeof el !== "number") {
7304
+ violations.push({ collection: collectionName, id: recId, field, refTo: descriptor.target, refId: el, mode: descriptor.mode });
7305
+ continue;
7306
+ }
7307
+ if (!await target.get(String(el))) {
7308
+ violations.push({ collection: collectionName, id: recId, field, refTo: descriptor.target, refId: el, mode: descriptor.mode });
7309
+ }
7310
+ }
7311
+ continue;
7312
+ }
6463
7313
  if (typeof rawId !== "string" && typeof rawId !== "number") {
6464
7314
  violations.push({
6465
7315
  collection: collectionName,
@@ -6472,7 +7322,6 @@ var Vault = class {
6472
7322
  continue;
6473
7323
  }
6474
7324
  const refId = String(rawId);
6475
- const target = this.collection(descriptor.target);
6476
7325
  const exists = await target.get(refId);
6477
7326
  if (!exists) {
6478
7327
  violations.push({
@@ -6487,6 +7336,19 @@ var Vault = class {
6487
7336
  }
6488
7337
  }
6489
7338
  }
7339
+ for (const [name, spec] of this.linkRegistry) {
7340
+ const linkColl = linkCollectionName(name);
7341
+ const rows = await this.links(name).list();
7342
+ for (const row of rows) {
7343
+ const rowKey = linkRowKey(row.a, row.b);
7344
+ if (await this.collection(spec.a).get(row.a) === null) {
7345
+ violations.push({ collection: linkColl, id: rowKey, field: "a", refTo: spec.a, refId: row.a, mode: spec.onDelete ?? "cascade" });
7346
+ }
7347
+ if (await this.collection(spec.b).get(row.b) === null) {
7348
+ violations.push({ collection: linkColl, id: rowKey, field: "b", refTo: spec.b, refId: row.b, mode: spec.onDelete ?? "cascade" });
7349
+ }
7350
+ }
7351
+ }
6490
7352
  return { violations };
6491
7353
  }
6492
7354
  /**
@@ -6530,6 +7392,218 @@ var Vault = class {
6530
7392
  }
6531
7393
  return this.ledgerStore;
6532
7394
  }
7395
+ // ─── GDPR right-to-erasure (#304) ────────────────────────────────
7396
+ /** @internal — add a subject→record ref to the encrypted subject index. */
7397
+ async _addSubjectRef(subjectId, ref2) {
7398
+ await addSubjectRef(this.adapter, this.name, this.getDEK, this.encrypted, subjectId, ref2);
7399
+ }
7400
+ /** @internal — drop a subject→record ref from the encrypted subject index. */
7401
+ async _removeSubjectRef(subjectId, ref2) {
7402
+ await removeSubjectRef(this.adapter, this.name, this.getDEK, this.encrypted, subjectId, ref2);
7403
+ }
7404
+ /**
7405
+ * Rebuild the encrypted subject index from canonical records. The recovery
7406
+ * path for the documented read-modify-write race (RISK #3). Returns the
7407
+ * number of distinct subjects re-indexed.
7408
+ */
7409
+ async rebuildSubjectIndex() {
7410
+ if (Object.keys(this.forgetStrategy.subjects).length === 0) {
7411
+ throw new ForgetStrategyNotConfiguredError();
7412
+ }
7413
+ return rebuildSubjectIndex(
7414
+ this.adapter,
7415
+ this.name,
7416
+ this.getDEK,
7417
+ this.encrypted,
7418
+ this.forgetStrategy.subjects,
7419
+ async (collectionName, id, env) => {
7420
+ const coll = this.collection(collectionName);
7421
+ return coll._decodeEnvelope(env, id);
7422
+ }
7423
+ );
7424
+ }
7425
+ /**
7426
+ * GDPR crypto-shred of a data subject (#304). Consults the encrypted subject
7427
+ * index and, per matching record:
7428
+ * - rewrites the LIVE envelope to a tombstone (drops `_iv`/`_data`/`_cek`/`_det`),
7429
+ * - tombstones every `_history` version of the record,
7430
+ * so the body and all prior versions become permanently undecryptable while
7431
+ * the collection DEK and every OTHER record stay intact. Then appends ONE
7432
+ * `op:'forget'` ledger entry whose `payloadHash` is `sha256Hex(subjectId)` —
7433
+ * the chain still `verify()`s, PROVING the subject existed and was erased
7434
+ * without retaining any plaintext.
7435
+ *
7436
+ * Reports — but does not silently swallow — two completeness gaps:
7437
+ * - `unmigratedRecords`: a record whose body was NOT yet migrated to a
7438
+ * per-record CEK (legacy body still under the shared collection DEK). It
7439
+ * is still tombstoned, but its pre-shred ciphertext (if leaked to a
7440
+ * backup before migration) stays decryptable. Migrate, then re-forget.
7441
+ * - `blobResidueCollections`: a shredded record still has blob attachments,
7442
+ * which are keyed off a separate `_blob` DEK and are out of scope here.
7443
+ *
7444
+ * @throws ForgetStrategyNotConfiguredError when no `withForgetCascade` was set.
7445
+ */
7446
+ async forget(subjectId) {
7447
+ if (Object.keys(this.forgetStrategy.subjects).length === 0) {
7448
+ throw new ForgetStrategyNotConfiguredError();
7449
+ }
7450
+ const refs = await lookupSubject(this.adapter, this.name, this.getDEK, this.encrypted, subjectId);
7451
+ let recordsShredded = 0;
7452
+ let historyVersionsShredded = 0;
7453
+ const collections = /* @__PURE__ */ new Set();
7454
+ const unmigratedRecords = [];
7455
+ const blobResidueCollections = /* @__PURE__ */ new Set();
7456
+ let blobsShredded = 0;
7457
+ let blobsRetainedShared = 0;
7458
+ const blobsEnabled = this.blobStrategy !== void 0;
7459
+ const actor = this.keyring.userId;
7460
+ for (const ref2 of refs) {
7461
+ const coll = this.collection(ref2.collection);
7462
+ const perRecordKeys = this.forgetStrategy.subjects[ref2.collection] !== void 0;
7463
+ const live = await this.adapter.get(this.name, ref2.collection, ref2.id);
7464
+ if (perRecordKeys && live && live._data && live._cek === void 0) {
7465
+ unmigratedRecords.push(`${ref2.collection}:${ref2.id}`);
7466
+ }
7467
+ const shred = await coll._writeTombstone(ref2.id, actor);
7468
+ if (shred !== null) {
7469
+ recordsShredded++;
7470
+ collections.add(ref2.collection);
7471
+ }
7472
+ historyVersionsShredded += await this.historyStrategy.tombstoneHistory(
7473
+ this.adapter,
7474
+ this.name,
7475
+ ref2.collection,
7476
+ ref2.id,
7477
+ actor
7478
+ );
7479
+ if (blobsEnabled) {
7480
+ const r = await this.collection(ref2.collection).blob(ref2.id).shredAllForRecord();
7481
+ blobsShredded += r.shredded.length;
7482
+ blobsRetainedShared += r.retainedShared.length;
7483
+ if (r.residue.length > 0) blobResidueCollections.add(ref2.collection);
7484
+ } else {
7485
+ try {
7486
+ const slotIds = await this.adapter.list(this.name, `_blob_slots_${ref2.collection}`);
7487
+ if (slotIds.includes(ref2.id)) blobResidueCollections.add(ref2.collection);
7488
+ } catch {
7489
+ }
7490
+ }
7491
+ await this._removeSubjectRef(subjectId, ref2);
7492
+ }
7493
+ const subjectHash = await sha256Hex2(subjectId);
7494
+ const ledger = this.getLedgerOrNull();
7495
+ if (!ledger) {
7496
+ throw new Error(
7497
+ 'vault.forget() requires the history strategy for the erasure-proof ledger entry. Pass `historyStrategy: withHistory()` from "@noy-db/hub/history" to createNoydb().'
7498
+ );
7499
+ }
7500
+ const ledgerEntry = await ledger.append({
7501
+ op: "forget",
7502
+ collection: "",
7503
+ id: "",
7504
+ version: 0,
7505
+ actor,
7506
+ payloadHash: subjectHash,
7507
+ reason: JSON.stringify({
7508
+ recordsShredded,
7509
+ historyVersionsShredded,
7510
+ collections: [...collections],
7511
+ unmigratedCount: unmigratedRecords.length,
7512
+ blobsShredded,
7513
+ blobsRetainedShared,
7514
+ blobResidueCollections: [...blobResidueCollections]
7515
+ })
7516
+ });
7517
+ return {
7518
+ subject: subjectId,
7519
+ recordsShredded,
7520
+ historyVersionsShredded,
7521
+ collections: [...collections],
7522
+ unmigratedRecords,
7523
+ blobsShredded,
7524
+ blobsRetainedShared,
7525
+ blobResidueCollections: [...blobResidueCollections],
7526
+ ledgerEntry
7527
+ };
7528
+ }
7529
+ // ─── Record-scoped CEK sealing (#306 slices 2-3) ──────────────────────
7530
+ /**
7531
+ * Seal ONE record's content-encryption key (CEK) to an `at-*` host so that
7532
+ * host — and only that host — can decrypt exactly that record, with no
7533
+ * access to the vault DEK and no ability to read any other record.
7534
+ *
7535
+ * The grantor (this caller, who holds the collection DEK) reads the record's
7536
+ * live `_cek`, unwraps it under the collection DEK, exports the raw CEK
7537
+ * bytes, builds a {@link SealedCekBinding} `{collection, id, cek, expiresAt}`,
7538
+ * seals that binding for the recipient host via the host's published hint,
7539
+ * and persists a thin {@link SealedCekDeliveryEnvelope} at
7540
+ * `_sealed_cek/<collection>/<id>/<pid>`. The binding (not the delivery
7541
+ * envelope) is the security boundary: the host re-verifies `{collection, id}`
7542
+ * and `expiresAt` from inside the sealed payload.
7543
+ *
7544
+ * Only works on a `perRecordKeys` record — a legacy record has no `_cek` to
7545
+ * seal (its body is under the shared collection DEK, which is never exposed
7546
+ * by sealing) → {@link RecordCekNotFoundError}.
7547
+ *
7548
+ * @param collection Collection holding the record.
7549
+ * @param id Record id.
7550
+ * @param hostSealer The recipient host's {@link RecipientSealer}.
7551
+ * @param opts.expiresAt REQUIRED authoritative expiry (ISO 8601), sealed into
7552
+ * the binding the host verifies.
7553
+ * @returns `{ pid, envelopeKey }` — the host provider id and the
7554
+ * `<collection>/<id>/<pid>` key the delivery envelope was written under.
7555
+ */
7556
+ async sealRecordToHost(collection, id, hostSealer, opts) {
7557
+ return sealRecordToHost(this.sealingContext(), collection, id, hostSealer, opts);
7558
+ }
7559
+ /**
7560
+ * Revoke a single sealed-CEK delivery envelope by deleting it from the store.
7561
+ * A soft revocation: it removes the host's copy of the sealed CEK, but a host
7562
+ * that already fetched the envelope keeps whatever it cached. For a hard
7563
+ * revocation that makes the live record undecryptable to every prior grant,
7564
+ * use {@link rotateRecordCek}.
7565
+ */
7566
+ async revokeSealedRecord(collection, id, pid) {
7567
+ return revokeSealedRecord(this.sealingContext(), collection, id, pid);
7568
+ }
7569
+ /**
7570
+ * HARD-rotate a record's CEK: decrypt the live body under the old CEK,
7571
+ * re-encrypt it under a freshly-minted CEK, write the new live envelope, evict
7572
+ * the in-memory caches, and delete EVERY sealed-CEK delivery envelope for the
7573
+ * record. After this, any host holding a previously-sealed CEK can still
7574
+ * decrypt PRE-rotation history versions (they keep their old `_cek`) but NOT
7575
+ * the rotated live record (its body is under the new CEK → the old CEK fails
7576
+ * the AES-GCM auth tag → `TamperedError`). That asymmetry IS the revocation:
7577
+ * old grants lose the live record.
7578
+ *
7579
+ * Administrative path — bypasses `Collection.put` deliberately (no guards, no
7580
+ * history snapshot, no materialized-view refresh): rotation is a key-rotation
7581
+ * operation, not a business write, and must not version-bump history (which
7582
+ * would re-encrypt the prior version under the NEW CEK and defeat the point).
7583
+ *
7584
+ * @throws {@link RecordCekNotFoundError} if the record is missing or has no `_cek`.
7585
+ */
7586
+ async rotateRecordCek(collection, id) {
7587
+ return rotateRecordCek(this.sealingContext(), collection, id);
7588
+ }
7589
+ /**
7590
+ * Build the {@link SealingContext} the record-keys grantor functions need:
7591
+ * the vault-bound adapter, DEK resolver, actor, and the dual-cache eviction
7592
+ * `rotateRecordCek` performs (per-record CEK cache + decrypted-record cache).
7593
+ */
7594
+ sealingContext() {
7595
+ return {
7596
+ adapter: this.adapter,
7597
+ vault: this.name,
7598
+ getDEK: (collection) => this.getDEK(collection),
7599
+ actor: this.keyring.userId,
7600
+ invalidateRecordCaches: async (collection, id) => {
7601
+ const coll = this.collection(collection);
7602
+ coll._invalidateCekCacheEntry(id);
7603
+ await coll._invalidateCacheEntry(id);
7604
+ }
7605
+ };
7606
+ }
6533
7607
  /**
6534
7608
  * @internal — called by `Noydb.openVault` after construction.
6535
7609
  * Dynamic-imports `GuardRegistry` + `ReadOnlyVaultFacade` and seeds
@@ -6545,12 +7619,12 @@ var Vault = class {
6545
7619
  if (handles.length === 0) return;
6546
7620
  const [{ GuardRegistry }, { ReadOnlyVaultFacade }] = await Promise.all([
6547
7621
  import("./registry-DKEXOJVO.js"),
6548
- import("./read-only-facade-ITU6L7BL.js")
7622
+ import("./read-only-facade-EX6WZZBP.js")
6549
7623
  ]);
6550
7624
  const registry = new GuardRegistry();
6551
7625
  for (const h of handles) registry.register(h.spec);
6552
7626
  this.guardRegistry = registry;
6553
- this.readOnlyFacade = new ReadOnlyVaultFacade(this);
7627
+ this.guardFacade = new ReadOnlyVaultFacade(this, "guard");
6554
7628
  }
6555
7629
  /**
6556
7630
  * @internal — The gate handler in Noydb.#registerGuardGate calls into
@@ -6572,8 +7646,8 @@ var Vault = class {
6572
7646
  async _initDerivations(handles) {
6573
7647
  if (handles.length === 0) return;
6574
7648
  const [{ DerivationRegistry }, { ReadOnlyVaultFacade }] = await Promise.all([
6575
- import("./registry-EB6SISTA.js"),
6576
- import("./read-only-facade-ITU6L7BL.js")
7649
+ import("./registry-DMS7OKBM.js"),
7650
+ import("./read-only-facade-EX6WZZBP.js")
6577
7651
  ]);
6578
7652
  const registry = new DerivationRegistry();
6579
7653
  for (const h of handles) {
@@ -6581,8 +7655,8 @@ var Vault = class {
6581
7655
  }
6582
7656
  registry.validate();
6583
7657
  this.derivationRegistry = registry;
6584
- if (this.readOnlyFacade === null) {
6585
- this.readOnlyFacade = new ReadOnlyVaultFacade(this);
7658
+ if (this.derivationFacade === null) {
7659
+ this.derivationFacade = new ReadOnlyVaultFacade(this, "derivation");
6586
7660
  }
6587
7661
  }
6588
7662
  /**
@@ -6603,7 +7677,7 @@ var Vault = class {
6603
7677
  */
6604
7678
  async _initMaterializedViews(handles) {
6605
7679
  if (handles.length === 0) return;
6606
- const { MaterializedViewRegistry } = await import("./registry-UTA4CLQS.js");
7680
+ const { MaterializedViewRegistry } = await import("./registry-WVXO6NH5.js");
6607
7681
  const registry = new MaterializedViewRegistry();
6608
7682
  this.materializedViewRegistry = registry;
6609
7683
  const db = this;
@@ -6627,7 +7701,7 @@ var Vault = class {
6627
7701
  */
6628
7702
  async _initOverlayedViews(handles) {
6629
7703
  if (handles.length === 0) return;
6630
- const { OverlayedViewRegistry } = await import("./registry-IUZQVVBB.js");
7704
+ const { OverlayedViewRegistry } = await import("./registry-3T2RZC5A.js");
6631
7705
  const registry = new OverlayedViewRegistry();
6632
7706
  const mvRegistry = this.materializedViewRegistry;
6633
7707
  const overlayNames = /* @__PURE__ */ new Set();
@@ -6674,13 +7748,13 @@ var Vault = class {
6674
7748
  if (!reg) {
6675
7749
  throw new Error(`refreshView: no MV registered with name "${name}"`);
6676
7750
  }
6677
- const { MaterializedViewExecutor } = await import("./executor-AZLS3KBK.js");
7751
+ const { MaterializedViewExecutor } = await import("./executor-IZ2NVXCY.js");
6678
7752
  const result = await MaterializedViewExecutor.refresh(reg, {
6679
7753
  getCollection: (n) => this.collection(n),
6680
7754
  getActiveTxContext: () => this.noydb._activeTxContextOrNull,
6681
7755
  getQueryContext: () => this
6682
7756
  });
6683
- const { clearMVStale } = await import("./stale-W5PQTRYH.js");
7757
+ const { clearMVStale } = await import("./stale-PGTEGJDI.js");
6684
7758
  clearMVStale(registry, name);
6685
7759
  return result;
6686
7760
  }
@@ -6696,10 +7770,10 @@ var Vault = class {
6696
7770
  if (registry === null) return { derived: 0, failed: 0 };
6697
7771
  const strategies = registry.strategiesForSource(sourceCollection);
6698
7772
  if (strategies.length === 0) return { derived: 0, failed: 0 };
6699
- const { DerivationExecutor } = await import("./executor-6ZDSDZ6V.js");
7773
+ const { DerivationExecutor } = await import("./executor-WLFDUTOM.js");
6700
7774
  const sourceColl = this.collection(sourceCollection);
6701
7775
  const records = await sourceColl.list();
6702
- const ctx = { vault: this.readOnlyFacade ?? new (await import("./read-only-facade-ITU6L7BL.js")).ReadOnlyVaultFacade(this) };
7776
+ const ctx = { vault: this.derivationFacade ?? new (await import("./read-only-facade-EX6WZZBP.js")).ReadOnlyVaultFacade(this, "derivation") };
6703
7777
  let derived = 0;
6704
7778
  let failed = 0;
6705
7779
  for (const record of records) {
@@ -6721,7 +7795,7 @@ var Vault = class {
6721
7795
  if (!outSpec) continue;
6722
7796
  const outputColl = this.collection(outSpec.collection);
6723
7797
  if (out.kind === "array") {
6724
- const { loadFanoutSidecar, saveFanoutSidecar } = await import("./fanout-sidecar-67CMI3UT.js");
7798
+ const { loadFanoutSidecar, saveFanoutSidecar } = await import("./fanout-sidecar-JGHXAJO5.js");
6725
7799
  const prior = await loadFanoutSidecar(this.adapter, this.name, spec.source, id, key);
6726
7800
  const prevKeys = new Set(prior?.keys ?? []);
6727
7801
  const newKeysList = out.entries.map((e) => e.key);
@@ -6764,17 +7838,18 @@ var Vault = class {
6764
7838
  * never see null).
6765
7839
  */
6766
7840
  _getReadOnlyFacade() {
6767
- return this.readOnlyFacade;
7841
+ return this.guardFacade;
6768
7842
  }
6769
7843
  /**
6770
- * Internal lazy-allocator for the read-only facade. Used as a
6771
- * defensive fallback; in practice `_initGuards()` eagerly
6772
- * instantiates this, so the lazy path is a no-op.
7844
+ * Internal lazy-allocator for the derivation read-only facade
7845
+ * (`layer:'derivation'`). Used as a defensive fallback; in practice
7846
+ * `_initDerivations()` eagerly instantiates this, so the lazy path is
7847
+ * a no-op.
6773
7848
  */
6774
7849
  _ensureReadOnlyFacade() {
6775
- if (this.readOnlyFacade !== null) return this.readOnlyFacade;
7850
+ if (this.derivationFacade !== null) return this.derivationFacade;
6776
7851
  throw new Error(
6777
- "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."
7852
+ "Vault: derivation hook fired before _initDerivations() 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."
6778
7853
  );
6779
7854
  }
6780
7855
  /**
@@ -6942,7 +8017,7 @@ var Vault = class {
6942
8017
  * collection.
6943
8018
  */
6944
8019
  async delegate(opts) {
6945
- const { issueDelegation, DELEGATIONS_COLLECTION } = await import("./delegation-NIQ43IPU.js");
8020
+ const { issueDelegation, DELEGATIONS_COLLECTION } = await import("./delegation-FMXNUWE6.js");
6946
8021
  if (!this.keyring.kek) {
6947
8022
  throw new ValidationError(
6948
8023
  "issueDelegation: keyring.kek is null \u2014 issuing a delegation requires a tier-1 unlock. Re-authenticate at tier 1 (passphrase) first."
@@ -6964,7 +8039,7 @@ var Vault = class {
6964
8039
  * if the id does not exist.
6965
8040
  */
6966
8041
  async revokeDelegation(id) {
6967
- const { revokeDelegation, DELEGATIONS_COLLECTION } = await import("./delegation-NIQ43IPU.js");
8042
+ const { revokeDelegation, DELEGATIONS_COLLECTION } = await import("./delegation-FMXNUWE6.js");
6968
8043
  await revokeDelegation(this.adapter, this.name, id);
6969
8044
  void DELEGATIONS_COLLECTION;
6970
8045
  }
@@ -7433,7 +8508,7 @@ var Vault = class {
7433
8508
  * @see docs/subsystems/public-envelope.md
7434
8509
  */
7435
8510
  async getPublicEnvelope(opts = {}) {
7436
- const { readPublicEnvelope: readPublicEnvelope2 } = await import("./public-envelope-YP2UWMLG.js");
8511
+ const { readPublicEnvelope: readPublicEnvelope2 } = await import("./public-envelope-HXOFHY4N.js");
7437
8512
  return readPublicEnvelope2(this.adapter, this.name, opts);
7438
8513
  }
7439
8514
  /**
@@ -8977,6 +10052,7 @@ var Noydb = class {
8977
10052
  policyEnforcers = /* @__PURE__ */ new Map();
8978
10053
  vaultTemplates = /* @__PURE__ */ new Map();
8979
10054
  txStrategy;
10055
+ forgetStrategy;
8980
10056
  sessionStrategy;
8981
10057
  syncStrategy;
8982
10058
  snapshotStrategy;
@@ -9004,6 +10080,7 @@ var Noydb = class {
9004
10080
  constructor(options) {
9005
10081
  this.options = options;
9006
10082
  this.txStrategy = options.txStrategy ?? NO_TX;
10083
+ this.forgetStrategy = options.forgetStrategy ?? NO_FORGET;
9007
10084
  this.sessionStrategy = options.sessionStrategy ?? NO_SESSION;
9008
10085
  this.syncStrategy = options.syncStrategy ?? NO_SYNC;
9009
10086
  this.snapshotStrategy = options.snapshotStrategy ?? NO_SNAPSHOTS;
@@ -9014,8 +10091,61 @@ var Noydb = class {
9014
10091
  }
9015
10092
  this.#registerGuardGate();
9016
10093
  this.#registerPeriodGate();
10094
+ this.#registerForgetHooks();
9017
10095
  this.resetSessionTimer();
9018
10096
  }
10097
+ /** @internal — resolved forget strategy (NO_FORGET when not configured). */
10098
+ get _forgetStrategy() {
10099
+ return this.forgetStrategy;
10100
+ }
10101
+ // #304 — GDPR subject-index maintenance. When `withForgetCascade` declares
10102
+ // any subject fields, keep the encrypted `_subject_index` in lock-step with
10103
+ // writes so `vault.forget(subjectId)` can find every record for a subject.
10104
+ //
10105
+ // Two consumers are required because they cover disjoint events:
10106
+ // - onAfterWrite fires on create/update (NOT delete) — add the new ref;
10107
+ // on an update that changed the subject value, drop the stale ref.
10108
+ // - the subsystemBus `afterDelete` observer fires on delete (onAfterWrite
10109
+ // does NOT) — drop the ref so a deleted record never lingers in the
10110
+ // index (RISK #2). Without it, forget() would try to shred a ghost.
10111
+ #registerForgetHooks() {
10112
+ const subjects = this.forgetStrategy.subjects;
10113
+ if (Object.keys(subjects).length === 0) return;
10114
+ const subjectFieldFor = (collection) => subjects[collection];
10115
+ this.writeHooks.onAfterWrite(async (event) => {
10116
+ const field = subjectFieldFor(event.collection);
10117
+ if (field === void 0) return;
10118
+ const vault = this.vaultCache.get(event.vault);
10119
+ if (!vault) return;
10120
+ if (event.after !== null && typeof event.after === "object") {
10121
+ const subjectValue = readDottedPath(event.after, field);
10122
+ if (subjectValue !== void 0 && subjectValue !== null) {
10123
+ await vault._addSubjectRef(coerceSubjectId(subjectValue), { collection: event.collection, id: event.docId });
10124
+ }
10125
+ }
10126
+ if (event.op === "update" && event.before !== null && typeof event.before === "object") {
10127
+ const beforeValue = readDottedPath(event.before, field);
10128
+ const afterValue = event.after !== null && typeof event.after === "object" ? readDottedPath(event.after, field) : void 0;
10129
+ const beforeId = beforeValue === void 0 || beforeValue === null ? void 0 : coerceSubjectId(beforeValue);
10130
+ const afterId = afterValue === void 0 || afterValue === null ? void 0 : coerceSubjectId(afterValue);
10131
+ if (beforeId !== void 0 && beforeId !== afterId) {
10132
+ await vault._removeSubjectRef(beforeId, { collection: event.collection, id: event.docId });
10133
+ }
10134
+ }
10135
+ });
10136
+ this.subsystemBus.register("afterDelete", async (event) => {
10137
+ const field = subjectFieldFor(event.collection);
10138
+ if (field === void 0) return;
10139
+ const vault = this.vaultCache.get(event.vault);
10140
+ if (!vault) return;
10141
+ if (event.before !== null && typeof event.before === "object") {
10142
+ const subjectValue = readDottedPath(event.before, field);
10143
+ if (subjectValue !== void 0 && subjectValue !== null) {
10144
+ await vault._removeSubjectRef(coerceSubjectId(subjectValue), { collection: event.collection, id: event.docId });
10145
+ }
10146
+ }
10147
+ });
10148
+ }
9019
10149
  // Track A — guards migration. Registers record-lock / field-freeze / onDelete
9020
10150
  // / amendment-collect as gate-bus handlers (only when guards are opted in, so
9021
10151
  // the write path is zero-cost otherwise). Resolves the live vault's
@@ -9043,7 +10173,7 @@ var Noydb = class {
9043
10173
  if (!facade) return;
9044
10174
  const ctx = { existing, vault: facade, userId: e.userId, role: e.role };
9045
10175
  await registry.runChecks(e.collection, incoming, ctx);
9046
- const { GuardExecutor } = await import("./executor-IDZDAFNH.js");
10176
+ const { GuardExecutor } = await import("./executor-THSEYEJG.js");
9047
10177
  for (const g of guards) {
9048
10178
  await GuardExecutor.checkFrozenFields(g, e.docId, existing, incoming, e.computedFieldNames);
9049
10179
  }
@@ -9236,6 +10366,7 @@ var Noydb = class {
9236
10366
  ...this.options.syncStrategy !== void 0 ? { syncStrategy: this.options.syncStrategy } : {},
9237
10367
  ...this.options.guardStrategies !== void 0 ? { guardStrategies: this.options.guardStrategies } : {},
9238
10368
  ...this.options.numbering !== void 0 ? { numberingConfigs: this.options.numbering } : {},
10369
+ forgetStrategy: this.forgetStrategy,
9239
10370
  locale: opts?.locale,
9240
10371
  // Thread the translator hook so Collection.put() can invoke it
9241
10372
  plaintextTranslator: this.options.plaintextTranslator ? (text, from, to, field, collection) => this.invokeTranslator(text, from, to, field, collection) : void 0,
@@ -9290,7 +10421,8 @@ var Noydb = class {
9290
10421
  ...this.options.i18nStrategy !== void 0 ? { i18nStrategy: this.options.i18nStrategy } : {},
9291
10422
  ...this.options.syncStrategy !== void 0 ? { syncStrategy: this.options.syncStrategy } : {},
9292
10423
  ...this.options.guardStrategies !== void 0 ? { guardStrategies: this.options.guardStrategies } : {},
9293
- ...this.options.numbering !== void 0 ? { numberingConfigs: this.options.numbering } : {}
10424
+ ...this.options.numbering !== void 0 ? { numberingConfigs: this.options.numbering } : {},
10425
+ forgetStrategy: this.forgetStrategy
9294
10426
  });
9295
10427
  this.vaultCache.set(name, comp2);
9296
10428
  return comp2;
@@ -9642,11 +10774,11 @@ var Noydb = class {
9642
10774
  if (name === STATE_VAULT_NAME) throw new ReservedVaultNameError(name);
9643
10775
  const template = this.vaultTemplates.get(opts.sharding.vaultTemplate);
9644
10776
  if (!template) throw new VaultTemplateNotFoundError(opts.sharding.vaultTemplate);
9645
- const { VaultGroup } = await import("./vault-group-DX2HFQMX.js");
9646
- const { StateManagementVault } = await import("./state-vault-TMXZRTY5.js");
10777
+ const { VaultGroup } = await import("./vault-group-DPZVFRI5.js");
10778
+ const { StateManagementVault } = await import("./state-vault-QKQKN3H3.js");
9647
10779
  const stateVault = opts.registry ? void 0 : await StateManagementVault.open(this);
9648
10780
  const registry = opts.registry ?? stateVault.registry;
9649
- const group = new VaultGroup(this, name, registry, opts.sharding, template);
10781
+ const group = new VaultGroup(this, name, registry, opts.sharding, template, opts.migrateOnOpen ?? false);
9650
10782
  if (stateVault) {
9651
10783
  group._attachStateVault(stateVault);
9652
10784
  await stateVault.recordManifest(opts.sharding.vaultTemplate, template);
@@ -9670,7 +10802,7 @@ var Noydb = class {
9670
10802
  */
9671
10803
  async openStateManagementVault() {
9672
10804
  if (this.closed) throw new ValidationError("Instance is closed");
9673
- const { StateManagementVault } = await import("./state-vault-TMXZRTY5.js");
10805
+ const { StateManagementVault } = await import("./state-vault-QKQKN3H3.js");
9674
10806
  return StateManagementVault.open(this);
9675
10807
  }
9676
10808
  /**
@@ -11172,6 +12304,7 @@ function normalizeSyncTargets(sync) {
11172
12304
 
11173
12305
  export {
11174
12306
  withArchive,
12307
+ compileSequenceFormat,
11175
12308
  resolveSequenceKey,
11176
12309
  SequenceStore,
11177
12310
  validateSchemaInput,
@@ -11179,10 +12312,15 @@ export {
11179
12312
  isZodSchema,
11180
12313
  derivePersistedSchema,
11181
12314
  persistSchemaIfNeeded,
12315
+ isRefArray,
11182
12316
  RefIntegrityError,
11183
12317
  RefScopeError,
11184
12318
  ref,
12319
+ refArray,
11185
12320
  RefRegistry,
12321
+ isLinkCollectionName,
12322
+ LinkEndpointError,
12323
+ LinkIntegrityError,
11186
12324
  QuickUnlockStore,
11187
12325
  UserApi,
11188
12326
  META_COLLECTION,
@@ -11211,4 +12349,4 @@ export {
11211
12349
  Noydb,
11212
12350
  createNoydb
11213
12351
  };
11214
- //# sourceMappingURL=chunk-667MB6AH.js.map
12352
+ //# sourceMappingURL=chunk-F2IJ2HGD.js.map