@noy-db/hub 0.2.0-pre.15 → 0.2.0-pre.17

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 (301) hide show
  1. package/dist/aggregate/index.cjs +106 -10
  2. package/dist/aggregate/index.cjs.map +1 -1
  3. package/dist/aggregate/index.d.cts +3 -2
  4. package/dist/aggregate/index.d.ts +3 -2
  5. package/dist/aggregate/index.js +4 -4
  6. package/dist/attestation/index.cjs.map +1 -1
  7. package/dist/attestation/index.d.cts +5 -3
  8. package/dist/attestation/index.d.ts +5 -3
  9. package/dist/attestation/index.js +6 -6
  10. package/dist/blobs/index.cjs +226 -11
  11. package/dist/blobs/index.cjs.map +1 -1
  12. package/dist/blobs/index.d.cts +6 -4
  13. package/dist/blobs/index.d.ts +6 -4
  14. package/dist/blobs/index.js +6 -5
  15. package/dist/blobs/index.js.map +1 -1
  16. package/dist/bundle/index.cjs +1268 -141
  17. package/dist/bundle/index.cjs.map +1 -1
  18. package/dist/bundle/index.d.cts +7 -5
  19. package/dist/bundle/index.d.ts +7 -5
  20. package/dist/bundle/index.js +21 -10
  21. package/dist/bundle/index.js.map +1 -1
  22. package/dist/{chunk-5LQG6ZO2.js → chunk-2FU2FTXD.js} +9 -4
  23. package/dist/chunk-2FU2FTXD.js.map +1 -0
  24. package/dist/{chunk-JD3OZAI4.js → chunk-3G3W65EQ.js} +2 -2
  25. package/dist/{chunk-XWH4MXIU.js → chunk-5AXTH4QZ.js} +2 -2
  26. package/dist/{chunk-4TBBMHVC.js → chunk-5LIROIDM.js} +2 -2
  27. package/dist/{chunk-3EWXMOK3.js → chunk-7H2GEJ3O.js} +28 -13
  28. package/dist/chunk-7H2GEJ3O.js.map +1 -0
  29. package/dist/{chunk-WGHU7BLI.js → chunk-AEIKD3PP.js} +52 -38
  30. package/dist/chunk-AEIKD3PP.js.map +1 -0
  31. package/dist/{chunk-KI6HAJWL.js → chunk-BH3X5L6A.js} +4 -4
  32. package/dist/{chunk-BQ65SS5A.js → chunk-BJSLBUJ7.js} +2 -2
  33. package/dist/{chunk-L2FE64BU.js → chunk-BL5GYANC.js} +3 -3
  34. package/dist/{chunk-A5ZOOZFB.js → chunk-BSZOCSDZ.js} +4 -4
  35. package/dist/{chunk-ZNQYHJXX.js → chunk-C3HYQPV4.js} +2 -2
  36. package/dist/{chunk-PE4AQGFH.js → chunk-CD2AVTEM.js} +5 -5
  37. package/dist/{chunk-7EFFHEN5.js → chunk-D77ZQSQQ.js} +852 -143
  38. package/dist/chunk-D77ZQSQQ.js.map +1 -0
  39. package/dist/{chunk-56DJ7JVK.js → chunk-DWEBTE2W.js} +5 -5
  40. package/dist/{chunk-Z4DO7YSI.js → chunk-DYYYUW5D.js} +2 -2
  41. package/dist/{chunk-NSCVNK5K.js → chunk-E77UKJYL.js} +5 -5
  42. package/dist/{chunk-KIP6JLTF.js → chunk-F4G63NTZ.js} +2 -2
  43. package/dist/{chunk-NU6Q3FOR.js → chunk-FEJDVE3Z.js} +12 -2
  44. package/dist/{chunk-NU6Q3FOR.js.map → chunk-FEJDVE3Z.js.map} +1 -1
  45. package/dist/{chunk-DQU36Q7I.js → chunk-GP3SDSH2.js} +14 -5
  46. package/dist/chunk-GP3SDSH2.js.map +1 -0
  47. package/dist/{chunk-IQLVUT37.js → chunk-H2MRGONI.js} +2 -2
  48. package/dist/{chunk-EYVQHAGH.js → chunk-HGVSHKZW.js} +8 -5
  49. package/dist/chunk-HGVSHKZW.js.map +1 -0
  50. package/dist/chunk-I5IUYN7B.js +125 -0
  51. package/dist/chunk-I5IUYN7B.js.map +1 -0
  52. package/dist/{chunk-CJORTUJ2.js → chunk-J7RWBXFY.js} +2 -2
  53. package/dist/{chunk-AAVWKNZW.js → chunk-JDWE6JMX.js} +2 -2
  54. package/dist/{chunk-6AJBSQU4.js → chunk-KCEHMDZF.js} +3 -3
  55. package/dist/{chunk-TS26M2SB.js → chunk-M476FOQ7.js} +2 -2
  56. package/dist/{chunk-F4OJZIWQ.js → chunk-NBBMMJ2H.js} +4 -4
  57. package/dist/{chunk-CZI2A4MQ.js → chunk-NYSYPFXJ.js} +3 -3
  58. package/dist/{chunk-EGD5DXFT.js → chunk-PDULVIBY.js} +14 -2
  59. package/dist/chunk-PDULVIBY.js.map +1 -0
  60. package/dist/{chunk-Z6FNBOTC.js → chunk-PDVP3C2I.js} +1 -1
  61. package/dist/{chunk-Z6FNBOTC.js.map → chunk-PDVP3C2I.js.map} +1 -1
  62. package/dist/{chunk-COFPAMX6.js → chunk-QHM6XEAH.js} +6 -6
  63. package/dist/{chunk-C5T5AFWN.js → chunk-QO6RGLLD.js} +12 -6
  64. package/dist/chunk-QO6RGLLD.js.map +1 -0
  65. package/dist/{chunk-7HT2MEZ5.js → chunk-ROPJVUG3.js} +23 -6
  66. package/dist/chunk-ROPJVUG3.js.map +1 -0
  67. package/dist/{chunk-VU7SWWT5.js → chunk-ROVO6NPJ.js} +11 -7
  68. package/dist/chunk-ROVO6NPJ.js.map +1 -0
  69. package/dist/{chunk-6RR3MNMG.js → chunk-SHX5QBCI.js} +3 -3
  70. package/dist/{chunk-GC4V7RU7.js → chunk-SISBMAPO.js} +1 -1
  71. package/dist/chunk-SISBMAPO.js.map +1 -0
  72. package/dist/{chunk-BIYRQQV6.js → chunk-SNMJ7SB3.js} +5 -5
  73. package/dist/{chunk-7PS7EOCF.js → chunk-TIDXB5DF.js} +4 -4
  74. package/dist/chunk-U5QCMH3W.js +151 -0
  75. package/dist/chunk-U5QCMH3W.js.map +1 -0
  76. package/dist/{chunk-YULZKK4F.js → chunk-UNTGHX5A.js} +37 -2
  77. package/dist/chunk-UNTGHX5A.js.map +1 -0
  78. package/dist/{chunk-FWPKCXTN.js → chunk-WIAOUFFB.js} +2 -2
  79. package/dist/{chunk-LX3CB26H.js → chunk-WV7WV6JO.js} +195 -17
  80. package/dist/chunk-WV7WV6JO.js.map +1 -0
  81. package/dist/{chunk-X73VS74Y.js → chunk-XJV6OB4D.js} +2 -2
  82. package/dist/{chunk-OHVFWCJP.js → chunk-XMHUK5PN.js} +49 -19
  83. package/dist/chunk-XMHUK5PN.js.map +1 -0
  84. package/dist/{chunk-WBAYSNUQ.js → chunk-XMVHEWF6.js} +4 -4
  85. package/dist/{chunk-HOR4R722.js → chunk-XPIHJ34I.js} +30 -4
  86. package/dist/chunk-XPIHJ34I.js.map +1 -0
  87. package/dist/{chunk-DKO2QFSA.js → chunk-YYVZYTWW.js} +3 -3
  88. package/dist/{chunk-535SSHBS.js → chunk-ZEGSDPB7.js} +81 -2
  89. package/dist/chunk-ZEGSDPB7.js.map +1 -0
  90. package/dist/{chunk-YHPM5D7Y.js → chunk-ZNGPEV5J.js} +63 -4
  91. package/dist/chunk-ZNGPEV5J.js.map +1 -0
  92. package/dist/consent/index.cjs.map +1 -1
  93. package/dist/consent/index.d.cts +6 -4
  94. package/dist/consent/index.d.ts +6 -4
  95. package/dist/consent/index.js +3 -3
  96. package/dist/{crypto-QXQOHMHF.js → crypto-7BN2HDWG.js} +7 -3
  97. package/dist/{delegation-NIQ43IPU.js → delegation-MGH5SODX.js} +5 -5
  98. package/dist/derivations/index.cjs +24 -3
  99. package/dist/derivations/index.cjs.map +1 -1
  100. package/dist/derivations/index.d.cts +7 -5
  101. package/dist/derivations/index.d.ts +7 -5
  102. package/dist/derivations/index.js +4 -4
  103. package/dist/{dev-unlock-iAS8z9jc.d.ts → dev-unlock-CI1ijTML.d.ts} +1 -1
  104. package/dist/{dev-unlock-nVkuRLLe.d.cts → dev-unlock-iXbYFAWl.d.cts} +1 -1
  105. package/dist/{strategy-CbneC7bS.d.ts → errors-Dz64FA65.d.cts} +98 -727
  106. package/dist/{strategy-CbneC7bS.d.cts → errors-Dz64FA65.d.ts} +98 -727
  107. package/dist/executor-3W63Y44O.js +11 -0
  108. package/dist/executor-CFFWPWBJ.js +8 -0
  109. package/dist/executor-VDQQOR4F.js +8 -0
  110. package/dist/{fanout-sidecar-N6OJX6QR.js → fanout-sidecar-FIJJ46YG.js} +2 -2
  111. package/dist/forget/index.cjs +43 -0
  112. package/dist/forget/index.cjs.map +1 -0
  113. package/dist/forget/index.d.cts +1 -0
  114. package/dist/forget/index.d.ts +1 -0
  115. package/dist/forget/index.js +14 -0
  116. package/dist/guards/index.cjs +9 -5
  117. package/dist/guards/index.cjs.map +1 -1
  118. package/dist/guards/index.d.cts +7 -5
  119. package/dist/guards/index.d.ts +7 -5
  120. package/dist/guards/index.js +6 -6
  121. package/dist/{hash-DHOnRarj.d.ts → hash-blk7Bkes.d.ts} +1 -1
  122. package/dist/{hash-Cv6byZs7.d.cts → hash-tEcM5fnv.d.cts} +1 -1
  123. package/dist/history/index.cjs +27 -4
  124. package/dist/history/index.cjs.map +1 -1
  125. package/dist/history/index.d.cts +7 -5
  126. package/dist/history/index.d.ts +7 -5
  127. package/dist/history/index.js +9 -7
  128. package/dist/history/index.js.map +1 -1
  129. package/dist/i18n/index.cjs +53 -0
  130. package/dist/i18n/index.cjs.map +1 -1
  131. package/dist/i18n/index.d.cts +6 -4
  132. package/dist/i18n/index.d.ts +6 -4
  133. package/dist/i18n/index.js +16 -8
  134. package/dist/i18n/index.js.map +1 -1
  135. package/dist/{immutable-guard-BehB1YGB.d.ts → immutable-guard-B5M95nbq.d.ts} +16 -1
  136. package/dist/{immutable-guard-yBEOYmif.d.cts → immutable-guard-qN3zF8o1.d.cts} +16 -1
  137. package/dist/index-C-SSRIxP.d.cts +348 -0
  138. package/dist/index-C-SSRIxP.d.ts +348 -0
  139. package/dist/{index-XNB2r6bX.d.ts → index-DpU6KWof.d.ts} +9 -1
  140. package/dist/{index-D95VK1Qy.d.cts → index-u-kWzSrL.d.cts} +9 -1
  141. package/dist/index.cjs +2715 -1302
  142. package/dist/index.cjs.map +1 -1
  143. package/dist/index.d.cts +16 -12
  144. package/dist/index.d.ts +16 -12
  145. package/dist/index.js +132 -106
  146. package/dist/index.js.map +1 -1
  147. package/dist/indexing/index.cjs.map +1 -1
  148. package/dist/indexing/index.js +4 -4
  149. package/dist/issue-TTMGHQ2J.js +12 -0
  150. package/dist/{ledger-CWSE3BLF.js → ledger-LFVLHE5H.js} +6 -6
  151. package/dist/materialized-views/index.cjs +407 -5
  152. package/dist/materialized-views/index.cjs.map +1 -1
  153. package/dist/materialized-views/index.d.cts +7 -5
  154. package/dist/materialized-views/index.d.ts +7 -5
  155. package/dist/materialized-views/index.js +12 -12
  156. package/dist/noydb-36S6GQNC.js +37 -0
  157. package/dist/overlay-views/index.cjs +47 -17
  158. package/dist/overlay-views/index.cjs.map +1 -1
  159. package/dist/overlay-views/index.d.cts +28 -8
  160. package/dist/overlay-views/index.d.ts +28 -8
  161. package/dist/overlay-views/index.js +4 -4
  162. package/dist/periods/index.cjs.map +1 -1
  163. package/dist/periods/index.d.cts +6 -4
  164. package/dist/periods/index.d.ts +6 -4
  165. package/dist/periods/index.js +6 -6
  166. package/dist/{public-envelope-SYHEYQ3X.js → public-envelope-RXZNP3V6.js} +4 -4
  167. package/dist/query/index.cjs +28 -11
  168. package/dist/query/index.cjs.map +1 -1
  169. package/dist/query/index.d.cts +3 -2
  170. package/dist/query/index.d.ts +3 -2
  171. package/dist/query/index.js +6 -6
  172. package/dist/registry-3YFLZ7WD.js +8 -0
  173. package/dist/{registry-DK5YWAAA.js → registry-SECUWSGY.js} +3 -3
  174. package/dist/registry-TGZISEWC.js +8 -0
  175. package/dist/{revoke-ZDFKMR5E.js → revoke-B54H2S2W.js} +6 -6
  176. package/dist/sealed-record/index.cjs +139 -0
  177. package/dist/sealed-record/index.cjs.map +1 -0
  178. package/dist/sealed-record/index.d.cts +123 -0
  179. package/dist/sealed-record/index.d.ts +123 -0
  180. package/dist/sealed-record/index.js +42 -0
  181. package/dist/sealed-record/index.js.map +1 -0
  182. package/dist/session/index.cjs.map +1 -1
  183. package/dist/session/index.d.cts +7 -5
  184. package/dist/session/index.d.ts +7 -5
  185. package/dist/session/index.js +3 -3
  186. package/dist/shadow/index.cjs.map +1 -1
  187. package/dist/shadow/index.d.cts +6 -4
  188. package/dist/shadow/index.d.ts +6 -4
  189. package/dist/shadow/index.js +2 -2
  190. package/dist/{signer-P5D7Y72U.js → signer-YSXZT574.js} +5 -5
  191. package/dist/snapshots/index.cjs.map +1 -1
  192. package/dist/snapshots/index.d.cts +6 -4
  193. package/dist/snapshots/index.d.ts +6 -4
  194. package/dist/snapshots/index.js +4 -4
  195. package/dist/{stale-JH67FU57.js → stale-TOA36SRK.js} +2 -2
  196. package/dist/stale-TOA36SRK.js.map +1 -0
  197. package/dist/{state-vault-TMXZRTY5.js → state-vault-W2OEABNO.js} +3 -3
  198. package/dist/store/index.cjs.map +1 -1
  199. package/dist/store/index.d.cts +6 -4
  200. package/dist/store/index.d.ts +6 -4
  201. package/dist/store/index.js +2 -2
  202. package/dist/strategy-4M9jo172.d.ts +739 -0
  203. package/dist/strategy-CLC1j79g.d.cts +739 -0
  204. package/dist/sync/index.cjs.map +1 -1
  205. package/dist/sync/index.d.cts +5 -3
  206. package/dist/sync/index.d.ts +5 -3
  207. package/dist/sync/index.js +4 -4
  208. package/dist/team/index.cjs.map +1 -1
  209. package/dist/team/index.d.cts +6 -4
  210. package/dist/team/index.d.ts +6 -4
  211. package/dist/team/index.js +8 -8
  212. package/dist/tx/index.cjs +66 -3
  213. package/dist/tx/index.cjs.map +1 -1
  214. package/dist/tx/index.d.cts +24 -6
  215. package/dist/tx/index.d.ts +24 -6
  216. package/dist/tx/index.js +9 -5
  217. package/dist/tx/index.js.map +1 -1
  218. package/dist/{types-4t1-tWS4.d.ts → types-CljIHm_J.d.ts} +1127 -606
  219. package/dist/{types-BpPV5uyy.d.cts → types-CrSpRDuG.d.cts} +1127 -606
  220. package/dist/{ulid-DAfenvFd.d.ts → ulid-CWfL2Vfv.d.ts} +1 -1
  221. package/dist/{ulid-CiPrpGqm.d.cts → ulid-CrI7PPbA.d.cts} +1 -1
  222. package/dist/util/index.cjs.map +1 -1
  223. package/dist/util/index.js +1 -1
  224. package/dist/{vault-group-KOM7QRJG.js → vault-group-DHAHFX2A.js} +4 -4
  225. package/dist/{with-derivation-OK9M2sJE.d.ts → with-derivation-BZ2y4bzF.d.ts} +1 -1
  226. package/dist/{with-derivation-DBqJB3dQ.d.cts → with-derivation-Bozs8DmD.d.cts} +1 -1
  227. package/dist/{with-materialized-view-Dt-ufPWQ.d.ts → with-materialized-view-B892zYZV.d.ts} +1 -1
  228. package/dist/{with-materialized-view-NzuxYPDF.d.cts → with-materialized-view-NzF71cG_.d.cts} +1 -1
  229. package/dist/{with-overlayed-view-eDvMs6LO.d.ts → with-overlayed-view-CR6m7CHe.d.ts} +1 -1
  230. package/dist/{with-overlayed-view-CC0_ocy-.d.cts → with-overlayed-view-UI8qSGL4.d.cts} +1 -1
  231. package/package.json +23 -3
  232. package/dist/chunk-3EWXMOK3.js.map +0 -1
  233. package/dist/chunk-535SSHBS.js.map +0 -1
  234. package/dist/chunk-5LQG6ZO2.js.map +0 -1
  235. package/dist/chunk-7EFFHEN5.js.map +0 -1
  236. package/dist/chunk-7HT2MEZ5.js.map +0 -1
  237. package/dist/chunk-C5T5AFWN.js.map +0 -1
  238. package/dist/chunk-DQU36Q7I.js.map +0 -1
  239. package/dist/chunk-EGD5DXFT.js.map +0 -1
  240. package/dist/chunk-EYVQHAGH.js.map +0 -1
  241. package/dist/chunk-GC4V7RU7.js.map +0 -1
  242. package/dist/chunk-HOR4R722.js.map +0 -1
  243. package/dist/chunk-LX3CB26H.js.map +0 -1
  244. package/dist/chunk-OHVFWCJP.js.map +0 -1
  245. package/dist/chunk-VU7SWWT5.js.map +0 -1
  246. package/dist/chunk-WGHU7BLI.js.map +0 -1
  247. package/dist/chunk-YHPM5D7Y.js.map +0 -1
  248. package/dist/chunk-YULZKK4F.js.map +0 -1
  249. package/dist/executor-6ZDSDZ6V.js +0 -8
  250. package/dist/executor-HSSRXDOB.js +0 -11
  251. package/dist/executor-IDZDAFNH.js +0 -8
  252. package/dist/issue-ADVS4OVP.js +0 -12
  253. package/dist/noydb-GZGFBA4E.js +0 -35
  254. package/dist/registry-IUZQVVBB.js +0 -8
  255. package/dist/registry-XGLNADIE.js +0 -8
  256. /package/dist/{chunk-JD3OZAI4.js.map → chunk-3G3W65EQ.js.map} +0 -0
  257. /package/dist/{chunk-XWH4MXIU.js.map → chunk-5AXTH4QZ.js.map} +0 -0
  258. /package/dist/{chunk-4TBBMHVC.js.map → chunk-5LIROIDM.js.map} +0 -0
  259. /package/dist/{chunk-KI6HAJWL.js.map → chunk-BH3X5L6A.js.map} +0 -0
  260. /package/dist/{chunk-BQ65SS5A.js.map → chunk-BJSLBUJ7.js.map} +0 -0
  261. /package/dist/{chunk-L2FE64BU.js.map → chunk-BL5GYANC.js.map} +0 -0
  262. /package/dist/{chunk-A5ZOOZFB.js.map → chunk-BSZOCSDZ.js.map} +0 -0
  263. /package/dist/{chunk-ZNQYHJXX.js.map → chunk-C3HYQPV4.js.map} +0 -0
  264. /package/dist/{chunk-PE4AQGFH.js.map → chunk-CD2AVTEM.js.map} +0 -0
  265. /package/dist/{chunk-56DJ7JVK.js.map → chunk-DWEBTE2W.js.map} +0 -0
  266. /package/dist/{chunk-Z4DO7YSI.js.map → chunk-DYYYUW5D.js.map} +0 -0
  267. /package/dist/{chunk-NSCVNK5K.js.map → chunk-E77UKJYL.js.map} +0 -0
  268. /package/dist/{chunk-KIP6JLTF.js.map → chunk-F4G63NTZ.js.map} +0 -0
  269. /package/dist/{chunk-IQLVUT37.js.map → chunk-H2MRGONI.js.map} +0 -0
  270. /package/dist/{chunk-CJORTUJ2.js.map → chunk-J7RWBXFY.js.map} +0 -0
  271. /package/dist/{chunk-AAVWKNZW.js.map → chunk-JDWE6JMX.js.map} +0 -0
  272. /package/dist/{chunk-6AJBSQU4.js.map → chunk-KCEHMDZF.js.map} +0 -0
  273. /package/dist/{chunk-TS26M2SB.js.map → chunk-M476FOQ7.js.map} +0 -0
  274. /package/dist/{chunk-F4OJZIWQ.js.map → chunk-NBBMMJ2H.js.map} +0 -0
  275. /package/dist/{chunk-CZI2A4MQ.js.map → chunk-NYSYPFXJ.js.map} +0 -0
  276. /package/dist/{chunk-COFPAMX6.js.map → chunk-QHM6XEAH.js.map} +0 -0
  277. /package/dist/{chunk-6RR3MNMG.js.map → chunk-SHX5QBCI.js.map} +0 -0
  278. /package/dist/{chunk-BIYRQQV6.js.map → chunk-SNMJ7SB3.js.map} +0 -0
  279. /package/dist/{chunk-7PS7EOCF.js.map → chunk-TIDXB5DF.js.map} +0 -0
  280. /package/dist/{chunk-FWPKCXTN.js.map → chunk-WIAOUFFB.js.map} +0 -0
  281. /package/dist/{chunk-X73VS74Y.js.map → chunk-XJV6OB4D.js.map} +0 -0
  282. /package/dist/{chunk-WBAYSNUQ.js.map → chunk-XMVHEWF6.js.map} +0 -0
  283. /package/dist/{chunk-DKO2QFSA.js.map → chunk-YYVZYTWW.js.map} +0 -0
  284. /package/dist/{crypto-QXQOHMHF.js.map → crypto-7BN2HDWG.js.map} +0 -0
  285. /package/dist/{delegation-NIQ43IPU.js.map → delegation-MGH5SODX.js.map} +0 -0
  286. /package/dist/{executor-6ZDSDZ6V.js.map → executor-3W63Y44O.js.map} +0 -0
  287. /package/dist/{executor-HSSRXDOB.js.map → executor-CFFWPWBJ.js.map} +0 -0
  288. /package/dist/{executor-IDZDAFNH.js.map → executor-VDQQOR4F.js.map} +0 -0
  289. /package/dist/{fanout-sidecar-N6OJX6QR.js.map → fanout-sidecar-FIJJ46YG.js.map} +0 -0
  290. /package/dist/{issue-ADVS4OVP.js.map → forget/index.js.map} +0 -0
  291. /package/dist/{ledger-CWSE3BLF.js.map → issue-TTMGHQ2J.js.map} +0 -0
  292. /package/dist/{noydb-GZGFBA4E.js.map → ledger-LFVLHE5H.js.map} +0 -0
  293. /package/dist/{public-envelope-SYHEYQ3X.js.map → noydb-36S6GQNC.js.map} +0 -0
  294. /package/dist/{registry-DK5YWAAA.js.map → public-envelope-RXZNP3V6.js.map} +0 -0
  295. /package/dist/{registry-IUZQVVBB.js.map → registry-3YFLZ7WD.js.map} +0 -0
  296. /package/dist/{registry-XGLNADIE.js.map → registry-SECUWSGY.js.map} +0 -0
  297. /package/dist/{revoke-ZDFKMR5E.js.map → registry-TGZISEWC.js.map} +0 -0
  298. /package/dist/{signer-P5D7Y72U.js.map → revoke-B54H2S2W.js.map} +0 -0
  299. /package/dist/{stale-JH67FU57.js.map → signer-YSXZT574.js.map} +0 -0
  300. /package/dist/{state-vault-TMXZRTY5.js.map → state-vault-W2OEABNO.js.map} +0 -0
  301. /package/dist/{vault-group-KOM7QRJG.js.map → vault-group-DHAHFX2A.js.map} +0 -0
@@ -31,7 +31,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
32
 
33
33
  // src/errors.ts
34
- var NoydbError, DecryptionError, TamperedError, InvalidKeyError, KeyringCorruptError, NoAccessError, ReadOnlyError, PermissionDeniedError, ExportCapabilityError, KeyringExpiredError, ImportCapabilityError, StoreCapabilityError, PrivilegeEscalationError, ReservedVaultNameError, FieldFrozenError, InvariantError, AmendmentForbiddenError, TierNotGrantedError, ElevationExpiredError, AlreadyElevatedError, TierDemoteDeniedError, DelegationTargetMissingError, ConflictError, LedgerContentionError, SequenceContentionError, SequenceOfflineError, NumberingUncertaintyError, BundleVersionConflictError, ValidationError, SchemaValidationError, SchemaUpdateError, SchemaFenceError, MigrationRequiredError, QuiesceTimeoutError, GroupCardinalityError, IndexRequiredError, UniqueConstraintError, UnsupportedIndexOptionError, IndexWriteFailureError, BundleIntegrityError, BundleSealMismatchError, ReservedCollectionNameError, LocaleNotSpecifiedError, TranslatorNotConfiguredError, BackupLedgerError, BackupCorruptedError, PartitionExtractionError, TransferSealError, AdoptionStateError, AttestationError, JoinTooLargeError, CrossJoinTooLargeError, CrossJoinSourceUnknownError, DanglingReferenceError, DerivationCycleError, DerivationOutputShapeError, DerivationCapExceededError, MaterializedViewCycleError, MaterializedViewSourceUnknownError, MaterializedViewTooLargeError, OverlayBaseIsVirtualError, OverlayCollectionUnavailableError, OverlayNameCollisionError, OverlayIdMismatchError, UnknownShardError, ShardProvisioningError, CrossShardJoinError, VaultTemplateNotFoundError;
34
+ var NoydbError, DecryptionError, TamperedError, InvalidKeyError, KeyringCorruptError, NoAccessError, ReadOnlyError, PermissionDeniedError, ExportCapabilityError, KeyringExpiredError, ImportCapabilityError, StoreCapabilityError, PrivilegeEscalationError, ReservedVaultNameError, FieldFrozenError, InvariantError, AmendmentForbiddenError, TierNotGrantedError, ElevationExpiredError, AlreadyElevatedError, TierDemoteDeniedError, DelegationTargetMissingError, ConflictError, LedgerContentionError, SequenceContentionError, SequenceOfflineError, NumberingUncertaintyError, BundleVersionConflictError, ValidationError, SchemaValidationError, SchemaUpdateError, SchemaFenceError, MigrationRequiredError, QuiesceTimeoutError, GroupCardinalityError, IndexRequiredError, UniqueConstraintError, UnsupportedIndexOptionError, IndexWriteFailureError, BundleIntegrityError, BundleSealMismatchError, ReservedCollectionNameError, LocaleNotSpecifiedError, StaticDictReadonlyError, UnknownDictCodeError, TranslatorNotConfiguredError, BackupLedgerError, BackupCorruptedError, PartitionExtractionError, TransferSealError, AdoptionStateError, AttestationError, JoinTooLargeError, CrossJoinTooLargeError, CrossJoinSourceUnknownError, DanglingReferenceError, DerivationCycleError, DerivationOutputShapeError, DerivationCapExceededError, MaterializedViewCycleError, MaterializedViewSourceUnknownError, MaterializedViewTooLargeError, OverlayBaseIsVirtualError, OverlayCollectionUnavailableError, OverlayNameCollisionError, OverlayIdMismatchError, UnknownShardError, ShardProvisioningError, CrossShardJoinError, VaultTemplateNotFoundError, ForgetStrategyNotConfiguredError, RecordCekNotFoundError;
35
35
  var init_errors = __esm({
36
36
  "src/errors.ts"() {
37
37
  "use strict";
@@ -485,6 +485,36 @@ Resolutions:
485
485
  this.field = field;
486
486
  }
487
487
  };
488
+ StaticDictReadonlyError = class extends NoydbError {
489
+ /** The static dictionary name that was the target of the mutation. */
490
+ dictionaryName;
491
+ constructor(dictionaryName) {
492
+ super(
493
+ "STATIC_DICT_READONLY",
494
+ `Dictionary "${dictionaryName}" is a staticDict \u2014 its labels are code constants with no mutation surface. put/putAll/rename/delete are not supported; change the label in the staticDict() table and redeploy.`
495
+ );
496
+ this.name = "StaticDictReadonlyError";
497
+ this.dictionaryName = dictionaryName;
498
+ }
499
+ };
500
+ UnknownDictCodeError = class extends NoydbError {
501
+ /** The static dictionary name. */
502
+ dictionaryName;
503
+ /** The field that carried the unknown code. */
504
+ field;
505
+ /** The offending code value. */
506
+ code;
507
+ constructor(dictionaryName, field, code) {
508
+ super(
509
+ "UNKNOWN_DICT_CODE",
510
+ `Field "${field}": code "${code}" is not a known key of staticDict "${dictionaryName}". Use a declared code, or pass { validateCodes: false } on the descriptor to allow open codes.`
511
+ );
512
+ this.name = "UnknownDictCodeError";
513
+ this.dictionaryName = dictionaryName;
514
+ this.field = field;
515
+ this.code = code;
516
+ }
517
+ };
488
518
  TranslatorNotConfiguredError = class extends NoydbError {
489
519
  /** The field that requested auto-translation. */
490
520
  field;
@@ -763,6 +793,25 @@ Resolutions:
763
793
  this.templateName = templateName;
764
794
  }
765
795
  };
796
+ ForgetStrategyNotConfiguredError = class extends NoydbError {
797
+ constructor(message = 'vault.forget() requires a forget strategy. Pass `forgetStrategy: withForgetCascade({ subjects: { <collection>: <subjectField> } })` from "@noy-db/hub/forget" to createNoydb().') {
798
+ super("FORGET_NOT_CONFIGURED", message);
799
+ this.name = "ForgetStrategyNotConfiguredError";
800
+ }
801
+ };
802
+ RecordCekNotFoundError = class extends NoydbError {
803
+ collection;
804
+ id;
805
+ constructor(collection, id) {
806
+ super(
807
+ "RECORD_CEK_NOT_FOUND",
808
+ `No per-record CEK for ${collection}/${id}. The record is missing, or its collection was not opened with { perRecordKeys: true } \u2014 only per-record-key records carry a sealable CEK.`
809
+ );
810
+ this.name = "RecordCekNotFoundError";
811
+ this.collection = collection;
812
+ this.id = id;
813
+ }
814
+ };
766
815
  }
767
816
  });
768
817
 
@@ -1016,6 +1065,39 @@ async function unwrapKey(wrappedBase64, kek) {
1016
1065
  throw new InvalidKeyError();
1017
1066
  }
1018
1067
  }
1068
+ async function asKwKey(dek) {
1069
+ const rawDek = await subtle.exportKey("raw", dek);
1070
+ const hkdfKey = await subtle.importKey("raw", rawDek, "HKDF", false, ["deriveBits"]);
1071
+ const salt = new TextEncoder().encode("noydb-cek-wrap");
1072
+ const info = new TextEncoder().encode("v1");
1073
+ const bits = await subtle.deriveBits(
1074
+ { name: "HKDF", hash: "SHA-256", salt, info },
1075
+ hkdfKey,
1076
+ KEY_BITS
1077
+ );
1078
+ return subtle.importKey("raw", bits, "AES-KW", false, ["wrapKey", "unwrapKey"]);
1079
+ }
1080
+ async function wrapCek(cek, dek) {
1081
+ const kw = await asKwKey(dek);
1082
+ const wrapped = await subtle.wrapKey("raw", cek, kw, "AES-KW");
1083
+ return bufferToBase64(wrapped);
1084
+ }
1085
+ async function unwrapCek(wrappedBase64, dek) {
1086
+ const kw = await asKwKey(dek);
1087
+ try {
1088
+ return await subtle.unwrapKey(
1089
+ "raw",
1090
+ base64ToBuffer(wrappedBase64),
1091
+ kw,
1092
+ "AES-KW",
1093
+ { name: "AES-GCM", length: KEY_BITS },
1094
+ true,
1095
+ ["encrypt", "decrypt"]
1096
+ );
1097
+ } catch {
1098
+ throw new InvalidKeyError();
1099
+ }
1100
+ }
1019
1101
  async function encrypt(plaintext, dek) {
1020
1102
  const iv = generateIV();
1021
1103
  const encoded = new TextEncoder().encode(plaintext);
@@ -1112,6 +1194,163 @@ var init_crypto = __esm({
1112
1194
  }
1113
1195
  });
1114
1196
 
1197
+ // src/record-keys/tombstone.ts
1198
+ function isTombstone(envelope, encrypted) {
1199
+ if (!encrypted) return false;
1200
+ return !envelope._data && envelope._cek === void 0;
1201
+ }
1202
+ function buildTombstone(version, actor) {
1203
+ return {
1204
+ _noydb: NOYDB_FORMAT_VERSION,
1205
+ _v: version,
1206
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
1207
+ _iv: "",
1208
+ _data: "",
1209
+ ...actor ? { _by: actor } : {}
1210
+ };
1211
+ }
1212
+ var init_tombstone = __esm({
1213
+ "src/record-keys/tombstone.ts"() {
1214
+ "use strict";
1215
+ init_types();
1216
+ }
1217
+ });
1218
+
1219
+ // src/record-keys/lifecycle.ts
1220
+ async function resolveStableCek(deps, id) {
1221
+ const cached = deps.cache?.get(id);
1222
+ if (cached) return cached;
1223
+ const live = await deps.getLive(id);
1224
+ if (live?._cek !== void 0) {
1225
+ const cek = await unwrapCek(live._cek, await deps.getDEK());
1226
+ deps.cache?.set(id, cek, 1);
1227
+ return cek;
1228
+ }
1229
+ const fresh = await generateDEK();
1230
+ deps.cache?.set(id, fresh, 1);
1231
+ return fresh;
1232
+ }
1233
+ async function rewrapBodyToDek(envelope, fromDek, toDek) {
1234
+ if (envelope._cek !== void 0) {
1235
+ const cek = await unwrapCek(envelope._cek, fromDek);
1236
+ const plaintext2 = await decrypt(envelope._iv, envelope._data, cek);
1237
+ const { iv: iv2, data: data2 } = await encrypt(plaintext2, cek);
1238
+ return { _iv: iv2, _data: data2, _cek: await wrapCek(cek, toDek), cek };
1239
+ }
1240
+ const plaintext = await decrypt(envelope._iv, envelope._data, fromDek);
1241
+ const { iv, data } = await encrypt(plaintext, toDek);
1242
+ return { _iv: iv, _data: data, cek: null };
1243
+ }
1244
+ var init_lifecycle = __esm({
1245
+ "src/record-keys/lifecycle.ts"() {
1246
+ "use strict";
1247
+ init_crypto();
1248
+ }
1249
+ });
1250
+
1251
+ // src/record-keys/sealing.ts
1252
+ async function sealRecordToHost(ctx, collection, id, hostSealer, opts) {
1253
+ if (collection.includes("/")) throw new ValidationError(`sealRecordToHost: collection "${collection}" must not contain "/"`);
1254
+ if (id.includes("/")) throw new ValidationError(`sealRecordToHost: id "${id}" must not contain "/"`);
1255
+ const live = await ctx.adapter.get(ctx.vault, collection, id);
1256
+ if (!live || live._cek === void 0) {
1257
+ throw new RecordCekNotFoundError(collection, id);
1258
+ }
1259
+ const dek = await ctx.getDEK(collection);
1260
+ const cek = await unwrapCek(live._cek, dek);
1261
+ const rawCek = await subtle2.exportKey("raw", cek);
1262
+ const cekB64 = bufferToBase64(rawCek);
1263
+ const hint = await hostSealer.publishRecipientHint();
1264
+ if (hint.pid.includes("/")) throw new ValidationError(`sealRecordToHost: recipient pid "${hint.pid}" must not contain "/"`);
1265
+ const binding = {
1266
+ collection,
1267
+ id,
1268
+ cek: cekB64,
1269
+ expiresAt: opts.expiresAt
1270
+ };
1271
+ const sealed = await hostSealer.sealForRecipient(
1272
+ new TextEncoder().encode(JSON.stringify(binding)),
1273
+ hint
1274
+ );
1275
+ const delivery = {
1276
+ v: 1,
1277
+ _noydb_sealed_cek: 1,
1278
+ pid: hint.pid,
1279
+ payload: bufferToBase64(sealed),
1280
+ expiresAt: opts.expiresAt
1281
+ };
1282
+ const envelopeKey = `${collection}/${id}/${hint.pid}`;
1283
+ const prior = await ctx.adapter.get(ctx.vault, SEALED_CEK_NS, envelopeKey);
1284
+ const env = {
1285
+ _noydb: NOYDB_FORMAT_VERSION,
1286
+ _v: (prior?._v ?? 0) + 1,
1287
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
1288
+ // AES-GCM bypassed — the sealing layer is the security boundary, exactly
1289
+ // like the managed-passphrase `_meta/sealed-passphrase` envelope.
1290
+ _iv: "",
1291
+ _data: JSON.stringify(delivery),
1292
+ ...ctx.actor ? { _by: ctx.actor } : {}
1293
+ };
1294
+ await ctx.adapter.put(ctx.vault, SEALED_CEK_NS, envelopeKey, env);
1295
+ return { pid: hint.pid, envelopeKey };
1296
+ }
1297
+ async function revokeSealedRecord(ctx, collection, id, pid) {
1298
+ await ctx.adapter.delete(ctx.vault, SEALED_CEK_NS, `${collection}/${id}/${pid}`);
1299
+ }
1300
+ async function rotateRecordCek(ctx, collection, id) {
1301
+ const live = await ctx.adapter.get(ctx.vault, collection, id);
1302
+ if (!live || live._cek === void 0) {
1303
+ throw new RecordCekNotFoundError(collection, id);
1304
+ }
1305
+ const dek = await ctx.getDEK(collection);
1306
+ const oldCek = await unwrapCek(live._cek, dek);
1307
+ const json = await decrypt(live._iv, live._data, oldCek);
1308
+ const newCek = await generateDEK();
1309
+ const { iv, data } = await encrypt(json, newCek);
1310
+ const env = {
1311
+ _noydb: NOYDB_FORMAT_VERSION,
1312
+ _v: live._v + 1,
1313
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
1314
+ _iv: iv,
1315
+ _data: data,
1316
+ _cek: await wrapCek(newCek, dek),
1317
+ ...ctx.actor ? { _by: ctx.actor } : {},
1318
+ ...live._tier !== void 0 ? { _tier: live._tier } : {},
1319
+ ...live._det !== void 0 ? { _det: live._det } : {}
1320
+ };
1321
+ await ctx.adapter.put(ctx.vault, collection, id, env);
1322
+ await ctx.invalidateRecordCaches(collection, id);
1323
+ const prefix = `${collection}/${id}/`;
1324
+ const keys = await ctx.adapter.list(ctx.vault, SEALED_CEK_NS);
1325
+ for (const key of keys) {
1326
+ if (key.startsWith(prefix)) {
1327
+ await ctx.adapter.delete(ctx.vault, SEALED_CEK_NS, key);
1328
+ }
1329
+ }
1330
+ }
1331
+ var subtle2, SEALED_CEK_NS;
1332
+ var init_sealing = __esm({
1333
+ "src/record-keys/sealing.ts"() {
1334
+ "use strict";
1335
+ init_crypto();
1336
+ init_types();
1337
+ init_errors();
1338
+ subtle2 = globalThis.crypto.subtle;
1339
+ SEALED_CEK_NS = "_sealed_cek";
1340
+ }
1341
+ });
1342
+
1343
+ // src/record-keys/index.ts
1344
+ var init_record_keys = __esm({
1345
+ "src/record-keys/index.ts"() {
1346
+ "use strict";
1347
+ init_crypto();
1348
+ init_tombstone();
1349
+ init_lifecycle();
1350
+ init_sealing();
1351
+ }
1352
+ });
1353
+
1115
1354
  // src/persisted-schemas/storage.ts
1116
1355
  async function loadPersistedSchema(store, vault, collection, dek) {
1117
1356
  const envelope = await store.get(vault, SCHEMAS_COLLECTION, collection);
@@ -2815,11 +3054,11 @@ async function mintWrappedDeksBlob(deks, credential) {
2815
3054
  const wrappingKey = await deriveWrappingKey(credential, salt);
2816
3055
  const exported = {};
2817
3056
  for (const [coll, dek] of deks) {
2818
- const raw = await subtle2.exportKey("raw", dek);
3057
+ const raw = await subtle3.exportKey("raw", dek);
2819
3058
  exported[coll] = bytesToBase643(new Uint8Array(raw));
2820
3059
  }
2821
3060
  const plaintext = new TextEncoder().encode(JSON.stringify({ deks: exported }));
2822
- const ciphertext = await subtle2.encrypt(
3061
+ const ciphertext = await subtle3.encrypt(
2823
3062
  { name: "AES-GCM", iv },
2824
3063
  wrappingKey,
2825
3064
  plaintext
@@ -2832,7 +3071,7 @@ async function mintWrappedDeksBlob(deks, credential) {
2832
3071
  }
2833
3072
  async function unwrapDeksFromBlob(blob, credential) {
2834
3073
  const wrappingKey = await deriveWrappingKey(credential, base64ToBytes3(blob.salt));
2835
- const plaintext = await subtle2.decrypt(
3074
+ const plaintext = await subtle3.decrypt(
2836
3075
  { name: "AES-GCM", iv: base64ToBytes3(blob.iv) },
2837
3076
  wrappingKey,
2838
3077
  base64ToBytes3(blob.wrappedDeks)
@@ -2841,7 +3080,7 @@ async function unwrapDeksFromBlob(blob, credential) {
2841
3080
  const deks = /* @__PURE__ */ new Map();
2842
3081
  for (const [coll, b64] of Object.entries(parsed.deks)) {
2843
3082
  const raw = base64ToBytes3(b64);
2844
- const key = await subtle2.importKey(
3083
+ const key = await subtle3.importKey(
2845
3084
  "raw",
2846
3085
  raw,
2847
3086
  { name: "AES-GCM", length: 256 },
@@ -2853,14 +3092,14 @@ async function unwrapDeksFromBlob(blob, credential) {
2853
3092
  return deks;
2854
3093
  }
2855
3094
  async function deriveWrappingKey(credential, salt) {
2856
- const ikm = await subtle2.importKey(
3095
+ const ikm = await subtle3.importKey(
2857
3096
  "raw",
2858
3097
  new TextEncoder().encode(credential),
2859
3098
  "PBKDF2",
2860
3099
  false,
2861
3100
  ["deriveKey"]
2862
3101
  );
2863
- return subtle2.deriveKey(
3102
+ return subtle3.deriveKey(
2864
3103
  {
2865
3104
  name: "PBKDF2",
2866
3105
  salt,
@@ -2884,14 +3123,14 @@ function base64ToBytes3(b64) {
2884
3123
  for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
2885
3124
  return out;
2886
3125
  }
2887
- var PBKDF2_ITERATIONS2, SALT_BYTES2, IV_BYTES2, subtle2;
3126
+ var PBKDF2_ITERATIONS2, SALT_BYTES2, IV_BYTES2, subtle3;
2888
3127
  var init_wrapped_deks = __esm({
2889
3128
  "src/team/wrapped-deks.ts"() {
2890
3129
  "use strict";
2891
3130
  PBKDF2_ITERATIONS2 = 6e5;
2892
3131
  SALT_BYTES2 = 32;
2893
3132
  IV_BYTES2 = 12;
2894
- subtle2 = globalThis.crypto.subtle;
3133
+ subtle3 = globalThis.crypto.subtle;
2895
3134
  }
2896
3135
  });
2897
3136
 
@@ -3696,6 +3935,21 @@ var init_core = __esm({
3696
3935
  }
3697
3936
  });
3698
3937
 
3938
+ // src/i18n/dictionary.ts
3939
+ function isDictCollectionName(name) {
3940
+ return name.startsWith(DICT_COLLECTION_PREFIX);
3941
+ }
3942
+ function isStaticDictDescriptor(x) {
3943
+ return typeof x === "object" && x !== null && x._noydbStaticDict === true;
3944
+ }
3945
+ var DICT_COLLECTION_PREFIX;
3946
+ var init_dictionary = __esm({
3947
+ "src/i18n/dictionary.ts"() {
3948
+ "use strict";
3949
+ DICT_COLLECTION_PREFIX = "_dict_";
3950
+ }
3951
+ });
3952
+
3699
3953
  // src/money/fixed-point.ts
3700
3954
  function expandExponent(s) {
3701
3955
  const m = /^([+-]?)(\d+)(?:\.(\d+))?[eE]([+-]?\d+)$/.exec(s);
@@ -4315,6 +4569,9 @@ var init_strategy3 = __esm({
4315
4569
  async clearHistory() {
4316
4570
  return 0;
4317
4571
  },
4572
+ async tombstoneHistory() {
4573
+ return 0;
4574
+ },
4318
4575
  async envelopePayloadHash() {
4319
4576
  return "";
4320
4577
  },
@@ -4834,13 +5091,22 @@ var init_strategy4 = __esm({
4834
5091
  });
4835
5092
 
4836
5093
  // src/money/money-reducer.ts
4837
- function toScaledInt(v) {
4838
- if (typeof v === "string" || typeof v === "number" || typeof v === "bigint") {
4839
- try {
4840
- return BigInt(v);
4841
- } catch {
4842
- return null;
5094
+ function toScaledIntFromAny(v, scale) {
5095
+ if (typeof v === "bigint") return v;
5096
+ if (typeof v === "number") {
5097
+ const r = parseToScaledInt(v, scale);
5098
+ return r.ok ? r.value : null;
5099
+ }
5100
+ if (typeof v === "string") {
5101
+ if (!v.includes(".")) {
5102
+ try {
5103
+ return BigInt(v);
5104
+ } catch {
5105
+ return null;
5106
+ }
4843
5107
  }
5108
+ const r = parseToScaledInt(v, scale);
5109
+ return r.ok ? r.value : null;
4844
5110
  }
4845
5111
  return null;
4846
5112
  }
@@ -4848,13 +5114,15 @@ function readMoney(record, field, desc) {
4848
5114
  const raw = readPath(record, field);
4849
5115
  if (raw === null || raw === void 0) return null;
4850
5116
  if (desc.mode === "fixed") {
4851
- const value2 = toScaledInt(raw);
4852
- return value2 === null ? null : { currency: desc.fixedCurrency, value: value2 };
5117
+ const cur = desc.fixedCurrency;
5118
+ const value2 = toScaledIntFromAny(raw, desc.scaleFor(cur));
5119
+ return value2 === null ? null : { currency: cur, value: value2 };
4853
5120
  }
4854
5121
  if (typeof raw !== "object") return null;
4855
5122
  const o = raw;
4856
5123
  if (typeof o.currency !== "string") return null;
4857
- const value = toScaledInt(o.amount);
5124
+ const scale = desc.allows(o.currency) ? desc.scaleFor(o.currency) : 0;
5125
+ const value = toScaledIntFromAny(o.amount, scale);
4858
5126
  return value === null ? null : { currency: o.currency, value };
4859
5127
  }
4860
5128
  function targetScaleFor(desc, currency) {
@@ -5268,6 +5536,7 @@ function buildDictLabelResolver(joinCtx, field) {
5268
5536
  const dictSource = joinCtx.resolveDictSource(field);
5269
5537
  if (!dictSource) return void 0;
5270
5538
  const snapshot = dictSource.snapshot();
5539
+ const displayLocale = dictSource.displayLocale;
5271
5540
  const dictMap = /* @__PURE__ */ new Map();
5272
5541
  for (const entry of snapshot) {
5273
5542
  const k = entry["key"];
@@ -5277,9 +5546,11 @@ function buildDictLabelResolver(joinCtx, field) {
5277
5546
  }
5278
5547
  }
5279
5548
  return async (key, locale, fallback) => {
5549
+ const effLocale = locale || displayLocale;
5550
+ if (!effLocale) return void 0;
5280
5551
  const labels = dictMap.get(key);
5281
5552
  if (!labels) return void 0;
5282
- if (labels[locale] !== void 0) return labels[locale];
5553
+ if (labels[effLocale] !== void 0) return labels[effLocale];
5283
5554
  const chain = Array.isArray(fallback) ? fallback : fallback ? [fallback] : [];
5284
5555
  for (const fb of chain) {
5285
5556
  if (fb === "any") {
@@ -6005,11 +6276,14 @@ function warnCardinalityApproaching(fields, observed) {
6005
6276
  `[noy-db] .groupBy(${label}) produced ${observed} distinct groups, ${Math.round(observed / GROUPBY_MAX_CARDINALITY * 100)}% of the ${GROUPBY_MAX_CARDINALITY}-group ceiling. Narrow the query with .where() before grouping, or switch to a lower-cardinality field.`
6006
6277
  );
6007
6278
  }
6008
- function groupAndReduce(records, fieldOrFields, spec) {
6279
+ function groupAndReduce(records, fieldOrFields, spec, moneyFields) {
6009
6280
  const fields = typeof fieldOrFields === "string" ? [fieldOrFields] : fieldOrFields;
6010
6281
  if (fields.length === 0) {
6011
6282
  throw new Error(".groupBy() requires at least one field");
6012
6283
  }
6284
+ if (moneyFields) {
6285
+ spec = wrapMoneyReducers(spec, moneyFields);
6286
+ }
6013
6287
  const buckets = /* @__PURE__ */ new Map();
6014
6288
  const fieldLabel = fields.length === 1 ? fields[0] : `[${fields.join(", ")}]`;
6015
6289
  for (const record of records) {
@@ -6065,6 +6339,7 @@ var init_groupby = __esm({
6065
6339
  init_predicate();
6066
6340
  init_canonical_key();
6067
6341
  init_errors();
6342
+ init_money_reducer();
6068
6343
  GROUPBY_WARN_CARDINALITY = 1e4;
6069
6344
  GROUPBY_MAX_CARDINALITY = 1e5;
6070
6345
  warnedCardinalityFields = /* @__PURE__ */ new Set();
@@ -7466,10 +7741,14 @@ function summarizeQueryPlan(query) {
7466
7741
  });
7467
7742
  }
7468
7743
  function summarizeUnionPlan(spec) {
7469
- const arms = (spec.unionSources ?? []).map((s) => s.collection).join(",");
7744
+ const arms = (spec.unionSources ?? []).map((s) => {
7745
+ const joins = s.join?.length ? `[${s.join.map((j) => `${j.field}\u2192${j.as}`).join(";")}]` : "";
7746
+ return `${s.collection}${joins}`;
7747
+ }).join(",");
7470
7748
  const groupBy = Array.isArray(spec.groupBy) ? [...spec.groupBy].sort().join(",") : typeof spec.groupBy === "string" ? spec.groupBy : "";
7471
7749
  const aggKeys = spec.aggregate ? Object.keys(spec.aggregate).sort().join(",") : "";
7472
- return `union(${arms})|groupBy(${groupBy})|aggregate(${aggKeys})`;
7750
+ const moneyKeys = spec.moneyFields ? Object.keys(spec.moneyFields).sort().join(",") : "";
7751
+ return `union(${arms})|groupBy(${groupBy})|aggregate(${aggKeys})|money(${moneyKeys})`;
7473
7752
  }
7474
7753
  var init_dependency_analyzer = __esm({
7475
7754
  "src/materialized-views/dependency-analyzer.ts"() {
@@ -7596,6 +7875,7 @@ var init_registry = __esm({
7596
7875
  let isQuery = false;
7597
7876
  if (spec.unionSources) {
7598
7877
  dependencies = new Set(spec.unionSources.map((s) => s.collection));
7878
+ if (spec.sources) for (const s of spec.sources) dependencies.add(s);
7599
7879
  queryPlanSummary = summarizeUnionPlan(spec);
7600
7880
  } else {
7601
7881
  const q = spec.query(dbForQuery);
@@ -7750,7 +8030,13 @@ async function materializeUnionResult(spec, db) {
7750
8030
  const unified = [];
7751
8031
  for (const arm of spec.unionSources) {
7752
8032
  const coll = db.collection(arm.collection);
7753
- const sourceRows = coll.query().toArray();
8033
+ let q = coll.query();
8034
+ if (arm.join?.length) {
8035
+ for (const leg of arm.join) {
8036
+ q = q.join(leg.field, { as: leg.as, maxRows: leg.maxRows, strategy: leg.strategy });
8037
+ }
8038
+ }
8039
+ const sourceRows = q.toArray();
7754
8040
  for (const r of sourceRows) {
7755
8041
  const mapped = arm.map(r);
7756
8042
  if (mapped == null) continue;
@@ -7767,7 +8053,7 @@ async function materializeUnionResult(spec, db) {
7767
8053
  }
7768
8054
  return [...seen.values()];
7769
8055
  }
7770
- return groupAndReduce(unified, groupFields, spec.aggregate);
8056
+ return groupAndReduce(unified, groupFields, spec.aggregate, spec.moneyFields);
7771
8057
  }
7772
8058
  async function listOutputIds(outputColl) {
7773
8059
  const cAny = outputColl;
@@ -8038,12 +8324,14 @@ var init_collection = __esm({
8038
8324
  init_types();
8039
8325
  init_strategy();
8040
8326
  init_core();
8327
+ init_dictionary();
8041
8328
  init_normalize();
8042
8329
  init_paths();
8043
8330
  init_computed();
8044
8331
  init_strategy2();
8045
8332
  init_policy();
8046
8333
  init_crypto();
8334
+ init_record_keys();
8047
8335
  init_errors();
8048
8336
  init_tiers();
8049
8337
  init_keyring();
@@ -8241,6 +8529,25 @@ var init_collection = __esm({
8241
8529
  * is inactive for this collection; a frozen `Set` otherwise.
8242
8530
  */
8243
8531
  deterministicFields;
8532
+ /**
8533
+ * Per-record CEK opt-in (`perRecordKeys: true`). When set, writes mint /
8534
+ * reuse a per-record content-encryption key and stamp `_cek` on the
8535
+ * envelope (see {@link EncryptedEnvelope._cek}). OFF by default — a
8536
+ * non-adopting collection takes the byte-identical legacy path. The READ
8537
+ * path does not consult this flag: `_cek` presence on the envelope is the
8538
+ * format discriminant, so a mixed vault (and a recipient that never set the
8539
+ * flag) still decrypts CEK records.
8540
+ */
8541
+ perRecordCek;
8542
+ /**
8543
+ * Session-scoped `(id) → CEK` cache for this collection. Lets updates
8544
+ * reuse a record's stable CEK and lets repeated reads skip the AES-KW
8545
+ * unwrap. Bounded by LRU; never persisted. Dropped when the owning
8546
+ * collection instance is discarded — `vault.load()` clears the
8547
+ * collectionCache, so a keyring refresh drops every CEK alongside the
8548
+ * DEK cache. `null` unless `perRecordCek` is set.
8549
+ */
8550
+ cekCache;
8244
8551
  /**
8245
8552
  * declared tiers for this collection. `null` when
8246
8553
  * tier-aware methods are disabled. Tier 0 is implicit and never
@@ -8387,19 +8694,24 @@ var init_collection = __esm({
8387
8694
  } else {
8388
8695
  this.deterministicFields = null;
8389
8696
  }
8697
+ this.perRecordCek = opts.perRecordKeys === true;
8698
+ this.cekCache = this.perRecordCek ? new Lru({ maxRecords: 4096 }) : null;
8390
8699
  if (opts.crdt && opts.onRegisterConflictResolver) {
8391
8700
  const crdtMode = opts.crdt;
8392
- const crdtResolver = async (_id, local, remote) => {
8701
+ const crdtResolver = async (id, local, remote) => {
8393
8702
  if (crdtMode === "yjs") {
8394
8703
  return local._v >= remote._v ? local : remote;
8395
8704
  }
8396
- const localJson = await this.decryptJsonString(local);
8397
- const remoteJson = await this.decryptJsonString(remote);
8705
+ const localJson = await this.decryptJsonString(local, id);
8706
+ const remoteJson = await this.decryptJsonString(remote, id);
8707
+ if (localJson === null) return local;
8708
+ if (remoteJson === null) return remote;
8398
8709
  const localState = JSON.parse(localJson);
8399
8710
  const remoteState = JSON.parse(remoteJson);
8400
8711
  const merged = this.crdtStrategy.mergeCrdtStates(localState, remoteState);
8401
8712
  const mergedVersion = Math.max(local._v, remote._v) + 1;
8402
- return this.encryptJsonString(JSON.stringify(merged), mergedVersion);
8713
+ const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
8714
+ return this.encryptJsonString(JSON.stringify(merged), mergedVersion, cek);
8403
8715
  };
8404
8716
  opts.onRegisterConflictResolver(this.name, crdtResolver);
8405
8717
  }
@@ -8439,12 +8751,15 @@ var init_collection = __esm({
8439
8751
  });
8440
8752
  } else {
8441
8753
  const mergeFn = policy;
8442
- resolver = async (_id, local, remote) => {
8443
- const localRecord = await this.decryptRecord(local, { skipValidation: true });
8444
- const remoteRecord = await this.decryptRecord(remote, { skipValidation: true });
8754
+ resolver = async (id, local, remote) => {
8755
+ const localRecord = await this.decryptRecord(local, { skipValidation: true, id });
8756
+ const remoteRecord = await this.decryptRecord(remote, { skipValidation: true, id });
8757
+ if (localRecord === null) return local;
8758
+ if (remoteRecord === null) return remote;
8445
8759
  const merged = mergeFn(localRecord, remoteRecord);
8446
8760
  const mergedVersion = Math.max(local._v, remote._v) + 1;
8447
- return this.encryptRecord(merged, mergedVersion);
8761
+ const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
8762
+ return this.encryptRecord(merged, mergedVersion, cek);
8448
8763
  };
8449
8764
  }
8450
8765
  opts.onRegisterConflictResolver(collectionName, resolver);
@@ -8536,7 +8851,9 @@ var init_collection = __esm({
8536
8851
  } else {
8537
8852
  const envelope = await this.adapter.get(this.vault, this.name, id);
8538
8853
  if (!envelope) return null;
8539
- record = await this.decryptRecord(envelope);
8854
+ if (isTombstone(envelope, this.encrypted)) return null;
8855
+ record = await this.decryptRecord(envelope, { id });
8856
+ if (record === null) return null;
8540
8857
  this.lru.set(id, { record, version: envelope._v }, estimateRecordBytes(record));
8541
8858
  }
8542
8859
  } else {
@@ -8563,6 +8880,7 @@ var init_collection = __esm({
8563
8880
  const envelope = await this.adapter.get(this.vault, this.name, id);
8564
8881
  if (!envelope) return null;
8565
8882
  const json = await this.decryptJsonString(envelope);
8883
+ if (json === null) return null;
8566
8884
  return JSON.parse(json);
8567
8885
  }
8568
8886
  /**
@@ -8651,7 +8969,7 @@ var init_collection = __esm({
8651
8969
  if (cached2) return { record: cached2.record, version: cached2.version };
8652
8970
  const env = await this.adapter.get(this.vault, this.name, id);
8653
8971
  if (!env) return { record: null, version: 0 };
8654
- return { record: await this.decryptRecord(env, { skipValidation: true }), version: env._v };
8972
+ return { record: await this.decryptRecord(env, { skipValidation: true }) ?? null, version: env._v };
8655
8973
  }
8656
8974
  await this.ensureHydrated();
8657
8975
  const cached = this.cache.get(id);
@@ -8764,9 +9082,11 @@ var init_collection = __esm({
8764
9082
  let existingState;
8765
9083
  if (existingEnvelope) {
8766
9084
  const prevJson = await this.decryptJsonString(existingEnvelope);
8767
- const prevParsed = JSON.parse(prevJson);
8768
- if (prevParsed !== null && typeof prevParsed === "object" && "_crdt" in prevParsed) {
8769
- existingState = prevParsed;
9085
+ if (prevJson !== null) {
9086
+ const prevParsed = JSON.parse(prevJson);
9087
+ if (prevParsed !== null && typeof prevParsed === "object" && "_crdt" in prevParsed) {
9088
+ existingState = prevParsed;
9089
+ }
8770
9090
  }
8771
9091
  }
8772
9092
  crdtState = this.crdtStrategy.buildLwwMapState(record, existingState, now);
@@ -8774,9 +9094,11 @@ var init_collection = __esm({
8774
9094
  let existingState;
8775
9095
  if (existingEnvelope) {
8776
9096
  const prevJson = await this.decryptJsonString(existingEnvelope);
8777
- const prevParsed = JSON.parse(prevJson);
8778
- if (prevParsed !== null && typeof prevParsed === "object" && "_crdt" in prevParsed) {
8779
- existingState = prevParsed;
9097
+ if (prevJson !== null) {
9098
+ const prevParsed = JSON.parse(prevJson);
9099
+ if (prevParsed !== null && typeof prevParsed === "object" && "_crdt" in prevParsed) {
9100
+ existingState = prevParsed;
9101
+ }
8780
9102
  }
8781
9103
  }
8782
9104
  const arr = Array.isArray(record) ? record : [record];
@@ -8785,12 +9107,14 @@ var init_collection = __esm({
8785
9107
  crdtState = { _crdt: "yjs", update: record };
8786
9108
  }
8787
9109
  const version2 = existingVersion + 1;
8788
- const envelope2 = await this.encryptJsonString(JSON.stringify(crdtState), version2);
9110
+ const cek2 = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
9111
+ const envelope2 = await this.encryptJsonString(JSON.stringify(crdtState), version2, cek2);
8789
9112
  await this.adapter.put(this.vault, this.name, id, envelope2);
8790
9113
  const resolvedRecord = this.crdtStrategy.resolveCrdtSnapshot(crdtState);
8791
- const existingResolved = existingEnvelope ? { record: await this.decryptRecord(existingEnvelope, { skipValidation: true }), version: existingVersion } : void 0;
9114
+ const existingResolvedRecord = existingEnvelope ? await this.decryptRecord(existingEnvelope, { skipValidation: true }) : null;
9115
+ const existingResolved = existingResolvedRecord !== null ? { record: existingResolvedRecord, version: existingVersion } : void 0;
8792
9116
  if (existingResolved && this.historyConfig.enabled !== false) {
8793
- const histEnvelope = await this.encryptRecord(existingResolved.record, existingResolved.version);
9117
+ const histEnvelope = await this.encryptRecord(existingResolved.record, existingResolved.version, cek2);
8794
9118
  await this.historyStrategy.saveHistory(this.adapter, this.vault, this.name, id, histEnvelope);
8795
9119
  this.emitter.emit("history:save", { vault: this.vault, collection: this.name, id, version: existingResolved.version });
8796
9120
  if (this.historyConfig.maxVersions) {
@@ -8836,7 +9160,9 @@ var init_collection = __esm({
8836
9160
  const previousEnvelope = await this.adapter.get(this.vault, this.name, id);
8837
9161
  if (previousEnvelope) {
8838
9162
  const previousRecord = await this.decryptRecord(previousEnvelope);
8839
- existing = { record: previousRecord, version: previousEnvelope._v };
9163
+ if (previousRecord !== null) {
9164
+ existing = { record: previousRecord, version: previousEnvelope._v };
9165
+ }
8840
9166
  }
8841
9167
  }
8842
9168
  } else {
@@ -8845,8 +9171,9 @@ var init_collection = __esm({
8845
9171
  }
8846
9172
  const version = existing ? existing.version + 1 : 1;
8847
9173
  this.uniqueConstraints?.check(id, record);
9174
+ const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
8848
9175
  if (existing && this.historyConfig.enabled !== false) {
8849
- const historyEnvelope = await this.encryptRecord(existing.record, existing.version);
9176
+ const historyEnvelope = await this.encryptRecord(existing.record, existing.version, cek);
8850
9177
  await this.historyStrategy.saveHistory(this.adapter, this.vault, this.name, id, historyEnvelope);
8851
9178
  this.emitter.emit("history:save", {
8852
9179
  vault: this.vault,
@@ -8860,7 +9187,7 @@ var init_collection = __esm({
8860
9187
  });
8861
9188
  }
8862
9189
  }
8863
- const envelope = await this.encryptRecord(record, version);
9190
+ const envelope = await this.encryptRecord(record, version, cek);
8864
9191
  await this.adapter.put(this.vault, this.name, id, envelope);
8865
9192
  if (this.ledger) {
8866
9193
  const appendInput = {
@@ -8963,9 +9290,18 @@ var init_collection = __esm({
8963
9290
  if (DerivationExecutor2 === null) {
8964
9291
  ({ DerivationExecutor: DerivationExecutor2 } = await Promise.resolve().then(() => (init_executor(), executor_exports)));
8965
9292
  }
8966
- const sourceWithId = { ...incoming, id };
9293
+ let sourceWithId;
9294
+ let sourceVersion = version;
9295
+ if (spec.source === this.name) {
9296
+ sourceWithId = { ...incoming, id };
9297
+ } else {
9298
+ const primary = await this.derivationSource.getCollection(spec.source).get(id);
9299
+ if (primary === null || primary === void 0) continue;
9300
+ sourceWithId = { ...primary, id };
9301
+ sourceVersion = 0;
9302
+ }
8967
9303
  const ctx = { vault: this.derivationSource.getReadOnlyFacade() };
8968
- const result = await DerivationExecutor2.run(spec, sourceWithId, version, strategyHash, ctx);
9304
+ const result = await DerivationExecutor2.run(spec, sourceWithId, sourceVersion, strategyHash, ctx);
8969
9305
  for (const key of Object.keys(spec.outputs)) {
8970
9306
  const out = result.outputs[key];
8971
9307
  if (!out) continue;
@@ -9087,11 +9423,14 @@ var init_collection = __esm({
9087
9423
  let count = 0;
9088
9424
  for (const id of ids) {
9089
9425
  const env = await this.adapter.get(this.vault, this.name, id);
9090
- if (!env) continue;
9091
- const record = await this.decryptRecord(env, { skipValidation: true });
9426
+ if (!env || isTombstone(env, this.encrypted)) continue;
9427
+ const decoded = await this.decryptRecord(env, { skipValidation: true, id });
9428
+ if (decoded === null) continue;
9429
+ const record = decoded;
9092
9430
  const next = transform(record);
9093
9431
  const nextVersion = (env._v ?? 0) + 1;
9094
- const newEnv = await this.encryptRecord(next, nextVersion);
9432
+ const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
9433
+ const newEnv = await this.encryptRecord(next, nextVersion, cek);
9095
9434
  await this.adapter.put(this.vault, this.name, id, newEnv);
9096
9435
  await this._invalidateCacheEntry(id);
9097
9436
  if (this.ledger) {
@@ -9203,14 +9542,17 @@ var init_collection = __esm({
9203
9542
  const previousEnvelope2 = await this.adapter.get(this.vault, this.name, id);
9204
9543
  if (previousEnvelope2) {
9205
9544
  const previousRecord = await this.decryptRecord(previousEnvelope2);
9206
- existing = { record: previousRecord, version: previousEnvelope2._v };
9545
+ if (previousRecord !== null) {
9546
+ existing = { record: previousRecord, version: previousEnvelope2._v };
9547
+ }
9207
9548
  }
9208
9549
  }
9209
9550
  } else {
9210
9551
  existing = this.cache.get(id);
9211
9552
  }
9212
9553
  if (existing && this.historyConfig.enabled !== false) {
9213
- const historyEnvelope = await this.encryptRecord(existing.record, existing.version);
9554
+ const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
9555
+ const historyEnvelope = await this.encryptRecord(existing.record, existing.version, cek);
9214
9556
  await this.historyStrategy.saveHistory(this.adapter, this.vault, this.name, id, historyEnvelope);
9215
9557
  }
9216
9558
  const previousEnvelope = await this.adapter.get(this.vault, this.name, id);
@@ -9251,6 +9593,50 @@ var init_collection = __esm({
9251
9593
  await this.dispatchArrayDerivationsOnDelete(id);
9252
9594
  }
9253
9595
  }
9596
+ /**
9597
+ * @internal — GDPR crypto-shred a LIVE record to a tombstone (#304).
9598
+ *
9599
+ * Rewrites the on-disk envelope to `{ _noydb, _v, _ts, _by, _iv:'', _data:'' }`,
9600
+ * dropping `_iv`/`_data`/`_cek`/`_det`. The wrapped per-record CEK is gone, so
9601
+ * the body — and (via {@link tombstoneHistory}) every history version under
9602
+ * the same CEK — is permanently undecryptable; the collection DEK and every
9603
+ * other record are untouched. `_det` is stripped too, so `findByDet` no
9604
+ * longer matches the shredded record (avoiding a post-shred TamperedError).
9605
+ *
9606
+ * Unlike `delete()`/`_internalDelete`, this:
9607
+ * - does NOT fire onDelete guards / MV / derivation dispatch (a shred is an
9608
+ * erasure, not a domain delete — re-running those would be wrong),
9609
+ * - does NOT append a per-record ledger entry (`vault.forget()` appends a
9610
+ * single `op:'forget'` summary for the whole subject),
9611
+ * - keeps the record KEY present (it's an overwrite, not an adapter delete)
9612
+ * so the version counter + "record existed" survive for audit.
9613
+ *
9614
+ * Idempotent: returns `null` when the record is absent or already a tombstone.
9615
+ * Otherwise returns `{ previousVersion }`. Invalidates the eager cache, the
9616
+ * lazy LRU, and the per-record CEK cache for this id.
9617
+ */
9618
+ /**
9619
+ * @internal — decrypt an envelope to a plain record for subject-index
9620
+ * rebuild (#304). Returns `null` for a tombstone or unreadable envelope.
9621
+ * Skips schema validation — the rebuild only reads the subject field.
9622
+ */
9623
+ async _decodeEnvelope(envelope, id) {
9624
+ try {
9625
+ const rec = await this.decryptRecord(envelope, { skipValidation: true, id });
9626
+ return rec === null ? null : rec;
9627
+ } catch {
9628
+ return null;
9629
+ }
9630
+ }
9631
+ async _writeTombstone(id, actor) {
9632
+ const live = await this.adapter.get(this.vault, this.name, id);
9633
+ if (!live || isTombstone(live, this.encrypted)) return null;
9634
+ await this.adapter.put(this.vault, this.name, id, buildTombstone(live._v, actor));
9635
+ this.cache.delete(id);
9636
+ this.lru?.remove(id);
9637
+ this.cekCache?.remove(id);
9638
+ return { previousVersion: live._v };
9639
+ }
9254
9640
  /**
9255
9641
  * Cascade deletes of array-shape derived rows when a source row is
9256
9642
  * deleted. Reads each registered strategy's fanout sidecar
@@ -9663,6 +10049,7 @@ var init_collection = __esm({
9663
10049
  const entries = [];
9664
10050
  for (const env of envelopes) {
9665
10051
  const record = await this.decryptRecord(env, { skipValidation: true });
10052
+ if (record === null) continue;
9666
10053
  entries.push({
9667
10054
  version: env._v,
9668
10055
  timestamp: env._ts,
@@ -9799,6 +10186,7 @@ var init_collection = __esm({
9799
10186
  const envelope = await this.adapter.get(this.vault, this.name, id);
9800
10187
  if (envelope) {
9801
10188
  const record = await this.decryptRecord(envelope);
10189
+ if (record === null) continue;
9802
10190
  items.push(record);
9803
10191
  if (!this.lazy && !this.cache.has(id)) {
9804
10192
  this.cache.set(id, { record, version: envelope._v });
@@ -9875,6 +10263,7 @@ var init_collection = __esm({
9875
10263
  const out = [];
9876
10264
  for (const { id, envelope } of items) {
9877
10265
  const record = await this.decryptRecord(envelope);
10266
+ if (record === null) continue;
9878
10267
  out.push({ id, record, version: envelope._v });
9879
10268
  }
9880
10269
  return out;
@@ -9896,6 +10285,18 @@ var init_collection = __esm({
9896
10285
  * the cache entry (record still present) or deletes it (record was
9897
10286
  * gone before the tx and the revert deleted it again).
9898
10287
  */
10288
+ /**
10289
+ * @internal — evict ONLY the per-record CEK cache entry for `id`. Used by
10290
+ * `vault.rotateRecordCek()`: after a hard CEK rotation the cached unwrapped
10291
+ * CEK is stale (it would decrypt the pre-rotation body and fail GCM auth on
10292
+ * the post-rotation body). Eviction must be synchronous with the live-envelope
10293
+ * rewrite so no concurrent read observes the old CEK. Paired with
10294
+ * {@link _invalidateCacheEntry} (which refreshes the decrypted-record cache).
10295
+ * No-op when the collection is not `perRecordKeys`.
10296
+ */
10297
+ _invalidateCekCacheEntry(id) {
10298
+ this.cekCache?.remove(id);
10299
+ }
9899
10300
  async _invalidateCacheEntry(id) {
9900
10301
  if (this.lazy && this.lru) {
9901
10302
  this.lru.remove(id);
@@ -9913,6 +10314,14 @@ var init_collection = __esm({
9913
10314
  return;
9914
10315
  }
9915
10316
  const record = await this.decryptRecord(envelope);
10317
+ if (record === null) {
10318
+ this.cache.delete(id);
10319
+ if (previous) {
10320
+ this.indexes?.remove(id, previous.record);
10321
+ this.uniqueConstraints?.remove(id, previous.record);
10322
+ }
10323
+ return;
10324
+ }
9916
10325
  this.cache.set(id, { record, version: envelope._v });
9917
10326
  this.indexes?.upsert(id, record, previous ? previous.record : null);
9918
10327
  this.uniqueConstraints?.upsert(id, record, previous?.record);
@@ -9937,8 +10346,9 @@ var init_collection = __esm({
9937
10346
  const ids = await this.adapter.list(this.vault, this.name);
9938
10347
  for (const id of ids) {
9939
10348
  const envelope = await this.adapter.get(this.vault, this.name, id);
9940
- if (envelope) {
9941
- const record = await this.decryptRecord(envelope);
10349
+ if (envelope && !isTombstone(envelope, this.encrypted)) {
10350
+ const record = await this.decryptRecord(envelope, { id });
10351
+ if (record === null) continue;
9942
10352
  this.cache.set(id, { record, version: envelope._v });
9943
10353
  }
9944
10354
  }
@@ -9949,7 +10359,9 @@ var init_collection = __esm({
9949
10359
  /** Hydrate from a pre-loaded snapshot (used by Vault). */
9950
10360
  async hydrateFromSnapshot(records) {
9951
10361
  for (const [id, envelope] of Object.entries(records)) {
9952
- const record = await this.decryptRecord(envelope);
10362
+ if (isTombstone(envelope, this.encrypted)) continue;
10363
+ const record = await this.decryptRecord(envelope, { id });
10364
+ if (record === null) continue;
9953
10365
  this.cache.set(id, { record, version: envelope._v });
9954
10366
  }
9955
10367
  this.hydrated = true;
@@ -10037,6 +10449,7 @@ var init_collection = __esm({
10037
10449
  const envelope = await this.adapter.get(this.vault, this.name, recordId2);
10038
10450
  if (!envelope) continue;
10039
10451
  const record = await this.decryptRecord(envelope, { skipValidation: true });
10452
+ if (record === null) continue;
10040
10453
  await this.maintainPersistedIndexesOnPut(recordId2, record, null, envelope._v);
10041
10454
  }
10042
10455
  this.persistedIndexesLoaded = true;
@@ -10087,8 +10500,13 @@ var init_collection = __esm({
10087
10500
  const env = await this.adapter.get(this.vault, this.name, id);
10088
10501
  if (!env) continue;
10089
10502
  try {
10090
- const body = JSON.parse(await this.decryptJsonString(env));
10091
- sidecar.set(decoded.recordId, body.value);
10503
+ const sidecarJson = await this.decryptJsonString(env);
10504
+ if (sidecarJson === null) {
10505
+ sidecar.set(decoded.recordId, void 0);
10506
+ } else {
10507
+ const body = JSON.parse(sidecarJson);
10508
+ sidecar.set(decoded.recordId, body.value);
10509
+ }
10092
10510
  } catch {
10093
10511
  sidecar.set(decoded.recordId, void 0);
10094
10512
  }
@@ -10102,6 +10520,7 @@ var init_collection = __esm({
10102
10520
  const env = await this.adapter.get(this.vault, this.name, id);
10103
10521
  if (!env) continue;
10104
10522
  const record = await this.decryptRecord(env, { skipValidation: true });
10523
+ if (record === null) continue;
10105
10524
  const live = readPersistedValue(record, field);
10106
10525
  const stored = sidecar.get(id);
10107
10526
  const hasSidecar = sidecarIds.has(id);
@@ -10184,7 +10603,8 @@ var init_collection = __esm({
10184
10603
  recordId: id,
10185
10604
  getDEK: this.getDEK,
10186
10605
  encrypted: this.encrypted,
10187
- userId: this.keyring.userId
10606
+ userId: this.keyring.userId,
10607
+ erasableBlobs: this.perRecordCek
10188
10608
  });
10189
10609
  }
10190
10610
  /** Get all records as encrypted envelopes (for dump). */
@@ -10192,7 +10612,8 @@ var init_collection = __esm({
10192
10612
  await this.ensureHydrated();
10193
10613
  const result = {};
10194
10614
  for (const [id, entry] of this.cache) {
10195
- result[id] = await this.encryptRecord(entry.record, entry.version);
10615
+ const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
10616
+ result[id] = await this.encryptRecord(entry.record, entry.version, cek);
10196
10617
  }
10197
10618
  return result;
10198
10619
  }
@@ -10220,8 +10641,11 @@ var init_collection = __esm({
10220
10641
  if (hasMoney && this.moneyFields) {
10221
10642
  result = decodeMoneyFields(result, this.moneyFields, typeof locale === "string" ? locale : void 0);
10222
10643
  }
10223
- if (!locale) return result;
10224
- if (hasI18n && this.i18nFields) {
10644
+ const hasStaticDisplay = hasDict && this.dictKeyFields !== void 0 && Object.values(this.dictKeyFields).some(
10645
+ (d) => isStaticDictDescriptor(d) && d.displayLocale !== void 0
10646
+ );
10647
+ if (!locale && !hasStaticDisplay) return result;
10648
+ if (locale && hasI18n && this.i18nFields) {
10225
10649
  result = this.i18nStrategy.applyI18nLocale(result, this.i18nFields, locale, localeOpts?.fallback);
10226
10650
  }
10227
10651
  if (hasDict && this.dictKeyFields && this.dictLabelResolver && locale !== "raw") {
@@ -10230,13 +10654,23 @@ var init_collection = __esm({
10230
10654
  for (const [field, desc] of Object.entries(this.dictKeyFields)) {
10231
10655
  const policy = desc.onMissing ? resolvePolicy(desc.onMissing, "read") : "null";
10232
10656
  const fallback = policy === "substitute" ? localeOpts?.fallback ?? desc.substitute : localeOpts?.fallback;
10657
+ const effLocale = locale ?? (isStaticDictDescriptor(desc) ? desc.displayLocale : void 0);
10233
10658
  const resolveKey = async (key) => {
10234
- const label = await resolver(desc.name, key, locale, fallback);
10659
+ if (!effLocale) {
10660
+ if (policy === "throw") {
10661
+ throw new LocaleNotSpecifiedError(
10662
+ field,
10663
+ `dictKey "${field}": no locale active to resolve key "${key}".`
10664
+ );
10665
+ }
10666
+ return null;
10667
+ }
10668
+ const label = await resolver(desc.name, key, effLocale, fallback);
10235
10669
  if (label === void 0) {
10236
10670
  if (policy === "throw") {
10237
10671
  throw new LocaleNotSpecifiedError(
10238
10672
  field,
10239
- `dictKey "${field}": no label for key "${key}" in locale "${locale}".`
10673
+ `dictKey "${field}": no label for key "${key}" in locale "${effLocale}".`
10240
10674
  );
10241
10675
  }
10242
10676
  return null;
@@ -10393,6 +10827,7 @@ var init_collection = __esm({
10393
10827
  if (!envelope) continue;
10394
10828
  try {
10395
10829
  const json = await this.decryptJsonString(envelope);
10830
+ if (json === null) continue;
10396
10831
  const body = JSON.parse(json);
10397
10832
  if (typeof body.recordId !== "string") continue;
10398
10833
  const rows = byField.get(decoded.field) ?? [];
@@ -10502,7 +10937,31 @@ var init_collection = __esm({
10502
10937
  };
10503
10938
  return new LazyQuery(source);
10504
10939
  }
10505
- async encryptJsonString(json, version) {
10940
+ /**
10941
+ * Resolve the stable CEK for a record on the WRITE path — see
10942
+ * {@link resolveStableCek}. Thin delegate that supplies the collection's
10943
+ * CEK cache, live-envelope reader, and DEK resolver.
10944
+ */
10945
+ resolveRecordCek(id) {
10946
+ return resolveStableCek(
10947
+ {
10948
+ cache: this.cekCache,
10949
+ getLive: (rid) => this.adapter.get(this.vault, this.name, rid),
10950
+ getDEK: () => this.getDEK(this.name)
10951
+ },
10952
+ id
10953
+ );
10954
+ }
10955
+ /**
10956
+ * Encrypt a JSON body into an envelope.
10957
+ *
10958
+ * When `cek` is supplied (per-record CEK collections), the body is
10959
+ * encrypted under the CEK and the CEK is AES-KW-wrapped under the
10960
+ * collection DEK and stamped on `_cek`. When `cek` is omitted, the legacy
10961
+ * path encrypts the body directly under the collection DEK — byte-identical
10962
+ * to pre-CEK behaviour, so non-adopting collections pay nothing.
10963
+ */
10964
+ async encryptJsonString(json, version, cek) {
10506
10965
  const by = this.keyring.userId;
10507
10966
  if (!this.encrypted) {
10508
10967
  return {
@@ -10515,6 +10974,19 @@ var init_collection = __esm({
10515
10974
  };
10516
10975
  }
10517
10976
  const dek = await this.getDEK(this.name);
10977
+ if (cek !== void 0) {
10978
+ const { iv: iv2, data: data2 } = await encrypt(json, cek);
10979
+ const wrapped = await wrapCek(cek, dek);
10980
+ return {
10981
+ _noydb: NOYDB_FORMAT_VERSION,
10982
+ _v: version,
10983
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
10984
+ _iv: iv2,
10985
+ _data: data2,
10986
+ _by: by,
10987
+ _cek: wrapped
10988
+ };
10989
+ }
10518
10990
  const { iv, data } = await encrypt(json, dek);
10519
10991
  return {
10520
10992
  _noydb: NOYDB_FORMAT_VERSION,
@@ -10525,8 +10997,8 @@ var init_collection = __esm({
10525
10997
  _by: by
10526
10998
  };
10527
10999
  }
10528
- async encryptRecord(record, version) {
10529
- const base = await this.encryptJsonString(JSON.stringify(record), version);
11000
+ async encryptRecord(record, version, cek) {
11001
+ const base = await this.encryptJsonString(JSON.stringify(record), version, cek);
10530
11002
  if (!this.deterministicFields || !this.encrypted) return base;
10531
11003
  const dek = await this.getDEK(this.name);
10532
11004
  const rec = record;
@@ -10604,7 +11076,8 @@ var init_collection = __esm({
10604
11076
  const env = await this.adapter.get(this.vault, this.name, id);
10605
11077
  if (!env || !env._det) continue;
10606
11078
  if (env._det[field] === target) {
10607
- matches.push(await this.decryptRecord(env));
11079
+ const rec = await this.decryptRecord(env);
11080
+ if (rec !== null) matches.push(rec);
10608
11081
  }
10609
11082
  }
10610
11083
  return matches;
@@ -10705,7 +11178,14 @@ var init_collection = __esm({
10705
11178
  return null;
10706
11179
  }
10707
11180
  const dek = await this.getDEK(key);
10708
- const plaintext = await decrypt(envelope._iv, envelope._data, dek);
11181
+ let plaintext;
11182
+ if (envelope._cek !== void 0) {
11183
+ const cek = await unwrapCek(envelope._cek, dek);
11184
+ this.cekCache?.set(id, cek, 1);
11185
+ plaintext = await decrypt(envelope._iv, envelope._data, cek);
11186
+ } else {
11187
+ plaintext = await decrypt(envelope._iv, envelope._data, dek);
11188
+ }
10709
11189
  const record = JSON.parse(plaintext);
10710
11190
  this.emitCrossTierEvent({
10711
11191
  actor: this.keyring.userId,
@@ -10761,18 +11241,19 @@ var init_collection = __esm({
10761
11241
  const toKey = dekKey(this.name, toTier);
10762
11242
  const fromDek = await this.getDEK(fromKey);
10763
11243
  const toDek = await this.getDEK(toKey);
10764
- const plaintext = await decrypt(envelope._iv, envelope._data, fromDek);
10765
- const { iv, data } = await encrypt(plaintext, toDek);
10766
11244
  const now = (/* @__PURE__ */ new Date()).toISOString();
11245
+ const body = await rewrapBodyToDek(envelope, fromDek, toDek);
11246
+ if (body.cek) this.cekCache?.set(id, body.cek, 1);
10767
11247
  const next = {
10768
11248
  _noydb: NOYDB_FORMAT_VERSION,
10769
11249
  _v: envelope._v + 1,
10770
11250
  _ts: now,
10771
- _iv: iv,
10772
- _data: data,
11251
+ _iv: body._iv,
11252
+ _data: body._data,
10773
11253
  _by: this.keyring.userId,
10774
11254
  _tier: toTier,
10775
- _elevatedBy: this.keyring.userId
11255
+ _elevatedBy: this.keyring.userId,
11256
+ ...body._cek !== void 0 ? { _cek: body._cek } : {}
10776
11257
  };
10777
11258
  await this.adapter.put(this.vault, this.name, id, next);
10778
11259
  this.emitCrossTierEvent({
@@ -10808,17 +11289,18 @@ var init_collection = __esm({
10808
11289
  if (toTier > 0) this.assertDeclaredTier(toTier);
10809
11290
  const fromDek = await this.getDEK(dekKey(this.name, fromTier));
10810
11291
  const toDek = await this.getDEK(dekKey(this.name, toTier));
10811
- const plaintext = await decrypt(envelope._iv, envelope._data, fromDek);
10812
- const { iv, data } = await encrypt(plaintext, toDek);
10813
11292
  const now = (/* @__PURE__ */ new Date()).toISOString();
11293
+ const body = await rewrapBodyToDek(envelope, fromDek, toDek);
11294
+ if (body.cek) this.cekCache?.set(id, body.cek, 1);
10814
11295
  const next = {
10815
11296
  _noydb: NOYDB_FORMAT_VERSION,
10816
11297
  _v: envelope._v + 1,
10817
11298
  _ts: now,
10818
- _iv: iv,
10819
- _data: data,
11299
+ _iv: body._iv,
11300
+ _data: body._data,
10820
11301
  _by: this.keyring.userId,
10821
- ...toTier > 0 && { _tier: toTier }
11302
+ ...toTier > 0 && { _tier: toTier },
11303
+ ...body._cek !== void 0 ? { _cek: body._cek } : {}
10822
11304
  };
10823
11305
  await this.adapter.put(this.vault, this.name, id, next);
10824
11306
  this.emitCrossTierEvent({
@@ -10840,10 +11322,30 @@ var init_collection = __esm({
10840
11322
  } catch {
10841
11323
  }
10842
11324
  }
10843
- /** Low-level: decrypt an envelope and return the raw JSON string. */
10844
- async decryptJsonString(envelope) {
11325
+ /**
11326
+ * Low-level: decrypt an envelope and return the raw JSON string.
11327
+ *
11328
+ * `_cek` presence is the format discriminant (NOT `this.perRecordCek`),
11329
+ * so a mixed vault — and a recipient that never opted into
11330
+ * `perRecordKeys` — decrypts both legacy and CEK records:
11331
+ * - `_cek` present → unwrap the CEK under the collection DEK, decrypt the
11332
+ * body under the CEK (cache the unwrapped CEK so repeated reads skip it).
11333
+ * - `_cek` absent → legacy path, body decrypts directly under the
11334
+ * collection DEK.
11335
+ *
11336
+ * The optional `id` lets reads populate the CEK cache; it is omitted by
11337
+ * callers (history, conflict merge) that have only the envelope.
11338
+ */
11339
+ async decryptJsonString(envelope, id) {
11340
+ if (isTombstone(envelope, this.encrypted)) return null;
10845
11341
  if (!this.encrypted) return envelope._data;
10846
11342
  const dek = await this.getDEK(this.name);
11343
+ if (envelope._cek !== void 0) {
11344
+ const cached = id !== void 0 ? this.cekCache?.get(id) : void 0;
11345
+ const cek = cached ?? await unwrapCek(envelope._cek, dek);
11346
+ if (cached === void 0 && id !== void 0) this.cekCache?.set(id, cek, 1);
11347
+ return decrypt(envelope._iv, envelope._data, cek);
11348
+ }
10847
11349
  return decrypt(envelope._iv, envelope._data, dek);
10848
11350
  }
10849
11351
  /**
@@ -10862,7 +11364,8 @@ var init_collection = __esm({
10862
11364
  * false positive. Every non-history read leaves this flag `false`.
10863
11365
  */
10864
11366
  async decryptRecord(envelope, opts = {}) {
10865
- const json = await this.decryptJsonString(envelope);
11367
+ const json = await this.decryptJsonString(envelope, opts.id);
11368
+ if (json === null) return null;
10866
11369
  let parsed = JSON.parse(json);
10867
11370
  if (this.crdtMode && parsed !== null && typeof parsed === "object" && "_crdt" in parsed) {
10868
11371
  parsed = this.crdtStrategy.resolveCrdtSnapshot(parsed);
@@ -10917,37 +11420,34 @@ var init_virtual_collection = __esm({
10917
11420
  /** Get the merged row by id. */
10918
11421
  async get(id) {
10919
11422
  const overlayRow = await this.overlayCollection.get(id);
10920
- if (overlayRow !== null && this.shadowPredicateApplies(overlayRow)) {
10921
- return overlayRow;
10922
- }
10923
11423
  const baseRow = await this.baseCollection.get(id);
10924
- if (baseRow !== null) return baseRow;
10925
- return null;
11424
+ return this.mergeRows(overlayRow, baseRow);
10926
11425
  }
10927
11426
  /** List union of base + overlay ids, applying the merge per row. */
10928
11427
  async list() {
10929
11428
  const baseRows = await this.baseCollection.list();
10930
11429
  const overlayRows = await this.overlayCollection.list();
10931
- const merged = /* @__PURE__ */ new Map();
10932
11430
  const idOf = (row) => {
10933
11431
  if (this.baseRowKey) return this.baseRowKey(row);
10934
11432
  const idField = row.id;
10935
11433
  return typeof idField === "string" ? idField : "";
10936
11434
  };
11435
+ const baseById = /* @__PURE__ */ new Map();
11436
+ const overlayById = /* @__PURE__ */ new Map();
10937
11437
  for (const row of baseRows) {
10938
11438
  const id = idOf(row);
10939
- if (id) merged.set(id, row);
11439
+ if (id) baseById.set(id, row);
10940
11440
  }
10941
11441
  for (const row of overlayRows) {
10942
11442
  const id = idOf(row);
10943
- if (!id) continue;
10944
- if (this.shadowPredicateApplies(row)) {
10945
- merged.set(id, row);
10946
- } else if (!merged.has(id)) {
10947
- continue;
10948
- }
11443
+ if (id) overlayById.set(id, row);
10949
11444
  }
10950
- return [...merged.values()];
11445
+ const out = [];
11446
+ for (const id of /* @__PURE__ */ new Set([...baseById.keys(), ...overlayById.keys()])) {
11447
+ const merged = this.mergeRows(overlayById.get(id) ?? null, baseById.get(id) ?? null);
11448
+ if (merged !== null) out.push(merged);
11449
+ }
11450
+ return out;
10951
11451
  }
10952
11452
  /**
10953
11453
  * Write to the overlay. Two forms:
@@ -10987,9 +11487,42 @@ var init_virtual_collection = __esm({
10987
11487
  async delete(id) {
10988
11488
  await this.overlayCollection.delete(id);
10989
11489
  }
10990
- /** True when `overlay[shadowField] === shadowValue`. */
10991
- shadowPredicateApplies(row) {
10992
- return row[this.spec.shadowField] === this.spec.shadowValue;
11490
+ /**
11491
+ * Merge a single id's overlay + base rows into the visible row.
11492
+ *
11493
+ * Priority (first match wins):
11494
+ * 1. Binary shadow win — overlay present AND
11495
+ * `overlay[shadowField] === shadowValue` → return the overlay row
11496
+ * entirely. This stays FIRST so the original binary behaviour is
11497
+ * unchanged whether or not `mergeMode` is configured.
11498
+ * 2. Field-level merge — overlay present, `mergeMode` configured,
11499
+ * and a rule whose `whenStatus` equals `overlay[shadowField]`.
11500
+ * The matched rule pulls its `overlayFields` (those present on
11501
+ * the overlay row) on top of the base row. With no base row, the
11502
+ * overlay row is returned as-is.
11503
+ * 3. Fallback — return the base row (possibly `null`). An
11504
+ * overlay-only row that qualifies under neither (1) nor (2) and
11505
+ * has no base is therefore NOT surfaced.
11506
+ */
11507
+ mergeRows(overlayRow, baseRow) {
11508
+ const shadowField = this.spec.shadowField;
11509
+ if (overlayRow !== null && overlayRow[shadowField] === this.spec.shadowValue) {
11510
+ return overlayRow;
11511
+ }
11512
+ if (overlayRow !== null && this.spec.mergeMode) {
11513
+ const status = overlayRow[shadowField];
11514
+ const rule = this.spec.mergeMode.rules.find((r) => r.whenStatus === status);
11515
+ if (rule) {
11516
+ if (baseRow === null) return overlayRow;
11517
+ const overlaySrc = overlayRow;
11518
+ const picked = {};
11519
+ for (const field of rule.overlayFields) {
11520
+ if (field in overlaySrc) picked[field] = overlaySrc[field];
11521
+ }
11522
+ return { ...baseRow, ...picked };
11523
+ }
11524
+ }
11525
+ return baseRow;
10993
11526
  }
10994
11527
  // ─── Throw-stubs for the unimplemented Collection<T> surface ───────
10995
11528
  //
@@ -11139,6 +11672,21 @@ var init_archive = __esm({
11139
11672
  });
11140
11673
 
11141
11674
  // src/sequence/index.ts
11675
+ function resolveSequenceKey(series, opts) {
11676
+ const partition = opts?.partition;
11677
+ if (!partition || partition.length === 0) return series;
11678
+ const parts = partition.map((p) => {
11679
+ if (typeof p === "number" && !Number.isFinite(p)) {
11680
+ throw new ValidationError(`sequence partition component must be a finite number, got ${p}`);
11681
+ }
11682
+ const s = String(p);
11683
+ if (s === "") {
11684
+ throw new ValidationError("sequence partition component must not be empty");
11685
+ }
11686
+ return encodeURIComponent(s);
11687
+ });
11688
+ return `${series}\0${parts.join("/")}`;
11689
+ }
11142
11690
  async function sleepBackoff2(attempt) {
11143
11691
  const ceil = Math.min(2 ** attempt, 32);
11144
11692
  const ms = Math.floor(Math.random() * ceil);
@@ -11178,7 +11726,8 @@ var init_sequence = __esm({
11178
11726
  handle(name) {
11179
11727
  return {
11180
11728
  next: () => this.next(name),
11181
- peek: () => this.peek(name)
11729
+ peek: () => this.peek(name),
11730
+ seedTo: (n) => this.seedTo(name, n)
11182
11731
  };
11183
11732
  }
11184
11733
  assertOnline() {
@@ -11231,6 +11780,30 @@ var init_sequence = __esm({
11231
11780
  void lastConflict;
11232
11781
  throw new SequenceContentionError(name, MAX_NEXT_ATTEMPTS);
11233
11782
  }
11783
+ async seedTo(name, n) {
11784
+ this.assertOnline();
11785
+ if (n <= 0) return;
11786
+ let lastConflict;
11787
+ for (let attempt = 0; attempt < MAX_NEXT_ATTEMPTS; attempt++) {
11788
+ const { env, value } = await this.read(name);
11789
+ if (value >= n) return;
11790
+ const expectedVersion = env?._v ?? 0;
11791
+ const envelope = await this.encryptState({ value: n }, expectedVersion + 1);
11792
+ try {
11793
+ await this.adapter.put(this.vault, SEQUENCE_COLLECTION, name, envelope, expectedVersion);
11794
+ return;
11795
+ } catch (err) {
11796
+ if (err instanceof ConflictError) {
11797
+ lastConflict = err;
11798
+ if (attempt < MAX_NEXT_ATTEMPTS - 1) await sleepBackoff2(attempt);
11799
+ continue;
11800
+ }
11801
+ throw err;
11802
+ }
11803
+ }
11804
+ void lastConflict;
11805
+ throw new SequenceContentionError(name, MAX_NEXT_ATTEMPTS);
11806
+ }
11234
11807
  };
11235
11808
  }
11236
11809
  });
@@ -11397,9 +11970,125 @@ var init_numbering = __esm({
11397
11970
  }
11398
11971
  });
11399
11972
 
11973
+ // src/forget/strategy.ts
11974
+ var NO_FORGET;
11975
+ var init_strategy7 = __esm({
11976
+ "src/forget/strategy.ts"() {
11977
+ "use strict";
11978
+ NO_FORGET = { subjects: {} };
11979
+ }
11980
+ });
11981
+
11982
+ // src/forget/subject-index.ts
11983
+ async function sha256HexString(input) {
11984
+ const bytes = new TextEncoder().encode(input);
11985
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
11986
+ return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
11987
+ }
11988
+ async function subjectKey(subjectId) {
11989
+ return sha256HexString(subjectId);
11990
+ }
11991
+ async function readRefs(adapter, vault, getDEK, encrypted, key) {
11992
+ const env = await adapter.get(vault, SUBJECT_INDEX_COLLECTION, key);
11993
+ if (!env || !env._data) return [];
11994
+ if (!encrypted) return JSON.parse(env._data);
11995
+ const dek = await getDEK(SUBJECT_INDEX_COLLECTION);
11996
+ const json = await decrypt(env._iv, env._data, dek);
11997
+ return JSON.parse(json);
11998
+ }
11999
+ async function writeRefs(adapter, vault, getDEK, encrypted, key, refs) {
12000
+ const json = JSON.stringify(refs);
12001
+ let env;
12002
+ if (!encrypted) {
12003
+ env = { _noydb: NOYDB_FORMAT_VERSION, _v: 1, _ts: (/* @__PURE__ */ new Date()).toISOString(), _iv: "", _data: json };
12004
+ } else {
12005
+ const dek = await getDEK(SUBJECT_INDEX_COLLECTION);
12006
+ const { iv, data } = await encrypt(json, dek);
12007
+ env = { _noydb: NOYDB_FORMAT_VERSION, _v: 1, _ts: (/* @__PURE__ */ new Date()).toISOString(), _iv: iv, _data: data };
12008
+ }
12009
+ await adapter.put(vault, SUBJECT_INDEX_COLLECTION, key, env);
12010
+ }
12011
+ async function addSubjectRef(adapter, vault, getDEK, encrypted, subjectId, ref) {
12012
+ const key = await subjectKey(subjectId);
12013
+ const refs = await readRefs(adapter, vault, getDEK, encrypted, key);
12014
+ if (refs.some((r) => r.collection === ref.collection && r.id === ref.id)) return;
12015
+ refs.push(ref);
12016
+ await writeRefs(adapter, vault, getDEK, encrypted, key, refs);
12017
+ }
12018
+ async function removeSubjectRef(adapter, vault, getDEK, encrypted, subjectId, ref) {
12019
+ const key = await subjectKey(subjectId);
12020
+ const refs = await readRefs(adapter, vault, getDEK, encrypted, key);
12021
+ const next = refs.filter((r) => !(r.collection === ref.collection && r.id === ref.id));
12022
+ if (next.length === refs.length) return;
12023
+ if (next.length === 0) {
12024
+ await adapter.delete(vault, SUBJECT_INDEX_COLLECTION, key);
12025
+ return;
12026
+ }
12027
+ await writeRefs(adapter, vault, getDEK, encrypted, key, next);
12028
+ }
12029
+ async function lookupSubject(adapter, vault, getDEK, encrypted, subjectId) {
12030
+ const key = await subjectKey(subjectId);
12031
+ return readRefs(adapter, vault, getDEK, encrypted, key);
12032
+ }
12033
+ async function rebuildSubjectIndex(adapter, vault, getDEK, encrypted, subjects, decodeRecord) {
12034
+ const existing = await adapter.list(vault, SUBJECT_INDEX_COLLECTION);
12035
+ for (const k of existing) {
12036
+ await adapter.delete(vault, SUBJECT_INDEX_COLLECTION, k);
12037
+ }
12038
+ const bySubject = /* @__PURE__ */ new Map();
12039
+ for (const [collection, field] of Object.entries(subjects)) {
12040
+ const ids = await adapter.list(vault, collection);
12041
+ for (const id of ids) {
12042
+ if (id.startsWith("_")) continue;
12043
+ const env = await adapter.get(vault, collection, id);
12044
+ if (!env || !env._data) continue;
12045
+ const record = await decodeRecord(collection, id, env);
12046
+ if (record === null) continue;
12047
+ const subjectValue = readDottedPath(record, field);
12048
+ if (subjectValue === void 0 || subjectValue === null) continue;
12049
+ const subjectId = coerceSubjectId(subjectValue);
12050
+ const list = bySubject.get(subjectId) ?? [];
12051
+ list.push({ collection, id });
12052
+ bySubject.set(subjectId, list);
12053
+ }
12054
+ }
12055
+ let entries = 0;
12056
+ for (const [subjectId, refs] of bySubject) {
12057
+ const key = await subjectKey(subjectId);
12058
+ await writeRefs(adapter, vault, getDEK, encrypted, key, refs);
12059
+ entries++;
12060
+ }
12061
+ return entries;
12062
+ }
12063
+ function coerceSubjectId(value) {
12064
+ if (typeof value === "string") return value;
12065
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
12066
+ return String(value);
12067
+ }
12068
+ return JSON.stringify(value);
12069
+ }
12070
+ function readDottedPath(record, field) {
12071
+ if (!field.includes(".")) return record[field];
12072
+ let cursor = record;
12073
+ for (const segment of field.split(".")) {
12074
+ if (cursor === null || cursor === void 0) return void 0;
12075
+ cursor = cursor[segment];
12076
+ }
12077
+ return cursor;
12078
+ }
12079
+ var SUBJECT_INDEX_COLLECTION;
12080
+ var init_subject_index = __esm({
12081
+ "src/forget/subject-index.ts"() {
12082
+ "use strict";
12083
+ init_crypto();
12084
+ init_types();
12085
+ SUBJECT_INDEX_COLLECTION = "_subject_index";
12086
+ }
12087
+ });
12088
+
11400
12089
  // src/shadow/strategy.ts
11401
12090
  var NOT_ENABLED3, NO_SHADOW;
11402
- var init_strategy7 = __esm({
12091
+ var init_strategy8 = __esm({
11403
12092
  "src/shadow/strategy.ts"() {
11404
12093
  "use strict";
11405
12094
  NOT_ENABLED3 = new Error(
@@ -11415,7 +12104,7 @@ var init_strategy7 = __esm({
11415
12104
 
11416
12105
  // src/consent/strategy.ts
11417
12106
  var NO_CONSENT;
11418
- var init_strategy8 = __esm({
12107
+ var init_strategy9 = __esm({
11419
12108
  "src/consent/strategy.ts"() {
11420
12109
  "use strict";
11421
12110
  NO_CONSENT = {
@@ -11430,7 +12119,7 @@ var init_strategy8 = __esm({
11430
12119
 
11431
12120
  // src/periods/strategy.ts
11432
12121
  var NOT_ENABLED4, NO_PERIODS;
11433
- var init_strategy9 = __esm({
12122
+ var init_strategy10 = __esm({
11434
12123
  "src/periods/strategy.ts"() {
11435
12124
  "use strict";
11436
12125
  NOT_ENABLED4 = new Error(
@@ -11542,18 +12231,6 @@ var init_refs = __esm({
11542
12231
  }
11543
12232
  });
11544
12233
 
11545
- // src/i18n/dictionary.ts
11546
- function isDictCollectionName(name) {
11547
- return name.startsWith(DICT_COLLECTION_PREFIX);
11548
- }
11549
- var DICT_COLLECTION_PREFIX;
11550
- var init_dictionary = __esm({
11551
- "src/i18n/dictionary.ts"() {
11552
- "use strict";
11553
- DICT_COLLECTION_PREFIX = "_dict_";
11554
- }
11555
- });
11556
-
11557
12234
  // src/periods/periods.ts
11558
12235
  var PERIODS_COLLECTION;
11559
12236
  var init_periods = __esm({
@@ -13245,11 +13922,15 @@ var init_read_only_facade = __esm({
13245
13922
  });
13246
13923
 
13247
13924
  // src/derivations/strategy-hash.ts
13248
- async function computeStrategyHash(source, outputKeys, derive) {
13925
+ async function computeStrategyHash(source, outputKeys, derive, sources) {
13249
13926
  const canonical2 = JSON.stringify({
13250
13927
  source,
13251
13928
  outputs: [...outputKeys].sort(),
13252
- derive: derive.toString()
13929
+ derive: derive.toString(),
13930
+ // Declared sibling sources (#344) — adding/removing a trigger
13931
+ // collection invalidates cached derived records. Omitted when empty
13932
+ // so strategies without siblings keep their existing hash.
13933
+ ...sources?.length ? { sources: [...sources].sort() } : {}
13253
13934
  });
13254
13935
  const bytes = new TextEncoder().encode(canonical2);
13255
13936
  const digest = await crypto.subtle.digest("SHA-256", bytes);
@@ -13278,11 +13959,16 @@ var init_registry3 = __esm({
13278
13959
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
13279
13960
  async register(spec) {
13280
13961
  const outputKeys = Object.keys(spec.outputs);
13281
- const strategyHash = await computeStrategyHash(spec.source, outputKeys, spec.derive);
13962
+ const strategyHash = await computeStrategyHash(spec.source, outputKeys, spec.derive, spec.sources);
13282
13963
  const reg = { spec, strategyHash };
13283
13964
  const fromSource = this._bySource.get(spec.source);
13284
13965
  if (fromSource) fromSource.push(reg);
13285
13966
  else this._bySource.set(spec.source, [reg]);
13967
+ for (const extra of spec.sources ?? []) {
13968
+ const fromExtra = this._bySource.get(extra);
13969
+ if (fromExtra) fromExtra.push(reg);
13970
+ else this._bySource.set(extra, [reg]);
13971
+ }
13286
13972
  for (const key of outputKeys) {
13287
13973
  const output = spec.outputs[key];
13288
13974
  if (!output) continue;
@@ -13513,6 +14199,19 @@ var init_delegation = __esm({
13513
14199
  });
13514
14200
 
13515
14201
  // src/vault.ts
14202
+ function resolveLabelFromMap(labels, locale, fallback) {
14203
+ if (labels[locale] !== void 0) return labels[locale];
14204
+ const chain = Array.isArray(fallback) ? fallback : fallback ? [fallback] : [];
14205
+ for (const fb of chain) {
14206
+ if (fb === "any") {
14207
+ const any = Object.values(labels)[0];
14208
+ if (any !== void 0) return any;
14209
+ } else if (labels[fb] !== void 0) {
14210
+ return labels[fb];
14211
+ }
14212
+ }
14213
+ return void 0;
14214
+ }
13516
14215
  var Vault, ELEVATION_AUDIT_COLLECTION, ElevatedHandle;
13517
14216
  var init_vault = __esm({
13518
14217
  "src/vault.ts"() {
@@ -13531,8 +14230,11 @@ var init_vault = __esm({
13531
14230
  init_entry();
13532
14231
  init_strategy3();
13533
14232
  init_strategy7();
14233
+ init_subject_index();
14234
+ init_errors();
13534
14235
  init_strategy8();
13535
14236
  init_strategy9();
14237
+ init_strategy10();
13536
14238
  init_refs();
13537
14239
  init_dictionary();
13538
14240
  init_core();
@@ -13541,6 +14243,7 @@ var init_vault = __esm({
13541
14243
  init_errors();
13542
14244
  init_periods2();
13543
14245
  init_crypto();
14246
+ init_record_keys();
13544
14247
  init_export_blobs();
13545
14248
  init_blob_compaction();
13546
14249
  init_magic_link_grant();
@@ -13596,6 +14299,7 @@ var init_vault = __esm({
13596
14299
  periodsStrategy;
13597
14300
  shadowStrategy;
13598
14301
  historyStrategy;
14302
+ forgetStrategy;
13599
14303
  i18nStrategy;
13600
14304
  syncStrategy;
13601
14305
  /**
@@ -13762,6 +14466,27 @@ var init_vault = __esm({
13762
14466
  * Populated by `collection()` when the `dictKeyFields` option is passed.
13763
14467
  */
13764
14468
  dictKeyFieldRegistry = /* @__PURE__ */ new Map();
14469
+ /**
14470
+ * Names of dictionaries backed by a `staticDict()` descriptor (#291).
14471
+ * A static dict skips the `dictKeyFieldRegistry` rename machinery, but the
14472
+ * vault must still *know* a name is static so `vault.dictionary(name)` can
14473
+ * refuse mutation (`StaticDictReadonlyError`). Populated at `collection()`
14474
+ * config time whenever a `StaticDictDescriptor` is seen.
14475
+ */
14476
+ staticDictNames = /* @__PURE__ */ new Set();
14477
+ /**
14478
+ * Static-dict descriptors keyed by dictionary name (#291). Backs the
14479
+ * read-path label resolver (resolve from the in-memory table) and the
14480
+ * query-seam `resolveDictSource` snapshot. Last writer wins when the same
14481
+ * name is registered by multiple collections (identical-across-vaults by
14482
+ * construction, so the tables match).
14483
+ */
14484
+ staticByName = /* @__PURE__ */ new Map();
14485
+ /**
14486
+ * Per-collection map of field name → StaticDictDescriptor (#291). Used by
14487
+ * `enforceStaticDictOnPut` to validate stored codes against `desc.keys`.
14488
+ */
14489
+ staticDescriptorByField = /* @__PURE__ */ new Map();
13765
14490
  /**
13766
14491
  * Registry of i18nText fields declared across all collections. Keyed
13767
14492
  * by collection name → field name → I18nTextDescriptor. Used by
@@ -13808,6 +14533,7 @@ var init_vault = __esm({
13808
14533
  this.periodsStrategy = opts.periodsStrategy ?? NO_PERIODS;
13809
14534
  this.shadowStrategy = opts.shadowStrategy ?? NO_SHADOW;
13810
14535
  this.historyStrategy = opts.historyStrategy ?? NO_HISTORY;
14536
+ this.forgetStrategy = opts.forgetStrategy ?? NO_FORGET;
13811
14537
  this.i18nStrategy = opts.i18nStrategy ?? NO_I18N;
13812
14538
  this.syncStrategy = opts.syncStrategy ?? NO_SYNC;
13813
14539
  void opts.guardStrategies;
@@ -13912,10 +14638,22 @@ var init_vault = __esm({
13912
14638
  }
13913
14639
  if (options?.dictKeyFields) {
13914
14640
  const dictFieldMap = {};
14641
+ const staticFieldMap = {};
13915
14642
  for (const [field, desc] of Object.entries(options.dictKeyFields)) {
13916
- dictFieldMap[field] = desc.name;
14643
+ if (isStaticDictDescriptor(desc)) {
14644
+ staticFieldMap[field] = desc;
14645
+ this.staticDictNames.add(desc.name);
14646
+ this.staticByName.set(desc.name, desc);
14647
+ } else {
14648
+ dictFieldMap[field] = desc.name;
14649
+ }
14650
+ }
14651
+ if (Object.keys(dictFieldMap).length > 0) {
14652
+ this.dictKeyFieldRegistry.set(collectionName, dictFieldMap);
14653
+ }
14654
+ if (Object.keys(staticFieldMap).length > 0) {
14655
+ this.staticDescriptorByField.set(collectionName, staticFieldMap);
13917
14656
  }
13918
- this.dictKeyFieldRegistry.set(collectionName, dictFieldMap);
13919
14657
  }
13920
14658
  if ((options?.schemaUpdate?.length ?? 0) > 0) {
13921
14659
  this.#schemaUpdateNames.set(collectionName, (options.schemaUpdate ?? []).map((s) => s.name));
@@ -14017,6 +14755,17 @@ var init_vault = __esm({
14017
14755
  if (options?.acknowledgeDeterministicRisk !== void 0) {
14018
14756
  collOpts.acknowledgeDeterministicRisk = options.acknowledgeDeterministicRisk;
14019
14757
  }
14758
+ if (options?.perRecordKeys !== void 0) {
14759
+ collOpts.perRecordKeys = options.perRecordKeys;
14760
+ }
14761
+ if (this.forgetStrategy.subjects[collectionName] !== void 0) {
14762
+ if (options?.perRecordKeys === false) {
14763
+ console.warn(
14764
+ `[noy-db] Collection "${collectionName}" is declared in withForgetCascade but opened with perRecordKeys: false. Forcing perRecordKeys: true \u2014 GDPR crypto-shred requires per-record CEKs.`
14765
+ );
14766
+ }
14767
+ collOpts.perRecordKeys = true;
14768
+ }
14020
14769
  if (options?.tiers !== void 0) collOpts.tiers = options.tiers;
14021
14770
  if (options?.tierMode !== void 0) collOpts.tierMode = options.tierMode;
14022
14771
  collOpts.onCrossTierAccess = (event) => this.emitCrossTier(event);
@@ -14026,6 +14775,11 @@ var init_vault = __esm({
14026
14775
  if (options?.computed !== void 0) collOpts.computed = options.computed;
14027
14776
  if (options?.dictKeyFields !== void 0) {
14028
14777
  collOpts.dictLabelResolver = async (dictName, key, locale, fallback) => {
14778
+ const stat = this.staticByName.get(dictName);
14779
+ if (stat) {
14780
+ const labels = stat.table[key];
14781
+ return labels ? resolveLabelFromMap(labels, locale, fallback) : void 0;
14782
+ }
14029
14783
  const handle = this.dictionary(dictName);
14030
14784
  return handle.resolveLabel(key, locale, fallback);
14031
14785
  };
@@ -14034,6 +14788,7 @@ var init_vault = __esm({
14034
14788
  if (options?.i18nFields !== void 0 || options?.dictKeyFields !== void 0) {
14035
14789
  collOpts.i18nPutValidator = (record) => {
14036
14790
  this.enforceI18nOnPut(collectionName, record);
14791
+ this.enforceStaticDictOnPut(collectionName, record);
14037
14792
  };
14038
14793
  }
14039
14794
  if (options?.i18nFields !== void 0 && this.translateText) {
@@ -14173,6 +14928,34 @@ var init_vault = __esm({
14173
14928
  }
14174
14929
  }
14175
14930
  }
14931
+ /**
14932
+ * Validate staticDict codes on a `put()` (#291). For each `staticDict()`
14933
+ * field, every stored code must be a declared key of the descriptor's
14934
+ * table, else `UnknownDictCodeError`. Opt out per descriptor with
14935
+ * `{ validateCodes: false }`. Supports scalar, dotted, and `[].`-wildcard
14936
+ * field paths via `getAtPath` (same path support as i18n validation).
14937
+ */
14938
+ enforceStaticDictOnPut(collectionName, record) {
14939
+ const staticFields = this.staticDescriptorByField.get(collectionName);
14940
+ if (!staticFields || Object.keys(staticFields).length === 0) return;
14941
+ if (!record || typeof record !== "object") return;
14942
+ const obj = record;
14943
+ for (const [field, desc] of Object.entries(staticFields)) {
14944
+ if (desc.validateCodes === false) continue;
14945
+ const known = new Set(desc.keys);
14946
+ const values = getAtPath(obj, field);
14947
+ for (const value of values) {
14948
+ if (value === void 0 || value === null) continue;
14949
+ const codes = Array.isArray(value) ? value : [value];
14950
+ for (const code of codes) {
14951
+ if (typeof code !== "string") continue;
14952
+ if (!known.has(code)) {
14953
+ throw new UnknownDictCodeError(desc.name, field, code);
14954
+ }
14955
+ }
14956
+ }
14957
+ }
14958
+ }
14176
14959
  /**
14177
14960
  * Apply locale resolution to a record for the given collection.
14178
14961
  *
@@ -14181,14 +14964,18 @@ var init_vault = __esm({
14181
14964
  */
14182
14965
  async applyLocale(collectionName, record, localeOpts) {
14183
14966
  const locale = localeOpts.locale ?? this.locale;
14184
- if (!locale) return record;
14967
+ const staticFields = this.staticDescriptorByField.get(collectionName);
14968
+ const hasStaticDisplay = staticFields !== void 0 && Object.values(staticFields).some((d) => d.displayLocale !== void 0);
14969
+ if (!locale && !hasStaticDisplay) return record;
14185
14970
  let result = record;
14186
- const i18nFields = this.i18nFieldRegistry.get(collectionName);
14187
- if (i18nFields && Object.keys(i18nFields).length > 0) {
14188
- result = this.i18nStrategy.applyI18nLocale(result, i18nFields, locale, localeOpts.fallback);
14971
+ if (locale) {
14972
+ const i18nFields = this.i18nFieldRegistry.get(collectionName);
14973
+ if (i18nFields && Object.keys(i18nFields).length > 0) {
14974
+ result = this.i18nStrategy.applyI18nLocale(result, i18nFields, locale, localeOpts.fallback);
14975
+ }
14189
14976
  }
14190
14977
  const dictFields = this.dictKeyFieldRegistry.get(collectionName);
14191
- if (dictFields && Object.keys(dictFields).length > 0 && locale !== "raw") {
14978
+ if (locale && dictFields && Object.keys(dictFields).length > 0 && locale !== "raw") {
14192
14979
  const withLabels = { ...result };
14193
14980
  for (const [field, dictName] of Object.entries(dictFields)) {
14194
14981
  const key = result[field];
@@ -14201,6 +14988,22 @@ var init_vault = __esm({
14201
14988
  }
14202
14989
  result = withLabels;
14203
14990
  }
14991
+ if (staticFields && Object.keys(staticFields).length > 0 && locale !== "raw") {
14992
+ const withLabels = { ...result };
14993
+ for (const [field, desc] of Object.entries(staticFields)) {
14994
+ const effLocale = locale ?? desc.displayLocale;
14995
+ if (!effLocale) continue;
14996
+ const key = result[field];
14997
+ if (typeof key !== "string") continue;
14998
+ const labels = desc.table[key];
14999
+ if (!labels) continue;
15000
+ const label = resolveLabelFromMap(labels, effLocale, localeOpts.fallback ?? desc.substitute);
15001
+ if (label !== void 0) {
15002
+ withLabels[`${field}Label`] = label;
15003
+ }
15004
+ }
15005
+ result = withLabels;
15006
+ }
14204
15007
  return result;
14205
15008
  }
14206
15009
  /**
@@ -14222,6 +15025,9 @@ var init_vault = __esm({
14222
15025
  * ```
14223
15026
  */
14224
15027
  dictionary(name, options = {}) {
15028
+ if (this.staticDictNames.has(name)) {
15029
+ throw new StaticDictReadonlyError(name);
15030
+ }
14225
15031
  let handle = this.dictionaryCache.get(name);
14226
15032
  if (!handle) {
14227
15033
  handle = this.i18nStrategy.buildDictionaryHandle({
@@ -14291,6 +15097,26 @@ var init_vault = __esm({
14291
15097
  * Returns `null` when `field` is not a dictKey in `leftCollection`.
14292
15098
  */
14293
15099
  resolveDictSource(leftCollection, field) {
15100
+ const staticFields = this.staticDescriptorByField.get(leftCollection);
15101
+ if (staticFields && field in staticFields) {
15102
+ const desc = staticFields[field];
15103
+ const rows = Object.entries(desc.table).map(
15104
+ ([key, labels]) => ({ key, labels, ...labels })
15105
+ );
15106
+ const source = {
15107
+ snapshot() {
15108
+ return rows;
15109
+ },
15110
+ lookupById(id) {
15111
+ return rows.find((e) => e["key"] === id);
15112
+ }
15113
+ };
15114
+ if (desc.displayLocale !== void 0) {
15115
+ ;
15116
+ source.displayLocale = desc.displayLocale;
15117
+ }
15118
+ return source;
15119
+ }
14294
15120
  const dictFields = this.dictKeyFieldRegistry.get(leftCollection);
14295
15121
  if (!dictFields || !(field in dictFields)) return null;
14296
15122
  const dictName = dictFields[field];
@@ -14458,17 +15284,23 @@ var init_vault = __esm({
14458
15284
  * const cur = await vault.sequence('invoice-2026').peek() // current value, no allocation
14459
15285
  * ```
14460
15286
  */
14461
- sequence(name) {
14462
- if (this.numberingConfigs.has(name)) {
15287
+ sequence(series, opts) {
15288
+ if (series.includes("\0")) {
15289
+ throw new ValidationError(`sequence("${series}"): series name must not contain a null byte (\\x00).`);
15290
+ }
15291
+ if (this.numberingConfigs.has(series)) {
14463
15292
  const eng = this.deferred();
14464
15293
  return {
14465
- next: async (opts) => {
14466
- if (!opts?.for) {
14467
- throw new ValidationError(`sequence("${name}") is a deferred-numbering series; call next({ for: recordId }).`);
15294
+ next: async (nextOpts) => {
15295
+ if (!nextOpts?.for) {
15296
+ throw new ValidationError(`sequence("${series}") is a deferred-numbering series; call next({ for: recordId }).`);
14468
15297
  }
14469
- return (await eng.enqueue(name, opts.for)).assigned;
15298
+ return (await eng.enqueue(series, nextOpts.for)).assigned;
14470
15299
  },
14471
- peek: () => eng.peek(name)
15300
+ peek: () => eng.peek(series),
15301
+ seedTo: () => {
15302
+ throw new ValidationError(`sequence("${series}") is a deferred-numbering series; seedTo is CAS-only.`);
15303
+ }
14472
15304
  };
14473
15305
  }
14474
15306
  if (!this.sequenceStore) {
@@ -14480,7 +15312,7 @@ var init_vault = __esm({
14480
15312
  actor: this.keyring.userId
14481
15313
  });
14482
15314
  }
14483
- return this.sequenceStore.handle(name);
15315
+ return this.sequenceStore.handle(resolveSequenceKey(series, opts));
14484
15316
  }
14485
15317
  /** @internal — lazily build the deferred-numbering engine with a cache-coherent stamp. */
14486
15318
  deferred() {
@@ -14772,9 +15604,24 @@ var init_vault = __esm({
14772
15604
  });
14773
15605
  }
14774
15606
  if (rule.mode === "cascade") {
15607
+ const txCtx = this.noydb._activeTxContextOrNull;
14775
15608
  for (const match of matches) {
14776
15609
  const matchId = match["id"] ?? null;
14777
15610
  if (matchId === null) continue;
15611
+ if (txCtx !== null) {
15612
+ const prior = await this.adapter.get(this.name, rule.collection, matchId);
15613
+ if (prior !== null) {
15614
+ txCtx._executed.push({
15615
+ op: {
15616
+ type: "delete",
15617
+ vaultName: this.name,
15618
+ collectionName: rule.collection,
15619
+ id: matchId
15620
+ },
15621
+ priorEnvelope: prior
15622
+ });
15623
+ }
15624
+ }
14778
15625
  await fromCollection.delete(matchId);
14779
15626
  }
14780
15627
  }
@@ -14913,6 +15760,218 @@ var init_vault = __esm({
14913
15760
  }
14914
15761
  return this.ledgerStore;
14915
15762
  }
15763
+ // ─── GDPR right-to-erasure (#304) ────────────────────────────────
15764
+ /** @internal — add a subject→record ref to the encrypted subject index. */
15765
+ async _addSubjectRef(subjectId, ref) {
15766
+ await addSubjectRef(this.adapter, this.name, this.getDEK, this.encrypted, subjectId, ref);
15767
+ }
15768
+ /** @internal — drop a subject→record ref from the encrypted subject index. */
15769
+ async _removeSubjectRef(subjectId, ref) {
15770
+ await removeSubjectRef(this.adapter, this.name, this.getDEK, this.encrypted, subjectId, ref);
15771
+ }
15772
+ /**
15773
+ * Rebuild the encrypted subject index from canonical records. The recovery
15774
+ * path for the documented read-modify-write race (RISK #3). Returns the
15775
+ * number of distinct subjects re-indexed.
15776
+ */
15777
+ async rebuildSubjectIndex() {
15778
+ if (Object.keys(this.forgetStrategy.subjects).length === 0) {
15779
+ throw new ForgetStrategyNotConfiguredError();
15780
+ }
15781
+ return rebuildSubjectIndex(
15782
+ this.adapter,
15783
+ this.name,
15784
+ this.getDEK,
15785
+ this.encrypted,
15786
+ this.forgetStrategy.subjects,
15787
+ async (collectionName, id, env) => {
15788
+ const coll = this.collection(collectionName);
15789
+ return coll._decodeEnvelope(env, id);
15790
+ }
15791
+ );
15792
+ }
15793
+ /**
15794
+ * GDPR crypto-shred of a data subject (#304). Consults the encrypted subject
15795
+ * index and, per matching record:
15796
+ * - rewrites the LIVE envelope to a tombstone (drops `_iv`/`_data`/`_cek`/`_det`),
15797
+ * - tombstones every `_history` version of the record,
15798
+ * so the body and all prior versions become permanently undecryptable while
15799
+ * the collection DEK and every OTHER record stay intact. Then appends ONE
15800
+ * `op:'forget'` ledger entry whose `payloadHash` is `sha256Hex(subjectId)` —
15801
+ * the chain still `verify()`s, PROVING the subject existed and was erased
15802
+ * without retaining any plaintext.
15803
+ *
15804
+ * Reports — but does not silently swallow — two completeness gaps:
15805
+ * - `unmigratedRecords`: a record whose body was NOT yet migrated to a
15806
+ * per-record CEK (legacy body still under the shared collection DEK). It
15807
+ * is still tombstoned, but its pre-shred ciphertext (if leaked to a
15808
+ * backup before migration) stays decryptable. Migrate, then re-forget.
15809
+ * - `blobResidueCollections`: a shredded record still has blob attachments,
15810
+ * which are keyed off a separate `_blob` DEK and are out of scope here.
15811
+ *
15812
+ * @throws ForgetStrategyNotConfiguredError when no `withForgetCascade` was set.
15813
+ */
15814
+ async forget(subjectId) {
15815
+ if (Object.keys(this.forgetStrategy.subjects).length === 0) {
15816
+ throw new ForgetStrategyNotConfiguredError();
15817
+ }
15818
+ const refs = await lookupSubject(this.adapter, this.name, this.getDEK, this.encrypted, subjectId);
15819
+ let recordsShredded = 0;
15820
+ let historyVersionsShredded = 0;
15821
+ const collections = /* @__PURE__ */ new Set();
15822
+ const unmigratedRecords = [];
15823
+ const blobResidueCollections = /* @__PURE__ */ new Set();
15824
+ let blobsShredded = 0;
15825
+ let blobsRetainedShared = 0;
15826
+ const blobsEnabled = this.blobStrategy !== void 0;
15827
+ const actor = this.keyring.userId;
15828
+ for (const ref of refs) {
15829
+ const coll = this.collection(ref.collection);
15830
+ const perRecordKeys = this.forgetStrategy.subjects[ref.collection] !== void 0;
15831
+ const live = await this.adapter.get(this.name, ref.collection, ref.id);
15832
+ if (perRecordKeys && live && live._data && live._cek === void 0) {
15833
+ unmigratedRecords.push(`${ref.collection}:${ref.id}`);
15834
+ }
15835
+ const shred = await coll._writeTombstone(ref.id, actor);
15836
+ if (shred !== null) {
15837
+ recordsShredded++;
15838
+ collections.add(ref.collection);
15839
+ }
15840
+ historyVersionsShredded += await this.historyStrategy.tombstoneHistory(
15841
+ this.adapter,
15842
+ this.name,
15843
+ ref.collection,
15844
+ ref.id,
15845
+ actor
15846
+ );
15847
+ if (blobsEnabled) {
15848
+ const r = await this.collection(ref.collection).blob(ref.id).shredAllForRecord();
15849
+ blobsShredded += r.shredded.length;
15850
+ blobsRetainedShared += r.retainedShared.length;
15851
+ if (r.residue.length > 0) blobResidueCollections.add(ref.collection);
15852
+ } else {
15853
+ try {
15854
+ const slotIds = await this.adapter.list(this.name, `_blob_slots_${ref.collection}`);
15855
+ if (slotIds.includes(ref.id)) blobResidueCollections.add(ref.collection);
15856
+ } catch {
15857
+ }
15858
+ }
15859
+ await this._removeSubjectRef(subjectId, ref);
15860
+ }
15861
+ const subjectHash = await sha256Hex3(subjectId);
15862
+ const ledger = this.getLedgerOrNull();
15863
+ if (!ledger) {
15864
+ throw new Error(
15865
+ 'vault.forget() requires the history strategy for the erasure-proof ledger entry. Pass `historyStrategy: withHistory()` from "@noy-db/hub/history" to createNoydb().'
15866
+ );
15867
+ }
15868
+ const ledgerEntry = await ledger.append({
15869
+ op: "forget",
15870
+ collection: "",
15871
+ id: "",
15872
+ version: 0,
15873
+ actor,
15874
+ payloadHash: subjectHash,
15875
+ reason: JSON.stringify({
15876
+ recordsShredded,
15877
+ historyVersionsShredded,
15878
+ collections: [...collections],
15879
+ unmigratedCount: unmigratedRecords.length,
15880
+ blobsShredded,
15881
+ blobsRetainedShared,
15882
+ blobResidueCollections: [...blobResidueCollections]
15883
+ })
15884
+ });
15885
+ return {
15886
+ subject: subjectId,
15887
+ recordsShredded,
15888
+ historyVersionsShredded,
15889
+ collections: [...collections],
15890
+ unmigratedRecords,
15891
+ blobsShredded,
15892
+ blobsRetainedShared,
15893
+ blobResidueCollections: [...blobResidueCollections],
15894
+ ledgerEntry
15895
+ };
15896
+ }
15897
+ // ─── Record-scoped CEK sealing (#306 slices 2-3) ──────────────────────
15898
+ /**
15899
+ * Seal ONE record's content-encryption key (CEK) to an `at-*` host so that
15900
+ * host — and only that host — can decrypt exactly that record, with no
15901
+ * access to the vault DEK and no ability to read any other record.
15902
+ *
15903
+ * The grantor (this caller, who holds the collection DEK) reads the record's
15904
+ * live `_cek`, unwraps it under the collection DEK, exports the raw CEK
15905
+ * bytes, builds a {@link SealedCekBinding} `{collection, id, cek, expiresAt}`,
15906
+ * seals that binding for the recipient host via the host's published hint,
15907
+ * and persists a thin {@link SealedCekDeliveryEnvelope} at
15908
+ * `_sealed_cek/<collection>/<id>/<pid>`. The binding (not the delivery
15909
+ * envelope) is the security boundary: the host re-verifies `{collection, id}`
15910
+ * and `expiresAt` from inside the sealed payload.
15911
+ *
15912
+ * Only works on a `perRecordKeys` record — a legacy record has no `_cek` to
15913
+ * seal (its body is under the shared collection DEK, which is never exposed
15914
+ * by sealing) → {@link RecordCekNotFoundError}.
15915
+ *
15916
+ * @param collection Collection holding the record.
15917
+ * @param id Record id.
15918
+ * @param hostSealer The recipient host's {@link RecipientSealer}.
15919
+ * @param opts.expiresAt REQUIRED authoritative expiry (ISO 8601), sealed into
15920
+ * the binding the host verifies.
15921
+ * @returns `{ pid, envelopeKey }` — the host provider id and the
15922
+ * `<collection>/<id>/<pid>` key the delivery envelope was written under.
15923
+ */
15924
+ async sealRecordToHost(collection, id, hostSealer, opts) {
15925
+ return sealRecordToHost(this.sealingContext(), collection, id, hostSealer, opts);
15926
+ }
15927
+ /**
15928
+ * Revoke a single sealed-CEK delivery envelope by deleting it from the store.
15929
+ * A soft revocation: it removes the host's copy of the sealed CEK, but a host
15930
+ * that already fetched the envelope keeps whatever it cached. For a hard
15931
+ * revocation that makes the live record undecryptable to every prior grant,
15932
+ * use {@link rotateRecordCek}.
15933
+ */
15934
+ async revokeSealedRecord(collection, id, pid) {
15935
+ return revokeSealedRecord(this.sealingContext(), collection, id, pid);
15936
+ }
15937
+ /**
15938
+ * HARD-rotate a record's CEK: decrypt the live body under the old CEK,
15939
+ * re-encrypt it under a freshly-minted CEK, write the new live envelope, evict
15940
+ * the in-memory caches, and delete EVERY sealed-CEK delivery envelope for the
15941
+ * record. After this, any host holding a previously-sealed CEK can still
15942
+ * decrypt PRE-rotation history versions (they keep their old `_cek`) but NOT
15943
+ * the rotated live record (its body is under the new CEK → the old CEK fails
15944
+ * the AES-GCM auth tag → `TamperedError`). That asymmetry IS the revocation:
15945
+ * old grants lose the live record.
15946
+ *
15947
+ * Administrative path — bypasses `Collection.put` deliberately (no guards, no
15948
+ * history snapshot, no materialized-view refresh): rotation is a key-rotation
15949
+ * operation, not a business write, and must not version-bump history (which
15950
+ * would re-encrypt the prior version under the NEW CEK and defeat the point).
15951
+ *
15952
+ * @throws {@link RecordCekNotFoundError} if the record is missing or has no `_cek`.
15953
+ */
15954
+ async rotateRecordCek(collection, id) {
15955
+ return rotateRecordCek(this.sealingContext(), collection, id);
15956
+ }
15957
+ /**
15958
+ * Build the {@link SealingContext} the record-keys grantor functions need:
15959
+ * the vault-bound adapter, DEK resolver, actor, and the dual-cache eviction
15960
+ * `rotateRecordCek` performs (per-record CEK cache + decrypted-record cache).
15961
+ */
15962
+ sealingContext() {
15963
+ return {
15964
+ adapter: this.adapter,
15965
+ vault: this.name,
15966
+ getDEK: (collection) => this.getDEK(collection),
15967
+ actor: this.keyring.userId,
15968
+ invalidateRecordCaches: async (collection, id) => {
15969
+ const coll = this.collection(collection);
15970
+ coll._invalidateCekCacheEntry(id);
15971
+ await coll._invalidateCacheEntry(id);
15972
+ }
15973
+ };
15974
+ }
14916
15975
  /**
14917
15976
  * @internal — called by `Noydb.openVault` after construction.
14918
15977
  * Dynamic-imports `GuardRegistry` + `ReadOnlyVaultFacade` and seeds
@@ -17015,7 +18074,7 @@ var init_unlock_state = __esm({
17015
18074
 
17016
18075
  // src/snapshots/strategy.ts
17017
18076
  var NOT_ENABLED5, NO_SNAPSHOTS;
17018
- var init_strategy10 = __esm({
18077
+ var init_strategy11 = __esm({
17019
18078
  "src/snapshots/strategy.ts"() {
17020
18079
  "use strict";
17021
18080
  NOT_ENABLED5 = new Error(
@@ -17156,14 +18215,14 @@ var init_scheduler = __esm({
17156
18215
 
17157
18216
  // src/tx/strategy.ts
17158
18217
  var NOT_ENABLED6, NO_TX;
17159
- var init_strategy11 = __esm({
18218
+ var init_strategy12 = __esm({
17160
18219
  "src/tx/strategy.ts"() {
17161
18220
  "use strict";
17162
18221
  NOT_ENABLED6 = new Error(
17163
18222
  'Multi-record transactions require the tx strategy. Import `{ withTransactions }` from "@noy-db/hub/tx" and pass it to `createNoydb({ txStrategy: withTransactions() })`.'
17164
18223
  );
17165
18224
  NO_TX = {
17166
- async runTransaction() {
18225
+ async runTransaction(_db, _fn, _options, _txInvariants) {
17167
18226
  throw NOT_ENABLED6;
17168
18227
  },
17169
18228
  async runDryRun() {
@@ -17192,7 +18251,7 @@ function notEnabled4(op) {
17192
18251
  );
17193
18252
  }
17194
18253
  var NO_SESSION;
17195
- var init_strategy12 = __esm({
18254
+ var init_strategy13 = __esm({
17196
18255
  "src/session/strategy.ts"() {
17197
18256
  "use strict";
17198
18257
  NO_SESSION = {
@@ -18418,12 +19477,14 @@ var init_noydb = __esm({
18418
19477
  init_authenticators();
18419
19478
  init_unlock_state();
18420
19479
  init_sync_strategy();
18421
- init_strategy10();
19480
+ init_strategy11();
18422
19481
  init_scheduler();
18423
19482
  init_transaction();
18424
- init_strategy11();
18425
- init_sync_policy();
18426
19483
  init_strategy12();
19484
+ init_strategy7();
19485
+ init_subject_index();
19486
+ init_sync_policy();
19487
+ init_strategy13();
18427
19488
  init_policy3();
18428
19489
  ROLE_RANK = {
18429
19490
  client: 1,
@@ -18479,6 +19540,7 @@ var init_noydb = __esm({
18479
19540
  policyEnforcers = /* @__PURE__ */ new Map();
18480
19541
  vaultTemplates = /* @__PURE__ */ new Map();
18481
19542
  txStrategy;
19543
+ forgetStrategy;
18482
19544
  sessionStrategy;
18483
19545
  syncStrategy;
18484
19546
  snapshotStrategy;
@@ -18506,6 +19568,7 @@ var init_noydb = __esm({
18506
19568
  constructor(options) {
18507
19569
  this.options = options;
18508
19570
  this.txStrategy = options.txStrategy ?? NO_TX;
19571
+ this.forgetStrategy = options.forgetStrategy ?? NO_FORGET;
18509
19572
  this.sessionStrategy = options.sessionStrategy ?? NO_SESSION;
18510
19573
  this.syncStrategy = options.syncStrategy ?? NO_SYNC;
18511
19574
  this.snapshotStrategy = options.snapshotStrategy ?? NO_SNAPSHOTS;
@@ -18516,8 +19579,61 @@ var init_noydb = __esm({
18516
19579
  }
18517
19580
  this.#registerGuardGate();
18518
19581
  this.#registerPeriodGate();
19582
+ this.#registerForgetHooks();
18519
19583
  this.resetSessionTimer();
18520
19584
  }
19585
+ /** @internal — resolved forget strategy (NO_FORGET when not configured). */
19586
+ get _forgetStrategy() {
19587
+ return this.forgetStrategy;
19588
+ }
19589
+ // #304 — GDPR subject-index maintenance. When `withForgetCascade` declares
19590
+ // any subject fields, keep the encrypted `_subject_index` in lock-step with
19591
+ // writes so `vault.forget(subjectId)` can find every record for a subject.
19592
+ //
19593
+ // Two consumers are required because they cover disjoint events:
19594
+ // - onAfterWrite fires on create/update (NOT delete) — add the new ref;
19595
+ // on an update that changed the subject value, drop the stale ref.
19596
+ // - the subsystemBus `afterDelete` observer fires on delete (onAfterWrite
19597
+ // does NOT) — drop the ref so a deleted record never lingers in the
19598
+ // index (RISK #2). Without it, forget() would try to shred a ghost.
19599
+ #registerForgetHooks() {
19600
+ const subjects = this.forgetStrategy.subjects;
19601
+ if (Object.keys(subjects).length === 0) return;
19602
+ const subjectFieldFor = (collection) => subjects[collection];
19603
+ this.writeHooks.onAfterWrite(async (event) => {
19604
+ const field = subjectFieldFor(event.collection);
19605
+ if (field === void 0) return;
19606
+ const vault = this.vaultCache.get(event.vault);
19607
+ if (!vault) return;
19608
+ if (event.after !== null && typeof event.after === "object") {
19609
+ const subjectValue = readDottedPath(event.after, field);
19610
+ if (subjectValue !== void 0 && subjectValue !== null) {
19611
+ await vault._addSubjectRef(coerceSubjectId(subjectValue), { collection: event.collection, id: event.docId });
19612
+ }
19613
+ }
19614
+ if (event.op === "update" && event.before !== null && typeof event.before === "object") {
19615
+ const beforeValue = readDottedPath(event.before, field);
19616
+ const afterValue = event.after !== null && typeof event.after === "object" ? readDottedPath(event.after, field) : void 0;
19617
+ const beforeId = beforeValue === void 0 || beforeValue === null ? void 0 : coerceSubjectId(beforeValue);
19618
+ const afterId = afterValue === void 0 || afterValue === null ? void 0 : coerceSubjectId(afterValue);
19619
+ if (beforeId !== void 0 && beforeId !== afterId) {
19620
+ await vault._removeSubjectRef(beforeId, { collection: event.collection, id: event.docId });
19621
+ }
19622
+ }
19623
+ });
19624
+ this.subsystemBus.register("afterDelete", async (event) => {
19625
+ const field = subjectFieldFor(event.collection);
19626
+ if (field === void 0) return;
19627
+ const vault = this.vaultCache.get(event.vault);
19628
+ if (!vault) return;
19629
+ if (event.before !== null && typeof event.before === "object") {
19630
+ const subjectValue = readDottedPath(event.before, field);
19631
+ if (subjectValue !== void 0 && subjectValue !== null) {
19632
+ await vault._removeSubjectRef(coerceSubjectId(subjectValue), { collection: event.collection, id: event.docId });
19633
+ }
19634
+ }
19635
+ });
19636
+ }
18521
19637
  // Track A — guards migration. Registers record-lock / field-freeze / onDelete
18522
19638
  // / amendment-collect as gate-bus handlers (only when guards are opted in, so
18523
19639
  // the write path is zero-cost otherwise). Resolves the live vault's
@@ -18738,6 +19854,7 @@ var init_noydb = __esm({
18738
19854
  ...this.options.syncStrategy !== void 0 ? { syncStrategy: this.options.syncStrategy } : {},
18739
19855
  ...this.options.guardStrategies !== void 0 ? { guardStrategies: this.options.guardStrategies } : {},
18740
19856
  ...this.options.numbering !== void 0 ? { numberingConfigs: this.options.numbering } : {},
19857
+ forgetStrategy: this.forgetStrategy,
18741
19858
  locale: opts?.locale,
18742
19859
  // Thread the translator hook so Collection.put() can invoke it
18743
19860
  plaintextTranslator: this.options.plaintextTranslator ? (text, from, to, field, collection) => this.invokeTranslator(text, from, to, field, collection) : void 0,
@@ -18792,7 +19909,8 @@ var init_noydb = __esm({
18792
19909
  ...this.options.i18nStrategy !== void 0 ? { i18nStrategy: this.options.i18nStrategy } : {},
18793
19910
  ...this.options.syncStrategy !== void 0 ? { syncStrategy: this.options.syncStrategy } : {},
18794
19911
  ...this.options.guardStrategies !== void 0 ? { guardStrategies: this.options.guardStrategies } : {},
18795
- ...this.options.numbering !== void 0 ? { numberingConfigs: this.options.numbering } : {}
19912
+ ...this.options.numbering !== void 0 ? { numberingConfigs: this.options.numbering } : {},
19913
+ forgetStrategy: this.forgetStrategy
18796
19914
  });
18797
19915
  this.vaultCache.set(name, comp2);
18798
19916
  return comp2;
@@ -21577,6 +22695,7 @@ async function describeExtraction(vault, opts) {
21577
22695
  // src/bundle/extract-partition.ts
21578
22696
  init_types();
21579
22697
  init_crypto();
22698
+ init_record_keys();
21580
22699
  init_errors();
21581
22700
  init_ulid();
21582
22701
  init_storage2();
@@ -21596,6 +22715,14 @@ async function reKeyClosure(vault, closure) {
21596
22715
  for (const id of ids) {
21597
22716
  const env = await adapter.get(vaultName, collectionName, id);
21598
22717
  if (!env) continue;
22718
+ if (env._cek !== void 0) {
22719
+ const cek = await unwrapCek(env._cek, srcDek);
22720
+ const plaintext2 = await decrypt(env._iv, env._data, cek);
22721
+ const { iv: iv2, data: data2 } = await encrypt(plaintext2, cek);
22722
+ const wrapped = await wrapCek(cek, destDek);
22723
+ out[id] = { ...env, _iv: iv2, _data: data2, _cek: wrapped };
22724
+ continue;
22725
+ }
21599
22726
  const plaintext = await decrypt(env._iv, env._data, srcDek);
21600
22727
  const { iv, data } = await encrypt(plaintext, destDek);
21601
22728
  out[id] = { ...env, _iv: iv, _data: data };