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

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 +227 -3
  2. package/dist/aggregate/index.cjs.map +1 -1
  3. package/dist/aggregate/index.d.cts +3 -3
  4. package/dist/aggregate/index.d.ts +3 -3
  5. package/dist/aggregate/index.js +5 -4
  6. package/dist/aggregate/index.js.map +1 -1
  7. package/dist/attestation/index.cjs.map +1 -1
  8. package/dist/attestation/index.d.cts +5 -5
  9. package/dist/attestation/index.d.ts +5 -5
  10. package/dist/attestation/index.js +6 -6
  11. package/dist/blobs/index.cjs +4 -10
  12. package/dist/blobs/index.cjs.map +1 -1
  13. package/dist/blobs/index.d.cts +6 -6
  14. package/dist/blobs/index.d.ts +6 -6
  15. package/dist/blobs/index.js +6 -6
  16. package/dist/bundle/index.cjs +1587 -392
  17. package/dist/bundle/index.cjs.map +1 -1
  18. package/dist/bundle/index.d.cts +7 -7
  19. package/dist/bundle/index.d.ts +7 -7
  20. package/dist/bundle/index.js +10 -10
  21. package/dist/{chunk-NBBMMJ2H.js → chunk-3FSMVWBN.js} +4 -4
  22. package/dist/{chunk-HGVSHKZW.js → chunk-3Q2AOPLT.js} +100 -29
  23. package/dist/chunk-3Q2AOPLT.js.map +1 -0
  24. package/dist/{chunk-SHX5QBCI.js → chunk-4ULLGYPA.js} +3 -3
  25. package/dist/{chunk-CD2AVTEM.js → chunk-5IGWRMEC.js} +5 -5
  26. package/dist/{chunk-QO6RGLLD.js → chunk-6KESZO5D.js} +35 -7
  27. package/dist/chunk-6KESZO5D.js.map +1 -0
  28. package/dist/{chunk-GP3SDSH2.js → chunk-6OSOE6BY.js} +15 -2
  29. package/dist/chunk-6OSOE6BY.js.map +1 -0
  30. package/dist/{chunk-F4G63NTZ.js → chunk-7C6VFNIY.js} +2 -2
  31. package/dist/{chunk-XJV6OB4D.js → chunk-7HD67R6U.js} +2 -2
  32. package/dist/{chunk-UMLVJTYV.js → chunk-ADB7GPM3.js} +7 -4
  33. package/dist/chunk-ADB7GPM3.js.map +1 -0
  34. package/dist/{chunk-NYSYPFXJ.js → chunk-B6E5IRPJ.js} +3 -3
  35. package/dist/chunk-CYNTFU2D.js +129 -0
  36. package/dist/chunk-CYNTFU2D.js.map +1 -0
  37. package/dist/{chunk-ZEGSDPB7.js → chunk-DJF3FXW5.js} +35 -1
  38. package/dist/chunk-DJF3FXW5.js.map +1 -0
  39. package/dist/{chunk-3G3W65EQ.js → chunk-DY3EOJEN.js} +2 -2
  40. package/dist/{chunk-YYVZYTWW.js → chunk-E66DSTJP.js} +3 -3
  41. package/dist/{chunk-5LIROIDM.js → chunk-FBLAWK6A.js} +2 -2
  42. package/dist/{chunk-E77UKJYL.js → chunk-FPHRTW2Z.js} +5 -5
  43. package/dist/{state-vault-W2OEABNO.js → chunk-G4PYA575.js} +24 -7
  44. package/dist/chunk-G4PYA575.js.map +1 -0
  45. package/dist/{chunk-U5QCMH3W.js → chunk-GKQAU52M.js} +4 -4
  46. package/dist/{chunk-2FU2FTXD.js → chunk-GYAWXHFO.js} +2 -2
  47. package/dist/{chunk-ROPJVUG3.js → chunk-H42KZXNV.js} +5 -210
  48. package/dist/chunk-H42KZXNV.js.map +1 -0
  49. package/dist/{chunk-XPIHJ34I.js → chunk-IBVTH4JR.js} +4 -4
  50. package/dist/{chunk-C3HYQPV4.js → chunk-IVP5IVON.js} +2 -2
  51. package/dist/{chunk-BL5GYANC.js → chunk-KEDJDWWQ.js} +3 -3
  52. package/dist/{chunk-I5IUYN7B.js → chunk-KNKNOJFS.js} +3 -3
  53. package/dist/chunk-KNKNOJFS.js.map +1 -0
  54. package/dist/{chunk-D77ZQSQQ.js → chunk-KYGGXXT6.js} +829 -170
  55. package/dist/chunk-KYGGXXT6.js.map +1 -0
  56. package/dist/{chunk-J7RWBXFY.js → chunk-LSIIPKYT.js} +2 -2
  57. package/dist/{chunk-BSZOCSDZ.js → chunk-M3FPNTO2.js} +4 -4
  58. package/dist/{chunk-XMVHEWF6.js → chunk-MI36HL5G.js} +4 -4
  59. package/dist/{chunk-ROVO6NPJ.js → chunk-NN6IISZO.js} +58 -3
  60. package/dist/chunk-NN6IISZO.js.map +1 -0
  61. package/dist/{chunk-7H2GEJ3O.js → chunk-OBMYMKGO.js} +29 -6
  62. package/dist/{chunk-7H2GEJ3O.js.map → chunk-OBMYMKGO.js.map} +1 -1
  63. package/dist/{chunk-UNTGHX5A.js → chunk-OKOKPYWH.js} +2 -2
  64. package/dist/{chunk-WV7WV6JO.js → chunk-OY7RX2VL.js} +9 -15
  65. package/dist/chunk-OY7RX2VL.js.map +1 -0
  66. package/dist/{chunk-H2MRGONI.js → chunk-PTGQPWMV.js} +2 -2
  67. package/dist/{chunk-BJSLBUJ7.js → chunk-PWFTQHYX.js} +2 -2
  68. package/dist/{chunk-5AXTH4QZ.js → chunk-Q5MCHUXZ.js} +2 -2
  69. package/dist/{chunk-QHM6XEAH.js → chunk-S22UOMHM.js} +6 -6
  70. package/dist/{chunk-WIAOUFFB.js → chunk-S3XA7G35.js} +2 -2
  71. package/dist/{chunk-SISBMAPO.js → chunk-SHIUFIPW.js} +1 -1
  72. package/dist/chunk-SHIUFIPW.js.map +1 -0
  73. package/dist/{chunk-KCEHMDZF.js → chunk-U7JNBSS3.js} +3 -3
  74. package/dist/{chunk-ZNGPEV5J.js → chunk-V3VIRTTE.js} +3 -3
  75. package/dist/{chunk-TIDXB5DF.js → chunk-V5FZWQNN.js} +4 -4
  76. package/dist/chunk-VEIVAYJ7.js +361 -0
  77. package/dist/chunk-VEIVAYJ7.js.map +1 -0
  78. package/dist/{chunk-AEIKD3PP.js → chunk-VNUE6FHP.js} +3 -3
  79. package/dist/{chunk-DYYYUW5D.js → chunk-WFK2EVYU.js} +10 -2
  80. package/dist/chunk-WFK2EVYU.js.map +1 -0
  81. package/dist/{chunk-XMHUK5PN.js → chunk-X7FJMKT3.js} +2 -2
  82. package/dist/{chunk-FEJDVE3Z.js → chunk-XPH3FWME.js} +7 -2
  83. package/dist/{chunk-FEJDVE3Z.js.map → chunk-XPH3FWME.js.map} +1 -1
  84. package/dist/{chunk-SNMJ7SB3.js → chunk-Y5J63SMF.js} +5 -5
  85. package/dist/{chunk-M476FOQ7.js → chunk-YLRRU72W.js} +2 -2
  86. package/dist/{chunk-DWEBTE2W.js → chunk-YX333DPS.js} +4 -4
  87. package/dist/{chunk-BH3X5L6A.js → chunk-YZE6C3TQ.js} +3 -3
  88. package/dist/consent/index.cjs.map +1 -1
  89. package/dist/consent/index.d.cts +6 -6
  90. package/dist/consent/index.d.ts +6 -6
  91. package/dist/consent/index.js +3 -3
  92. package/dist/{crypto-7BN2HDWG.js → crypto-B46VNH6X.js} +3 -3
  93. package/dist/{delegation-MGH5SODX.js → delegation-5HON72PV.js} +5 -5
  94. package/dist/derivations/index.cjs +82 -2
  95. package/dist/derivations/index.cjs.map +1 -1
  96. package/dist/derivations/index.d.cts +7 -7
  97. package/dist/derivations/index.d.ts +7 -7
  98. package/dist/derivations/index.js +8 -6
  99. package/dist/{dev-unlock-iXbYFAWl.d.cts → dev-unlock-BR1rMOS-.d.cts} +1 -1
  100. package/dist/{dev-unlock-CI1ijTML.d.ts → dev-unlock-whL49sxV.d.ts} +1 -1
  101. package/dist/{errors-Dz64FA65.d.cts → errors-DL-zTrrF.d.cts} +29 -1
  102. package/dist/{errors-Dz64FA65.d.ts → errors-DL-zTrrF.d.ts} +29 -1
  103. package/dist/executor-44R5CUS2.js +12 -0
  104. package/dist/executor-AOACUK7Z.js +8 -0
  105. package/dist/executor-OKFLQCDW.js +8 -0
  106. package/dist/{fanout-sidecar-FIJJ46YG.js → fanout-sidecar-DCQWJQ6S.js} +2 -2
  107. package/dist/forget/index.cjs.map +1 -1
  108. package/dist/forget/index.d.cts +1 -1
  109. package/dist/forget/index.d.ts +1 -1
  110. package/dist/forget/index.js +4 -4
  111. package/dist/guards/index.cjs +80 -3
  112. package/dist/guards/index.cjs.map +1 -1
  113. package/dist/guards/index.d.cts +7 -7
  114. package/dist/guards/index.d.ts +7 -7
  115. package/dist/guards/index.js +8 -4
  116. package/dist/{hash-tEcM5fnv.d.cts → hash-BEUBmmI4.d.cts} +1 -1
  117. package/dist/{hash-blk7Bkes.d.ts → hash-Dtb7FwWd.d.ts} +1 -1
  118. package/dist/history/index.cjs.map +1 -1
  119. package/dist/history/index.d.cts +7 -7
  120. package/dist/history/index.d.ts +7 -7
  121. package/dist/history/index.js +5 -5
  122. package/dist/i18n/index.cjs +149 -132
  123. package/dist/i18n/index.cjs.map +1 -1
  124. package/dist/i18n/index.d.cts +6 -6
  125. package/dist/i18n/index.d.ts +6 -6
  126. package/dist/i18n/index.js +14 -14
  127. package/dist/{index-u-kWzSrL.d.cts → index-BM7O48Ur.d.cts} +85 -9
  128. package/dist/{index-C-SSRIxP.d.cts → index-BMmajblo.d.cts} +14 -0
  129. package/dist/{index-C-SSRIxP.d.ts → index-BMmajblo.d.ts} +14 -0
  130. package/dist/{index-DpU6KWof.d.ts → index-BelbyUwz.d.ts} +85 -9
  131. package/dist/index.cjs +2206 -992
  132. package/dist/index.cjs.map +1 -1
  133. package/dist/index.d.cts +29 -16
  134. package/dist/index.d.ts +29 -16
  135. package/dist/index.js +76 -54
  136. package/dist/index.js.map +1 -1
  137. package/dist/indexing/index.cjs.map +1 -1
  138. package/dist/indexing/index.js +4 -4
  139. package/dist/issue-EPA2PSWP.js +12 -0
  140. package/dist/{ledger-LFVLHE5H.js → ledger-LS6GXCBP.js} +5 -5
  141. package/dist/materialized-views/index.cjs +257 -4
  142. package/dist/materialized-views/index.cjs.map +1 -1
  143. package/dist/materialized-views/index.d.cts +7 -7
  144. package/dist/materialized-views/index.d.ts +7 -7
  145. package/dist/materialized-views/index.js +8 -7
  146. package/dist/noydb-BVKFP74P.js +38 -0
  147. package/dist/overlay-views/index.cjs.map +1 -1
  148. package/dist/overlay-views/index.d.cts +7 -7
  149. package/dist/overlay-views/index.d.ts +7 -7
  150. package/dist/overlay-views/index.js +4 -4
  151. package/dist/periods/index.cjs.map +1 -1
  152. package/dist/periods/index.d.cts +6 -6
  153. package/dist/periods/index.d.ts +6 -6
  154. package/dist/periods/index.js +5 -5
  155. package/dist/{public-envelope-RXZNP3V6.js → public-envelope-AGU6SS4Z.js} +4 -4
  156. package/dist/query/index.cjs +320 -28
  157. package/dist/query/index.cjs.map +1 -1
  158. package/dist/query/index.d.cts +3 -3
  159. package/dist/query/index.d.ts +3 -3
  160. package/dist/query/index.js +7 -6
  161. package/dist/read-only-facade-EX6WZZBP.js +7 -0
  162. package/dist/registry-ERNAMRDE.js +8 -0
  163. package/dist/registry-EXTHSXQW.js +8 -0
  164. package/dist/{registry-SECUWSGY.js → registry-RDPTFXQ7.js} +3 -3
  165. package/dist/{revoke-B54H2S2W.js → revoke-IFLXEZA5.js} +6 -6
  166. package/dist/sealed-record/index.cjs.map +1 -1
  167. package/dist/sealed-record/index.d.cts +1 -1
  168. package/dist/sealed-record/index.d.ts +1 -1
  169. package/dist/sealed-record/index.js +2 -2
  170. package/dist/session/index.cjs.map +1 -1
  171. package/dist/session/index.d.cts +7 -7
  172. package/dist/session/index.d.ts +7 -7
  173. package/dist/session/index.js +3 -3
  174. package/dist/shadow/index.cjs.map +1 -1
  175. package/dist/shadow/index.d.cts +6 -6
  176. package/dist/shadow/index.d.ts +6 -6
  177. package/dist/shadow/index.js +2 -2
  178. package/dist/{signer-YSXZT574.js → signer-UNWOUJAK.js} +5 -5
  179. package/dist/snapshots/index.cjs.map +1 -1
  180. package/dist/snapshots/index.d.cts +6 -6
  181. package/dist/snapshots/index.d.ts +6 -6
  182. package/dist/snapshots/index.js +4 -4
  183. package/dist/{stale-TOA36SRK.js → stale-NTEV5SLX.js} +2 -2
  184. package/dist/state-vault-TUTFRTOA.js +14 -0
  185. package/dist/state-vault-TUTFRTOA.js.map +1 -0
  186. package/dist/store/index.cjs +8 -0
  187. package/dist/store/index.cjs.map +1 -1
  188. package/dist/store/index.d.cts +13 -6
  189. package/dist/store/index.d.ts +13 -6
  190. package/dist/store/index.js +2 -2
  191. package/dist/{strategy-4M9jo172.d.ts → strategy-BDxQnnTX.d.ts} +315 -4
  192. package/dist/{strategy-CLC1j79g.d.cts → strategy-C5ol6NdV.d.cts} +315 -4
  193. package/dist/sync/index.cjs.map +1 -1
  194. package/dist/sync/index.d.cts +5 -5
  195. package/dist/sync/index.d.ts +5 -5
  196. package/dist/sync/index.js +4 -4
  197. package/dist/team/index.cjs.map +1 -1
  198. package/dist/team/index.d.cts +6 -6
  199. package/dist/team/index.d.ts +6 -6
  200. package/dist/team/index.js +8 -8
  201. package/dist/transition-guard-B1N82hMf.d.cts +165 -0
  202. package/dist/transition-guard-C__YeF3_.d.ts +165 -0
  203. package/dist/tx/index.cjs.map +1 -1
  204. package/dist/tx/index.d.cts +6 -6
  205. package/dist/tx/index.d.ts +6 -6
  206. package/dist/tx/index.js +3 -3
  207. package/dist/{types-CljIHm_J.d.ts → types-CraiZOyO.d.ts} +609 -305
  208. package/dist/{types-CrSpRDuG.d.cts → types-D-gr5t0G.d.cts} +609 -305
  209. package/dist/{ulid-CrI7PPbA.d.cts → ulid-DQnSAP5W.d.cts} +1 -1
  210. package/dist/{ulid-CWfL2Vfv.d.ts → ulid-FFRRHkVf.d.ts} +1 -1
  211. package/dist/util/index.cjs.map +1 -1
  212. package/dist/util/index.js +1 -1
  213. package/dist/{vault-group-DHAHFX2A.js → vault-group-27EV7KB4.js} +205 -8
  214. package/dist/vault-group-27EV7KB4.js.map +1 -0
  215. package/dist/{with-materialized-view-NzF71cG_.d.cts → with-materialized-view-BboqxyV3.d.cts} +1 -1
  216. package/dist/{with-materialized-view-B892zYZV.d.ts → with-materialized-view-CguCeVcT.d.ts} +1 -1
  217. package/dist/{with-overlayed-view-CR6m7CHe.d.ts → with-overlayed-view-DO08u_tx.d.ts} +1 -1
  218. package/dist/{with-overlayed-view-UI8qSGL4.d.cts → with-overlayed-view-mmsg5Of3.d.cts} +1 -1
  219. package/dist/with-rollup-_TyBzz3T.d.ts +47 -0
  220. package/dist/with-rollup-aaxOZcIb.d.cts +47 -0
  221. package/package.json +3 -3
  222. package/dist/chunk-D77ZQSQQ.js.map +0 -1
  223. package/dist/chunk-DYYYUW5D.js.map +0 -1
  224. package/dist/chunk-GP3SDSH2.js.map +0 -1
  225. package/dist/chunk-HGVSHKZW.js.map +0 -1
  226. package/dist/chunk-I5IUYN7B.js.map +0 -1
  227. package/dist/chunk-JDWE6JMX.js +0 -139
  228. package/dist/chunk-JDWE6JMX.js.map +0 -1
  229. package/dist/chunk-PDULVIBY.js +0 -63
  230. package/dist/chunk-PDULVIBY.js.map +0 -1
  231. package/dist/chunk-QO6RGLLD.js.map +0 -1
  232. package/dist/chunk-ROPJVUG3.js.map +0 -1
  233. package/dist/chunk-ROVO6NPJ.js.map +0 -1
  234. package/dist/chunk-SISBMAPO.js.map +0 -1
  235. package/dist/chunk-UMLVJTYV.js.map +0 -1
  236. package/dist/chunk-WV7WV6JO.js.map +0 -1
  237. package/dist/chunk-ZEGSDPB7.js.map +0 -1
  238. package/dist/executor-3W63Y44O.js +0 -11
  239. package/dist/executor-CFFWPWBJ.js +0 -8
  240. package/dist/executor-VDQQOR4F.js +0 -8
  241. package/dist/immutable-guard-B5M95nbq.d.ts +0 -82
  242. package/dist/immutable-guard-qN3zF8o1.d.cts +0 -82
  243. package/dist/issue-TTMGHQ2J.js +0 -12
  244. package/dist/noydb-36S6GQNC.js +0 -37
  245. package/dist/read-only-facade-ITU6L7BL.js +0 -7
  246. package/dist/registry-3YFLZ7WD.js +0 -8
  247. package/dist/registry-TGZISEWC.js +0 -8
  248. package/dist/state-vault-W2OEABNO.js.map +0 -1
  249. package/dist/vault-group-DHAHFX2A.js.map +0 -1
  250. package/dist/with-derivation-BZ2y4bzF.d.ts +0 -13
  251. package/dist/with-derivation-Bozs8DmD.d.cts +0 -13
  252. /package/dist/{chunk-NBBMMJ2H.js.map → chunk-3FSMVWBN.js.map} +0 -0
  253. /package/dist/{chunk-SHX5QBCI.js.map → chunk-4ULLGYPA.js.map} +0 -0
  254. /package/dist/{chunk-CD2AVTEM.js.map → chunk-5IGWRMEC.js.map} +0 -0
  255. /package/dist/{chunk-F4G63NTZ.js.map → chunk-7C6VFNIY.js.map} +0 -0
  256. /package/dist/{chunk-XJV6OB4D.js.map → chunk-7HD67R6U.js.map} +0 -0
  257. /package/dist/{chunk-NYSYPFXJ.js.map → chunk-B6E5IRPJ.js.map} +0 -0
  258. /package/dist/{chunk-3G3W65EQ.js.map → chunk-DY3EOJEN.js.map} +0 -0
  259. /package/dist/{chunk-YYVZYTWW.js.map → chunk-E66DSTJP.js.map} +0 -0
  260. /package/dist/{chunk-5LIROIDM.js.map → chunk-FBLAWK6A.js.map} +0 -0
  261. /package/dist/{chunk-E77UKJYL.js.map → chunk-FPHRTW2Z.js.map} +0 -0
  262. /package/dist/{chunk-U5QCMH3W.js.map → chunk-GKQAU52M.js.map} +0 -0
  263. /package/dist/{chunk-2FU2FTXD.js.map → chunk-GYAWXHFO.js.map} +0 -0
  264. /package/dist/{chunk-XPIHJ34I.js.map → chunk-IBVTH4JR.js.map} +0 -0
  265. /package/dist/{chunk-C3HYQPV4.js.map → chunk-IVP5IVON.js.map} +0 -0
  266. /package/dist/{chunk-BL5GYANC.js.map → chunk-KEDJDWWQ.js.map} +0 -0
  267. /package/dist/{chunk-J7RWBXFY.js.map → chunk-LSIIPKYT.js.map} +0 -0
  268. /package/dist/{chunk-BSZOCSDZ.js.map → chunk-M3FPNTO2.js.map} +0 -0
  269. /package/dist/{chunk-XMVHEWF6.js.map → chunk-MI36HL5G.js.map} +0 -0
  270. /package/dist/{chunk-UNTGHX5A.js.map → chunk-OKOKPYWH.js.map} +0 -0
  271. /package/dist/{chunk-H2MRGONI.js.map → chunk-PTGQPWMV.js.map} +0 -0
  272. /package/dist/{chunk-BJSLBUJ7.js.map → chunk-PWFTQHYX.js.map} +0 -0
  273. /package/dist/{chunk-5AXTH4QZ.js.map → chunk-Q5MCHUXZ.js.map} +0 -0
  274. /package/dist/{chunk-QHM6XEAH.js.map → chunk-S22UOMHM.js.map} +0 -0
  275. /package/dist/{chunk-WIAOUFFB.js.map → chunk-S3XA7G35.js.map} +0 -0
  276. /package/dist/{chunk-KCEHMDZF.js.map → chunk-U7JNBSS3.js.map} +0 -0
  277. /package/dist/{chunk-ZNGPEV5J.js.map → chunk-V3VIRTTE.js.map} +0 -0
  278. /package/dist/{chunk-TIDXB5DF.js.map → chunk-V5FZWQNN.js.map} +0 -0
  279. /package/dist/{chunk-AEIKD3PP.js.map → chunk-VNUE6FHP.js.map} +0 -0
  280. /package/dist/{chunk-XMHUK5PN.js.map → chunk-X7FJMKT3.js.map} +0 -0
  281. /package/dist/{chunk-SNMJ7SB3.js.map → chunk-Y5J63SMF.js.map} +0 -0
  282. /package/dist/{chunk-M476FOQ7.js.map → chunk-YLRRU72W.js.map} +0 -0
  283. /package/dist/{chunk-DWEBTE2W.js.map → chunk-YX333DPS.js.map} +0 -0
  284. /package/dist/{chunk-BH3X5L6A.js.map → chunk-YZE6C3TQ.js.map} +0 -0
  285. /package/dist/{crypto-7BN2HDWG.js.map → crypto-B46VNH6X.js.map} +0 -0
  286. /package/dist/{delegation-MGH5SODX.js.map → delegation-5HON72PV.js.map} +0 -0
  287. /package/dist/{executor-3W63Y44O.js.map → executor-44R5CUS2.js.map} +0 -0
  288. /package/dist/{executor-CFFWPWBJ.js.map → executor-AOACUK7Z.js.map} +0 -0
  289. /package/dist/{executor-VDQQOR4F.js.map → executor-OKFLQCDW.js.map} +0 -0
  290. /package/dist/{fanout-sidecar-FIJJ46YG.js.map → fanout-sidecar-DCQWJQ6S.js.map} +0 -0
  291. /package/dist/{issue-TTMGHQ2J.js.map → issue-EPA2PSWP.js.map} +0 -0
  292. /package/dist/{ledger-LFVLHE5H.js.map → ledger-LS6GXCBP.js.map} +0 -0
  293. /package/dist/{noydb-36S6GQNC.js.map → noydb-BVKFP74P.js.map} +0 -0
  294. /package/dist/{public-envelope-RXZNP3V6.js.map → public-envelope-AGU6SS4Z.js.map} +0 -0
  295. /package/dist/{read-only-facade-ITU6L7BL.js.map → read-only-facade-EX6WZZBP.js.map} +0 -0
  296. /package/dist/{registry-3YFLZ7WD.js.map → registry-ERNAMRDE.js.map} +0 -0
  297. /package/dist/{registry-SECUWSGY.js.map → registry-EXTHSXQW.js.map} +0 -0
  298. /package/dist/{registry-TGZISEWC.js.map → registry-RDPTFXQ7.js.map} +0 -0
  299. /package/dist/{revoke-B54H2S2W.js.map → revoke-IFLXEZA5.js.map} +0 -0
  300. /package/dist/{signer-YSXZT574.js.map → signer-UNWOUJAK.js.map} +0 -0
  301. /package/dist/{stale-TOA36SRK.js.map → stale-NTEV5SLX.js.map} +0 -0
package/dist/index.cjs CHANGED
@@ -46,7 +46,7 @@ var init_types = __esm({
46
46
  });
47
47
 
48
48
  // src/errors.ts
49
- var NoydbError, DecryptionError, TamperedError, InvalidKeyError, KeyringCorruptError, NoAccessError, ReadOnlyError, ReadOnlyAtInstantError, ReadOnlyFrameError, PermissionDeniedError, ExportCapabilityError, KeyringExpiredError, ImportCapabilityError, StoreCapabilityError, PrivilegeEscalationError, ReservedVaultNameError, PeriodClosedError, RecordLockedError, FieldFrozenError, InvariantError, AmendmentForbiddenError, DirectoryDisabledError, TierNotGrantedError, ElevationExpiredError, AlreadyElevatedError, TierDemoteDeniedError, DelegationTargetMissingError, ConflictError, LedgerContentionError, SequenceContentionError, SequenceOfflineError, NumberingUncertaintyError, BundleVersionConflictError, NetworkError, NotFoundError, ValidationError, SchemaValidationError, SchemaUpdateError, NonAdditiveSchemaChangeError, SchemaLockedError, SchemaFenceError, MigrationRequiredError, QuiesceTimeoutError, GroupCardinalityError, IndexRequiredError, UniqueConstraintError, UnsupportedIndexOptionError, IndexWriteFailureError, BundleIntegrityError, BundleSealMismatchError, ReservedCollectionNameError, DictKeyMissingError, DictKeyInUseError, MissingTranslationError, LocaleNotSpecifiedError, ScriptViolationError, StaticDictReadonlyError, UnknownDictCodeError, TranslatorNotConfiguredError, BackupLedgerError, BackupCorruptedError, AttestationError, SessionExpiredError, SessionNotFoundError, SessionPolicyError, JoinTooLargeError, CrossJoinTooLargeError, CrossJoinSourceUnknownError, DanglingReferenceError, FilenameSanitizationError, PathEscapeError, DerivationCycleError, DerivationDepthError, DerivationOutputUnknownError, DerivationOutputShapeError, DerivationCapExceededError, MaterializedViewCycleError, MaterializedViewSourceUnknownError, MaterializedViewTooLargeError, MaterializedViewConfigError, OverlayBaseIsVirtualError, OverlayCollectionUnavailableError, OverlayNameCollisionError, OverlayIdMismatchError, SnapshotNotFoundError, UnknownShardError, ShardProvisioningError, CrossShardJoinError, VaultTemplateNotFoundError, ForgetStrategyNotConfiguredError, SealedRecordExpiredError, SealedRecordMismatchError, RecordCekNotFoundError;
49
+ var NoydbError, DecryptionError, TamperedError, InvalidKeyError, KeyringCorruptError, NoAccessError, ReadOnlyError, ReadOnlyAtInstantError, ReadOnlyFrameError, PermissionDeniedError, ExportCapabilityError, KeyringExpiredError, ImportCapabilityError, StoreCapabilityError, PrivilegeEscalationError, ReservedVaultNameError, PeriodClosedError, RecordLockedError, FieldFrozenError, IllegalTransitionError, InvariantError, AmendmentForbiddenError, DirectoryDisabledError, TierNotGrantedError, ElevationExpiredError, AlreadyElevatedError, TierDemoteDeniedError, DelegationTargetMissingError, ConflictError, LedgerContentionError, SequenceContentionError, SequenceOfflineError, NumberingUncertaintyError, BundleVersionConflictError, NetworkError, NotFoundError, ValidationError, SchemaValidationError, SchemaUpdateError, NonAdditiveSchemaChangeError, SchemaLockedError, SchemaFenceError, MigrationRequiredError, QuiesceTimeoutError, GroupCardinalityError, IndexRequiredError, UniqueConstraintError, UnsupportedIndexOptionError, IndexWriteFailureError, BundleIntegrityError, BundleSealMismatchError, ReservedCollectionNameError, DictKeyMissingError, DictKeyInUseError, MissingTranslationError, LocaleNotSpecifiedError, ScriptViolationError, StaticDictReadonlyError, UnknownDictCodeError, TranslatorNotConfiguredError, BackupLedgerError, BackupCorruptedError, AttestationError, SessionExpiredError, SessionNotFoundError, SessionPolicyError, JoinTooLargeError, CrossJoinTooLargeError, CrossJoinSourceUnknownError, DanglingReferenceError, FilenameSanitizationError, PathEscapeError, DerivationCycleError, DerivationDepthError, DerivationOutputUnknownError, DerivationOutputShapeError, DerivationCapExceededError, MaterializedViewCycleError, MaterializedViewSourceUnknownError, MaterializedViewTooLargeError, MaterializedViewConfigError, OverlayBaseIsVirtualError, OverlayCollectionUnavailableError, OverlayNameCollisionError, OverlayIdMismatchError, SnapshotNotFoundError, UnknownShardError, ShardProvisioningError, DataResidencyError, CrossShardJoinError, VaultTemplateNotFoundError, ForgetStrategyNotConfiguredError, SealedRecordExpiredError, SealedRecordMismatchError, RecordCekNotFoundError;
50
50
  var init_errors = __esm({
51
51
  "src/errors.ts"() {
52
52
  "use strict";
@@ -245,6 +245,23 @@ var init_errors = __esm({
245
245
  this.fields = fields;
246
246
  }
247
247
  };
248
+ IllegalTransitionError = class extends NoydbError {
249
+ collection;
250
+ id;
251
+ from;
252
+ to;
253
+ constructor(collection, id, from, to) {
254
+ super(
255
+ "ILLEGAL_TRANSITION",
256
+ `Cannot transition ${collection}/${id} from "${from}" to "${to}" \u2014 not a declared arc. Use withTransactions({ amendment: true, reason }) with admin/owner role to override.`
257
+ );
258
+ this.name = "IllegalTransitionError";
259
+ this.collection = collection;
260
+ this.id = id;
261
+ this.from = from;
262
+ this.to = to;
263
+ }
264
+ };
248
265
  InvariantError = class extends NoydbError {
249
266
  constructor(message) {
250
267
  super("INVARIANT_VIOLATED", message);
@@ -496,14 +513,14 @@ var init_errors = __esm({
496
513
  recordId;
497
514
  fields;
498
515
  conflictingId;
499
- constructor(collection, recordId3, fields, conflictingId) {
516
+ constructor(collection, recordId4, fields, conflictingId) {
500
517
  super(
501
518
  "UNIQUE_CONSTRAINT",
502
- `Unique constraint on ${collection}.[${fields.join(", ")}] violated: record "${recordId3}" duplicates a value already held by "${conflictingId}".`
519
+ `Unique constraint on ${collection}.[${fields.join(", ")}] violated: record "${recordId4}" duplicates a value already held by "${conflictingId}".`
503
520
  );
504
521
  this.name = "UniqueConstraintError";
505
522
  this.collection = collection;
506
- this.recordId = recordId3;
523
+ this.recordId = recordId4;
507
524
  this.fields = fields;
508
525
  this.conflictingId = conflictingId;
509
526
  }
@@ -1018,6 +1035,21 @@ Resolutions:
1018
1035
  this.vaultId = vaultId;
1019
1036
  }
1020
1037
  };
1038
+ DataResidencyError = class extends NoydbError {
1039
+ vaultId;
1040
+ requiredRegion;
1041
+ backendRegion;
1042
+ constructor(vaultId, requiredRegion, backendRegion) {
1043
+ super(
1044
+ "DATA_RESIDENCY",
1045
+ `Shard "${vaultId}" requires region "${requiredRegion}" but its placement backend declares region ${backendRegion === void 0 ? "(none)" : `"${backendRegion}"`}. Refusing to provision \u2014 route this shard to a region-correct backend via routeStore({ vaultRoutes }) (e.g. a region-encoded partition key) before retrying.`
1046
+ );
1047
+ this.name = "DataResidencyError";
1048
+ this.vaultId = vaultId;
1049
+ this.requiredRegion = requiredRegion;
1050
+ this.backendRegion = backendRegion;
1051
+ }
1052
+ };
1021
1053
  CrossShardJoinError = class extends NoydbError {
1022
1054
  constructor(message) {
1023
1055
  super("CROSS_SHARD_JOIN", message);
@@ -2422,196 +2454,558 @@ var init_public_envelope = __esm({
2422
2454
  }
2423
2455
  });
2424
2456
 
2425
- // src/money/fixed-point.ts
2426
- function expandExponent(s) {
2427
- const m = /^([+-]?)(\d+)(?:\.(\d+))?[eE]([+-]?\d+)$/.exec(s);
2428
- if (!m) return s;
2429
- const sign = m[1] === "-" ? "-" : "";
2430
- const intp = m[2];
2431
- const frac = m[3] ?? "";
2432
- const exp = Number(m[4]);
2433
- const digits = intp + frac;
2434
- const pointPos = intp.length + exp;
2435
- let body;
2436
- if (pointPos <= 0) {
2437
- body = "0." + "0".repeat(-pointPos) + digits;
2438
- } else if (pointPos >= digits.length) {
2439
- body = digits + "0".repeat(pointPos - digits.length);
2440
- } else {
2441
- body = digits.slice(0, pointPos) + "." + digits.slice(pointPos);
2442
- }
2443
- return sign + body;
2457
+ // src/i18n/policy.ts
2458
+ function resolvePolicy(onMissing, layer) {
2459
+ const explicit = onMissing && typeof onMissing === "object" ? onMissing[layer] : void 0;
2460
+ const scalar = typeof onMissing === "string" ? onMissing : void 0;
2461
+ const layerDefault = layer === "guard" ? "substitute" : void 0;
2462
+ return explicit ?? layerDefault ?? scalar ?? "throw";
2444
2463
  }
2445
- function toCanonicalDecimalString(input) {
2446
- let s;
2447
- if (typeof input === "number") {
2448
- if (!Number.isFinite(input)) return null;
2449
- s = String(input);
2450
- } else {
2451
- s = input.trim();
2464
+ var init_policy = __esm({
2465
+ "src/i18n/policy.ts"() {
2466
+ "use strict";
2452
2467
  }
2453
- s = expandExponent(s);
2454
- if (s.startsWith("+")) s = s.slice(1);
2455
- if (!/^-?(\d+(\.\d*)?|\.\d+)$/.test(s)) return null;
2456
- return s;
2468
+ });
2469
+
2470
+ // src/i18n/script.ts
2471
+ function inferScripts(locale) {
2472
+ const parts = locale.split("-");
2473
+ const subtag = parts.find((t) => /^[A-Z][a-z]{3}$/.test(t));
2474
+ if (subtag && SUBTAG_SCRIPTS[subtag]) return SUBTAG_SCRIPTS[subtag];
2475
+ const base = (parts[0] ?? "").toLowerCase();
2476
+ if (LATIN_BASE.has(base)) return ["Latin"];
2477
+ const primary = SCRIPT_TABLE[base];
2478
+ if (primary) return [...primary, "Latin"];
2479
+ return ["Latin"];
2457
2480
  }
2458
- function shouldRoundUp(negative, lastKeptDigit, firstDiscarded, hasMoreNonZeroAfterFirst, mode) {
2459
- switch (mode) {
2460
- case "up":
2461
- return true;
2462
- case "down":
2463
- return false;
2464
- case "ceil":
2465
- return !negative;
2466
- case "floor":
2467
- return negative;
2468
- case "half-up":
2469
- return firstDiscarded >= 5;
2470
- case "half-down":
2471
- return firstDiscarded > 5 || firstDiscarded === 5 && hasMoreNonZeroAfterFirst;
2472
- case "half-even":
2473
- if (firstDiscarded > 5) return true;
2474
- if (firstDiscarded < 5) return false;
2475
- return hasMoreNonZeroAfterFirst || lastKeptDigit % 2 === 1;
2481
+ function allowedFor(descriptor, locale) {
2482
+ const script = descriptor.options.script;
2483
+ if (script && script !== "auto") {
2484
+ const explicit = script[locale];
2485
+ if (explicit) return explicit;
2476
2486
  }
2487
+ return inferScripts(locale);
2477
2488
  }
2478
- function parseToScaledInt(input, scale, rounding) {
2479
- const canonical2 = toCanonicalDecimalString(input);
2480
- if (canonical2 === null) return { ok: false, reason: "nonfinite" };
2481
- const negative = canonical2.startsWith("-");
2482
- const unsigned = negative ? canonical2.slice(1) : canonical2;
2483
- const dot = unsigned.indexOf(".");
2484
- const intPart = dot === -1 ? unsigned : unsigned.slice(0, dot);
2485
- const fracPart = dot === -1 ? "" : unsigned.slice(dot + 1);
2486
- const intDigits = intPart === "" ? "0" : intPart;
2487
- if (fracPart.length <= scale) {
2488
- const keep2 = fracPart.padEnd(scale, "0");
2489
- const magnitude2 = BigInt(intDigits + keep2);
2490
- return { ok: true, value: negative && magnitude2 !== 0n ? -magnitude2 : magnitude2 };
2491
- }
2492
- const keep = fracPart.slice(0, scale);
2493
- const tail = fracPart.slice(scale);
2494
- const magnitudeDigits = intDigits + keep;
2495
- let magnitude = BigInt(magnitudeDigits);
2496
- if (/^0+$/.test(tail)) {
2497
- return { ok: true, value: negative && magnitude !== 0n ? -magnitude : magnitude };
2498
- }
2499
- if (rounding === void 0) return { ok: false, reason: "precision" };
2500
- const lastKeptDigit = Number(magnitudeDigits[magnitudeDigits.length - 1]);
2501
- const firstDiscarded = Number(tail[0]);
2502
- const hasMoreNonZeroAfterFirst = /[1-9]/.test(tail.slice(1));
2503
- if (shouldRoundUp(negative, lastKeptDigit, firstDiscarded, hasMoreNonZeroAfterFirst, rounding)) {
2504
- magnitude += 1n;
2505
- }
2506
- return { ok: true, value: negative && magnitude !== 0n ? -magnitude : magnitude };
2489
+ function fullMatcher(scripts) {
2490
+ const cls = scripts.map((s) => `\\p{Script=${s}}`).join("");
2491
+ return new RegExp(`^[${BASELINE}${cls}]*$`, "u");
2507
2492
  }
2508
- function decimalScaleOf(input) {
2509
- const s = toCanonicalDecimalString(input);
2510
- if (s === null) return null;
2511
- const dot = s.indexOf(".");
2512
- return dot === -1 ? 0 : s.length - dot - 1;
2493
+ function charMatcher(scripts) {
2494
+ const cls = scripts.map((s) => `\\p{Script=${s}}`).join("");
2495
+ return new RegExp(`[${BASELINE}${cls}]`, "u");
2513
2496
  }
2514
- function rescaleScaledInt(value, fromScale, toScale, rounding = "half-up") {
2515
- if (toScale >= fromScale) return value * 10n ** BigInt(toScale - fromScale);
2516
- const drop = fromScale - toScale;
2517
- const negative = value < 0n;
2518
- const absStr = (negative ? -value : value).toString().padStart(drop + 1, "0");
2519
- const keptStr = absStr.slice(0, absStr.length - drop);
2520
- const tail = absStr.slice(absStr.length - drop);
2521
- let magnitude = BigInt(keptStr);
2522
- if (!/^0+$/.test(tail)) {
2523
- const lastKeptDigit = Number(keptStr[keptStr.length - 1]);
2524
- const firstDiscarded = Number(tail[0]);
2525
- const hasMoreNonZeroAfterFirst = /[1-9]/.test(tail.slice(1));
2526
- if (shouldRoundUp(negative, lastKeptDigit, firstDiscarded, hasMoreNonZeroAfterFirst, rounding)) {
2527
- magnitude += 1n;
2528
- }
2497
+ function offendingSample(str, scripts) {
2498
+ const ok = charMatcher(scripts);
2499
+ const bad = [];
2500
+ for (const ch of str) {
2501
+ if (!ok.test(ch)) bad.push(ch);
2502
+ if (bad.length >= 8) break;
2529
2503
  }
2530
- return negative && magnitude !== 0n ? -magnitude : magnitude;
2504
+ return bad.join("");
2531
2505
  }
2532
- function formatScaledInt(value, scale) {
2533
- const negative = value < 0n;
2534
- const abs = (negative ? -value : value).toString();
2535
- if (scale === 0) return (negative ? "-" : "") + abs;
2536
- const padded = abs.padStart(scale + 1, "0");
2537
- const cut = padded.length - scale;
2538
- const intPart = padded.slice(0, cut);
2539
- const fracPart = padded.slice(cut);
2540
- return (negative ? "-" : "") + intPart + "." + fracPart;
2506
+ function stripDisallowed(str, scripts) {
2507
+ const ok = charMatcher(scripts);
2508
+ let out = "";
2509
+ for (const ch of str) if (ok.test(ch)) out += ch;
2510
+ return out;
2541
2511
  }
2542
- var init_fixed_point = __esm({
2543
- "src/money/fixed-point.ts"() {
2544
- "use strict";
2512
+ function enforceScript(value, field, descriptor) {
2513
+ const opt = descriptor.options;
2514
+ if (!opt.script) return { value, warnings: [] };
2515
+ const mode = opt.onScriptViolation ?? "reject";
2516
+ const warnings = [];
2517
+ let out = value;
2518
+ for (const [locale, raw] of Object.entries(value)) {
2519
+ if (typeof raw !== "string") continue;
2520
+ const allowed = allowedFor(descriptor, locale);
2521
+ if (fullMatcher(allowed).test(raw)) continue;
2522
+ const sample = offendingSample(raw, allowed);
2523
+ if (mode === "reject") {
2524
+ throw new ScriptViolationError(field, locale, allowed, sample);
2525
+ }
2526
+ warnings.push({ field, locale, expected: allowed, sample });
2527
+ if (mode === "filter") {
2528
+ if (out === value) out = { ...value };
2529
+ out[locale] = stripDisallowed(raw, allowed);
2530
+ }
2545
2531
  }
2546
- });
2547
-
2548
- // src/money/iso4217.ts
2549
- function scaleForCurrency(code) {
2550
- const v = MINOR_UNITS[code];
2551
- return v === void 0 ? null : v;
2532
+ return { value: out, warnings };
2552
2533
  }
2553
- var MINOR_UNITS;
2554
- var init_iso4217 = __esm({
2555
- "src/money/iso4217.ts"() {
2534
+ var LATIN_BASE, SCRIPT_TABLE, SUBTAG_SCRIPTS, BASELINE;
2535
+ var init_script = __esm({
2536
+ "src/i18n/script.ts"() {
2556
2537
  "use strict";
2557
- MINOR_UNITS = {
2558
- // 2-decimal majors
2559
- EUR: 2,
2560
- USD: 2,
2561
- GBP: 2,
2562
- CHF: 2,
2563
- CAD: 2,
2564
- AUD: 2,
2565
- NZD: 2,
2566
- SGD: 2,
2567
- HKD: 2,
2568
- CNY: 2,
2569
- INR: 2,
2570
- BRL: 2,
2571
- MXN: 2,
2572
- ZAR: 2,
2573
- RUB: 2,
2574
- TRY: 2,
2575
- PLN: 2,
2576
- SEK: 2,
2577
- NOK: 2,
2578
- DKK: 2,
2579
- CZK: 2,
2580
- HUF: 2,
2581
- RON: 2,
2582
- ILS: 2,
2583
- THB: 2,
2584
- PHP: 2,
2585
- MYR: 2,
2586
- IDR: 2,
2587
- AED: 2,
2588
- SAR: 2,
2589
- QAR: 2,
2590
- EGP: 2,
2591
- // 0-decimal
2592
- JPY: 0,
2593
- KRW: 0,
2594
- ISK: 0,
2595
- CLP: 0,
2596
- VND: 0,
2597
- XOF: 0,
2598
- XAF: 0,
2599
- PYG: 0,
2600
- // 3-decimal
2601
- BHD: 3,
2602
- KWD: 3,
2603
- OMR: 3,
2604
- TND: 3,
2605
- JOD: 3,
2606
- IQD: 3,
2607
- LYD: 3
2538
+ init_errors();
2539
+ LATIN_BASE = /* @__PURE__ */ new Set([
2540
+ "en",
2541
+ "fr",
2542
+ "de",
2543
+ "es",
2544
+ "it",
2545
+ "pt",
2546
+ "nl",
2547
+ "sv",
2548
+ "no",
2549
+ "da",
2550
+ "fi",
2551
+ "is",
2552
+ "pl",
2553
+ "cs",
2554
+ "sk",
2555
+ "hu",
2556
+ "ro",
2557
+ "hr",
2558
+ "sl",
2559
+ "et",
2560
+ "lv",
2561
+ "lt",
2562
+ "tr",
2563
+ "vi",
2564
+ "id",
2565
+ "ms",
2566
+ "tl",
2567
+ "sw",
2568
+ "af",
2569
+ "ca",
2570
+ "gl",
2571
+ "eu",
2572
+ "cy",
2573
+ "ga"
2574
+ ]);
2575
+ SCRIPT_TABLE = {
2576
+ th: ["Thai"],
2577
+ ko: ["Hangul", "Han"],
2578
+ ja: ["Han", "Hiragana", "Katakana"],
2579
+ zh: ["Han"],
2580
+ ar: ["Arabic"],
2581
+ fa: ["Arabic"],
2582
+ ur: ["Arabic"],
2583
+ ru: ["Cyrillic"],
2584
+ uk: ["Cyrillic"],
2585
+ bg: ["Cyrillic"],
2586
+ sr: ["Cyrillic"],
2587
+ he: ["Hebrew"],
2588
+ el: ["Greek"],
2589
+ hi: ["Devanagari"],
2590
+ ta: ["Tamil"],
2591
+ km: ["Khmer"],
2592
+ lo: ["Lao"],
2593
+ my: ["Myanmar"]
2594
+ };
2595
+ SUBTAG_SCRIPTS = {
2596
+ Latn: ["Latin"],
2597
+ Cyrl: ["Cyrillic", "Latin"],
2598
+ Hans: ["Han", "Latin"],
2599
+ Hant: ["Han", "Latin"],
2600
+ Thai: ["Thai", "Latin"],
2601
+ Arab: ["Arabic", "Latin"]
2608
2602
  };
2603
+ BASELINE = String.raw`\p{White_Space}\p{Script=Common}\p{Script=Inherited}\p{Mark}`;
2609
2604
  }
2610
2605
  });
2611
2606
 
2612
- // src/money/descriptor.ts
2613
- function isMultiOptions(o) {
2614
- return "currencies" in o;
2607
+ // src/i18n/core.ts
2608
+ function i18nText(options) {
2609
+ return { _noydbI18nText: true, options };
2610
+ }
2611
+ function isI18nTextDescriptor(x) {
2612
+ return typeof x === "object" && x !== null && x._noydbI18nText === true;
2613
+ }
2614
+ function validateI18nTextValue(value, field, descriptor) {
2615
+ const { options } = descriptor;
2616
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
2617
+ throw new MissingTranslationError(
2618
+ field,
2619
+ options.languages,
2620
+ `Field "${field}" must be a { [locale]: string } map, got ${typeof value}.`
2621
+ );
2622
+ }
2623
+ const map = value;
2624
+ for (const [locale, v] of Object.entries(map)) {
2625
+ if (typeof v !== "string") {
2626
+ throw new MissingTranslationError(
2627
+ field,
2628
+ [locale],
2629
+ `Field "${field}": locale "${locale}" must be a string, got ${typeof v}.`
2630
+ );
2631
+ }
2632
+ }
2633
+ const { required } = options;
2634
+ if (required === "all") {
2635
+ const missing = options.languages.filter(
2636
+ (lang) => !(lang in map) || map[lang] === ""
2637
+ );
2638
+ if (missing.length > 0) {
2639
+ throw new MissingTranslationError(
2640
+ field,
2641
+ missing,
2642
+ `Field "${field}" requires all declared languages. Missing: ${missing.join(", ")}.`
2643
+ );
2644
+ }
2645
+ } else if (required === "any") {
2646
+ const present = options.languages.some(
2647
+ (lang) => lang in map && map[lang] !== ""
2648
+ );
2649
+ if (!present) {
2650
+ throw new MissingTranslationError(
2651
+ field,
2652
+ options.languages,
2653
+ `Field "${field}" requires at least one declared language. None present.`
2654
+ );
2655
+ }
2656
+ } else {
2657
+ const requiredList = required;
2658
+ const missing = requiredList.filter(
2659
+ (lang) => !(lang in map) || map[lang] === ""
2660
+ );
2661
+ if (missing.length > 0) {
2662
+ throw new MissingTranslationError(
2663
+ field,
2664
+ missing,
2665
+ `Field "${field}" requires: ${requiredList.join(", ")}. Missing: ${missing.join(", ")}.`
2666
+ );
2667
+ }
2668
+ }
2669
+ }
2670
+ function toChain(fallback) {
2671
+ return Array.isArray(fallback) ? fallback : fallback ? [fallback] : [];
2672
+ }
2673
+ function pickFromChain(value, chain) {
2674
+ for (const fb of chain) {
2675
+ if (fb === "any") {
2676
+ const any = Object.values(value).find((v) => v !== "");
2677
+ if (any !== void 0) return any;
2678
+ } else if (value[fb] !== void 0 && value[fb] !== "") {
2679
+ return value[fb];
2680
+ }
2681
+ }
2682
+ return void 0;
2683
+ }
2684
+ function resolveI18nText(value, locale, fallback, field, opts) {
2685
+ if (locale === "raw") {
2686
+ return value;
2687
+ }
2688
+ if (!locale) {
2689
+ throw new LocaleNotSpecifiedError(field ?? "<unknown>");
2690
+ }
2691
+ if (value[locale] !== void 0 && value[locale] !== "") {
2692
+ return value[locale];
2693
+ }
2694
+ const policy = opts?.policy ?? "throw";
2695
+ const callerChain = toChain(fallback);
2696
+ const callerHit = pickFromChain(value, callerChain);
2697
+ if (callerHit !== void 0) return callerHit;
2698
+ if (policy === "substitute") {
2699
+ const subHit = pickFromChain(value, toChain(opts?.substitute));
2700
+ if (subHit !== void 0) return subHit;
2701
+ if (opts?.smartSubstitute) {
2702
+ const smartHit = pickNearestScript(value, locale);
2703
+ if (smartHit !== void 0) return smartHit;
2704
+ }
2705
+ }
2706
+ if (policy === "throw") {
2707
+ throw new LocaleNotSpecifiedError(
2708
+ field ?? "<unknown>",
2709
+ `No translation available for locale "${locale}"` + (callerChain.length > 0 ? ` or fallback chain [${callerChain.join(", ")}]` : "") + "."
2710
+ );
2711
+ }
2712
+ return null;
2713
+ }
2714
+ function pickNearestScript(value, target) {
2715
+ const targetScript = inferScripts(target)[0] ?? "Latin";
2716
+ let best;
2717
+ for (const [loc, v] of Object.entries(value)) {
2718
+ if (typeof v !== "string" || v === "") continue;
2719
+ const s = inferScripts(loc)[0] ?? "Latin";
2720
+ const score = s === targetScript ? 0 : s === "Latin" ? 1 : 2;
2721
+ if (best === void 0 || score < best.score) best = { score, v };
2722
+ if (score === 0) break;
2723
+ }
2724
+ return best?.v;
2725
+ }
2726
+ function getAtPath(obj, path) {
2727
+ const arrayIdx = path.indexOf("[].");
2728
+ if (arrayIdx !== -1) {
2729
+ const arrayKey = path.slice(0, arrayIdx);
2730
+ const restPath = path.slice(arrayIdx + 3);
2731
+ const arr = obj[arrayKey];
2732
+ if (!Array.isArray(arr)) return [];
2733
+ return arr.flatMap((item) => {
2734
+ if (!item || typeof item !== "object" || Array.isArray(item)) return [];
2735
+ return getAtPath(item, restPath);
2736
+ });
2737
+ }
2738
+ const dotIdx = path.indexOf(".");
2739
+ if (dotIdx !== -1) {
2740
+ const head = path.slice(0, dotIdx);
2741
+ const rest = path.slice(dotIdx + 1);
2742
+ const nested = obj[head];
2743
+ if (!nested || typeof nested !== "object" || Array.isArray(nested)) return [];
2744
+ return getAtPath(nested, rest);
2745
+ }
2746
+ const val = obj[path];
2747
+ return val !== void 0 ? [val] : [];
2748
+ }
2749
+ function setAtPathInPlace(obj, path, value) {
2750
+ const dotIdx = path.indexOf(".");
2751
+ if (dotIdx !== -1) {
2752
+ const head = path.slice(0, dotIdx);
2753
+ const rest = path.slice(dotIdx + 1);
2754
+ const nested = obj[head];
2755
+ if (!nested || typeof nested !== "object" || Array.isArray(nested)) return;
2756
+ setAtPathInPlace(nested, rest, value);
2757
+ return;
2758
+ }
2759
+ obj[path] = value;
2760
+ }
2761
+ function applyAtPath(obj, path, locale, fallback, opts) {
2762
+ const arrayIdx = path.indexOf("[].");
2763
+ if (arrayIdx !== -1) {
2764
+ const arrayKey = path.slice(0, arrayIdx);
2765
+ const restPath = path.slice(arrayIdx + 3);
2766
+ const arr = obj[arrayKey];
2767
+ if (!Array.isArray(arr)) return obj;
2768
+ return {
2769
+ ...obj,
2770
+ [arrayKey]: arr.map((item) => {
2771
+ if (!item || typeof item !== "object" || Array.isArray(item)) return item;
2772
+ return applyAtPath(item, restPath, locale, fallback, opts);
2773
+ })
2774
+ };
2775
+ }
2776
+ const dotIdx = path.indexOf(".");
2777
+ if (dotIdx !== -1) {
2778
+ const head = path.slice(0, dotIdx);
2779
+ const rest = path.slice(dotIdx + 1);
2780
+ const nested = obj[head];
2781
+ if (!nested || typeof nested !== "object" || Array.isArray(nested)) return obj;
2782
+ return {
2783
+ ...obj,
2784
+ [head]: applyAtPath(nested, rest, locale, fallback, opts)
2785
+ };
2786
+ }
2787
+ const raw = obj[path];
2788
+ if (raw === void 0 || raw === null) return obj;
2789
+ if (typeof raw !== "object" || Array.isArray(raw)) return obj;
2790
+ return {
2791
+ ...obj,
2792
+ [path]: resolveI18nText(raw, locale, fallback, path, opts)
2793
+ };
2794
+ }
2795
+ function applyI18nLocale(record, i18nFields, locale, fallback, layer = "read") {
2796
+ const fieldNames = Object.keys(i18nFields);
2797
+ if (fieldNames.length === 0) return record;
2798
+ let result = record;
2799
+ for (const [field, descriptor] of Object.entries(i18nFields)) {
2800
+ const { onMissing, substitute, smartSubstitute } = descriptor.options;
2801
+ const opts = {
2802
+ policy: resolvePolicy(onMissing, layer),
2803
+ ...substitute !== void 0 ? { substitute } : {},
2804
+ ...smartSubstitute ? { smartSubstitute } : {}
2805
+ };
2806
+ result = applyAtPath(result, field, locale, fallback, opts);
2807
+ }
2808
+ return result;
2809
+ }
2810
+ var init_core = __esm({
2811
+ "src/i18n/core.ts"() {
2812
+ "use strict";
2813
+ init_errors();
2814
+ init_policy();
2815
+ init_script();
2816
+ }
2817
+ });
2818
+
2819
+ // src/money/fixed-point.ts
2820
+ function expandExponent(s) {
2821
+ const m = /^([+-]?)(\d+)(?:\.(\d+))?[eE]([+-]?\d+)$/.exec(s);
2822
+ if (!m) return s;
2823
+ const sign = m[1] === "-" ? "-" : "";
2824
+ const intp = m[2];
2825
+ const frac = m[3] ?? "";
2826
+ const exp = Number(m[4]);
2827
+ const digits = intp + frac;
2828
+ const pointPos = intp.length + exp;
2829
+ let body;
2830
+ if (pointPos <= 0) {
2831
+ body = "0." + "0".repeat(-pointPos) + digits;
2832
+ } else if (pointPos >= digits.length) {
2833
+ body = digits + "0".repeat(pointPos - digits.length);
2834
+ } else {
2835
+ body = digits.slice(0, pointPos) + "." + digits.slice(pointPos);
2836
+ }
2837
+ return sign + body;
2838
+ }
2839
+ function toCanonicalDecimalString(input) {
2840
+ let s;
2841
+ if (typeof input === "number") {
2842
+ if (!Number.isFinite(input)) return null;
2843
+ s = String(input);
2844
+ } else {
2845
+ s = input.trim();
2846
+ }
2847
+ s = expandExponent(s);
2848
+ if (s.startsWith("+")) s = s.slice(1);
2849
+ if (!/^-?(\d+(\.\d*)?|\.\d+)$/.test(s)) return null;
2850
+ return s;
2851
+ }
2852
+ function shouldRoundUp(negative, lastKeptDigit, firstDiscarded, hasMoreNonZeroAfterFirst, mode) {
2853
+ switch (mode) {
2854
+ case "up":
2855
+ return true;
2856
+ case "down":
2857
+ return false;
2858
+ case "ceil":
2859
+ return !negative;
2860
+ case "floor":
2861
+ return negative;
2862
+ case "half-up":
2863
+ return firstDiscarded >= 5;
2864
+ case "half-down":
2865
+ return firstDiscarded > 5 || firstDiscarded === 5 && hasMoreNonZeroAfterFirst;
2866
+ case "half-even":
2867
+ if (firstDiscarded > 5) return true;
2868
+ if (firstDiscarded < 5) return false;
2869
+ return hasMoreNonZeroAfterFirst || lastKeptDigit % 2 === 1;
2870
+ }
2871
+ }
2872
+ function parseToScaledInt(input, scale, rounding) {
2873
+ const canonical2 = toCanonicalDecimalString(input);
2874
+ if (canonical2 === null) return { ok: false, reason: "nonfinite" };
2875
+ const negative = canonical2.startsWith("-");
2876
+ const unsigned = negative ? canonical2.slice(1) : canonical2;
2877
+ const dot = unsigned.indexOf(".");
2878
+ const intPart = dot === -1 ? unsigned : unsigned.slice(0, dot);
2879
+ const fracPart = dot === -1 ? "" : unsigned.slice(dot + 1);
2880
+ const intDigits = intPart === "" ? "0" : intPart;
2881
+ if (fracPart.length <= scale) {
2882
+ const keep2 = fracPart.padEnd(scale, "0");
2883
+ const magnitude2 = BigInt(intDigits + keep2);
2884
+ return { ok: true, value: negative && magnitude2 !== 0n ? -magnitude2 : magnitude2 };
2885
+ }
2886
+ const keep = fracPart.slice(0, scale);
2887
+ const tail = fracPart.slice(scale);
2888
+ const magnitudeDigits = intDigits + keep;
2889
+ let magnitude = BigInt(magnitudeDigits);
2890
+ if (/^0+$/.test(tail)) {
2891
+ return { ok: true, value: negative && magnitude !== 0n ? -magnitude : magnitude };
2892
+ }
2893
+ if (rounding === void 0) return { ok: false, reason: "precision" };
2894
+ const lastKeptDigit = Number(magnitudeDigits[magnitudeDigits.length - 1]);
2895
+ const firstDiscarded = Number(tail[0]);
2896
+ const hasMoreNonZeroAfterFirst = /[1-9]/.test(tail.slice(1));
2897
+ if (shouldRoundUp(negative, lastKeptDigit, firstDiscarded, hasMoreNonZeroAfterFirst, rounding)) {
2898
+ magnitude += 1n;
2899
+ }
2900
+ return { ok: true, value: negative && magnitude !== 0n ? -magnitude : magnitude };
2901
+ }
2902
+ function decimalScaleOf(input) {
2903
+ const s = toCanonicalDecimalString(input);
2904
+ if (s === null) return null;
2905
+ const dot = s.indexOf(".");
2906
+ return dot === -1 ? 0 : s.length - dot - 1;
2907
+ }
2908
+ function rescaleScaledInt(value, fromScale, toScale, rounding = "half-up") {
2909
+ if (toScale >= fromScale) return value * 10n ** BigInt(toScale - fromScale);
2910
+ const drop = fromScale - toScale;
2911
+ const negative = value < 0n;
2912
+ const absStr = (negative ? -value : value).toString().padStart(drop + 1, "0");
2913
+ const keptStr = absStr.slice(0, absStr.length - drop);
2914
+ const tail = absStr.slice(absStr.length - drop);
2915
+ let magnitude = BigInt(keptStr);
2916
+ if (!/^0+$/.test(tail)) {
2917
+ const lastKeptDigit = Number(keptStr[keptStr.length - 1]);
2918
+ const firstDiscarded = Number(tail[0]);
2919
+ const hasMoreNonZeroAfterFirst = /[1-9]/.test(tail.slice(1));
2920
+ if (shouldRoundUp(negative, lastKeptDigit, firstDiscarded, hasMoreNonZeroAfterFirst, rounding)) {
2921
+ magnitude += 1n;
2922
+ }
2923
+ }
2924
+ return negative && magnitude !== 0n ? -magnitude : magnitude;
2925
+ }
2926
+ function formatScaledInt(value, scale) {
2927
+ const negative = value < 0n;
2928
+ const abs = (negative ? -value : value).toString();
2929
+ if (scale === 0) return (negative ? "-" : "") + abs;
2930
+ const padded = abs.padStart(scale + 1, "0");
2931
+ const cut = padded.length - scale;
2932
+ const intPart = padded.slice(0, cut);
2933
+ const fracPart = padded.slice(cut);
2934
+ return (negative ? "-" : "") + intPart + "." + fracPart;
2935
+ }
2936
+ var init_fixed_point = __esm({
2937
+ "src/money/fixed-point.ts"() {
2938
+ "use strict";
2939
+ }
2940
+ });
2941
+
2942
+ // src/money/iso4217.ts
2943
+ function scaleForCurrency(code) {
2944
+ const v = MINOR_UNITS[code];
2945
+ return v === void 0 ? null : v;
2946
+ }
2947
+ var MINOR_UNITS;
2948
+ var init_iso4217 = __esm({
2949
+ "src/money/iso4217.ts"() {
2950
+ "use strict";
2951
+ MINOR_UNITS = {
2952
+ // 2-decimal majors
2953
+ EUR: 2,
2954
+ USD: 2,
2955
+ GBP: 2,
2956
+ CHF: 2,
2957
+ CAD: 2,
2958
+ AUD: 2,
2959
+ NZD: 2,
2960
+ SGD: 2,
2961
+ HKD: 2,
2962
+ CNY: 2,
2963
+ INR: 2,
2964
+ BRL: 2,
2965
+ MXN: 2,
2966
+ ZAR: 2,
2967
+ RUB: 2,
2968
+ TRY: 2,
2969
+ PLN: 2,
2970
+ SEK: 2,
2971
+ NOK: 2,
2972
+ DKK: 2,
2973
+ CZK: 2,
2974
+ HUF: 2,
2975
+ RON: 2,
2976
+ ILS: 2,
2977
+ THB: 2,
2978
+ PHP: 2,
2979
+ MYR: 2,
2980
+ IDR: 2,
2981
+ AED: 2,
2982
+ SAR: 2,
2983
+ QAR: 2,
2984
+ EGP: 2,
2985
+ // 0-decimal
2986
+ JPY: 0,
2987
+ KRW: 0,
2988
+ ISK: 0,
2989
+ CLP: 0,
2990
+ VND: 0,
2991
+ XOF: 0,
2992
+ XAF: 0,
2993
+ PYG: 0,
2994
+ // 3-decimal
2995
+ BHD: 3,
2996
+ KWD: 3,
2997
+ OMR: 3,
2998
+ TND: 3,
2999
+ JOD: 3,
3000
+ IQD: 3,
3001
+ LYD: 3
3002
+ };
3003
+ }
3004
+ });
3005
+
3006
+ // src/money/descriptor.ts
3007
+ function isMultiOptions(o) {
3008
+ return "currencies" in o;
2615
3009
  }
2616
3010
  function money(options) {
2617
3011
  const hasFixed = "currency" in options;
@@ -3401,6 +3795,7 @@ var init_groupby = __esm({
3401
3795
  init_canonical_key();
3402
3796
  init_errors();
3403
3797
  init_money_reducer();
3798
+ init_core();
3404
3799
  GROUPBY_WARN_CARDINALITY = 1e4;
3405
3800
  GROUPBY_MAX_CARDINALITY = 1e5;
3406
3801
  warnedCardinalityFields = /* @__PURE__ */ new Set();
@@ -3469,9 +3864,29 @@ var init_groupby = __esm({
3469
3864
  upstreams;
3470
3865
  dictLabelResolver;
3471
3866
  fields;
3472
- /** Execute the query, group, reduce, and return an array of rows. */
3473
- run() {
3474
- return groupAndReduce(this.executeRecords(), this.fields, this.spec);
3867
+ /**
3868
+ * Execute the query, group, reduce, and return an array of rows.
3869
+ *
3870
+ * `opts` (#285 query-form MV grouping): when a `locale` + `i18nFields` are
3871
+ * given, the declared group-key `i18nText` fields are resolved to that locale
3872
+ * at the `mv` layer BEFORE bucketing — so an i18n group key is a stable string
3873
+ * instead of a raw `{locale}` map. The MV executor passes the MV's
3874
+ * `i18nLocale`/`i18nFields`; ordinary `.run()` callers pass nothing and are
3875
+ * unaffected.
3876
+ */
3877
+ run(opts) {
3878
+ let records = this.executeRecords();
3879
+ if (opts?.locale !== void 0 && opts.i18nFields !== void 0) {
3880
+ const groupI18n = {};
3881
+ for (const f of this.fields) {
3882
+ const d = opts.i18nFields[f];
3883
+ if (d !== void 0) groupI18n[f] = d;
3884
+ }
3885
+ if (Object.keys(groupI18n).length > 0) {
3886
+ records = records.map((r) => applyI18nLocale(r, groupI18n, opts.locale, void 0, "mv"));
3887
+ }
3888
+ }
3889
+ return groupAndReduce(records, this.fields, this.spec);
3475
3890
  }
3476
3891
  /**
3477
3892
  * Execute the query, group, reduce, and resolve `<field>Label` for
@@ -4056,12 +4471,13 @@ var executor_exports3 = {};
4056
4471
  __export(executor_exports3, {
4057
4472
  MaterializedViewExecutor: () => MaterializedViewExecutor
4058
4473
  });
4059
- async function materializeQueryResult(q, mvName) {
4474
+ async function materializeQueryResult(q, mvName, i18nLocale, i18nFields) {
4060
4475
  if (typeof q?.toArray === "function") {
4061
4476
  return await q.toArray();
4062
4477
  }
4063
4478
  if (typeof q?.run === "function") {
4064
- const result = await Promise.resolve(q.run());
4479
+ const runOpts = i18nLocale !== void 0 ? { locale: i18nLocale, i18nFields } : void 0;
4480
+ const result = await Promise.resolve(q.run(runOpts));
4065
4481
  if (Array.isArray(result)) {
4066
4482
  return result;
4067
4483
  }
@@ -4090,6 +4506,29 @@ async function materializeUnionResult(spec, db) {
4090
4506
  }
4091
4507
  if (!spec.groupBy) return unified;
4092
4508
  const groupFields = typeof spec.groupBy === "string" ? [spec.groupBy] : spec.groupBy;
4509
+ if (spec.i18nLocale !== void 0 && spec.i18nFields !== void 0) {
4510
+ const groupI18n = {};
4511
+ for (const f of groupFields) {
4512
+ const d = spec.i18nFields[f];
4513
+ if (d !== void 0) groupI18n[f] = d;
4514
+ }
4515
+ if (Object.keys(groupI18n).length > 0) {
4516
+ for (let i = 0; i < unified.length; i++) {
4517
+ unified[i] = applyI18nLocale(unified[i], groupI18n, spec.i18nLocale, void 0, "mv");
4518
+ }
4519
+ }
4520
+ }
4521
+ for (const f of groupFields) {
4522
+ for (const row of unified) {
4523
+ const v = row[f];
4524
+ if (v !== null && typeof v === "object") {
4525
+ throw new LocaleNotSpecifiedError(
4526
+ f,
4527
+ `Materialized view "${spec.name}" groups by "${f}", whose value is a raw i18n locale map \u2014 an unstable object group key. Declare { i18nLocale, i18nFields } on the MV to resolve it at the 'mv' layer, or group by a dictKey/staticDict code (the stable key) and resolve the label at read time.`
4528
+ );
4529
+ }
4530
+ }
4531
+ }
4093
4532
  if (!spec.aggregate) {
4094
4533
  const seen = /* @__PURE__ */ new Map();
4095
4534
  for (const row of unified) {
@@ -4121,6 +4560,7 @@ var init_executor3 = __esm({
4121
4560
  init_registry();
4122
4561
  init_groupby();
4123
4562
  init_canonical_key();
4563
+ init_core();
4124
4564
  DEFAULT_MAX_ROWS = 1e5;
4125
4565
  MaterializedViewExecutor = {
4126
4566
  async refresh(reg, accessor) {
@@ -4136,7 +4576,7 @@ var init_executor3 = __esm({
4136
4576
  rows = await materializeUnionResult(spec, ctxForQuery);
4137
4577
  } else {
4138
4578
  const q = spec.query(ctxForQuery);
4139
- rows = await materializeQueryResult(q, spec.name);
4579
+ rows = await materializeQueryResult(q, spec.name, spec.i18nLocale, spec.i18nFields);
4140
4580
  }
4141
4581
  if (rows.length > maxRows) {
4142
4582
  throw new MaterializedViewTooLargeError(spec.name, rows.length, maxRows);
@@ -4637,14 +5077,17 @@ var init_read_only_facade = __esm({
4637
5077
  "use strict";
4638
5078
  ReadOnlyVaultFacade = class {
4639
5079
  _vault;
4640
- constructor(vault) {
5080
+ _layer;
5081
+ constructor(vault, layer = "read") {
4641
5082
  this._vault = vault;
5083
+ this._layer = layer;
4642
5084
  }
4643
5085
  collection(name) {
4644
5086
  const c = this._vault.collection(name);
5087
+ const layer = this._layer;
4645
5088
  return {
4646
- get: (id) => c.get(id),
4647
- list: () => c.list(),
5089
+ get: (id) => c.get(id, { _layer: layer }),
5090
+ list: () => c.list({ _layer: layer }),
4648
5091
  query: () => c.query()
4649
5092
  };
4650
5093
  }
@@ -4700,6 +5143,16 @@ var init_registry3 = __esm({
4700
5143
  if (fromExtra) fromExtra.push(reg);
4701
5144
  else this._bySource.set(extra, [reg]);
4702
5145
  }
5146
+ for (const t of spec.triggerBy ?? []) {
5147
+ const fromTrigger = this._bySource.get(t.collection);
5148
+ if (fromTrigger) fromTrigger.push(reg);
5149
+ else this._bySource.set(t.collection, [reg]);
5150
+ }
5151
+ if (spec.rollup) {
5152
+ const fromRollup = this._bySource.get(spec.rollup.from);
5153
+ if (fromRollup) fromRollup.push(reg);
5154
+ else this._bySource.set(spec.rollup.from, [reg]);
5155
+ }
4703
5156
  for (const key of outputKeys) {
4704
5157
  const output = spec.outputs[key];
4705
5158
  if (!output) continue;
@@ -4753,6 +5206,9 @@ var init_registry3 = __esm({
4753
5206
  for (const key of Object.keys(s.spec.outputs)) {
4754
5207
  const output = s.spec.outputs[key];
4755
5208
  if (!output) continue;
5209
+ if (output.shape === "record" && output.collection === s.spec.source && output.denorm !== void 0) {
5210
+ continue;
5211
+ }
4756
5212
  visit(output.collection);
4757
5213
  }
4758
5214
  }
@@ -4929,6 +5385,177 @@ var init_delegation = __esm({
4929
5385
  }
4930
5386
  });
4931
5387
 
5388
+ // src/federation/schema-manifest.ts
5389
+ function captureBlueprint(configure) {
5390
+ const recorded = [];
5391
+ const collectionStub = new Proxy(
5392
+ {},
5393
+ {
5394
+ get: () => () => collectionStub
5395
+ }
5396
+ );
5397
+ const proxy = new Proxy(
5398
+ {},
5399
+ {
5400
+ get: (_t, prop) => {
5401
+ if (prop === "collection") {
5402
+ return (name, opts) => {
5403
+ recorded.push({
5404
+ name,
5405
+ indexes: opts?.indexes ?? [],
5406
+ persistJsonSchema: !!opts?.persistJsonSchema
5407
+ });
5408
+ return collectionStub;
5409
+ };
5410
+ }
5411
+ return () => proxy;
5412
+ }
5413
+ }
5414
+ );
5415
+ configure(proxy);
5416
+ const sorted = [...recorded].sort((a, b) => a.name.localeCompare(b.name));
5417
+ const indexes = {};
5418
+ const persistJsonSchema = [];
5419
+ for (const c of sorted) {
5420
+ indexes[c.name] = c.indexes;
5421
+ if (c.persistJsonSchema) persistJsonSchema.push(c.name);
5422
+ }
5423
+ return {
5424
+ // `persistJsonSchema` is already name-sorted: it is populated while
5425
+ // iterating `sorted` (collections in name order).
5426
+ collections: sorted.map((c) => c.name),
5427
+ indexes,
5428
+ persistJsonSchema
5429
+ };
5430
+ }
5431
+ function canonical(value) {
5432
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
5433
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
5434
+ const obj = value;
5435
+ const keys = Object.keys(obj).sort();
5436
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonical(obj[k])}`).join(",")}}`;
5437
+ }
5438
+ async function fingerprintBlueprint(bp) {
5439
+ return sha256Hex(new TextEncoder().encode(canonical(bp)));
5440
+ }
5441
+ var init_schema_manifest = __esm({
5442
+ "src/federation/schema-manifest.ts"() {
5443
+ "use strict";
5444
+ init_crypto();
5445
+ }
5446
+ });
5447
+
5448
+ // src/federation/state-vault.ts
5449
+ var state_vault_exports = {};
5450
+ __export(state_vault_exports, {
5451
+ STATE_VAULT_NAME: () => STATE_VAULT_NAME,
5452
+ StateManagementVault: () => StateManagementVault
5453
+ });
5454
+ var REGISTRY, MANIFEST, EVENTS, MIGRATION_STATUS, StateManagementVault;
5455
+ var init_state_vault = __esm({
5456
+ "src/federation/state-vault.ts"() {
5457
+ "use strict";
5458
+ init_schema_manifest();
5459
+ init_constants();
5460
+ init_ulid();
5461
+ init_constants();
5462
+ REGISTRY = "vaultRegistry";
5463
+ MANIFEST = "schemaManifest";
5464
+ EVENTS = "deploymentEvents";
5465
+ MIGRATION_STATUS = "migrationStatus";
5466
+ StateManagementVault = class _StateManagementVault {
5467
+ constructor(registry, schemaManifest, events, migrationStatus) {
5468
+ this.registry = registry;
5469
+ this.schemaManifest = schemaManifest;
5470
+ this.#events = events;
5471
+ this.#migrationStatus = migrationStatus;
5472
+ }
5473
+ registry;
5474
+ schemaManifest;
5475
+ /**
5476
+ * The append-only deployment-events log is kept truly private so the raw
5477
+ * mutable Collection is never surfaced — events may only be written via
5478
+ * `appendEvent` and read via `queryEvents`. (`registry` and
5479
+ * `schemaManifest` are deliberately public: consumers read and write them.)
5480
+ */
5481
+ #events;
5482
+ /** Per-shard fleet-migration progress (#271). Surfaced via typed methods only. */
5483
+ #migrationStatus;
5484
+ /** Idempotently open the reserved state vault and bind the control-plane collections. */
5485
+ static async open(db) {
5486
+ const vault = await db.openVault(STATE_VAULT_NAME);
5487
+ return new _StateManagementVault(
5488
+ vault.collection(REGISTRY),
5489
+ vault.collection(MANIFEST),
5490
+ vault.collection(EVENTS),
5491
+ vault.collection(MIGRATION_STATUS)
5492
+ );
5493
+ }
5494
+ /** Read one shard's migration status (or null). */
5495
+ async getMigrationStatus(vaultId) {
5496
+ return this.#migrationStatus.get(vaultId);
5497
+ }
5498
+ /** All migration-status rows (hydrates first). */
5499
+ async listMigrationStatus() {
5500
+ await this.#migrationStatus.list();
5501
+ return this.#migrationStatus.query().toArray();
5502
+ }
5503
+ /** Upsert one shard's migration status (keyed by vaultId). */
5504
+ async upsertMigrationStatus(row) {
5505
+ await this.#migrationStatus.put(row.vaultId, row);
5506
+ }
5507
+ /** Read-only query over the append-only deployment-events log. */
5508
+ queryEvents() {
5509
+ return this.#events.query();
5510
+ }
5511
+ /**
5512
+ * Append a deployment event with a fresh unique (ULID) id. This is the
5513
+ * only write path to the events log; no update/delete is exposed.
5514
+ * Callers should treat failures as non-fatal — this method does not
5515
+ * swallow errors, so wrap the call site in try/catch where appropriate.
5516
+ */
5517
+ async appendEvent(event) {
5518
+ const ts = event.ts ?? Date.now();
5519
+ const id = generateULID();
5520
+ await this.#events.put(id, { ...event, id, ts });
5521
+ }
5522
+ /**
5523
+ * Ensure a manifest row exists for `(templateName, template.version)`.
5524
+ * Safe to call repeatedly: the `fingerprint` is a deterministic hash of
5525
+ * the template's declared shape (stable across calls), though each call
5526
+ * refreshes `recordedAt`.
5527
+ */
5528
+ async recordManifest(templateName, template) {
5529
+ const bp = captureBlueprint(template.configure);
5530
+ const fingerprint = await fingerprintBlueprint(bp);
5531
+ await this.schemaManifest.put(`${templateName}:${template.version}`, {
5532
+ templateName,
5533
+ version: template.version,
5534
+ collections: bp.collections,
5535
+ indexes: bp.indexes,
5536
+ persistJsonSchema: bp.persistJsonSchema,
5537
+ fingerprint,
5538
+ recordedAt: Date.now()
5539
+ });
5540
+ return fingerprint;
5541
+ }
5542
+ /**
5543
+ * True when `template`'s current declared shape does not match the recorded
5544
+ * manifest for `(templateName, template.version)`. Because shards carry no
5545
+ * schema state independent of their template, this catches "a template's
5546
+ * shape changed without bumping `version`" — not independent per-shard drift.
5547
+ * A missing manifest is treated as drift (nothing to verify against).
5548
+ */
5549
+ async detectDrift(templateName, template) {
5550
+ const row = await this.schemaManifest.get(`${templateName}:${template.version}`);
5551
+ if (!row) return true;
5552
+ const current = await fingerprintBlueprint(captureBlueprint(template.configure));
5553
+ return current !== row.fingerprint;
5554
+ }
5555
+ };
5556
+ }
5557
+ });
5558
+
4932
5559
  // src/federation/classify-skip.ts
4933
5560
  function classifyShardSkip(err) {
4934
5561
  return err instanceof NoAccessError ? "no-grant" : "error";
@@ -5213,6 +5840,7 @@ var SHARD_SEPARATOR, SAFE_PARTITION_KEY, VaultGroup, ShardedCollection, ShardedQ
5213
5840
  var init_vault_group = __esm({
5214
5841
  "src/federation/vault-group.ts"() {
5215
5842
  "use strict";
5843
+ init_state_vault();
5216
5844
  init_errors();
5217
5845
  init_constants();
5218
5846
  init_classify_skip();
@@ -5222,12 +5850,13 @@ var init_vault_group = __esm({
5222
5850
  SHARD_SEPARATOR = "--";
5223
5851
  SAFE_PARTITION_KEY = /^[A-Za-z0-9._-]+$/;
5224
5852
  VaultGroup = class {
5225
- constructor(db, name, registry, sharding, template) {
5853
+ constructor(db, name, registry, sharding, template, migrateOnOpen = false) {
5226
5854
  this.db = db;
5227
5855
  this.name = name;
5228
5856
  this.registry = registry;
5229
5857
  this.sharding = sharding;
5230
5858
  this.template = template;
5859
+ this.migrateOnOpen = migrateOnOpen;
5231
5860
  if (name.includes(SHARD_SEPARATOR)) {
5232
5861
  throw new ValidationError(
5233
5862
  `VaultGroup name "${name}" must not contain "--" (reserved shard vault-id separator).`
@@ -5239,6 +5868,7 @@ var init_vault_group = __esm({
5239
5868
  registry;
5240
5869
  sharding;
5241
5870
  template;
5871
+ migrateOnOpen;
5242
5872
  /** @internal — set when the group is managed (no explicit registry). */
5243
5873
  stateVault;
5244
5874
  /** @internal */
@@ -5272,8 +5902,22 @@ var init_vault_group = __esm({
5272
5902
  const rows = this.registry.query().toArray();
5273
5903
  return rows.filter((r) => r.group === this.name);
5274
5904
  }
5275
- /** Open an existing shard and apply the template. */
5905
+ /**
5906
+ * Open an existing shard and apply the template. When `migrateOnOpen` is set
5907
+ * (#271) and the shard's registry version is behind the template, its cutover
5908
+ * runs inline first — so a behind shard never surfaces a stale handle.
5909
+ */
5276
5910
  async openShard(partitionKey) {
5911
+ if (this.migrateOnOpen) {
5912
+ const row = await this.registry.get(this.registryId(partitionKey));
5913
+ if (row && row.schemaVersion < this.template.version) {
5914
+ await this.migrateShard(partitionKey);
5915
+ }
5916
+ }
5917
+ return this._openShardRaw(partitionKey);
5918
+ }
5919
+ /** @internal — open + configure with no migrate-on-open hook (used by the migration path itself to avoid recursion). */
5920
+ async _openShardRaw(partitionKey) {
5277
5921
  const vault = await this.db.openVault(this.shardVaultId(partitionKey), { create: false });
5278
5922
  this.template.configure(vault);
5279
5923
  return vault;
@@ -5285,13 +5929,21 @@ var init_vault_group = __esm({
5285
5929
  * - row + vault present → no-op, return handle
5286
5930
  * - row present, vault gone → ShardProvisioningError
5287
5931
  * - row absent (vault present or not) → open-or-create, configure, write row
5932
+ *
5933
+ * When `region` is given (the routing `put` passes `sharding.regionOf(record)`),
5934
+ * the candidate backend's `capabilities.region` must match or this throws
5935
+ * `DataResidencyError` BEFORE provisioning (#271 data-residency guard).
5288
5936
  */
5289
- async createShard(partitionKey) {
5937
+ async createShard(partitionKey, region) {
5290
5938
  const vaultId = this.shardVaultId(partitionKey);
5291
5939
  const row = await this.registry.get(this.registryId(partitionKey));
5292
5940
  const provisioned = await this.db._shardVaultProvisioned(vaultId);
5293
5941
  if (row && !provisioned) throw new ShardProvisioningError(vaultId, partitionKey);
5294
5942
  if (row && provisioned) return this.openShard(partitionKey);
5943
+ if (region !== void 0) {
5944
+ const backendRegion = this.db._resolveBackend(vaultId).capabilities?.region;
5945
+ if (backendRegion !== region) throw new DataResidencyError(vaultId, region, backendRegion);
5946
+ }
5295
5947
  const vault = await this.db.openVault(vaultId);
5296
5948
  this.template.configure(vault);
5297
5949
  await this.registry.put(this.registryId(partitionKey), {
@@ -5336,20 +5988,186 @@ var init_vault_group = __esm({
5336
5988
  /** @internal — eligible (openable-candidate) rows + drift/divergence skips. */
5337
5989
  async resolveEligible(options = {}) {
5338
5990
  const rows = await this.allRows();
5339
- const skipped = [];
5340
- const versionOk = [];
5341
- for (const row of rows) {
5342
- if (options.minVersion !== void 0 && row.schemaVersion < options.minVersion) {
5343
- skipped.push({ vaultId: row.vaultId, reason: "schema-drift" });
5344
- } else versionOk.push(row);
5991
+ const skipped = [];
5992
+ const versionOk = [];
5993
+ for (const row of rows) {
5994
+ if (options.minVersion !== void 0 && row.schemaVersion < options.minVersion) {
5995
+ skipped.push({ vaultId: row.vaultId, reason: "schema-drift" });
5996
+ } else versionOk.push(row);
5997
+ }
5998
+ const provisioned = await Promise.all(versionOk.map((r) => this.db._shardVaultProvisioned(r.vaultId)));
5999
+ const eligible = [];
6000
+ versionOk.forEach((row, i) => {
6001
+ if (provisioned[i]) eligible.push(row);
6002
+ else skipped.push({ vaultId: row.vaultId, reason: "error", error: new ShardProvisioningError(row.vaultId, row.partitionKey) });
6003
+ });
6004
+ return { eligible, skipped };
6005
+ }
6006
+ /** @internal — registered push-model cross-vault derivations (#271 Insight Vault). */
6007
+ crossVaultDerivations = [];
6008
+ /**
6009
+ * Register a push-model cross-vault derivation — the Insight Vault pattern
6010
+ * (#271, Layer 4). Drive it with {@link refreshInsights}.
6011
+ *
6012
+ * For each shard, `derive(records, ctx)` runs on that shard's `source`
6013
+ * records and its return value is written into the analytics
6014
+ * (`target.vault` / `target.collection`) vault, keyed by partition key —
6015
+ * one summary row per shard. The derivation runs in-process under THIS
6016
+ * group's `Noydb` (which already holds both the shard and Insight Vault
6017
+ * keyrings); the shard's decrypted records are reduced to a summary that is
6018
+ * re-encrypted under the Insight Vault's own DEK, so no shard ciphertext
6019
+ * crosses a DEK boundary.
6020
+ *
6021
+ * **Zero-knowledge note:** the Insight Vault backend sees aggregated
6022
+ * structure (totals, counts, timestamps) drawn from many shards — a weaker
6023
+ * ZK profile than the per-shard vaults. Opt-in; keep summaries to aggregate
6024
+ * scalars (no embeddings / no raw records).
6025
+ *
6026
+ * v1 is explicit-refresh (no write-path push); call `refreshInsights()`
6027
+ * after a batch of writes, or on a schedule.
6028
+ *
6029
+ * The `target.vault` must NOT be the group itself or one of its shards —
6030
+ * a summary writing back into client-shard data would breach the Insight
6031
+ * Vault's separate-DEK-boundary contract. Such a target throws a
6032
+ * `ValidationError` at registration (#271 Insight-write isolation).
6033
+ */
6034
+ withCrossVaultDerivation(spec) {
6035
+ const target = spec.target.vault;
6036
+ if (target === this.name || target.startsWith(`${this.name}${SHARD_SEPARATOR}`)) {
6037
+ throw new ValidationError(
6038
+ `withCrossVaultDerivation: target.vault "${target}" is the "${this.name}" group itself or one of its shards \u2014 an Insight summary must target a SEPARATE analytics vault, never write back into client-shard data (it would breach the per-shard DEK boundary). Use a distinct vault name.`
6039
+ );
6040
+ }
6041
+ this.crossVaultDerivations.push(spec);
6042
+ }
6043
+ /**
6044
+ * Run every registered {@link withCrossVaultDerivation}: read each eligible
6045
+ * shard's source records, derive a per-shard summary, and write it into the
6046
+ * Insight Vault keyed by partition key. Shards behind `minVersion`,
6047
+ * unprovisioned, or whose read errors are reported in `skippedVaults` and
6048
+ * are not written (a stale summary is never left behind for a failed shard).
6049
+ */
6050
+ async refreshInsights(options = {}) {
6051
+ if (this.crossVaultDerivations.length === 0) return { written: 0, skippedVaults: [] };
6052
+ const { eligible, skipped } = await this.resolveEligible(
6053
+ options.minVersion !== void 0 ? { minVersion: options.minVersion } : {}
6054
+ );
6055
+ let written = 0;
6056
+ for (const spec of this.crossVaultDerivations) {
6057
+ const results = await this.db.queryAcross(
6058
+ eligible.map((r) => r.vaultId),
6059
+ async (vault) => {
6060
+ this.template.configure(vault);
6061
+ return vault.collection(spec.source).list();
6062
+ },
6063
+ { create: false, ...options.concurrency !== void 0 ? { concurrency: options.concurrency } : {} }
6064
+ );
6065
+ const insight = await this.db.openVault(spec.target.vault);
6066
+ const out = insight.collection(spec.target.collection);
6067
+ for (let i = 0; i < eligible.length; i++) {
6068
+ const row = eligible[i];
6069
+ const res = results[i];
6070
+ if (!res || res.result === void 0) {
6071
+ skipped.push({ vaultId: row.vaultId, reason: "error", ...res?.error ? { error: res.error } : {} });
6072
+ continue;
6073
+ }
6074
+ const ctx = {
6075
+ vaultId: row.vaultId,
6076
+ partitionKey: row.partitionKey,
6077
+ schemaVersion: row.schemaVersion
6078
+ };
6079
+ const summary = spec.derive(res.result, ctx);
6080
+ await out.put(row.partitionKey, summary);
6081
+ written++;
6082
+ }
6083
+ }
6084
+ return { written, skippedVaults: skipped };
6085
+ }
6086
+ /** @internal — the control-plane vault for migration status; lazily opened. */
6087
+ async ensureStateVault() {
6088
+ if (!this.stateVault) this.stateVault = await StateManagementVault.open(this.db);
6089
+ return this.stateVault;
6090
+ }
6091
+ /**
6092
+ * Migrate ONE shard to the template's current version (#271 fleet runner,
6093
+ * per-shard step). Opens the shard (applying the template, which arms the
6094
+ * M12 cutover), drains schema-write detection, runs `vault.runSchemaCutover()`
6095
+ * (the per-vault drain-barrier-transform protocol), then advances the
6096
+ * registry row's `schemaVersion` and records `migration-status`. A shard
6097
+ * already at the template version is a no-op (`status: 'done'`, migrated 0).
6098
+ * Never throws on a cutover failure — it records `status: 'failed'` and
6099
+ * returns the row, so a fleet run continues past a bad shard.
6100
+ */
6101
+ async migrateShard(partitionKey) {
6102
+ const vaultId = this.shardVaultId(partitionKey);
6103
+ const row = await this.registry.get(this.registryId(partitionKey));
6104
+ if (!row) throw new UnknownShardError(partitionKey, this.name);
6105
+ const target = this.template.version;
6106
+ const sv = await this.ensureStateVault();
6107
+ const base = { vaultId, group: this.name, currentVersion: row.schemaVersion, targetVersion: target };
6108
+ if (row.schemaVersion >= target) {
6109
+ const done = { ...base, status: "done", migrated: 0, finishedAt: Date.now() };
6110
+ await sv.upsertMigrationStatus(done);
6111
+ return done;
6112
+ }
6113
+ await sv.upsertMigrationStatus({ ...base, status: "running", startedAt: Date.now() });
6114
+ try {
6115
+ await sv.appendEvent({ type: "migration-started", group: this.name, vaultId, version: target });
6116
+ } catch {
6117
+ }
6118
+ try {
6119
+ const vault = await this._openShardRaw(partitionKey);
6120
+ await vault._drainPendingSchemaWrites();
6121
+ const { migrated } = await vault.runSchemaCutover();
6122
+ await this.registry.put(this.registryId(partitionKey), { ...row, schemaVersion: target });
6123
+ const done = { ...base, currentVersion: target, status: "done", migrated, finishedAt: Date.now() };
6124
+ await sv.upsertMigrationStatus(done);
6125
+ try {
6126
+ await sv.appendEvent({ type: "migration-completed", group: this.name, vaultId, version: target });
6127
+ } catch {
6128
+ }
6129
+ return done;
6130
+ } catch (err) {
6131
+ const error = err instanceof Error ? err.message : String(err);
6132
+ const failed = { ...base, status: "failed", error, finishedAt: Date.now() };
6133
+ await sv.upsertMigrationStatus(failed);
6134
+ try {
6135
+ await sv.appendEvent({ type: "migration-failed", group: this.name, vaultId, version: target, detail: error });
6136
+ } catch {
6137
+ }
6138
+ return failed;
6139
+ }
6140
+ }
6141
+ /**
6142
+ * Active batch runner (#271): migrate every shard behind the template version
6143
+ * to it, in controlled batches. **Resumable + crash-safe** — shards already at
6144
+ * the target are skipped (the registry version is the source of truth), so a
6145
+ * re-run after a crash only picks up the unfinished + previously-failed shards.
6146
+ *
6147
+ * - `cohort` — restrict to these partition keys (the staged / canary rollout:
6148
+ * migrate a small cohort, verify the Insight Vault, then run the rest).
6149
+ * - `batchSize` — max shards migrated concurrently per batch (back-pressure).
6150
+ * Default 4. Batches run sequentially; shards within a batch run in parallel.
6151
+ */
6152
+ async migrateFleet(options = {}) {
6153
+ const target = this.template.version;
6154
+ const rows = await this.allRows();
6155
+ const cohort = options.cohort;
6156
+ const todo = rows.filter(
6157
+ (r) => r.schemaVersion < target && (cohort === void 0 || cohort.includes(r.partitionKey))
6158
+ );
6159
+ const batchSize = Math.max(1, options.batchSize ?? 4);
6160
+ const migrated = [];
6161
+ const failed = [];
6162
+ for (let i = 0; i < todo.length; i += batchSize) {
6163
+ const batch = todo.slice(i, i + batchSize);
6164
+ const settled = await Promise.all(batch.map((r) => this.migrateShard(r.partitionKey)));
6165
+ for (const res of settled) {
6166
+ if (res.status === "done") migrated.push(res.vaultId);
6167
+ else failed.push({ vaultId: res.vaultId, error: res.error ?? "unknown" });
6168
+ }
5345
6169
  }
5346
- const provisioned = await Promise.all(versionOk.map((r) => this.db._shardVaultProvisioned(r.vaultId)));
5347
- const eligible = [];
5348
- versionOk.forEach((row, i) => {
5349
- if (provisioned[i]) eligible.push(row);
5350
- else skipped.push({ vaultId: row.vaultId, reason: "error", error: new ShardProvisioningError(row.vaultId, row.partitionKey) });
5351
- });
5352
- return { eligible, skipped };
6170
+ return { target, migrated, failed };
5353
6171
  }
5354
6172
  };
5355
6173
  ShardedCollection = class {
@@ -5368,7 +6186,7 @@ var init_vault_group = __esm({
5368
6186
  if (this.group.sharding.autoCreate === false) {
5369
6187
  throw new UnknownShardError(key, this.group.name);
5370
6188
  }
5371
- vault = await this.group.createShard(key);
6189
+ vault = await this.group.createShard(key, this.group.sharding.regionOf?.(record));
5372
6190
  } else {
5373
6191
  vault = await this.group.openShard(key);
5374
6192
  }
@@ -5558,159 +6376,6 @@ var init_vault_group = __esm({
5558
6376
  }
5559
6377
  });
5560
6378
 
5561
- // src/federation/schema-manifest.ts
5562
- function captureBlueprint(configure) {
5563
- const recorded = [];
5564
- const collectionStub = new Proxy(
5565
- {},
5566
- {
5567
- get: () => () => collectionStub
5568
- }
5569
- );
5570
- const proxy = new Proxy(
5571
- {},
5572
- {
5573
- get: (_t, prop) => {
5574
- if (prop === "collection") {
5575
- return (name, opts) => {
5576
- recorded.push({
5577
- name,
5578
- indexes: opts?.indexes ?? [],
5579
- persistJsonSchema: !!opts?.persistJsonSchema
5580
- });
5581
- return collectionStub;
5582
- };
5583
- }
5584
- return () => proxy;
5585
- }
5586
- }
5587
- );
5588
- configure(proxy);
5589
- const sorted = [...recorded].sort((a, b) => a.name.localeCompare(b.name));
5590
- const indexes = {};
5591
- const persistJsonSchema = [];
5592
- for (const c of sorted) {
5593
- indexes[c.name] = c.indexes;
5594
- if (c.persistJsonSchema) persistJsonSchema.push(c.name);
5595
- }
5596
- return {
5597
- // `persistJsonSchema` is already name-sorted: it is populated while
5598
- // iterating `sorted` (collections in name order).
5599
- collections: sorted.map((c) => c.name),
5600
- indexes,
5601
- persistJsonSchema
5602
- };
5603
- }
5604
- function canonical(value) {
5605
- if (value === null || typeof value !== "object") return JSON.stringify(value);
5606
- if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
5607
- const obj = value;
5608
- const keys = Object.keys(obj).sort();
5609
- return `{${keys.map((k) => `${JSON.stringify(k)}:${canonical(obj[k])}`).join(",")}}`;
5610
- }
5611
- async function fingerprintBlueprint(bp) {
5612
- return sha256Hex(new TextEncoder().encode(canonical(bp)));
5613
- }
5614
- var init_schema_manifest = __esm({
5615
- "src/federation/schema-manifest.ts"() {
5616
- "use strict";
5617
- init_crypto();
5618
- }
5619
- });
5620
-
5621
- // src/federation/state-vault.ts
5622
- var state_vault_exports = {};
5623
- __export(state_vault_exports, {
5624
- STATE_VAULT_NAME: () => STATE_VAULT_NAME,
5625
- StateManagementVault: () => StateManagementVault
5626
- });
5627
- var REGISTRY, MANIFEST, EVENTS, StateManagementVault;
5628
- var init_state_vault = __esm({
5629
- "src/federation/state-vault.ts"() {
5630
- "use strict";
5631
- init_schema_manifest();
5632
- init_constants();
5633
- init_ulid();
5634
- init_constants();
5635
- REGISTRY = "vaultRegistry";
5636
- MANIFEST = "schemaManifest";
5637
- EVENTS = "deploymentEvents";
5638
- StateManagementVault = class _StateManagementVault {
5639
- constructor(registry, schemaManifest, events) {
5640
- this.registry = registry;
5641
- this.schemaManifest = schemaManifest;
5642
- this.#events = events;
5643
- }
5644
- registry;
5645
- schemaManifest;
5646
- /**
5647
- * The append-only deployment-events log is kept truly private so the raw
5648
- * mutable Collection is never surfaced — events may only be written via
5649
- * `appendEvent` and read via `queryEvents`. (`registry` and
5650
- * `schemaManifest` are deliberately public: consumers read and write them.)
5651
- */
5652
- #events;
5653
- /** Idempotently open the reserved state vault and bind the three control-plane collections. */
5654
- static async open(db) {
5655
- const vault = await db.openVault(STATE_VAULT_NAME);
5656
- return new _StateManagementVault(
5657
- vault.collection(REGISTRY),
5658
- vault.collection(MANIFEST),
5659
- vault.collection(EVENTS)
5660
- );
5661
- }
5662
- /** Read-only query over the append-only deployment-events log. */
5663
- queryEvents() {
5664
- return this.#events.query();
5665
- }
5666
- /**
5667
- * Append a deployment event with a fresh unique (ULID) id. This is the
5668
- * only write path to the events log; no update/delete is exposed.
5669
- * Callers should treat failures as non-fatal — this method does not
5670
- * swallow errors, so wrap the call site in try/catch where appropriate.
5671
- */
5672
- async appendEvent(event) {
5673
- const ts = event.ts ?? Date.now();
5674
- const id = generateULID();
5675
- await this.#events.put(id, { ...event, id, ts });
5676
- }
5677
- /**
5678
- * Ensure a manifest row exists for `(templateName, template.version)`.
5679
- * Safe to call repeatedly: the `fingerprint` is a deterministic hash of
5680
- * the template's declared shape (stable across calls), though each call
5681
- * refreshes `recordedAt`.
5682
- */
5683
- async recordManifest(templateName, template) {
5684
- const bp = captureBlueprint(template.configure);
5685
- const fingerprint = await fingerprintBlueprint(bp);
5686
- await this.schemaManifest.put(`${templateName}:${template.version}`, {
5687
- templateName,
5688
- version: template.version,
5689
- collections: bp.collections,
5690
- indexes: bp.indexes,
5691
- persistJsonSchema: bp.persistJsonSchema,
5692
- fingerprint,
5693
- recordedAt: Date.now()
5694
- });
5695
- return fingerprint;
5696
- }
5697
- /**
5698
- * True when `template`'s current declared shape does not match the recorded
5699
- * manifest for `(templateName, template.version)`. Because shards carry no
5700
- * schema state independent of their template, this catches "a template's
5701
- * shape changed without bumping `version`" — not independent per-shard drift.
5702
- * A missing manifest is treated as drift (nothing to verify against).
5703
- */
5704
- async detectDrift(templateName, template) {
5705
- const row = await this.schemaManifest.get(`${templateName}:${template.version}`);
5706
- if (!row) return true;
5707
- const current = await fingerprintBlueprint(captureBlueprint(template.configure));
5708
- return current !== row.fingerprint;
5709
- }
5710
- };
5711
- }
5712
- });
5713
-
5714
6379
  // src/index.ts
5715
6380
  var src_exports = {};
5716
6381
  __export(src_exports, {
@@ -5748,6 +6413,7 @@ __export(src_exports, {
5748
6413
  DICT_COLLECTION_PREFIX: () => DICT_COLLECTION_PREFIX,
5749
6414
  DIRECTORY_RECORD_ID: () => DIRECTORY_RECORD_ID,
5750
6415
  DanglingReferenceError: () => DanglingReferenceError,
6416
+ DataResidencyError: () => DataResidencyError,
5751
6417
  DecryptionError: () => DecryptionError,
5752
6418
  DelegationTargetMissingError: () => DelegationTargetMissingError,
5753
6419
  DerivationCapExceededError: () => DerivationCapExceededError,
@@ -5773,6 +6439,7 @@ __export(src_exports, {
5773
6439
  GroupedQuery: () => GroupedQuery,
5774
6440
  GroupedQueryN: () => GroupedQueryN,
5775
6441
  INDEXED_STORE_POLICY: () => INDEXED_STORE_POLICY,
6442
+ IllegalTransitionError: () => IllegalTransitionError,
5776
6443
  ImportCapabilityError: () => ImportCapabilityError,
5777
6444
  IndexRequiredError: () => IndexRequiredError,
5778
6445
  IndexWriteFailureError: () => IndexWriteFailureError,
@@ -5785,6 +6452,8 @@ __export(src_exports, {
5785
6452
  LEDGER_DELTAS_COLLECTION: () => LEDGER_DELTAS_COLLECTION,
5786
6453
  LedgerContentionError: () => LedgerContentionError,
5787
6454
  LedgerStore: () => LedgerStore,
6455
+ LinkEndpointError: () => LinkEndpointError,
6456
+ LinkIntegrityError: () => LinkIntegrityError,
5788
6457
  LocaleNotSpecifiedError: () => LocaleNotSpecifiedError,
5789
6458
  Lru: () => Lru,
5790
6459
  MAGIC_LINK_CONTENT_INFO_PREFIX: () => MAGIC_LINK_CONTENT_INFO_PREFIX,
@@ -5916,6 +6585,7 @@ __export(src_exports, {
5916
6585
  canonicalJson: () => canonicalJson,
5917
6586
  checkGate: () => checkGate,
5918
6587
  clearDevUnlock: () => clearDevUnlock,
6588
+ compileSequenceFormat: () => compileSequenceFormat,
5919
6589
  computePatch: () => computePatch,
5920
6590
  coordinatedCutover: () => coordinatedCutover,
5921
6591
  count: () => count,
@@ -5978,11 +6648,13 @@ __export(src_exports, {
5978
6648
  isDictKeyDescriptor: () => isDictKeyDescriptor,
5979
6649
  isDiscriminant: () => isDiscriminant,
5980
6650
  isI18nTextDescriptor: () => isI18nTextDescriptor,
6651
+ isLinkCollectionName: () => isLinkCollectionName,
5981
6652
  isMagicLinkGrantExpired: () => isMagicLinkGrantExpired,
5982
6653
  isMoneyDescriptor: () => isMoneyDescriptor,
5983
6654
  isMoneyString: () => isMoneyString,
5984
6655
  isPreCompressed: () => isPreCompressed,
5985
6656
  isPublicEnvelope: () => isPublicEnvelope,
6657
+ isRefArray: () => isRefArray,
5986
6658
  isSessionAlive: () => isSessionAlive,
5987
6659
  isStaticDictDescriptor: () => isStaticDictDescriptor,
5988
6660
  isULID: () => isULID,
@@ -6036,6 +6708,7 @@ __export(src_exports, {
6036
6708
  recoverUser: () => recoverUser,
6037
6709
  reduceRecords: () => reduceRecords,
6038
6710
  ref: () => ref,
6711
+ refArray: () => refArray,
6039
6712
  removeAuthenticator: () => removeAuthenticator,
6040
6713
  resetBrotliSupportCache: () => resetBrotliSupportCache,
6041
6714
  resetJoinWarnings: () => resetJoinWarnings,
@@ -6063,6 +6736,8 @@ __export(src_exports, {
6063
6736
  sha256Hex: () => sha256Hex3,
6064
6737
  staticDict: () => staticDict,
6065
6738
  sum: () => sum,
6739
+ tokenize: () => tokenize,
6740
+ transitionGuard: () => transitionGuard,
6066
6741
  unwrapDeksFromBlob: () => unwrapDeksFromBlob,
6067
6742
  unwrapDeksFromPaperEntry: () => unwrapDeksFromPaperEntry,
6068
6743
  unwrapDeksFromShamirEntry: () => unwrapDeksFromShamirEntry,
@@ -6086,6 +6761,7 @@ __export(src_exports, {
6086
6761
  withMetrics: () => withMetrics,
6087
6762
  withOverlayedView: () => withOverlayedView,
6088
6763
  withRetry: () => withRetry,
6764
+ withRollup: () => withRollup,
6089
6765
  wrapBundleStore: () => wrapBundleStore,
6090
6766
  wrapStore: () => wrapStore,
6091
6767
  writeMagicLinkGrant: () => writeMagicLinkGrant,
@@ -6459,11 +7135,8 @@ async function compressBytes(data) {
6459
7135
  if (typeof CompressionStream === "undefined") {
6460
7136
  return { bytes: data, algorithm: "none" };
6461
7137
  }
6462
- const cs = new CompressionStream("gzip");
6463
- const writer = cs.writable.getWriter();
6464
- await writer.write(data);
6465
- await writer.close();
6466
- const buf = await new Response(cs.readable).arrayBuffer();
7138
+ const piped = new Response(data).body.pipeThrough(new CompressionStream("gzip"));
7139
+ const buf = await new Response(piped).arrayBuffer();
6467
7140
  return { bytes: new Uint8Array(buf), algorithm: "gzip" };
6468
7141
  }
6469
7142
  async function decompressBytes(data) {
@@ -6472,11 +7145,8 @@ async function decompressBytes(data) {
6472
7145
  "[noy-db] DecompressionStream not available \u2014 cannot decompress blob chunk"
6473
7146
  );
6474
7147
  }
6475
- const ds = new DecompressionStream("gzip");
6476
- const writer = ds.writable.getWriter();
6477
- await writer.write(data);
6478
- await writer.close();
6479
- const buf = await new Response(ds.readable).arrayBuffer();
7148
+ const piped = new Response(data).body.pipeThrough(new DecompressionStream("gzip"));
7149
+ const buf = await new Response(piped).arrayBuffer();
6480
7150
  return new Uint8Array(buf);
6481
7151
  }
6482
7152
  function concatChunks(chunks) {
@@ -8008,6 +8678,14 @@ function routeStore(opts) {
8008
8678
  const q = {};
8009
8679
  for (const [k, v] of writeQueues) q[k] = v.writes.length;
8010
8680
  return { overrides: ov, suspended: [...suspended], queued: q };
8681
+ },
8682
+ resolveBackend(vaultId) {
8683
+ if (opts.vaultRoutes) {
8684
+ for (const [prefix, s] of Object.entries(opts.vaultRoutes)) {
8685
+ if (vaultId.startsWith(prefix)) return s;
8686
+ }
8687
+ }
8688
+ return primary;
8011
8689
  }
8012
8690
  };
8013
8691
  if (anyHas("listVaults")) {
@@ -8454,6 +9132,38 @@ init_crypto();
8454
9132
  init_errors();
8455
9133
  var SEQUENCE_COLLECTION = "_sequences";
8456
9134
  var MAX_NEXT_ATTEMPTS = 16;
9135
+ var SEQ_FORMAT_TOKEN = /\{([^{}]*)\}/g;
9136
+ var SEQ_PAD_TOKEN = /^seq:0(\d+)$/;
9137
+ var SEQ_PARTITION_TOKEN = /^partition\.(\d+)$/;
9138
+ function compileSequenceFormat(format, series, partition) {
9139
+ const parts = partition ?? [];
9140
+ for (const m of format.matchAll(SEQ_FORMAT_TOKEN)) {
9141
+ const token = m[1] ?? "";
9142
+ if (token === "seq") continue;
9143
+ if (SEQ_PAD_TOKEN.test(token)) continue;
9144
+ const partMatch = SEQ_PARTITION_TOKEN.exec(token);
9145
+ if (partMatch) {
9146
+ const idx = Number(partMatch[1]);
9147
+ if (idx >= parts.length) {
9148
+ throw new ValidationError(
9149
+ `sequence("${series}"): format token "{${token}}" references partition index ${idx}, but only ${parts.length} partition component(s) were supplied.`
9150
+ );
9151
+ }
9152
+ continue;
9153
+ }
9154
+ throw new ValidationError(
9155
+ `sequence("${series}"): format contains unknown token "{${token}}". Accepted tokens: {seq}, {seq:0N}, {partition.i}.`
9156
+ );
9157
+ }
9158
+ return (serial) => format.replace(SEQ_FORMAT_TOKEN, (full, token) => {
9159
+ if (token === "seq") return String(serial);
9160
+ const padMatch = SEQ_PAD_TOKEN.exec(token);
9161
+ if (padMatch) return String(serial).padStart(Number(padMatch[1]), "0");
9162
+ const partMatch = SEQ_PARTITION_TOKEN.exec(token);
9163
+ if (partMatch) return String(parts[Number(partMatch[1])]);
9164
+ return full;
9165
+ });
9166
+ }
8457
9167
  function resolveSequenceKey(series, opts) {
8458
9168
  const partition = opts?.partition;
8459
9169
  if (!partition || partition.length === 0) return series;
@@ -9476,15 +10186,15 @@ function isEquivalent(a, b) {
9476
10186
 
9477
10187
  // src/history/history.ts
9478
10188
  var HISTORY_COLLECTION = "_history";
9479
- function matchesPrefix(id, collection, recordId3) {
9480
- if (recordId3) {
9481
- return id.startsWith(`${collection}:${recordId3}:`);
10189
+ function matchesPrefix(id, collection, recordId4) {
10190
+ if (recordId4) {
10191
+ return id.startsWith(`${collection}:${recordId4}:`);
9482
10192
  }
9483
10193
  return id.startsWith(`${collection}:`);
9484
10194
  }
9485
- async function getHistory(adapter, vault, collection, recordId3, options) {
10195
+ async function getHistory(adapter, vault, collection, recordId4, options) {
9486
10196
  const allIds = await adapter.list(vault, HISTORY_COLLECTION);
9487
- const matchingIds = allIds.filter((id) => matchesPrefix(id, collection, recordId3)).sort().reverse();
10197
+ const matchingIds = allIds.filter((id) => matchesPrefix(id, collection, recordId4)).sort().reverse();
9488
10198
  const entries = [];
9489
10199
  for (const id of matchingIds) {
9490
10200
  const envelope = await adapter.get(vault, HISTORY_COLLECTION, id);
@@ -9737,6 +10447,9 @@ init_ledger();
9737
10447
 
9738
10448
  // src/refs.ts
9739
10449
  init_errors();
10450
+ function isRefArray(desc) {
10451
+ return desc.isArray === true;
10452
+ }
9740
10453
  var RefIntegrityError = class extends NoydbError {
9741
10454
  collection;
9742
10455
  id;
@@ -9773,6 +10486,17 @@ function ref(target, mode = "strict") {
9773
10486
  }
9774
10487
  return { target, mode };
9775
10488
  }
10489
+ function refArray(target, mode = "strict") {
10490
+ if (target.includes("/")) {
10491
+ throw new RefScopeError(target);
10492
+ }
10493
+ if (!target || target.startsWith("_")) {
10494
+ throw new Error(
10495
+ `refArray(): target collection name must be non-empty and cannot start with '_' (reserved for internal collections). Got "${target}".`
10496
+ );
10497
+ }
10498
+ return { target, mode, isArray: true };
10499
+ }
9776
10500
  var RefRegistry = class {
9777
10501
  outbound = /* @__PURE__ */ new Map();
9778
10502
  inbound = /* @__PURE__ */ new Map();
@@ -9797,7 +10521,7 @@ var RefRegistry = class {
9797
10521
  for (const k of existingKeys) {
9798
10522
  const a = existing[k];
9799
10523
  const b = refs[k];
9800
- if (!a || !b || a.target !== b.target || a.mode !== b.mode) {
10524
+ if (!a || !b || a.target !== b.target || a.mode !== b.mode || a.isArray !== b.isArray) {
9801
10525
  throw new Error(
9802
10526
  `RefRegistry: conflicting ref declarations for collection "${collection}" field "${k}"`
9803
10527
  );
@@ -9805,34 +10529,172 @@ var RefRegistry = class {
9805
10529
  }
9806
10530
  return;
9807
10531
  }
9808
- this.outbound.set(collection, { ...refs });
9809
- for (const [field, desc] of Object.entries(refs)) {
9810
- const list = this.inbound.get(desc.target) ?? [];
9811
- list.push({ collection, field, mode: desc.mode });
9812
- this.inbound.set(desc.target, list);
10532
+ this.outbound.set(collection, { ...refs });
10533
+ for (const [field, desc] of Object.entries(refs)) {
10534
+ const list = this.inbound.get(desc.target) ?? [];
10535
+ list.push({ collection, field, mode: desc.mode, ...desc.isArray ? { isArray: true } : {} });
10536
+ this.inbound.set(desc.target, list);
10537
+ }
10538
+ }
10539
+ /** Get the outbound refs declared by a collection (or `{}` if none). */
10540
+ getOutbound(collection) {
10541
+ return this.outbound.get(collection) ?? {};
10542
+ }
10543
+ /** Get the inbound refs that target a given collection (or `[]`). */
10544
+ getInbound(target) {
10545
+ return this.inbound.get(target) ?? [];
10546
+ }
10547
+ /**
10548
+ * Iterate every (collection → refs) pair that has at least one
10549
+ * declared reference. Used by `checkIntegrity` to walk the full
10550
+ * universe of outbound refs without needing to track collection
10551
+ * names elsewhere.
10552
+ */
10553
+ entries() {
10554
+ return [...this.outbound.entries()];
10555
+ }
10556
+ /** Clear the registry. Test-only escape hatch; never called from production code. */
10557
+ clear() {
10558
+ this.outbound.clear();
10559
+ this.inbound.clear();
10560
+ }
10561
+ };
10562
+
10563
+ // src/links/link-set.ts
10564
+ init_types();
10565
+ init_crypto();
10566
+ init_errors();
10567
+ var LINK_COLLECTION_PREFIX = "_links_";
10568
+ function linkCollectionName(name) {
10569
+ return `${LINK_COLLECTION_PREFIX}${name}`;
10570
+ }
10571
+ function isLinkCollectionName(name) {
10572
+ return name.startsWith(LINK_COLLECTION_PREFIX);
10573
+ }
10574
+ function linkRowKey(aId, bId) {
10575
+ return `${encodeURIComponent(aId)}|${encodeURIComponent(bId)}`;
10576
+ }
10577
+ var LinkSet = class {
10578
+ constructor(adapter, vault, name, spec, encrypted, getDEK, actor, emitter, endpointExists) {
10579
+ this.adapter = adapter;
10580
+ this.vault = vault;
10581
+ this.name = name;
10582
+ this.spec = spec;
10583
+ this.encrypted = encrypted;
10584
+ this.getDEK = getDEK;
10585
+ this.actor = actor;
10586
+ this.emitter = emitter;
10587
+ this.endpointExists = endpointExists;
10588
+ this.collName = linkCollectionName(name);
10589
+ }
10590
+ adapter;
10591
+ vault;
10592
+ name;
10593
+ spec;
10594
+ encrypted;
10595
+ getDEK;
10596
+ actor;
10597
+ emitter;
10598
+ endpointExists;
10599
+ collName;
10600
+ dekPromise = null;
10601
+ dek() {
10602
+ if (!this.dekPromise) this.dekPromise = this.getDEK(this.collName);
10603
+ return this.dekPromise;
10604
+ }
10605
+ async encryptEntry(entry, version) {
10606
+ const json = JSON.stringify(entry);
10607
+ const base = { _noydb: NOYDB_FORMAT_VERSION, _v: version, _ts: (/* @__PURE__ */ new Date()).toISOString(), _by: this.actor };
10608
+ if (!this.encrypted) return { ...base, _iv: "", _data: json };
10609
+ const { iv, data } = await encrypt(json, await this.dek());
10610
+ return { ...base, _iv: iv, _data: data };
10611
+ }
10612
+ async decryptEntry(env) {
10613
+ const json = this.encrypted ? await decrypt(env._iv, env._data, await this.dek()) : env._data;
10614
+ return JSON.parse(json);
10615
+ }
10616
+ async connect(aId, bId, meta) {
10617
+ if (!await this.endpointExists(this.spec.a, aId)) {
10618
+ throw new LinkEndpointError(this.name, this.spec.a, aId);
10619
+ }
10620
+ if (!await this.endpointExists(this.spec.b, bId)) {
10621
+ throw new LinkEndpointError(this.name, this.spec.b, bId);
10622
+ }
10623
+ const key = linkRowKey(aId, bId);
10624
+ const entry = meta !== void 0 ? { a: aId, b: bId, meta } : { a: aId, b: bId };
10625
+ const existing = await this.adapter.get(this.vault, this.collName, key);
10626
+ const env = await this.encryptEntry(entry, (existing?._v ?? 0) + 1);
10627
+ await this.adapter.put(this.vault, this.collName, key, env, existing?._v);
10628
+ this.emitter.emit("change", { vault: this.vault, collection: this.collName, id: key, action: "put" });
10629
+ }
10630
+ async disconnect(aId, bId) {
10631
+ const key = linkRowKey(aId, bId);
10632
+ const existing = await this.adapter.get(this.vault, this.collName, key);
10633
+ if (!existing) return;
10634
+ await this.adapter.delete(this.vault, this.collName, key);
10635
+ this.emitter.emit("change", { vault: this.vault, collection: this.collName, id: key, action: "delete" });
10636
+ }
10637
+ async has(aId, bId) {
10638
+ return await this.adapter.get(this.vault, this.collName, linkRowKey(aId, bId)) !== null;
10639
+ }
10640
+ async of(id) {
10641
+ const rows = await this.list();
10642
+ return rows.filter((r) => r.a === id || r.b === id);
10643
+ }
10644
+ async list() {
10645
+ const keys = await this.adapter.list(this.vault, this.collName);
10646
+ const out = [];
10647
+ for (const key of keys) {
10648
+ const env = await this.adapter.get(this.vault, this.collName, key);
10649
+ if (!env) continue;
10650
+ const e = await this.decryptEntry(env);
10651
+ out.push(e.meta !== void 0 ? { a: e.a, b: e.b, meta: e.meta } : { a: e.a, b: e.b });
9813
10652
  }
10653
+ return out;
9814
10654
  }
9815
- /** Get the outbound refs declared by a collection (or `{}` if none). */
9816
- getOutbound(collection) {
9817
- return this.outbound.get(collection) ?? {};
10655
+ // ── Vault-internal cascade helpers ──────────────────────────────────
10656
+ /** @internal — rows where the deleted endpoint id matches the relevant slot. */
10657
+ async _rowsTouchingEndpoint(collection, id) {
10658
+ const rows = await this.list();
10659
+ return rows.filter(
10660
+ (r) => this.spec.a === collection && r.a === id || this.spec.b === collection && r.b === id
10661
+ );
9818
10662
  }
9819
- /** Get the inbound refs that target a given collection (or `[]`). */
9820
- getInbound(target) {
9821
- return this.inbound.get(target) ?? [];
10663
+ /** @internal the storage collection name (for tx pre-image capture). */
10664
+ get _collectionName() {
10665
+ return this.collName;
9822
10666
  }
9823
- /**
9824
- * Iterate every (collection refs) pair that has at least one
9825
- * declared reference. Used by `checkIntegrity` to walk the full
9826
- * universe of outbound refs without needing to track collection
9827
- * names elsewhere.
9828
- */
9829
- entries() {
9830
- return [...this.outbound.entries()];
10667
+ };
10668
+ var LinkEndpointError = class extends NoydbError {
10669
+ link;
10670
+ endpoint;
10671
+ missingId;
10672
+ constructor(link, endpoint, missingId) {
10673
+ super(
10674
+ "LINK_ENDPOINT",
10675
+ `link("${link}").connect: endpoint "${endpoint}" has no record "${missingId}".`
10676
+ );
10677
+ this.name = "LinkEndpointError";
10678
+ this.link = link;
10679
+ this.endpoint = endpoint;
10680
+ this.missingId = missingId;
9831
10681
  }
9832
- /** Clear the registry. Test-only escape hatch; never called from production code. */
9833
- clear() {
9834
- this.outbound.clear();
9835
- this.inbound.clear();
10682
+ };
10683
+ var LinkIntegrityError = class extends NoydbError {
10684
+ link;
10685
+ endpoint;
10686
+ id;
10687
+ count;
10688
+ constructor(link, endpoint, id, count2) {
10689
+ super(
10690
+ "LINK_INTEGRITY",
10691
+ `Cannot delete "${endpoint}"/"${id}": ${count2} link(s) in "${link}" still reference it (onDelete: 'strict').`
10692
+ );
10693
+ this.name = "LinkIntegrityError";
10694
+ this.link = link;
10695
+ this.endpoint = endpoint;
10696
+ this.id = id;
10697
+ this.count = count2;
9836
10698
  }
9837
10699
  };
9838
10700
 
@@ -11967,309 +12829,114 @@ async function resolveManagedSecret(store, vault, provider) {
11967
12829
  globalThis.crypto.getRandomValues(random);
11968
12830
  const sealed = await provider.seal(random);
11969
12831
  await saveSealedPassphrase(store, vault, { providerId: provider.id, sealed });
11970
- return bytesToBase644(random);
11971
- }
11972
-
11973
- // src/team/peer-recover.ts
11974
- init_types();
11975
- init_crypto();
11976
- init_errors();
11977
- var ADMIN_RECOVERABLE_TARGETS = ["operator", "viewer", "client", "admin"];
11978
- function canRecover(callerRole, targetRole) {
11979
- if (callerRole === "owner") return true;
11980
- if (callerRole === "admin") return ADMIN_RECOVERABLE_TARGETS.includes(targetRole);
11981
- return false;
11982
- }
11983
- async function recoverUser(store, vault, callerKeyring, options) {
11984
- const env = await store.get(vault, "_keyring", options.userId);
11985
- if (!env) {
11986
- throw new NoAccessError(
11987
- `recoverUser: user "${options.userId}" has no keyring in vault "${vault}".`
11988
- );
11989
- }
11990
- const target = JSON.parse(env._data);
11991
- const targetRole = options.role ?? target.role;
11992
- if (!canRecover(callerKeyring.role, targetRole)) {
11993
- throw new PermissionDeniedError(
11994
- `Role "${callerKeyring.role}" cannot recover role "${targetRole}"`
11995
- );
11996
- }
11997
- if (!canRecover(callerKeyring.role, target.role)) {
11998
- throw new PermissionDeniedError(
11999
- `Role "${callerKeyring.role}" cannot recover role "${target.role}"`
12000
- );
12001
- }
12002
- for (const coll of Object.keys(target.deks)) {
12003
- if (!callerKeyring.deks.has(coll)) {
12004
- throw new PrivilegeEscalationError(coll);
12005
- }
12006
- }
12007
- if (options.validatePassphrase && !options.allowWeakPassphrase) {
12008
- assertStrongPassphrase(options.passphrase, options.passphrasePolicy);
12009
- }
12010
- const newSalt = generateSalt();
12011
- const newKek = await deriveKey(options.passphrase, newSalt);
12012
- const wrappedDeks = {};
12013
- for (const coll of Object.keys(target.deks)) {
12014
- const callerDek = callerKeyring.deks.get(coll);
12015
- if (!callerDek) {
12016
- throw new PrivilegeEscalationError(coll);
12017
- }
12018
- wrappedDeks[coll] = await wrapKey(callerDek, newKek);
12019
- }
12020
- const canary = await mintKeyringCanary(newKek);
12021
- const next = {
12022
- ...target,
12023
- _noydb_keyring: NOYDB_KEYRING_VERSION,
12024
- role: targetRole,
12025
- display_name: options.displayName ?? target.display_name,
12026
- deks: wrappedDeks,
12027
- salt: bufferToBase64(newSalt),
12028
- granted_by: callerKeyring.userId,
12029
- authenticators: [],
12030
- canary
12031
- };
12032
- const envelope = {
12033
- _noydb: 1,
12034
- _v: 1,
12035
- _ts: (/* @__PURE__ */ new Date()).toISOString(),
12036
- _iv: "",
12037
- _data: JSON.stringify(next)
12038
- };
12039
- await store.put(vault, "_keyring", options.userId, envelope);
12040
- }
12041
-
12042
- // src/index.ts
12043
- init_errors();
12044
-
12045
- // src/noydb.ts
12046
- init_errors();
12047
- init_constants();
12048
- init_ulid();
12049
- init_public_envelope();
12050
-
12051
- // src/vault.ts
12052
- init_types();
12053
-
12054
- // src/collection.ts
12055
- init_types();
12056
-
12057
- // src/crdt/strategy.ts
12058
- var NOT_ENABLED = new Error(
12059
- 'CRDT mode requires the CRDT strategy. Import `{ withCrdt }` from "@noy-db/hub/crdt" and pass it to `createNoydb({ crdtStrategy: withCrdt() })`.'
12060
- );
12061
- var NO_CRDT = {
12062
- buildLwwMapState() {
12063
- throw NOT_ENABLED;
12064
- },
12065
- buildRgaState() {
12066
- throw NOT_ENABLED;
12067
- },
12068
- mergeCrdtStates() {
12069
- throw NOT_ENABLED;
12070
- },
12071
- resolveCrdtSnapshot() {
12072
- throw NOT_ENABLED;
12073
- }
12074
- };
12075
-
12076
- // src/i18n/core.ts
12077
- init_errors();
12078
-
12079
- // src/i18n/policy.ts
12080
- function resolvePolicy(onMissing, layer) {
12081
- const explicit = onMissing && typeof onMissing === "object" ? onMissing[layer] : void 0;
12082
- const scalar = typeof onMissing === "string" ? onMissing : void 0;
12083
- const layerDefault = layer === "guard" ? "substitute" : void 0;
12084
- return explicit ?? layerDefault ?? scalar ?? "throw";
12085
- }
12086
-
12087
- // src/i18n/core.ts
12088
- function i18nText(options) {
12089
- return { _noydbI18nText: true, options };
12090
- }
12091
- function isI18nTextDescriptor(x) {
12092
- return typeof x === "object" && x !== null && x._noydbI18nText === true;
12093
- }
12094
- function validateI18nTextValue(value, field, descriptor) {
12095
- const { options } = descriptor;
12096
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
12097
- throw new MissingTranslationError(
12098
- field,
12099
- options.languages,
12100
- `Field "${field}" must be a { [locale]: string } map, got ${typeof value}.`
12101
- );
12102
- }
12103
- const map = value;
12104
- for (const [locale, v] of Object.entries(map)) {
12105
- if (typeof v !== "string") {
12106
- throw new MissingTranslationError(
12107
- field,
12108
- [locale],
12109
- `Field "${field}": locale "${locale}" must be a string, got ${typeof v}.`
12110
- );
12111
- }
12112
- }
12113
- const { required } = options;
12114
- if (required === "all") {
12115
- const missing = options.languages.filter(
12116
- (lang) => !(lang in map) || map[lang] === ""
12117
- );
12118
- if (missing.length > 0) {
12119
- throw new MissingTranslationError(
12120
- field,
12121
- missing,
12122
- `Field "${field}" requires all declared languages. Missing: ${missing.join(", ")}.`
12123
- );
12124
- }
12125
- } else if (required === "any") {
12126
- const present = options.languages.some(
12127
- (lang) => lang in map && map[lang] !== ""
12128
- );
12129
- if (!present) {
12130
- throw new MissingTranslationError(
12131
- field,
12132
- options.languages,
12133
- `Field "${field}" requires at least one declared language. None present.`
12134
- );
12135
- }
12136
- } else {
12137
- const requiredList = required;
12138
- const missing = requiredList.filter(
12139
- (lang) => !(lang in map) || map[lang] === ""
12140
- );
12141
- if (missing.length > 0) {
12142
- throw new MissingTranslationError(
12143
- field,
12144
- missing,
12145
- `Field "${field}" requires: ${requiredList.join(", ")}. Missing: ${missing.join(", ")}.`
12146
- );
12147
- }
12148
- }
12149
- }
12150
- function toChain(fallback) {
12151
- return Array.isArray(fallback) ? fallback : fallback ? [fallback] : [];
12152
- }
12153
- function pickFromChain(value, chain) {
12154
- for (const fb of chain) {
12155
- if (fb === "any") {
12156
- const any = Object.values(value).find((v) => v !== "");
12157
- if (any !== void 0) return any;
12158
- } else if (value[fb] !== void 0 && value[fb] !== "") {
12159
- return value[fb];
12160
- }
12161
- }
12162
- return void 0;
12163
- }
12164
- function resolveI18nText(value, locale, fallback, field, opts) {
12165
- if (locale === "raw") {
12166
- return value;
12167
- }
12168
- if (!locale) {
12169
- throw new LocaleNotSpecifiedError(field ?? "<unknown>");
12170
- }
12171
- if (value[locale] !== void 0 && value[locale] !== "") {
12172
- return value[locale];
12173
- }
12174
- const policy = opts?.policy ?? "throw";
12175
- const callerChain = toChain(fallback);
12176
- const callerHit = pickFromChain(value, callerChain);
12177
- if (callerHit !== void 0) return callerHit;
12178
- if (policy === "substitute") {
12179
- const subHit = pickFromChain(value, toChain(opts?.substitute));
12180
- if (subHit !== void 0) return subHit;
12181
- }
12182
- if (policy === "throw") {
12183
- throw new LocaleNotSpecifiedError(
12184
- field ?? "<unknown>",
12185
- `No translation available for locale "${locale}"` + (callerChain.length > 0 ? ` or fallback chain [${callerChain.join(", ")}]` : "") + "."
12832
+ return bytesToBase644(random);
12833
+ }
12834
+
12835
+ // src/team/peer-recover.ts
12836
+ init_types();
12837
+ init_crypto();
12838
+ init_errors();
12839
+ var ADMIN_RECOVERABLE_TARGETS = ["operator", "viewer", "client", "admin"];
12840
+ function canRecover(callerRole, targetRole) {
12841
+ if (callerRole === "owner") return true;
12842
+ if (callerRole === "admin") return ADMIN_RECOVERABLE_TARGETS.includes(targetRole);
12843
+ return false;
12844
+ }
12845
+ async function recoverUser(store, vault, callerKeyring, options) {
12846
+ const env = await store.get(vault, "_keyring", options.userId);
12847
+ if (!env) {
12848
+ throw new NoAccessError(
12849
+ `recoverUser: user "${options.userId}" has no keyring in vault "${vault}".`
12186
12850
  );
12187
12851
  }
12188
- return null;
12189
- }
12190
- function getAtPath(obj, path) {
12191
- const arrayIdx = path.indexOf("[].");
12192
- if (arrayIdx !== -1) {
12193
- const arrayKey = path.slice(0, arrayIdx);
12194
- const restPath = path.slice(arrayIdx + 3);
12195
- const arr = obj[arrayKey];
12196
- if (!Array.isArray(arr)) return [];
12197
- return arr.flatMap((item) => {
12198
- if (!item || typeof item !== "object" || Array.isArray(item)) return [];
12199
- return getAtPath(item, restPath);
12200
- });
12852
+ const target = JSON.parse(env._data);
12853
+ const targetRole = options.role ?? target.role;
12854
+ if (!canRecover(callerKeyring.role, targetRole)) {
12855
+ throw new PermissionDeniedError(
12856
+ `Role "${callerKeyring.role}" cannot recover role "${targetRole}"`
12857
+ );
12201
12858
  }
12202
- const dotIdx = path.indexOf(".");
12203
- if (dotIdx !== -1) {
12204
- const head = path.slice(0, dotIdx);
12205
- const rest = path.slice(dotIdx + 1);
12206
- const nested = obj[head];
12207
- if (!nested || typeof nested !== "object" || Array.isArray(nested)) return [];
12208
- return getAtPath(nested, rest);
12859
+ if (!canRecover(callerKeyring.role, target.role)) {
12860
+ throw new PermissionDeniedError(
12861
+ `Role "${callerKeyring.role}" cannot recover role "${target.role}"`
12862
+ );
12209
12863
  }
12210
- const val = obj[path];
12211
- return val !== void 0 ? [val] : [];
12212
- }
12213
- function setAtPathInPlace(obj, path, value) {
12214
- const dotIdx = path.indexOf(".");
12215
- if (dotIdx !== -1) {
12216
- const head = path.slice(0, dotIdx);
12217
- const rest = path.slice(dotIdx + 1);
12218
- const nested = obj[head];
12219
- if (!nested || typeof nested !== "object" || Array.isArray(nested)) return;
12220
- setAtPathInPlace(nested, rest, value);
12221
- return;
12864
+ for (const coll of Object.keys(target.deks)) {
12865
+ if (!callerKeyring.deks.has(coll)) {
12866
+ throw new PrivilegeEscalationError(coll);
12867
+ }
12222
12868
  }
12223
- obj[path] = value;
12224
- }
12225
- function applyAtPath(obj, path, locale, fallback, opts) {
12226
- const arrayIdx = path.indexOf("[].");
12227
- if (arrayIdx !== -1) {
12228
- const arrayKey = path.slice(0, arrayIdx);
12229
- const restPath = path.slice(arrayIdx + 3);
12230
- const arr = obj[arrayKey];
12231
- if (!Array.isArray(arr)) return obj;
12232
- return {
12233
- ...obj,
12234
- [arrayKey]: arr.map((item) => {
12235
- if (!item || typeof item !== "object" || Array.isArray(item)) return item;
12236
- return applyAtPath(item, restPath, locale, fallback, opts);
12237
- })
12238
- };
12869
+ if (options.validatePassphrase && !options.allowWeakPassphrase) {
12870
+ assertStrongPassphrase(options.passphrase, options.passphrasePolicy);
12239
12871
  }
12240
- const dotIdx = path.indexOf(".");
12241
- if (dotIdx !== -1) {
12242
- const head = path.slice(0, dotIdx);
12243
- const rest = path.slice(dotIdx + 1);
12244
- const nested = obj[head];
12245
- if (!nested || typeof nested !== "object" || Array.isArray(nested)) return obj;
12246
- return {
12247
- ...obj,
12248
- [head]: applyAtPath(nested, rest, locale, fallback, opts)
12249
- };
12872
+ const newSalt = generateSalt();
12873
+ const newKek = await deriveKey(options.passphrase, newSalt);
12874
+ const wrappedDeks = {};
12875
+ for (const coll of Object.keys(target.deks)) {
12876
+ const callerDek = callerKeyring.deks.get(coll);
12877
+ if (!callerDek) {
12878
+ throw new PrivilegeEscalationError(coll);
12879
+ }
12880
+ wrappedDeks[coll] = await wrapKey(callerDek, newKek);
12250
12881
  }
12251
- const raw = obj[path];
12252
- if (raw === void 0 || raw === null) return obj;
12253
- if (typeof raw !== "object" || Array.isArray(raw)) return obj;
12254
- return {
12255
- ...obj,
12256
- [path]: resolveI18nText(raw, locale, fallback, path, opts)
12882
+ const canary = await mintKeyringCanary(newKek);
12883
+ const next = {
12884
+ ...target,
12885
+ _noydb_keyring: NOYDB_KEYRING_VERSION,
12886
+ role: targetRole,
12887
+ display_name: options.displayName ?? target.display_name,
12888
+ deks: wrappedDeks,
12889
+ salt: bufferToBase64(newSalt),
12890
+ granted_by: callerKeyring.userId,
12891
+ authenticators: [],
12892
+ canary
12893
+ };
12894
+ const envelope = {
12895
+ _noydb: 1,
12896
+ _v: 1,
12897
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
12898
+ _iv: "",
12899
+ _data: JSON.stringify(next)
12257
12900
  };
12901
+ await store.put(vault, "_keyring", options.userId, envelope);
12258
12902
  }
12259
- function applyI18nLocale(record, i18nFields, locale, fallback, layer = "read") {
12260
- const fieldNames = Object.keys(i18nFields);
12261
- if (fieldNames.length === 0) return record;
12262
- let result = record;
12263
- for (const [field, descriptor] of Object.entries(i18nFields)) {
12264
- const { onMissing, substitute } = descriptor.options;
12265
- const opts = {
12266
- policy: resolvePolicy(onMissing, layer),
12267
- ...substitute !== void 0 ? { substitute } : {}
12268
- };
12269
- result = applyAtPath(result, field, locale, fallback, opts);
12903
+
12904
+ // src/index.ts
12905
+ init_errors();
12906
+
12907
+ // src/noydb.ts
12908
+ init_errors();
12909
+ init_constants();
12910
+ init_ulid();
12911
+ init_public_envelope();
12912
+
12913
+ // src/vault.ts
12914
+ init_types();
12915
+
12916
+ // src/collection.ts
12917
+ init_types();
12918
+
12919
+ // src/crdt/strategy.ts
12920
+ var NOT_ENABLED = new Error(
12921
+ 'CRDT mode requires the CRDT strategy. Import `{ withCrdt }` from "@noy-db/hub/crdt" and pass it to `createNoydb({ crdtStrategy: withCrdt() })`.'
12922
+ );
12923
+ var NO_CRDT = {
12924
+ buildLwwMapState() {
12925
+ throw NOT_ENABLED;
12926
+ },
12927
+ buildRgaState() {
12928
+ throw NOT_ENABLED;
12929
+ },
12930
+ mergeCrdtStates() {
12931
+ throw NOT_ENABLED;
12932
+ },
12933
+ resolveCrdtSnapshot() {
12934
+ throw NOT_ENABLED;
12270
12935
  }
12271
- return result;
12272
- }
12936
+ };
12937
+
12938
+ // src/collection.ts
12939
+ init_core();
12273
12940
 
12274
12941
  // src/i18n/dictionary.ts
12275
12942
  init_types();
@@ -12840,6 +13507,21 @@ function formatCurrency(decimal, currency, scale, locale) {
12840
13507
  });
12841
13508
  return fmt.format(decimal);
12842
13509
  }
13510
+ function moneyScaledValue(stored, desc) {
13511
+ let raw;
13512
+ if (desc.mode === "fixed") {
13513
+ raw = stored;
13514
+ } else {
13515
+ if (!isMoneyValueObject(stored)) return null;
13516
+ raw = stored.amount;
13517
+ }
13518
+ if (typeof raw !== "string" && typeof raw !== "number") return null;
13519
+ try {
13520
+ return BigInt(String(raw));
13521
+ } catch {
13522
+ return null;
13523
+ }
13524
+ }
12843
13525
  function decodeValue(stored, desc) {
12844
13526
  let currency;
12845
13527
  let scaledIntString;
@@ -12959,6 +13641,7 @@ var NO_I18N = {
12959
13641
  };
12960
13642
 
12961
13643
  // src/collection.ts
13644
+ init_policy();
12962
13645
  init_crypto();
12963
13646
  init_errors();
12964
13647
  init_tiers();
@@ -13010,6 +13693,7 @@ init_predicate();
13010
13693
  // src/query/join.ts
13011
13694
  init_predicate();
13012
13695
  init_errors();
13696
+ init_core();
13013
13697
  var DEFAULT_JOIN_MAX_ROWS = 5e4;
13014
13698
  var JOIN_WARN_FRACTION = 0.8;
13015
13699
  function coerceRefKey(value) {
@@ -13037,15 +13721,15 @@ function warnCeilingApproaching(target, side, rows, maxRows) {
13037
13721
  `[noy-db] .join() ${side} side is at ${pct}% of the ${maxRows}-row ceiling for target "${target}" (${rows} rows). Streaming joins over scan() are not yet supported for collections that need to exceed this.`
13038
13722
  );
13039
13723
  }
13040
- function applyJoins(rows, joins, context) {
13724
+ function applyJoins(rows, joins, context, locale) {
13041
13725
  if (joins.length === 0) return [...rows];
13042
13726
  let result = [...rows];
13043
13727
  for (const leg of joins) {
13044
- result = applyOneJoin(result, leg, context);
13728
+ result = applyOneJoin(result, leg, context, locale);
13045
13729
  }
13046
13730
  return result;
13047
13731
  }
13048
- function applyOneJoin(leftRows, leg, context) {
13732
+ function applyOneJoin(leftRows, leg, context, locale) {
13049
13733
  if (leg.isDictJoin) {
13050
13734
  const dictSource = context.resolveDictSource?.(leg.field);
13051
13735
  if (!dictSource) {
@@ -13100,24 +13784,27 @@ function applyOneJoin(leftRows, leg, context) {
13100
13784
  if (rightSnapshot.length > maxRows * JOIN_WARN_FRACTION) {
13101
13785
  warnCeilingApproaching(leg.target, "right", rightSnapshot.length, maxRows);
13102
13786
  }
13787
+ const effLocale = locale ?? context.defaultLocale;
13788
+ const i18nResolve = effLocale !== void 0 && source.i18nFields !== void 0 ? (right) => right !== null && typeof right === "object" ? applyI18nLocale(right, source.i18nFields, effLocale, void 0, "join") : right : void 0;
13103
13789
  const strategy = leg.strategy ?? (source.lookupById ? "nested" : "hash");
13104
13790
  if (strategy === "nested" && source.lookupById) {
13105
13791
  const lookup = (id) => source.lookupById?.(id);
13106
- return nestedLoopJoin(leftRows, leg, lookup);
13792
+ return nestedLoopJoin(leftRows, leg, lookup, i18nResolve);
13107
13793
  }
13108
- return hashJoin(leftRows, leg, rightSnapshot);
13794
+ return hashJoin(leftRows, leg, rightSnapshot, i18nResolve);
13109
13795
  }
13110
- function nestedLoopJoin(leftRows, leg, lookupById) {
13796
+ function nestedLoopJoin(leftRows, leg, lookupById, i18nResolve) {
13111
13797
  const out = [];
13112
13798
  for (const left of leftRows) {
13113
13799
  const rawId = readPath(left, leg.field);
13114
13800
  const key = coerceRefKey(rawId);
13115
- const right = key === null ? void 0 : lookupById(key);
13801
+ let right = key === null ? void 0 : lookupById(key);
13802
+ if (i18nResolve && right !== void 0) right = i18nResolve(right);
13116
13803
  out.push(attachJoin(left, leg, right, rawId));
13117
13804
  }
13118
13805
  return out;
13119
13806
  }
13120
- function hashJoin(leftRows, leg, rightSnapshot) {
13807
+ function hashJoin(leftRows, leg, rightSnapshot, i18nResolve) {
13121
13808
  const rightMap = /* @__PURE__ */ new Map();
13122
13809
  for (const record of rightSnapshot) {
13123
13810
  const rawId = readPath(record, "id");
@@ -13130,7 +13817,8 @@ function hashJoin(leftRows, leg, rightSnapshot) {
13130
13817
  for (const left of leftRows) {
13131
13818
  const rawId = readPath(left, leg.field);
13132
13819
  const key = coerceRefKey(rawId);
13133
- const right = key === null ? void 0 : rightMap.get(key);
13820
+ let right = key === null ? void 0 : rightMap.get(key);
13821
+ if (i18nResolve && right !== void 0) right = i18nResolve(right);
13134
13822
  out.push(attachJoin(left, leg, right, rawId));
13135
13823
  }
13136
13824
  return out;
@@ -13428,11 +14116,16 @@ var Query = class _Query {
13428
14116
  this.predicates
13429
14117
  );
13430
14118
  }
13431
- /** Sort by a field. Subsequent calls are tie-breakers. */
13432
- orderBy(field, direction = "asc") {
14119
+ /**
14120
+ * Sort by a field. Subsequent calls are tie-breakers. Pass
14121
+ * `{ by: 'label' }` to sort a `dictKey`/`staticDict` field by its resolved
14122
+ * label at the query locale instead of the stored code (#285).
14123
+ */
14124
+ orderBy(field, direction = "asc", opts) {
14125
+ const entry = opts?.by === "label" ? { field, direction, by: "label" } : { field, direction };
13433
14126
  return new _Query(
13434
14127
  this.source,
13435
- { ...this.plan, orderBy: [...this.plan.orderBy, { field, direction }] },
14128
+ { ...this.plan, orderBy: [...this.plan.orderBy, entry] },
13436
14129
  this.joinContext,
13437
14130
  this.aggregateStrategy,
13438
14131
  this.predicates
@@ -13635,16 +14328,21 @@ var Query = class _Query {
13635
14328
  * carries any join legs, they are applied after `where` / `orderBy`
13636
14329
  * / `limit` / `offset` narrow the left set. See the `.join()` doc
13637
14330
  * for the ordering rationale.
14331
+ *
14332
+ * `opts.locale` (#285 §3) resolves JOINED right-side i18n fields at the
14333
+ * `join` layer to that locale; without it, the owning collection's default
14334
+ * locale applies, and a locale-less query leaves joined i18n fields raw.
14335
+ * (Left/base i18n fields are resolved by `get`/`list`, not here.)
13638
14336
  */
13639
- toArray() {
13640
- const base = this.decodeMoney(executePlanWithSource(this.source, this.plan, this.joinContext));
14337
+ toArray(opts) {
14338
+ const base = this.decodeMoney(executePlanWithSource(this.source, this.plan, this.joinContext, opts?.locale));
13641
14339
  if (this.plan.joins.length === 0) return base;
13642
14340
  if (!this.joinContext) {
13643
14341
  throw new Error(
13644
14342
  `Query.toArray(): plan carries ${this.plan.joins.length} join leg(s) but no JoinContext is attached. This usually means the Query was constructed via the raw Query constructor with a plan that had joins pre-populated. Use collection.query().join(...) instead.`
13645
14343
  );
13646
14344
  }
13647
- return applyJoins(base, this.plan.joins, this.joinContext);
14345
+ return applyJoins(base, this.plan.joins, this.joinContext, opts?.locale);
13648
14346
  }
13649
14347
  /**
13650
14348
  * Decode this source's money fields on read (stored scaled-int → canonical
@@ -13663,9 +14361,9 @@ var Query = class _Query {
13663
14361
  if (!moneyFields || Object.keys(moneyFields).length === 0) return records;
13664
14362
  return records.map((r) => decodeMoneyFields(r, moneyFields, "raw"));
13665
14363
  }
13666
- /** Return the first matching record, or null. Joins are applied. */
13667
- first() {
13668
- const arr = this.limit(1).toArray();
14364
+ /** Return the first matching record, or null. Joins are applied. `opts.locale` resolves joined i18n fields (#285 §3). */
14365
+ first(opts) {
14366
+ const arr = this.limit(1).toArray(opts);
13669
14367
  return arr[0] ?? null;
13670
14368
  }
13671
14369
  /**
@@ -13908,7 +14606,7 @@ var Query = class _Query {
13908
14606
  return serializePlan(this.plan);
13909
14607
  }
13910
14608
  };
13911
- function executePlanWithSource(source, plan, joinContext) {
14609
+ function executePlanWithSource(source, plan, joinContext, locale) {
13912
14610
  const hasCrossJoins = plan.clauses.some((c) => c.type === "crossJoin");
13913
14611
  let result;
13914
14612
  if (hasCrossJoins) {
@@ -13923,7 +14621,8 @@ function executePlanWithSource(source, plan, joinContext) {
13923
14621
  result = remainingClauses.length === 0 ? [...candidates] : filterRecords(candidates, remainingClauses, fnViewDecoder(source));
13924
14622
  }
13925
14623
  if (plan.orderBy.length > 0) {
13926
- result = sortRecords(result, plan.orderBy);
14624
+ const labelMaps = buildOrderLabelMaps(plan.orderBy, joinContext, locale);
14625
+ result = sortRecords(result, plan.orderBy, source.moneyFields, labelMaps);
13927
14626
  }
13928
14627
  if (plan.offset > 0) {
13929
14628
  result = result.slice(plan.offset);
@@ -14080,17 +14779,55 @@ function applyCrossJoin(leftRel, clause, rightSource) {
14080
14779
  }
14081
14780
  return expanded;
14082
14781
  }
14083
- function sortRecords(records, orderBy) {
14782
+ function sortRecords(records, orderBy, moneyFields, labelMaps) {
14084
14783
  return [...records].sort((a, b) => {
14085
- for (const { field, direction } of orderBy) {
14086
- const av = readField(a, field);
14087
- const bv = readField(b, field);
14088
- const cmp = compareValues(av, bv);
14784
+ for (const { field, direction, by } of orderBy) {
14785
+ let av = readField(a, field);
14786
+ let bv = readField(b, field);
14787
+ const labelMap = by === "label" ? labelMaps?.get(field) : void 0;
14788
+ if (labelMap) {
14789
+ av = (typeof av === "string" ? labelMap.get(av) : void 0) ?? av;
14790
+ bv = (typeof bv === "string" ? labelMap.get(bv) : void 0) ?? bv;
14791
+ const cmp2 = compareValues(av, bv);
14792
+ if (cmp2 !== 0) return direction === "asc" ? cmp2 : -cmp2;
14793
+ continue;
14794
+ }
14795
+ const desc = moneyFields?.[field];
14796
+ const cmp = desc ? compareMoney(av, bv, desc) : compareValues(av, bv);
14089
14797
  if (cmp !== 0) return direction === "asc" ? cmp : -cmp;
14090
14798
  }
14091
14799
  return 0;
14092
14800
  });
14093
14801
  }
14802
+ function buildOrderLabelMaps(orderBy, joinContext, locale) {
14803
+ if (!joinContext?.resolveDictSource) return void 0;
14804
+ const resolveDict = joinContext.resolveDictSource.bind(joinContext);
14805
+ let maps;
14806
+ for (const { field, by } of orderBy) {
14807
+ if (by !== "label") continue;
14808
+ const dictSource = resolveDict(field);
14809
+ if (!dictSource) continue;
14810
+ const loc = locale ?? dictSource.displayLocale;
14811
+ if (loc === void 0) continue;
14812
+ const codeToLabel = /* @__PURE__ */ new Map();
14813
+ for (const entry of dictSource.snapshot()) {
14814
+ const k = entry["key"];
14815
+ const labels = entry["labels"];
14816
+ const label = labels?.[loc];
14817
+ if (typeof k === "string" && typeof label === "string") codeToLabel.set(k, label);
14818
+ }
14819
+ ;
14820
+ (maps ??= /* @__PURE__ */ new Map()).set(field, codeToLabel);
14821
+ }
14822
+ return maps;
14823
+ }
14824
+ function compareMoney(a, b, desc) {
14825
+ const av = moneyScaledValue(a, desc);
14826
+ const bv = moneyScaledValue(b, desc);
14827
+ if (av === null) return bv === null ? 0 : 1;
14828
+ if (bv === null) return -1;
14829
+ return av < bv ? -1 : av > bv ? 1 : 0;
14830
+ }
14094
14831
  function readField(record, field) {
14095
14832
  if (record === null || record === void 0) return void 0;
14096
14833
  if (!field.includes(".")) {
@@ -14879,8 +15616,8 @@ function coerceRefKey2(value) {
14879
15616
 
14880
15617
  // src/indexing/persisted-indexes.ts
14881
15618
  var IDX_PREFIX = "_idx/";
14882
- function encodeIdxId(field, recordId3) {
14883
- return `${IDX_PREFIX}${field}/${recordId3}`;
15619
+ function encodeIdxId(field, recordId4) {
15620
+ return `${IDX_PREFIX}${field}/${recordId4}`;
14884
15621
  }
14885
15622
  function decodeIdxId(id) {
14886
15623
  if (!id.startsWith(IDX_PREFIX)) return null;
@@ -14888,9 +15625,9 @@ function decodeIdxId(id) {
14888
15625
  const firstSlash = rest.indexOf("/");
14889
15626
  if (firstSlash <= 0) return null;
14890
15627
  const field = rest.slice(0, firstSlash);
14891
- const recordId3 = rest.slice(firstSlash + 1);
14892
- if (recordId3.length === 0) return null;
14893
- return { field, recordId: recordId3 };
15628
+ const recordId4 = rest.slice(firstSlash + 1);
15629
+ if (recordId4.length === 0) return null;
15630
+ return { field, recordId: recordId4 };
14894
15631
  }
14895
15632
 
14896
15633
  // src/indexing/lazy-builder.ts
@@ -15063,6 +15800,74 @@ var DISABLED_STATE = {
15063
15800
  getPersistedIndexes: () => null
15064
15801
  };
15065
15802
 
15803
+ // src/search/tokenize.ts
15804
+ var WORD = /[\p{L}\p{N}]+/gu;
15805
+ var tokenize = (text) => {
15806
+ if (!text) return [];
15807
+ return text.normalize("NFKC").toLowerCase().match(WORD) ?? [];
15808
+ };
15809
+
15810
+ // src/search/scan.ts
15811
+ var K1 = 1.2;
15812
+ var B = 0.75;
15813
+ function fieldText(record, field) {
15814
+ const v = record[field];
15815
+ if (typeof v === "string") return v;
15816
+ if (v === null || v === void 0) return "";
15817
+ if (typeof v === "number" || typeof v === "boolean") return String(v);
15818
+ return "";
15819
+ }
15820
+ function searchScan(entries, field, query, opts = {}, tokenizer = tokenize) {
15821
+ const queryTerms = tokenizer(query);
15822
+ if (queryTerms.length === 0) return [];
15823
+ const match = opts.match ?? "any";
15824
+ const usePrefix = opts.prefix ?? false;
15825
+ const exactTerms = usePrefix ? queryTerms.slice(0, -1) : queryTerms;
15826
+ const prefixTerm = usePrefix ? queryTerms[queryTerms.length - 1] : void 0;
15827
+ const docs = entries.map((e) => ({ id: e.id, record: e.record, terms: tokenizer(fieldText(e.record, field)) }));
15828
+ const N = docs.length || 1;
15829
+ const df = /* @__PURE__ */ new Map();
15830
+ let totalLen = 0;
15831
+ for (const d of docs) {
15832
+ totalLen += d.terms.length;
15833
+ for (const t of new Set(d.terms)) df.set(t, (df.get(t) ?? 0) + 1);
15834
+ }
15835
+ const avgdl = totalLen / N || 1;
15836
+ let prefixDf = 0;
15837
+ if (prefixTerm !== void 0) {
15838
+ for (const d of docs) {
15839
+ if (d.terms.some((t) => t.startsWith(prefixTerm))) prefixDf++;
15840
+ }
15841
+ }
15842
+ const requiredCount = exactTerms.length + (prefixTerm !== void 0 ? 1 : 0);
15843
+ const results = [];
15844
+ for (const d of docs) {
15845
+ const tf = /* @__PURE__ */ new Map();
15846
+ for (const t of d.terms) tf.set(t, (tf.get(t) ?? 0) + 1);
15847
+ const matched = [];
15848
+ for (const qt of exactTerms) {
15849
+ const c = tf.get(qt) ?? 0;
15850
+ if (c > 0) matched.push({ tf: c, df: df.get(qt) ?? 0 });
15851
+ }
15852
+ if (prefixTerm !== void 0) {
15853
+ let ptf = 0;
15854
+ for (const [t, c] of tf) if (t.startsWith(prefixTerm)) ptf += c;
15855
+ if (ptf > 0) matched.push({ tf: ptf, df: prefixDf });
15856
+ }
15857
+ if (matched.length === 0) continue;
15858
+ if (match === "all" && matched.length < requiredCount) continue;
15859
+ let score = 0;
15860
+ for (const m of matched) {
15861
+ const idf = Math.log(1 + (N - m.df + 0.5) / (m.df + 0.5));
15862
+ const denom = m.tf + K1 * (1 - B + B * (d.terms.length / avgdl));
15863
+ score += idf * (m.tf * (K1 + 1) / (denom || 1));
15864
+ }
15865
+ results.push({ id: d.id, score, record: d.record });
15866
+ }
15867
+ results.sort((a, b) => b.score - a.score);
15868
+ return opts.limit !== void 0 ? results.slice(0, opts.limit) : results;
15869
+ }
15870
+
15066
15871
  // src/collection.ts
15067
15872
  init_errors();
15068
15873
 
@@ -15780,6 +16585,15 @@ async function resolveStaleOnRead(accessor, outputCollection, id) {
15780
16585
  }
15781
16586
 
15782
16587
  // src/collection.ts
16588
+ function selfWriteFieldEqual(a, b) {
16589
+ if (a === b) return true;
16590
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
16591
+ try {
16592
+ return JSON.stringify(a) === JSON.stringify(b);
16593
+ } catch {
16594
+ return false;
16595
+ }
16596
+ }
15783
16597
  var fallbackWarned = /* @__PURE__ */ new Set();
15784
16598
  function warnOnceFallback(adapterName) {
15785
16599
  if (fallbackWarned.has(adapterName)) return;
@@ -16714,6 +17528,111 @@ var Collection = class {
16714
17528
  * output (carries `_derivedFrom`) — defensive guard against missed
16715
17529
  * cycle detection.
16716
17530
  */
17531
+ /**
17532
+ * @internal #376 — the RAW stored record (canonical-money form, i18n maps
17533
+ * intact), WITHOUT the locale resolution `get()` applies. Used as the
17534
+ * patch base for self-write reverse-denorm so writing back never clobbers
17535
+ * an i18n map or re-quantizes money incorrectly. Returns null for
17536
+ * missing / tombstoned records.
17537
+ */
17538
+ async _getStoredRecord(id) {
17539
+ let raw;
17540
+ if (this.lazy && this.lru) {
17541
+ const cached = this.lru.get(id);
17542
+ if (cached) raw = cached.record;
17543
+ else {
17544
+ const env = await this.adapter.get(this.vault, this.name, id);
17545
+ if (!env || isTombstone(env, this.encrypted)) return null;
17546
+ raw = await this.decryptRecord(env, { id });
17547
+ if (raw === null) return null;
17548
+ this.lru.set(id, { record: raw, version: env._v }, estimateRecordBytes(raw));
17549
+ }
17550
+ } else {
17551
+ await this.ensureHydrated();
17552
+ raw = this.cache.get(id)?.record ?? null;
17553
+ }
17554
+ if (raw === null) return null;
17555
+ return canonicalizeStoredMoney(raw, this.moneyFields);
17556
+ }
17557
+ /**
17558
+ * @internal #376 — ids of records whose top-level `field` equals `value`.
17559
+ * Uses the FK index when the field is indexed (O(matches)); otherwise a
17560
+ * linear scan (O(N) — fine for small child sets; index the FK to scale).
17561
+ */
17562
+ async _findMatchingIds(field, value) {
17563
+ const hit = this.getIndexes()?.lookupEqual(field, value);
17564
+ if (hit) return [...hit];
17565
+ const target = String(value);
17566
+ const matches = (rec) => {
17567
+ const fv = rec[field];
17568
+ return (typeof fv === "string" || typeof fv === "number") && String(fv) === target;
17569
+ };
17570
+ if (!this.lazy) {
17571
+ await this.ensureHydrated();
17572
+ const out2 = [];
17573
+ for (const [rid, e] of this.cache) {
17574
+ if (matches(e.record)) out2.push(rid);
17575
+ }
17576
+ return out2;
17577
+ }
17578
+ const ids = await this.adapter.list(this.vault, this.name);
17579
+ const out = [];
17580
+ for (const rid of ids) {
17581
+ const raw = await this._getStoredRecord(rid);
17582
+ if (raw !== null && matches(raw)) out.push(rid);
17583
+ }
17584
+ return out;
17585
+ }
17586
+ /**
17587
+ * @internal #376 slice 2 — recompute a rollup aggregate onto the parent.
17588
+ * Gathers every child of `parentId`, runs `compute`, and patches only the
17589
+ * rollup `field` onto the parent's raw stored record (value-equality
17590
+ * guarded). No-op when the parent record does not exist.
17591
+ */
17592
+ async recomputeRollup(spec, parentId) {
17593
+ if (this.derivationSource === void 0 || spec.rollup === void 0) return;
17594
+ const { from, key, field, compute } = spec.rollup;
17595
+ const into = spec.source;
17596
+ const intoColl = this.derivationSource.getCollection(into);
17597
+ const base = await intoColl._getStoredRecord(parentId);
17598
+ if (base === null) return;
17599
+ const fromColl = this.derivationSource.getCollection(from);
17600
+ const childIds = await fromColl._findMatchingIds(key, parentId);
17601
+ const children = [];
17602
+ for (const cid of childIds) {
17603
+ const c = await fromColl.get(cid);
17604
+ if (c !== null && c !== void 0) children.push(c);
17605
+ }
17606
+ const newValue = compute(children);
17607
+ if (selfWriteFieldEqual(base[field], newValue)) return;
17608
+ const patched = { ...base, [field]: newValue };
17609
+ const txCtx = this.derivationSource.getActiveTxContext();
17610
+ if (txCtx !== null) {
17611
+ const prior = await this.adapter.get(this.vault, into, parentId);
17612
+ txCtx._executed.push({
17613
+ op: { type: "put", vaultName: this.vault, collectionName: into, id: parentId },
17614
+ priorEnvelope: prior
17615
+ });
17616
+ }
17617
+ await intoColl.put(parentId, patched);
17618
+ }
17619
+ /**
17620
+ * @internal #376 slice 2 — fire any rollups for which THIS collection is the
17621
+ * child `from`, recomputing the affected parent after a child delete. Called
17622
+ * from the delete path with the just-removed record's key value. Other
17623
+ * derivation kinds do not react to deletes (unchanged).
17624
+ */
17625
+ async dispatchRollupsOnDelete(deleted) {
17626
+ if (this.derivationSource === void 0) return;
17627
+ const registry = this.derivationSource.registry();
17628
+ const rec = deleted;
17629
+ for (const { spec } of registry.strategiesForSource(this.name)) {
17630
+ if (!spec.rollup || spec.rollup.from !== this.name) continue;
17631
+ const kv = rec[spec.rollup.key];
17632
+ if (typeof kv !== "string" && typeof kv !== "number") continue;
17633
+ await this.recomputeRollup(spec, String(kv));
17634
+ }
17635
+ }
16717
17636
  async dispatchDerivations(id, record, version) {
16718
17637
  if (this.derivationSource === void 0) return;
16719
17638
  const incoming = canonicalizeStoredMoney(record, this.moneyFields);
@@ -16724,29 +17643,60 @@ var Collection = class {
16724
17643
  let DerivationExecutor2 = null;
16725
17644
  for (const { spec, strategyHash } of strategies) {
16726
17645
  const mode = typeof spec.lifecycle === "string" ? spec.lifecycle : spec.lifecycle.mode;
16727
- if (mode === "eager") {
16728
- if (DerivationExecutor2 === null) {
16729
- ({ DerivationExecutor: DerivationExecutor2 } = await Promise.resolve().then(() => (init_executor2(), executor_exports2)));
16730
- }
16731
- let sourceWithId;
16732
- let sourceVersion = version;
16733
- if (spec.source === this.name) {
16734
- sourceWithId = { ...incoming, id };
17646
+ if (spec.rollup) {
17647
+ if (mode !== "eager") continue;
17648
+ let parentId;
17649
+ if (this.name === spec.rollup.from) {
17650
+ const kv = incoming[spec.rollup.key];
17651
+ parentId = typeof kv === "string" || typeof kv === "number" ? String(kv) : null;
16735
17652
  } else {
16736
- const primary = await this.derivationSource.getCollection(spec.source).get(id);
16737
- if (primary === null || primary === void 0) continue;
16738
- sourceWithId = { ...primary, id };
16739
- sourceVersion = 0;
17653
+ parentId = id;
17654
+ }
17655
+ if (parentId !== null) await this.recomputeRollup(spec, parentId);
17656
+ continue;
17657
+ }
17658
+ const isSource = spec.source === this.name;
17659
+ const isSibling = !isSource && (spec.sources?.includes(this.name) ?? false);
17660
+ const trigger = !isSource && !isSibling ? spec.triggerBy?.find((t) => t.collection === this.name) : void 0;
17661
+ const runs = [];
17662
+ if (isSource) {
17663
+ runs.push({ input: { ...incoming, id }, base: incoming, runId: id, version });
17664
+ } else if (isSibling) {
17665
+ const p = await this.derivationSource.getCollection(spec.source).get(id);
17666
+ if (p !== null && p !== void 0) {
17667
+ const raw = await this.derivationSource.getCollection(spec.source)._getStoredRecord(id);
17668
+ runs.push({ input: { ...p, id }, base: raw ?? p, runId: id, version: 0 });
17669
+ }
17670
+ } else if (trigger) {
17671
+ const srcColl = this.derivationSource.getCollection(spec.source);
17672
+ const ids = await srcColl._findMatchingIds(trigger.on, id);
17673
+ if (trigger.maxFanout !== void 0 && ids.length > trigger.maxFanout) {
17674
+ throw new DerivationCapExceededError(`triggerBy ${this.name}\u2192${spec.source}`, ids.length, trigger.maxFanout);
16740
17675
  }
17676
+ for (const sid of ids) {
17677
+ const raw = await srcColl._getStoredRecord(sid);
17678
+ if (raw === null) continue;
17679
+ runs.push({ input: { ...raw, id: sid }, base: raw, runId: sid, version: 0 });
17680
+ }
17681
+ }
17682
+ if (runs.length === 0) continue;
17683
+ if (mode !== "eager") {
17684
+ for (const run of runs) await markStale(registry, spec, run.runId);
17685
+ continue;
17686
+ }
17687
+ if (DerivationExecutor2 === null) {
17688
+ ({ DerivationExecutor: DerivationExecutor2 } = await Promise.resolve().then(() => (init_executor2(), executor_exports2)));
17689
+ }
17690
+ for (const run of runs) {
16741
17691
  const ctx = { vault: this.derivationSource.getReadOnlyFacade() };
16742
- const result = await DerivationExecutor2.run(spec, sourceWithId, sourceVersion, strategyHash, ctx);
17692
+ const result = await DerivationExecutor2.run(spec, run.input, run.version, strategyHash, ctx);
16743
17693
  for (const key of Object.keys(spec.outputs)) {
16744
17694
  const out = result.outputs[key];
16745
17695
  if (!out) continue;
16746
17696
  if (out.kind === "failed") {
16747
17697
  const err = out.error;
16748
17698
  if (spec.strict) throw err;
16749
- console.warn(`[derivation] output "${key}" for source "${spec.source}" id="${id}" failed:`, err);
17699
+ console.warn(`[derivation] output "${key}" for source "${spec.source}" id="${run.runId}" failed:`, err);
16750
17700
  continue;
16751
17701
  }
16752
17702
  const outSpec = spec.outputs[key];
@@ -16759,7 +17709,7 @@ var Collection = class {
16759
17709
  this.adapter,
16760
17710
  this.vault,
16761
17711
  spec.source,
16762
- id,
17712
+ run.runId,
16763
17713
  key
16764
17714
  );
16765
17715
  const prevKeys = new Set(prior?.keys ?? []);
@@ -16786,7 +17736,7 @@ var Collection = class {
16786
17736
  }
16787
17737
  await saveFanoutSidecar2(this.adapter, this.vault, {
16788
17738
  source: spec.source,
16789
- sourceId: id,
17739
+ sourceId: run.runId,
16790
17740
  outputKey: key,
16791
17741
  outputCollection: outSpec.collection,
16792
17742
  keys: newKeysList
@@ -16794,25 +17744,44 @@ var Collection = class {
16794
17744
  continue;
16795
17745
  }
16796
17746
  if (out.skipped === true) {
16797
- await outputCollection._internalDelete(id, txCtx);
17747
+ await outputCollection._internalDelete(run.runId, txCtx);
17748
+ continue;
17749
+ }
17750
+ if (outSpec.shape === "record" && outSpec.denorm !== void 0 && outSpec.collection === spec.source) {
17751
+ const value = out.value;
17752
+ const patched = { ...run.base };
17753
+ let changed = false;
17754
+ for (const f of outSpec.denorm) {
17755
+ if (!selfWriteFieldEqual(run.base[f], value[f])) {
17756
+ patched[f] = value[f];
17757
+ changed = true;
17758
+ }
17759
+ }
17760
+ if (!changed) continue;
17761
+ if (txCtx !== null) {
17762
+ const prior = await this.adapter.get(this.vault, outSpec.collection, run.runId);
17763
+ txCtx._executed.push({
17764
+ op: { type: "put", vaultName: this.vault, collectionName: outSpec.collection, id: run.runId },
17765
+ priorEnvelope: prior
17766
+ });
17767
+ }
17768
+ await outputCollection.put(run.runId, patched);
16798
17769
  continue;
16799
17770
  }
16800
17771
  if (txCtx !== null) {
16801
- const prior = await this.adapter.get(this.vault, outSpec.collection, id);
17772
+ const prior = await this.adapter.get(this.vault, outSpec.collection, run.runId);
16802
17773
  txCtx._executed.push({
16803
17774
  op: {
16804
17775
  type: "put",
16805
17776
  vaultName: this.vault,
16806
17777
  collectionName: outSpec.collection,
16807
- id
17778
+ id: run.runId
16808
17779
  },
16809
17780
  priorEnvelope: prior
16810
17781
  });
16811
17782
  }
16812
- await outputCollection.put(id, out.value);
17783
+ await outputCollection.put(run.runId, out.value);
16813
17784
  }
16814
- } else {
16815
- await markStale(registry, spec, id);
16816
17785
  }
16817
17786
  }
16818
17787
  }
@@ -17029,6 +17998,7 @@ var Collection = class {
17029
17998
  if (!internal) {
17030
17999
  await this.dispatchMaterializedViewsOnDelete(id);
17031
18000
  await this.dispatchArrayDerivationsOnDelete(id);
18001
+ if (existing) await this.dispatchRollupsOnDelete(existing.record);
17032
18002
  }
17033
18003
  }
17034
18004
  /**
@@ -17185,6 +18155,29 @@ var Collection = class {
17185
18155
  hasReadTransforms() {
17186
18156
  return this.moneyFields !== void 0 && Object.keys(this.moneyFields).length > 0 || this.i18nFields !== void 0 && Object.keys(this.i18nFields).length > 0 || this.dictKeyFields !== void 0 && Object.keys(this.dictKeyFields).length > 0;
17187
18157
  }
18158
+ /**
18159
+ * Scan-mode full-text search over a plain-text `field` (#308). Decrypts the
18160
+ * collection in memory and ranks records by BM25 against the tokenized query.
18161
+ * **Zero added store leakage** — pure client-side scan; nothing searchable is
18162
+ * written to the store. (A store-usable blind index for at-scale search is a
18163
+ * separate, gated opt-in — see the #308 design note.) Eager mode only.
18164
+ *
18165
+ * `opts.match` (`'any'` default | `'all'`), `opts.prefix` (last query term as
18166
+ * a prefix → typeahead), `opts.limit` (top-N). Returns `{ id, score, record }`
18167
+ * ranked by descending score. The default tokenizer is word-boundary based —
18168
+ * see `src/search/tokenize.ts` for the Thai/CJK caveat.
18169
+ */
18170
+ async search(field, query, opts = {}) {
18171
+ if (this.lazy) {
18172
+ throw new Error(
18173
+ `Collection "${this.name}": search() (scan mode) requires eager mode (prefetch: true). A store-usable blind index for lazy / at-scale search is a separate gated opt-in (#308).`
18174
+ );
18175
+ }
18176
+ await this.ensureHydrated();
18177
+ const entries = [];
18178
+ for (const [id, e] of this.cache) entries.push({ id, record: e.record });
18179
+ return searchScan(entries, field, query, opts);
18180
+ }
17188
18181
  // ─── Bulk operations ─────────────────────────────────────
17189
18182
  /**
17190
18183
  * Put many records in one call. Each item is processed sequentially
@@ -17362,6 +18355,10 @@ var Collection = class {
17362
18355
  leftCollection,
17363
18356
  resolveRef: (field) => resolver.resolveRef(leftCollection, field),
17364
18357
  resolveSource: (collectionName) => resolver.resolveSource(collectionName),
18358
+ // #285 §3 — flow the vault/collection default locale to joins so a
18359
+ // joined i18n field resolves like get()/list() when no per-call
18360
+ // locale is given; toArray({ locale }) overrides it.
18361
+ ...this.defaultLocale !== void 0 ? { defaultLocale: this.defaultLocale } : {},
17365
18362
  ...resolver.resolveDictSource ? { resolveDictSource: (field) => resolver.resolveDictSource(leftCollection, field) } : {}
17366
18363
  } : void 0;
17367
18364
  return new Query(source, void 0, joinContext, this.aggregateStrategy);
@@ -17450,7 +18447,10 @@ var Collection = class {
17450
18447
  };
17451
18448
  this.emitter.on("change", handler);
17452
18449
  return () => this.emitter.off("change", handler);
17453
- }
18450
+ },
18451
+ // #285 §3 — expose this (right-side) collection's i18nText descriptors so
18452
+ // the join executor can resolve joined i18n fields at the `join` layer.
18453
+ ...this.i18nFields !== void 0 ? { i18nFields: this.i18nFields } : {}
17454
18454
  };
17455
18455
  }
17456
18456
  /**
@@ -17683,6 +18683,10 @@ var Collection = class {
17683
18683
  leftCollection,
17684
18684
  resolveRef: (field) => resolver.resolveRef(leftCollection, field),
17685
18685
  resolveSource: (collectionName) => resolver.resolveSource(collectionName),
18686
+ // #285 §3 — flow the vault/collection default locale to joins so a
18687
+ // joined i18n field resolves like get()/list() when no per-call
18688
+ // locale is given; toArray({ locale }) overrides it.
18689
+ ...this.defaultLocale !== void 0 ? { defaultLocale: this.defaultLocale } : {},
17686
18690
  ...resolver.resolveDictSource ? { resolveDictSource: (field) => resolver.resolveDictSource(leftCollection, field) } : {}
17687
18691
  } : void 0;
17688
18692
  return new ScanBuilder(
@@ -17883,12 +18887,12 @@ var Collection = class {
17883
18887
  }
17884
18888
  }
17885
18889
  persisted.clear();
17886
- for (const recordId3 of canonicalIds) {
17887
- const envelope = await this.adapter.get(this.vault, this.name, recordId3);
18890
+ for (const recordId4 of canonicalIds) {
18891
+ const envelope = await this.adapter.get(this.vault, this.name, recordId4);
17888
18892
  if (!envelope) continue;
17889
18893
  const record = await this.decryptRecord(envelope, { skipValidation: true });
17890
18894
  if (record === null) continue;
17891
- await this.maintainPersistedIndexesOnPut(recordId3, record, null, envelope._v);
18895
+ await this.maintainPersistedIndexesOnPut(recordId4, record, null, envelope._v);
17892
18896
  }
17893
18897
  this.persistedIndexesLoaded = true;
17894
18898
  }
@@ -18083,14 +19087,15 @@ var Collection = class {
18083
19087
  (d) => isStaticDictDescriptor(d) && d.displayLocale !== void 0
18084
19088
  );
18085
19089
  if (!locale && !hasStaticDisplay) return result;
19090
+ const layer = localeOpts?._layer ?? "read";
18086
19091
  if (locale && hasI18n && this.i18nFields) {
18087
- result = this.i18nStrategy.applyI18nLocale(result, this.i18nFields, locale, localeOpts?.fallback);
19092
+ result = this.i18nStrategy.applyI18nLocale(result, this.i18nFields, locale, localeOpts?.fallback, layer);
18088
19093
  }
18089
19094
  if (hasDict && this.dictKeyFields && this.dictLabelResolver && locale !== "raw") {
18090
19095
  const withLabels = { ...result };
18091
19096
  const resolver = this.dictLabelResolver;
18092
19097
  for (const [field, desc] of Object.entries(this.dictKeyFields)) {
18093
- const policy = desc.onMissing ? resolvePolicy(desc.onMissing, "read") : "null";
19098
+ const policy = desc.onMissing ? resolvePolicy(desc.onMissing, layer) : "null";
18094
19099
  const fallback = policy === "substitute" ? localeOpts?.fallback ?? desc.substitute : localeOpts?.fallback;
18095
19100
  const effLocale = locale ?? (isStaticDictDescriptor(desc) ? desc.displayLocale : void 0);
18096
19101
  const resolveKey = async (key) => {
@@ -18239,6 +19244,34 @@ var Collection = class {
18239
19244
  }
18240
19245
  }
18241
19246
  }
19247
+ /**
19248
+ * @internal — hard-delete this record's persisted `_idx/<field>/<recordId>`
19249
+ * side-cars for the erasure path (#401). `forget()` crypto-shreds the body but
19250
+ * keeps the collection DEK, under which these side-cars are encrypted — so
19251
+ * without this they leave the indexed field VALUES readable after a "forget".
19252
+ *
19253
+ * Content-free: the side-car id is `encodeIdxId(def.key, id)`, so it needs no
19254
+ * body decode (the body is being shredded). Eager mode has no durable side-car
19255
+ * → no-op. The in-memory mirror is left as-is: it is ephemeral (rebuilt from
19256
+ * the now-deleted side-cars on reopen) and live reads skip the tombstone, so a
19257
+ * stale mirror hit cannot surface the erased record. Returns the count deleted
19258
+ * + the `def.key`s whose delete FAILED (residue that still leaks the value).
19259
+ */
19260
+ async _purgePersistedIndexes(id) {
19261
+ const persisted = this.persistedIndexes;
19262
+ if (!persisted) return { purged: 0, residue: [] };
19263
+ let purged = 0;
19264
+ const residue = [];
19265
+ for (const def of persisted.definitions()) {
19266
+ try {
19267
+ await this.adapter.delete(this.vault, this.name, encodeIdxId(def.key, id));
19268
+ purged++;
19269
+ } catch {
19270
+ residue.push(def.key);
19271
+ }
19272
+ }
19273
+ return { purged, residue };
19274
+ }
18242
19275
  /**
18243
19276
  * Bulk-load the persisted-index mirror from `_idx/<field>/*` side-cars
18244
19277
  * on first lazy-mode query. Idempotent — subsequent calls short-circuit
@@ -19124,8 +20157,8 @@ var DeferredNumberingStore = class {
19124
20157
  }
19125
20158
  await this.adapter.put(this.vault, collection, id, env, expectedVersion);
19126
20159
  }
19127
- pendingId(series, recordId3) {
19128
- return `${series}::${recordId3}`;
20160
+ pendingId(series, recordId4) {
20161
+ return `${series}::${recordId4}`;
19129
20162
  }
19130
20163
  /** Current last-assigned serial for a series (0 if none). */
19131
20164
  async peek(series) {
@@ -19139,16 +20172,16 @@ var DeferredNumberingStore = class {
19139
20172
  * at the next pass (the record's `field` is the durable source of truth —
19140
20173
  * `assigned` is an in-process convenience that a crash may drop).
19141
20174
  */
19142
- async enqueue(series, recordId3) {
20175
+ async enqueue(series, recordId4) {
19143
20176
  const cfg = this.configs.get(series);
19144
20177
  if (!cfg) throw new NumberingUncertaintyError(series);
19145
20178
  if (typeof this.adapter.getStoreTime !== "function") throw new NumberingUncertaintyError(series);
19146
20179
  const st = await this.adapter.getStoreTime();
19147
- const id = this.pendingId(series, recordId3);
20180
+ const id = this.pendingId(series, recordId4);
19148
20181
  const { env } = await this.readJson(NUMBERING_PENDING_COLLECTION, id);
19149
20182
  const entry = {
19150
20183
  series,
19151
- recordId: recordId3,
20184
+ recordId: recordId4,
19152
20185
  collection: cfg.collection,
19153
20186
  field: cfg.field,
19154
20187
  storeEarliest: st.earliest,
@@ -19371,6 +20404,7 @@ var NO_PERIODS = {
19371
20404
  };
19372
20405
 
19373
20406
  // src/vault.ts
20407
+ init_core();
19374
20408
  init_errors();
19375
20409
 
19376
20410
  // src/periods/periods.ts
@@ -19505,13 +20539,13 @@ async function runCompaction(ctx, options = {}) {
19505
20539
  collectionsWithPolicy += 1;
19506
20540
  byCollection[collectionName] = { records: 0, evicted: 0 };
19507
20541
  const ids = await ctx.listRecords(collectionName);
19508
- for (const recordId3 of ids) {
20542
+ for (const recordId4 of ids) {
19509
20543
  if (evicted >= maxEvictions) break outer;
19510
- const record = await ctx.getRecord(collectionName, recordId3).catch(() => null);
20544
+ const record = await ctx.getRecord(collectionName, recordId4).catch(() => null);
19511
20545
  if (record === null) continue;
19512
20546
  records += 1;
19513
20547
  byCollection[collectionName].records += 1;
19514
- const slots = await ctx.listSlots(collectionName, recordId3).catch(() => []);
20548
+ const slots = await ctx.listSlots(collectionName, recordId4).catch(() => []);
19515
20549
  for (const slot of slots) {
19516
20550
  if (evicted >= maxEvictions) break outer;
19517
20551
  const policy = config[slot.name];
@@ -19523,11 +20557,11 @@ async function runCompaction(ctx, options = {}) {
19523
20557
  continue;
19524
20558
  }
19525
20559
  if (!dryRun) {
19526
- await ctx.deleteSlot(collectionName, recordId3, slot.name);
20560
+ await ctx.deleteSlot(collectionName, recordId4, slot.name);
19527
20561
  await writeAuditEntry(ctx, {
19528
- id: generateEvictionId(collectionName, recordId3, slot.name),
20562
+ id: generateEvictionId(collectionName, recordId4, slot.name),
19529
20563
  collection: collectionName,
19530
- recordId: recordId3,
20564
+ recordId: recordId4,
19531
20565
  slotName: slot.name,
19532
20566
  blobHash: slot.eTag,
19533
20567
  reason,
@@ -19594,11 +20628,11 @@ function evaluatePolicy(policy, record, slot, now) {
19594
20628
  if (predicateTriggered) return "predicate";
19595
20629
  return null;
19596
20630
  }
19597
- function generateEvictionId(collection, recordId3, slotName) {
20631
+ function generateEvictionId(collection, recordId4, slotName) {
19598
20632
  const rand = globalThis.crypto.getRandomValues(new Uint8Array(8));
19599
20633
  let suffix = "";
19600
20634
  for (const b of rand) suffix += b.toString(16).padStart(2, "0");
19601
- return `${collection}__${recordId3}__${slotName}__${suffix}`;
20635
+ return `${collection}__${recordId4}__${slotName}__${suffix}`;
19602
20636
  }
19603
20637
  async function writeAuditEntry(ctx, entry) {
19604
20638
  const json = JSON.stringify(entry);
@@ -19649,7 +20683,7 @@ async function deriveMagicLinkContentKey(serverSecret, token, vault) {
19649
20683
  ["encrypt", "decrypt"]
19650
20684
  );
19651
20685
  }
19652
- async function writeMagicLinkGrant(store, vault, grantor, contentKey, grantKek, recordId3, opts) {
20686
+ async function writeMagicLinkGrant(store, vault, grantor, contentKey, grantKek, recordId4, opts) {
19653
20687
  const collectionName = opts.collection ?? null;
19654
20688
  const sourceKey = collectionName ? dekKey(collectionName, opts.tier) : `__any#${opts.tier}`;
19655
20689
  const sourceDek = grantor.deks.get(sourceKey);
@@ -19662,7 +20696,7 @@ async function writeMagicLinkGrant(store, vault, grantor, contentKey, grantKek,
19662
20696
  const until = typeof opts.until === "string" ? opts.until : opts.until.toISOString();
19663
20697
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
19664
20698
  const payload = {
19665
- id: recordId3,
20699
+ id: recordId4,
19666
20700
  toUser: opts.toUser,
19667
20701
  fromUser: grantor.userId,
19668
20702
  tier: opts.tier,
@@ -19682,11 +20716,11 @@ async function writeMagicLinkGrant(store, vault, grantor, contentKey, grantKek,
19682
20716
  _data: data,
19683
20717
  _by: grantor.userId
19684
20718
  };
19685
- await store.put(vault, MAGIC_LINK_GRANTS_COLLECTION, recordId3, envelope);
19686
- return { recordId: recordId3, payload };
20719
+ await store.put(vault, MAGIC_LINK_GRANTS_COLLECTION, recordId4, envelope);
20720
+ return { recordId: recordId4, payload };
19687
20721
  }
19688
- async function readMagicLinkGrantRecord(store, vault, contentKey, recordId3) {
19689
- const env = await store.get(vault, MAGIC_LINK_GRANTS_COLLECTION, recordId3);
20722
+ async function readMagicLinkGrantRecord(store, vault, contentKey, recordId4) {
20723
+ const env = await store.get(vault, MAGIC_LINK_GRANTS_COLLECTION, recordId4);
19690
20724
  if (!env) return null;
19691
20725
  try {
19692
20726
  const json = await decrypt(env._iv, env._data, contentKey);
@@ -20342,13 +21376,17 @@ var Vault = class {
20342
21376
  */
20343
21377
  overlayedViewRegistry = null;
20344
21378
  /**
20345
- * Cached read-only facade handed to guard callbacks via `ctx.vault`,
20346
- * and to derivation callbacks via `derive(source, ctx)`. Allocated
20347
- * eagerly inside `_initGuards()` and/or `_initDerivations()` so read
21379
+ * Cached read-only facades handed to guard callbacks via `ctx.vault`
21380
+ * and to derivation callbacks via `derive(source, ctx)`. Split by
21381
+ * resolution layer (#285): the guard facade reads at `layer:'guard'`,
21382
+ * the derivation facade at `layer:'derivation'`, so i18nText / dictKey
21383
+ * fields resolve under that layer's `onMissing` policy. Allocated
21384
+ * eagerly inside `_initGuards()` / `_initDerivations()` so read
20348
21385
  * accessors stay synchronous (callers in `tx/transaction.ts` rely on
20349
- * that). Stays `null` for vaults with neither subsystem configured.
21386
+ * that). Each stays `null` for vaults without that subsystem.
20350
21387
  */
20351
- readOnlyFacade = null;
21388
+ guardFacade = null;
21389
+ derivationFacade = null;
20352
21390
  getDEK;
20353
21391
  /**
20354
21392
  * Per-principal user envelope API.
@@ -20510,6 +21548,10 @@ var Vault = class {
20510
21548
  i18nFieldRegistry = /* @__PURE__ */ new Map();
20511
21549
  /** Cache of DictionaryHandle instances, one per dictionary name. */
20512
21550
  dictionaryCache = /* @__PURE__ */ new Map();
21551
+ /** Registered link specs (#377-B), keyed by link name; set by `vault.link()`. */
21552
+ linkRegistry = /* @__PURE__ */ new Map();
21553
+ /** Cache of LinkSet handles, one per link name. */
21554
+ linkSetCache = /* @__PURE__ */ new Map();
20513
21555
  /** — subscribers for cross-tier access events. */
20514
21556
  crossTierSubs = /* @__PURE__ */ new Set();
20515
21557
  /** — currently-active elevation, or null. One per vault. */
@@ -20626,6 +21668,9 @@ var Vault = class {
20626
21668
  if (collectionName === SEQUENCE_COLLECTION) {
20627
21669
  throw new ReservedCollectionNameError(collectionName);
20628
21670
  }
21671
+ if (isLinkCollectionName(collectionName)) {
21672
+ throw new ReservedCollectionNameError(collectionName);
21673
+ }
20629
21674
  let coll = this.collectionCache.get(collectionName);
20630
21675
  if (coll && options?.moneyFields) {
20631
21676
  coll._applyMoneyFields(options.moneyFields);
@@ -20697,6 +21742,7 @@ var Vault = class {
20697
21742
  }));
20698
21743
  schemaUpdateGate = new SchemaUpdateGate(work);
20699
21744
  }
21745
+ const effectiveHistoryConfig = options?.historyConfig ?? this.historyConfig;
20700
21746
  const collOpts = {
20701
21747
  adapter: this.adapter,
20702
21748
  vault: this.name,
@@ -20712,7 +21758,7 @@ var Vault = class {
20712
21758
  schemaFence: this.schemaFence,
20713
21759
  getDEK: this.getDEK,
20714
21760
  onDirty: this.onDirty,
20715
- historyConfig: this.historyConfig,
21761
+ historyConfig: effectiveHistoryConfig,
20716
21762
  // thread the vault-wide blob strategy into every
20717
21763
  // collection. `undefined` is intentionally preserved so the
20718
21764
  // Collection constructor uses its NO_BLOBS default.
@@ -20723,7 +21769,11 @@ var Vault = class {
20723
21769
  historyStrategy: this.historyStrategy,
20724
21770
  i18nStrategy: this.i18nStrategy,
20725
21771
  syncStrategy: this.syncStrategy,
20726
- ledger: this.getLedgerOrNull() ?? void 0,
21772
+ // Per-collection ledger opt-out (#361): when this collection sets
21773
+ // `historyConfig.ledger: false`, withhold the ledger reference so all
21774
+ // four `if (this.ledger)` append sites in Collection no-op. The chain
21775
+ // stays valid — it simply never receives this collection's entries.
21776
+ ledger: effectiveHistoryConfig.ledger === false ? void 0 : this.getLedgerOrNull() ?? void 0,
20727
21777
  refEnforcer: this,
20728
21778
  joinResolver: this,
20729
21779
  defaultLocale: this.locale,
@@ -21084,6 +22134,68 @@ var Vault = class {
21084
22134
  }
21085
22135
  return handle;
21086
22136
  }
22137
+ /**
22138
+ * Declare a managed many-to-many link set (#377-B). Registers a
22139
+ * `_links_<name>` junction between two endpoint collections; access its
22140
+ * rows via `vault.links(name)`. Idempotent for an identical re-declaration;
22141
+ * a conflicting one throws. See {@link links}.
22142
+ *
22143
+ * ```ts
22144
+ * vault.link('saleLineLinks', { a: ref('saleLines'), b: ref('purchaseLines'), onDelete: 'cascade' })
22145
+ * ```
22146
+ *
22147
+ * `a` / `b` accept either a collection name or a `ref(target)` descriptor
22148
+ * (only its `target` is used — links manage their own integrity). `onDelete`
22149
+ * governs what happens to link rows when an endpoint record is deleted
22150
+ * (`'cascade'` default, `'strict'`, `'warn'`).
22151
+ */
22152
+ link(name, spec) {
22153
+ const a = typeof spec.a === "string" ? spec.a : spec.a.target;
22154
+ const b = typeof spec.b === "string" ? spec.b : spec.b.target;
22155
+ for (const [slot, target] of [["a", a], ["b", b]]) {
22156
+ if (!target || target.startsWith("_") || target.includes("/")) {
22157
+ throw new ValidationError(
22158
+ `vault.link("${name}"): endpoint "${slot}" must be a simple collection name, got "${target}".`
22159
+ );
22160
+ }
22161
+ }
22162
+ const resolved = { a, b, ...spec.onDelete ? { onDelete: spec.onDelete } : {} };
22163
+ const existing = this.linkRegistry.get(name);
22164
+ if (existing) {
22165
+ if (existing.a !== resolved.a || existing.b !== resolved.b || (existing.onDelete ?? "cascade") !== (resolved.onDelete ?? "cascade")) {
22166
+ throw new ValidationError(`vault.link("${name}"): conflicting re-declaration.`);
22167
+ }
22168
+ return;
22169
+ }
22170
+ this.linkRegistry.set(name, resolved);
22171
+ }
22172
+ /**
22173
+ * Access a declared link set (#377-B). Throws if `name` was not first
22174
+ * declared via {@link link}. Returns a cached {@link LinkSetHandle}:
22175
+ * `connect(a, b, meta?)`, `disconnect(a, b)`, `has(a, b)`, `of(id)`, `list()`.
22176
+ */
22177
+ links(name) {
22178
+ let handle = this.linkSetCache.get(name);
22179
+ if (!handle) {
22180
+ const spec = this.linkRegistry.get(name);
22181
+ if (!spec) {
22182
+ throw new ValidationError(`vault.links("${name}"): not declared. Call vault.link("${name}", { a, b }) first.`);
22183
+ }
22184
+ handle = new LinkSet(
22185
+ this.adapter,
22186
+ this.name,
22187
+ name,
22188
+ spec,
22189
+ this.encrypted,
22190
+ this.getDEK,
22191
+ this.keyring.userId,
22192
+ this.emitter,
22193
+ async (collection, id) => await this.collection(collection).get(id) !== null
22194
+ );
22195
+ this.linkSetCache.set(name, handle);
22196
+ }
22197
+ return handle;
22198
+ }
21087
22199
  /**
21088
22200
  * Build a `JoinableSource` for a dictKey field, for use in dict joins
21089
22201
  *. Returns a source whose snapshot contains `{ key, ...labels }`
@@ -21243,65 +22355,16 @@ var Vault = class {
21243
22355
  });
21244
22356
  }
21245
22357
  }
21246
- /**
21247
- * Bulk blob extraction primitive.
21248
- *
21249
- * Returns an async-iterable handle over every blob attached to
21250
- * records in the vault. Single capability check (`plaintext/blob`)
21251
- * at handle creation; single audit entry to `_export_audit` before
21252
- * the first yield. Per-blob decryption happens lazily as the
21253
- * consumer pulls tuples.
21254
- *
21255
- * ```ts
21256
- * const handle = vault.exportBlobs({
21257
- * collections: ['invoiceScans'],
21258
- * where: (rec) => (rec as { clientId?: string }).clientId === 'c-123',
21259
- * })
21260
- * for await (const { bytes, meta, recordRef } of handle) {
21261
- * await uploadToColdStorage(bytes, recordRef)
21262
- * }
21263
- * ```
21264
- *
21265
- * @see `@noy-db/hub/store/export-blobs` for the full option surface.
21266
- */
21267
- /**
21268
- * Evict blob slots per the per-collection `blobFields` retention
21269
- * policy.
21270
- *
21271
- * Iterates every collection declared with `{ blobFields: {...} }`.
21272
- * For each record, checks every configured slot against its
21273
- * policy — `retainDays` (age-based TTL) and/or `evictWhen(record)`
21274
- * (predicate) — and evicts matching slots. Every eviction writes
21275
- * one entry to `_blob_eviction_audit` (actor + eTag + reason +
21276
- * timestamp, no plaintext). Consumer-scheduled; noy-db never runs
21277
- * this on its own.
21278
- *
21279
- * ```ts
21280
- * await vault.compact() // run full pass
21281
- * await vault.compact({ dryRun: true }) // preview counts
21282
- * await vault.compact({ maxEvictions: 1000 }) // cap batch
21283
- * ```
21284
- */
21285
- /**
21286
- * Atomic, gap-free numbering. `vault.sequence('invoice-2026').next()`
21287
- * returns 1, 2, 3, … with no gaps or duplicates under concurrency, via
21288
- * an optimistic-CAS counter at `_sequences/<name>`. Each name is an
21289
- * independent sequence.
21290
- *
21291
- * **Online-only:** `next()` throws `SequenceOfflineError` unless the
21292
- * store advertises `capabilities.casAtomic` — gap-free numbering cannot
21293
- * be serialized by an offline / non-CAS writer.
21294
- *
21295
- * ```ts
21296
- * const n = await vault.sequence('invoice-2026').next() // 1, then 2, …
21297
- * const cur = await vault.sequence('invoice-2026').peek() // current value, no allocation
21298
- * ```
21299
- */
21300
22358
  sequence(series, opts) {
21301
22359
  if (series.includes("\0")) {
21302
22360
  throw new ValidationError(`sequence("${series}"): series name must not contain a null byte (\\x00).`);
21303
22361
  }
21304
22362
  if (this.numberingConfigs.has(series)) {
22363
+ if (opts?.format !== void 0) {
22364
+ throw new ValidationError(
22365
+ `sequence("${series}") is a deferred-numbering series; the format option applies to CAS sequences only.`
22366
+ );
22367
+ }
21305
22368
  const eng = this.deferred();
21306
22369
  return {
21307
22370
  next: async (nextOpts) => {
@@ -21325,7 +22388,17 @@ var Vault = class {
21325
22388
  actor: this.keyring.userId
21326
22389
  });
21327
22390
  }
21328
- return this.sequenceStore.handle(resolveSequenceKey(series, opts));
22391
+ const handle = this.sequenceStore.handle(resolveSequenceKey(series, opts));
22392
+ if (opts?.format === void 0) return handle;
22393
+ const render = compileSequenceFormat(opts.format, series, opts.partition);
22394
+ return {
22395
+ next: async (nextOpts) => {
22396
+ const serial = await handle.next(nextOpts);
22397
+ return { serial, formatted: render(serial) };
22398
+ },
22399
+ peek: () => handle.peek(),
22400
+ seedTo: (n) => handle.seedTo(n)
22401
+ };
21329
22402
  }
21330
22403
  /** @internal — lazily build the deferred-numbering engine with a cache-coherent stamp. */
21331
22404
  deferred() {
@@ -21340,11 +22413,11 @@ var Vault = class {
21340
22413
  // Stamp THROUGH the Collection layer so cache/indexes/MVs stay coherent —
21341
22414
  // `this.collection(name)` returns the shared cached instance, so a
21342
22415
  // subsequent user `collection.get(id)` sees the assigned serial.
21343
- stamp: async (collection, recordId3, field, serial) => {
22416
+ stamp: async (collection, recordId4, field, serial) => {
21344
22417
  const coll = this.collection(collection);
21345
- const rec = await coll.get(recordId3);
22418
+ const rec = await coll.get(recordId4);
21346
22419
  if (!rec) return false;
21347
- await coll.put(recordId3, { ...rec, [field]: serial });
22420
+ await coll.put(recordId4, { ...rec, [field]: serial });
21348
22421
  return true;
21349
22422
  }
21350
22423
  });
@@ -21552,6 +22625,43 @@ var Vault = class {
21552
22625
  if (descriptor.mode !== "strict") continue;
21553
22626
  const rawId = obj[field];
21554
22627
  if (rawId === null || rawId === void 0) continue;
22628
+ if (isRefArray(descriptor)) {
22629
+ if (!Array.isArray(rawId)) {
22630
+ throw new RefIntegrityError({
22631
+ collection: collectionName,
22632
+ id: obj["id"] ?? "<unknown>",
22633
+ field,
22634
+ refTo: descriptor.target,
22635
+ refId: null,
22636
+ message: `Array ref field "${collectionName}.${field}" must be an array, got ${typeof rawId}.`
22637
+ });
22638
+ }
22639
+ const arrTarget = this.collection(descriptor.target);
22640
+ for (const el of rawId) {
22641
+ if (typeof el !== "string" && typeof el !== "number") {
22642
+ throw new RefIntegrityError({
22643
+ collection: collectionName,
22644
+ id: obj["id"] ?? "<unknown>",
22645
+ field,
22646
+ refTo: descriptor.target,
22647
+ refId: null,
22648
+ message: `Array ref "${collectionName}.${field}" elements must be strings or numbers, got ${typeof el}.`
22649
+ });
22650
+ }
22651
+ const elId = String(el);
22652
+ if (!await arrTarget.get(elId)) {
22653
+ throw new RefIntegrityError({
22654
+ collection: collectionName,
22655
+ id: obj["id"] ?? "<unknown>",
22656
+ field,
22657
+ refTo: descriptor.target,
22658
+ refId: elId,
22659
+ message: `Strict array ref "${collectionName}.${field}" \u2192 "${descriptor.target}" cannot be satisfied: element id "${elId}" not found in "${descriptor.target}".`
22660
+ });
22661
+ }
22662
+ }
22663
+ continue;
22664
+ }
21555
22665
  if (typeof rawId !== "string" && typeof rawId !== "number") {
21556
22666
  throw new RefIntegrityError({
21557
22667
  collection: collectionName,
@@ -21601,6 +22711,11 @@ var Vault = class {
21601
22711
  const allRecords = await fromCollection.list();
21602
22712
  const matches = allRecords.filter((rec) => {
21603
22713
  const raw = rec[rule.field];
22714
+ if (rule.isArray) {
22715
+ return Array.isArray(raw) && raw.some(
22716
+ (el) => (typeof el === "string" || typeof el === "number") && String(el) === id
22717
+ );
22718
+ }
21604
22719
  if (typeof raw !== "string" && typeof raw !== "number") return false;
21605
22720
  return String(raw) === id;
21606
22721
  });
@@ -21639,10 +22754,45 @@ var Vault = class {
21639
22754
  }
21640
22755
  }
21641
22756
  }
22757
+ await this.enforceLinksOnDelete(collectionName, id);
21642
22758
  } finally {
21643
22759
  this.cascadeInProgress.delete(key);
21644
22760
  }
21645
22761
  }
22762
+ /**
22763
+ * @internal — apply link `onDelete` policy when an endpoint record is
22764
+ * deleted (#377-B). `'strict'` throws (blocks the delete), `'cascade'`
22765
+ * removes the touching link rows (tx-atomic when a transaction is active),
22766
+ * `'warn'` leaves orphans for `checkIntegrity()`.
22767
+ */
22768
+ async enforceLinksOnDelete(collectionName, id) {
22769
+ for (const [name, spec] of this.linkRegistry) {
22770
+ if (spec.a !== collectionName && spec.b !== collectionName) continue;
22771
+ const handle = this.links(name);
22772
+ const touching = await handle._rowsTouchingEndpoint(collectionName, id);
22773
+ if (touching.length === 0) continue;
22774
+ const mode = spec.onDelete ?? "cascade";
22775
+ if (mode === "warn") continue;
22776
+ if (mode === "strict") {
22777
+ throw new LinkIntegrityError(name, collectionName, id, touching.length);
22778
+ }
22779
+ const linkColl = handle._collectionName;
22780
+ const txCtx = this.noydb._activeTxContextOrNull;
22781
+ for (const row of touching) {
22782
+ const rowKey = linkRowKey(row.a, row.b);
22783
+ if (txCtx !== null) {
22784
+ const prior = await this.adapter.get(this.name, linkColl, rowKey);
22785
+ if (prior !== null) {
22786
+ txCtx._executed.push({
22787
+ op: { type: "delete", vaultName: this.name, collectionName: linkColl, id: rowKey },
22788
+ priorEnvelope: prior
22789
+ });
22790
+ }
22791
+ }
22792
+ await handle.disconnect(row.a, row.b);
22793
+ }
22794
+ }
22795
+ }
21646
22796
  // ─── Join resolver) ────────────────────
21647
22797
  /**
21648
22798
  * Look up the `RefDescriptor` the left collection declared for a
@@ -21703,6 +22853,23 @@ var Vault = class {
21703
22853
  for (const [field, descriptor] of Object.entries(refs)) {
21704
22854
  const rawId = record[field];
21705
22855
  if (rawId === null || rawId === void 0) continue;
22856
+ const target = this.collection(descriptor.target);
22857
+ if (isRefArray(descriptor)) {
22858
+ if (!Array.isArray(rawId)) {
22859
+ violations.push({ collection: collectionName, id: recId, field, refTo: descriptor.target, refId: rawId, mode: descriptor.mode });
22860
+ continue;
22861
+ }
22862
+ for (const el of rawId) {
22863
+ if (typeof el !== "string" && typeof el !== "number") {
22864
+ violations.push({ collection: collectionName, id: recId, field, refTo: descriptor.target, refId: el, mode: descriptor.mode });
22865
+ continue;
22866
+ }
22867
+ if (!await target.get(String(el))) {
22868
+ violations.push({ collection: collectionName, id: recId, field, refTo: descriptor.target, refId: el, mode: descriptor.mode });
22869
+ }
22870
+ }
22871
+ continue;
22872
+ }
21706
22873
  if (typeof rawId !== "string" && typeof rawId !== "number") {
21707
22874
  violations.push({
21708
22875
  collection: collectionName,
@@ -21715,7 +22882,6 @@ var Vault = class {
21715
22882
  continue;
21716
22883
  }
21717
22884
  const refId = String(rawId);
21718
- const target = this.collection(descriptor.target);
21719
22885
  const exists = await target.get(refId);
21720
22886
  if (!exists) {
21721
22887
  violations.push({
@@ -21730,6 +22896,19 @@ var Vault = class {
21730
22896
  }
21731
22897
  }
21732
22898
  }
22899
+ for (const [name, spec] of this.linkRegistry) {
22900
+ const linkColl = linkCollectionName(name);
22901
+ const rows = await this.links(name).list();
22902
+ for (const row of rows) {
22903
+ const rowKey = linkRowKey(row.a, row.b);
22904
+ if (await this.collection(spec.a).get(row.a) === null) {
22905
+ violations.push({ collection: linkColl, id: rowKey, field: "a", refTo: spec.a, refId: row.a, mode: spec.onDelete ?? "cascade" });
22906
+ }
22907
+ if (await this.collection(spec.b).get(row.b) === null) {
22908
+ violations.push({ collection: linkColl, id: rowKey, field: "b", refTo: spec.b, refId: row.b, mode: spec.onDelete ?? "cascade" });
22909
+ }
22910
+ }
22911
+ }
21733
22912
  return { violations };
21734
22913
  }
21735
22914
  /**
@@ -21836,6 +23015,8 @@ var Vault = class {
21836
23015
  const blobResidueCollections = /* @__PURE__ */ new Set();
21837
23016
  let blobsShredded = 0;
21838
23017
  let blobsRetainedShared = 0;
23018
+ let indexPostingsPurged = 0;
23019
+ const indexResidue = [];
21839
23020
  const blobsEnabled = this.blobStrategy !== void 0;
21840
23021
  const actor = this.keyring.userId;
21841
23022
  for (const ref2 of refs) {
@@ -21857,6 +23038,9 @@ var Vault = class {
21857
23038
  ref2.id,
21858
23039
  actor
21859
23040
  );
23041
+ const idxPurge = await coll._purgePersistedIndexes(ref2.id);
23042
+ indexPostingsPurged += idxPurge.purged;
23043
+ for (const field of idxPurge.residue) indexResidue.push(`${ref2.collection}:${ref2.id}:${field}`);
21860
23044
  if (blobsEnabled) {
21861
23045
  const r = await this.collection(ref2.collection).blob(ref2.id).shredAllForRecord();
21862
23046
  blobsShredded += r.shredded.length;
@@ -21892,7 +23076,9 @@ var Vault = class {
21892
23076
  unmigratedCount: unmigratedRecords.length,
21893
23077
  blobsShredded,
21894
23078
  blobsRetainedShared,
21895
- blobResidueCollections: [...blobResidueCollections]
23079
+ blobResidueCollections: [...blobResidueCollections],
23080
+ indexPostingsPurged,
23081
+ indexResidueCount: indexResidue.length
21896
23082
  })
21897
23083
  });
21898
23084
  return {
@@ -21904,6 +23090,8 @@ var Vault = class {
21904
23090
  blobsShredded,
21905
23091
  blobsRetainedShared,
21906
23092
  blobResidueCollections: [...blobResidueCollections],
23093
+ indexPostingsPurged,
23094
+ indexResidue,
21907
23095
  ledgerEntry
21908
23096
  };
21909
23097
  }
@@ -22005,7 +23193,7 @@ var Vault = class {
22005
23193
  const registry = new GuardRegistry2();
22006
23194
  for (const h of handles) registry.register(h.spec);
22007
23195
  this.guardRegistry = registry;
22008
- this.readOnlyFacade = new ReadOnlyVaultFacade2(this);
23196
+ this.guardFacade = new ReadOnlyVaultFacade2(this, "guard");
22009
23197
  }
22010
23198
  /**
22011
23199
  * @internal — The gate handler in Noydb.#registerGuardGate calls into
@@ -22036,8 +23224,8 @@ var Vault = class {
22036
23224
  }
22037
23225
  registry.validate();
22038
23226
  this.derivationRegistry = registry;
22039
- if (this.readOnlyFacade === null) {
22040
- this.readOnlyFacade = new ReadOnlyVaultFacade2(this);
23227
+ if (this.derivationFacade === null) {
23228
+ this.derivationFacade = new ReadOnlyVaultFacade2(this, "derivation");
22041
23229
  }
22042
23230
  }
22043
23231
  /**
@@ -22154,7 +23342,7 @@ var Vault = class {
22154
23342
  const { DerivationExecutor: DerivationExecutor2 } = await Promise.resolve().then(() => (init_executor2(), executor_exports2));
22155
23343
  const sourceColl = this.collection(sourceCollection);
22156
23344
  const records = await sourceColl.list();
22157
- const ctx = { vault: this.readOnlyFacade ?? new (await Promise.resolve().then(() => (init_read_only_facade(), read_only_facade_exports))).ReadOnlyVaultFacade(this) };
23345
+ const ctx = { vault: this.derivationFacade ?? new (await Promise.resolve().then(() => (init_read_only_facade(), read_only_facade_exports))).ReadOnlyVaultFacade(this, "derivation") };
22158
23346
  let derived = 0;
22159
23347
  let failed = 0;
22160
23348
  for (const record of records) {
@@ -22219,17 +23407,18 @@ var Vault = class {
22219
23407
  * never see null).
22220
23408
  */
22221
23409
  _getReadOnlyFacade() {
22222
- return this.readOnlyFacade;
23410
+ return this.guardFacade;
22223
23411
  }
22224
23412
  /**
22225
- * Internal lazy-allocator for the read-only facade. Used as a
22226
- * defensive fallback; in practice `_initGuards()` eagerly
22227
- * instantiates this, so the lazy path is a no-op.
23413
+ * Internal lazy-allocator for the derivation read-only facade
23414
+ * (`layer:'derivation'`). Used as a defensive fallback; in practice
23415
+ * `_initDerivations()` eagerly instantiates this, so the lazy path is
23416
+ * a no-op.
22228
23417
  */
22229
23418
  _ensureReadOnlyFacade() {
22230
- if (this.readOnlyFacade !== null) return this.readOnlyFacade;
23419
+ if (this.derivationFacade !== null) return this.derivationFacade;
22231
23420
  throw new Error(
22232
- "Vault: guard hook fired before _initGuards() completed. This typically means the vault was opened via the sync fallback path (Noydb.vault(name)) without first calling await db.openVault(name). See issue #132."
23421
+ "Vault: derivation hook fired before _initDerivations() completed. This typically means the vault was opened via the sync fallback path (Noydb.vault(name)) without first calling await db.openVault(name). See issue #132."
22233
23422
  );
22234
23423
  }
22235
23424
  /**
@@ -22350,7 +23539,7 @@ var Vault = class {
22350
23539
  *
22351
23540
  * @internal
22352
23541
  */
22353
- async _logConsent(op, collection, recordId3) {
23542
+ async _logConsent(op, collection, recordId4) {
22354
23543
  const ctx = this.consentContext;
22355
23544
  if (!ctx) return;
22356
23545
  await this.consentStrategy.write(
@@ -22363,7 +23552,7 @@ var Vault = class {
22363
23552
  consentHash: ctx.consentHash,
22364
23553
  op,
22365
23554
  collection,
22366
- recordId: recordId3
23555
+ recordId: recordId4
22367
23556
  },
22368
23557
  this.getDEK
22369
23558
  );
@@ -22546,14 +23735,14 @@ var Vault = class {
22546
23735
  * the HKDF derivation, record-id composition, and batch logic so the
22547
23736
  * grantor doesn't touch this method directly.
22548
23737
  */
22549
- async writeMagicLinkGrant(contentKey, grantKek, recordId3, opts) {
23738
+ async writeMagicLinkGrant(contentKey, grantKek, recordId4, opts) {
22550
23739
  return writeMagicLinkGrant(
22551
23740
  this.adapter,
22552
23741
  this.name,
22553
23742
  this.keyring,
22554
23743
  contentKey,
22555
23744
  grantKek,
22556
- recordId3,
23745
+ recordId4,
22557
23746
  opts
22558
23747
  );
22559
23748
  }
@@ -23165,6 +24354,8 @@ var Vault = class {
23165
24354
  */
23166
24355
  async *exportStream(opts = {}) {
23167
24356
  const granularity = opts.granularity ?? "collection";
24357
+ const exportLocale = opts.resolveLabels;
24358
+ const localeOpts = exportLocale !== void 0 ? { locale: exportLocale, _layer: "export" } : void 0;
23168
24359
  const snapshot = await this.adapter.loadAll(this.name);
23169
24360
  const collectionNames = Object.keys(snapshot).sort();
23170
24361
  const ledgerHead = opts.withLedgerHead ? await (async () => {
@@ -23174,19 +24365,21 @@ var Vault = class {
23174
24365
  return head ? { hash: head.hash, index: head.entry.index, ts: head.entry.ts } : void 0;
23175
24366
  })() : void 0;
23176
24367
  const dictSnapshotCache = /* @__PURE__ */ new Map();
23177
- for (const collectionName of collectionNames) {
23178
- const dictFields = this.dictKeyFieldRegistry.get(collectionName);
23179
- if (dictFields && Object.keys(dictFields).length > 0) {
23180
- const snap = {};
23181
- for (const [fieldName, dictName] of Object.entries(dictFields)) {
23182
- const entries = await this.dictionary(dictName).list();
23183
- const keyMap = {};
23184
- for (const entry of entries) {
23185
- keyMap[entry.key] = entry.labels;
24368
+ if (exportLocale === void 0) {
24369
+ for (const collectionName of collectionNames) {
24370
+ const dictFields = this.dictKeyFieldRegistry.get(collectionName);
24371
+ if (dictFields && Object.keys(dictFields).length > 0) {
24372
+ const snap = {};
24373
+ for (const [fieldName, dictName] of Object.entries(dictFields)) {
24374
+ const entries = await this.dictionary(dictName).list();
24375
+ const keyMap = {};
24376
+ for (const entry of entries) {
24377
+ keyMap[entry.key] = entry.labels;
24378
+ }
24379
+ snap[fieldName] = keyMap;
23186
24380
  }
23187
- snap[fieldName] = keyMap;
24381
+ dictSnapshotCache.set(collectionName, snap);
23188
24382
  }
23189
- dictSnapshotCache.set(collectionName, snap);
23190
24383
  }
23191
24384
  }
23192
24385
  for (const collectionName of collectionNames) {
@@ -23199,7 +24392,7 @@ var Vault = class {
23199
24392
  if (granularity === "collection") {
23200
24393
  const records = [];
23201
24394
  for (const id of ids) {
23202
- const record = await coll.get(id);
24395
+ const record = await coll.get(id, localeOpts);
23203
24396
  if (record !== null) records.push(record);
23204
24397
  }
23205
24398
  const chunk = {
@@ -23213,7 +24406,7 @@ var Vault = class {
23213
24406
  yield chunk;
23214
24407
  } else {
23215
24408
  for (const id of ids) {
23216
- const record = await coll.get(id);
24409
+ const record = await coll.get(id, localeOpts);
23217
24410
  if (record === null) continue;
23218
24411
  const chunk = {
23219
24412
  collection: collectionName,
@@ -23317,7 +24510,10 @@ var Vault = class {
23317
24510
  const allDictionaries = {};
23318
24511
  for await (const chunk of this.exportStream({
23319
24512
  granularity: "collection",
23320
- withLedgerHead: opts.withLedgerHead === true
24513
+ withLedgerHead: opts.withLedgerHead === true,
24514
+ // #285 export layer: thread the export locale so records are read at the
24515
+ // `export` layer (i18nText collapsed + dictKey/staticDict labels resolved).
24516
+ ...opts.resolveLabels !== void 0 ? { resolveLabels: opts.resolveLabels } : {}
23321
24517
  })) {
23322
24518
  collections[chunk.collection] = {
23323
24519
  schema: null,
@@ -25117,7 +26313,7 @@ var Noydb = class {
25117
26313
  const { StateManagementVault: StateManagementVault2 } = await Promise.resolve().then(() => (init_state_vault(), state_vault_exports));
25118
26314
  const stateVault = opts.registry ? void 0 : await StateManagementVault2.open(this);
25119
26315
  const registry = opts.registry ?? stateVault.registry;
25120
- const group = new VaultGroup2(this, name, registry, opts.sharding, template);
26316
+ const group = new VaultGroup2(this, name, registry, opts.sharding, template, opts.migrateOnOpen ?? false);
25121
26317
  if (stateVault) {
25122
26318
  group._attachStateVault(stateVault);
25123
26319
  await stateVault.recordManifest(opts.sharding.vaultTemplate, template);
@@ -25151,6 +26347,16 @@ var Noydb = class {
25151
26347
  async _shardVaultProvisioned(vaultId) {
25152
26348
  return (await this.options.store.list(vaultId, "_keyring")).length > 0;
25153
26349
  }
26350
+ /**
26351
+ * @internal — the physical backend store a vault id maps to. A
26352
+ * `routeStore` resolves the vault-prefix route via its `resolveBackend`;
26353
+ * a plain store is its own backend. Used by the federation data-residency
26354
+ * guard to read the placement backend's `capabilities.region` (#271).
26355
+ */
26356
+ _resolveBackend(vaultId) {
26357
+ const store = this.options.store;
26358
+ return store.resolveBackend ? store.resolveBackend(vaultId) : this.options.store;
26359
+ }
25154
26360
  /**
25155
26361
  * Change the current user's passphrase for a vault.
25156
26362
  *
@@ -27461,6 +28667,60 @@ function immutableGuard(config) {
27461
28667
  return withGuard(spec);
27462
28668
  }
27463
28669
 
28670
+ // src/guards/transition-guard.ts
28671
+ init_errors();
28672
+ function recordId3(record) {
28673
+ const id = record?.id;
28674
+ return typeof id === "string" ? id : "";
28675
+ }
28676
+ function stateOf(record, field) {
28677
+ const v = record[field];
28678
+ return typeof v === "string" ? v : String(v);
28679
+ }
28680
+ function transitionGuard(config) {
28681
+ const { collection, field, transitions, initial, amendmentRoles, amendmentInvariant } = config;
28682
+ const allowIdempotent = config.allowIdempotent ?? true;
28683
+ if (!field) {
28684
+ throw new ValidationError("transitionGuard: `field` is required");
28685
+ }
28686
+ if (transitions === void 0 || typeof transitions !== "object") {
28687
+ throw new ValidationError("transitionGuard: `transitions` must be a state\u2192states map");
28688
+ }
28689
+ const spec = {
28690
+ collection,
28691
+ check: (incoming, ctx) => {
28692
+ const rec = incoming;
28693
+ const to = stateOf(rec, field);
28694
+ if (ctx.existing === null) {
28695
+ if (initial !== void 0 && !initial.includes(to)) {
28696
+ throw new IllegalTransitionError(collection, recordId3(rec), "(none)", to);
28697
+ }
28698
+ return;
28699
+ }
28700
+ const from = stateOf(ctx.existing, field);
28701
+ if (from === to) {
28702
+ if (allowIdempotent) return;
28703
+ throw new IllegalTransitionError(collection, recordId3(rec), from, to);
28704
+ }
28705
+ const allowed = transitions[from] ?? [];
28706
+ if (!allowed.includes(to)) {
28707
+ throw new IllegalTransitionError(collection, recordId3(rec), from, to);
28708
+ }
28709
+ },
28710
+ // The authorized override: inside an amendment transaction the check
28711
+ // is skipped and the change is ledgered. By default no extra invariant
28712
+ // — the amendment itself is the sanctioned exception. Callers may
28713
+ // supply `amendmentInvariant` to keep a constraint inviolable even
28714
+ // under amendment; a throw reverts the amendment as `InvariantError`.
28715
+ amendment: {
28716
+ roles: amendmentRoles ?? ["admin", "owner"],
28717
+ invariant: amendmentInvariant ?? (() => {
28718
+ })
28719
+ }
28720
+ };
28721
+ return withGuard(spec);
28722
+ }
28723
+
27464
28724
  // src/derivations/with-derivation.ts
27465
28725
  init_errors();
27466
28726
  function withDerivation(spec) {
@@ -27488,8 +28748,37 @@ function withDerivation(spec) {
27488
28748
  }
27489
28749
  }
27490
28750
  }
28751
+ if (spec.triggerBy !== void 0) {
28752
+ for (const t of spec.triggerBy) {
28753
+ if (typeof t?.collection !== "string" || t.collection.length === 0) {
28754
+ throw new ValidationError("withDerivation: each triggerBy entry needs a non-empty `collection`");
28755
+ }
28756
+ if (t.collection === spec.source) {
28757
+ throw new ValidationError(
28758
+ `withDerivation: triggerBy.collection must not equal the source "${spec.source}" (use sources[] for same-id triggers)`
28759
+ );
28760
+ }
28761
+ if (typeof t.on !== "string" || t.on.length === 0) {
28762
+ throw new ValidationError(
28763
+ `withDerivation: triggerBy on "${t.collection}" needs a non-empty \`on\` (the FK field on the source)`
28764
+ );
28765
+ }
28766
+ if (t.maxFanout !== void 0 && (!Number.isInteger(t.maxFanout) || t.maxFanout < 1)) {
28767
+ throw new ValidationError(
28768
+ `withDerivation: triggerBy maxFanout on "${t.collection}" must be a positive integer (got ${String(t.maxFanout)}).`
28769
+ );
28770
+ }
28771
+ }
28772
+ }
27491
28773
  const lifecycleMode = typeof spec.lifecycle === "string" ? spec.lifecycle : spec.lifecycle.mode;
27492
28774
  for (const [outputKey, outputSpec] of Object.entries(spec.outputs)) {
28775
+ if (outputSpec.shape === "record" && outputSpec.collection === spec.source) {
28776
+ if (!outputSpec.denorm || outputSpec.denorm.length === 0) {
28777
+ throw new ValidationError(
28778
+ `withDerivation: self-write output "${outputKey}" (collection === source "${spec.source}") must declare \`denorm: [...]\` naming the fields it maintains.`
28779
+ );
28780
+ }
28781
+ }
27493
28782
  if (outputSpec.shape === "array") {
27494
28783
  if (lifecycleMode !== "eager") {
27495
28784
  throw new ValidationError(
@@ -27517,6 +28806,43 @@ function withDerivation(spec) {
27517
28806
  };
27518
28807
  }
27519
28808
 
28809
+ // src/derivations/with-rollup.ts
28810
+ init_errors();
28811
+ function withRollup(config) {
28812
+ const { from, key, into, field, compute } = config;
28813
+ if (!from || from.length === 0) {
28814
+ throw new ValidationError("withRollup: `from` (child collection) is required");
28815
+ }
28816
+ if (!into || into.length === 0) {
28817
+ throw new ValidationError("withRollup: `into` (parent collection) is required");
28818
+ }
28819
+ if (from === into) {
28820
+ throw new ValidationError("withRollup: `from` and `into` must be different collections");
28821
+ }
28822
+ if (!key || key.length === 0) {
28823
+ throw new ValidationError("withRollup: `key` (FK field on the child) is required");
28824
+ }
28825
+ if (!field || field.length === 0) {
28826
+ throw new ValidationError("withRollup: `field` (target field on the parent) is required");
28827
+ }
28828
+ if (typeof compute !== "function") {
28829
+ throw new ValidationError("withRollup: `compute` must be a function");
28830
+ }
28831
+ const spec = {
28832
+ source: into,
28833
+ // the parent record is what carries the rolled-up field
28834
+ deterministic: true,
28835
+ rollup: { from, key, field, compute },
28836
+ // Synthetic self-write output for registry / cycle bookkeeping. Dispatch
28837
+ // patches `field` directly (value-equality guarded); the executor is not run.
28838
+ outputs: { value: { shape: "record", collection: into, denorm: [field] } },
28839
+ derive: () => ({ value: {} }),
28840
+ // never invoked for a rollup strategy
28841
+ lifecycle: "eager"
28842
+ };
28843
+ return { __noydb_strategy: "derivation", spec };
28844
+ }
28845
+
27520
28846
  // src/index.ts
27521
28847
  init_errors();
27522
28848
 
@@ -27590,6 +28916,11 @@ function withMaterializedView(spec) {
27590
28916
  );
27591
28917
  }
27592
28918
  }
28919
+ if (spec.i18nLocale !== void 0 && spec.i18nFields === void 0) {
28920
+ throw new MaterializedViewConfigError(
28921
+ `withMaterializedView "${spec.name}": i18nLocale requires i18nFields \u2014 declare the i18nText descriptors of the group-key fields so they can be resolved at the mv layer before bucketing.`
28922
+ );
28923
+ }
27593
28924
  if (typeof spec.rowKey !== "function") {
27594
28925
  throw new ValidationError("withMaterializedView: rowKey is required (no default; see spec \xA7 Type surface)");
27595
28926
  }
@@ -27639,6 +28970,7 @@ function withOverlayedView(spec) {
27639
28970
  // src/index.ts
27640
28971
  init_errors();
27641
28972
  init_errors();
28973
+ init_core();
27642
28974
 
27643
28975
  // src/money/index.ts
27644
28976
  init_descriptor();
@@ -27755,138 +29087,9 @@ function isMoneyLike(value) {
27755
29087
  return decimalScaleOf(value) !== null;
27756
29088
  }
27757
29089
 
27758
- // src/i18n/script.ts
27759
- init_errors();
27760
- var LATIN_BASE = /* @__PURE__ */ new Set([
27761
- "en",
27762
- "fr",
27763
- "de",
27764
- "es",
27765
- "it",
27766
- "pt",
27767
- "nl",
27768
- "sv",
27769
- "no",
27770
- "da",
27771
- "fi",
27772
- "is",
27773
- "pl",
27774
- "cs",
27775
- "sk",
27776
- "hu",
27777
- "ro",
27778
- "hr",
27779
- "sl",
27780
- "et",
27781
- "lv",
27782
- "lt",
27783
- "tr",
27784
- "vi",
27785
- "id",
27786
- "ms",
27787
- "tl",
27788
- "sw",
27789
- "af",
27790
- "ca",
27791
- "gl",
27792
- "eu",
27793
- "cy",
27794
- "ga"
27795
- ]);
27796
- var SCRIPT_TABLE = {
27797
- th: ["Thai"],
27798
- ko: ["Hangul", "Han"],
27799
- ja: ["Han", "Hiragana", "Katakana"],
27800
- zh: ["Han"],
27801
- ar: ["Arabic"],
27802
- fa: ["Arabic"],
27803
- ur: ["Arabic"],
27804
- ru: ["Cyrillic"],
27805
- uk: ["Cyrillic"],
27806
- bg: ["Cyrillic"],
27807
- sr: ["Cyrillic"],
27808
- he: ["Hebrew"],
27809
- el: ["Greek"],
27810
- hi: ["Devanagari"],
27811
- ta: ["Tamil"],
27812
- km: ["Khmer"],
27813
- lo: ["Lao"],
27814
- my: ["Myanmar"]
27815
- };
27816
- var SUBTAG_SCRIPTS = {
27817
- Latn: ["Latin"],
27818
- Cyrl: ["Cyrillic", "Latin"],
27819
- Hans: ["Han", "Latin"],
27820
- Hant: ["Han", "Latin"],
27821
- Thai: ["Thai", "Latin"],
27822
- Arab: ["Arabic", "Latin"]
27823
- };
27824
- function inferScripts(locale) {
27825
- const parts = locale.split("-");
27826
- const subtag = parts.find((t) => /^[A-Z][a-z]{3}$/.test(t));
27827
- if (subtag && SUBTAG_SCRIPTS[subtag]) return SUBTAG_SCRIPTS[subtag];
27828
- const base = (parts[0] ?? "").toLowerCase();
27829
- if (LATIN_BASE.has(base)) return ["Latin"];
27830
- const primary = SCRIPT_TABLE[base];
27831
- if (primary) return [...primary, "Latin"];
27832
- return ["Latin"];
27833
- }
27834
- function allowedFor(descriptor, locale) {
27835
- const script = descriptor.options.script;
27836
- if (script && script !== "auto") {
27837
- const explicit = script[locale];
27838
- if (explicit) return explicit;
27839
- }
27840
- return inferScripts(locale);
27841
- }
27842
- var BASELINE = String.raw`\p{White_Space}\p{Script=Common}\p{Script=Inherited}\p{Mark}`;
27843
- function fullMatcher(scripts) {
27844
- const cls = scripts.map((s) => `\\p{Script=${s}}`).join("");
27845
- return new RegExp(`^[${BASELINE}${cls}]*$`, "u");
27846
- }
27847
- function charMatcher(scripts) {
27848
- const cls = scripts.map((s) => `\\p{Script=${s}}`).join("");
27849
- return new RegExp(`[${BASELINE}${cls}]`, "u");
27850
- }
27851
- function offendingSample(str, scripts) {
27852
- const ok = charMatcher(scripts);
27853
- const bad = [];
27854
- for (const ch of str) {
27855
- if (!ok.test(ch)) bad.push(ch);
27856
- if (bad.length >= 8) break;
27857
- }
27858
- return bad.join("");
27859
- }
27860
- function stripDisallowed(str, scripts) {
27861
- const ok = charMatcher(scripts);
27862
- let out = "";
27863
- for (const ch of str) if (ok.test(ch)) out += ch;
27864
- return out;
27865
- }
27866
- function enforceScript(value, field, descriptor) {
27867
- const opt = descriptor.options;
27868
- if (!opt.script) return { value, warnings: [] };
27869
- const mode = opt.onScriptViolation ?? "reject";
27870
- const warnings = [];
27871
- let out = value;
27872
- for (const [locale, raw] of Object.entries(value)) {
27873
- if (typeof raw !== "string") continue;
27874
- const allowed = allowedFor(descriptor, locale);
27875
- if (fullMatcher(allowed).test(raw)) continue;
27876
- const sample = offendingSample(raw, allowed);
27877
- if (mode === "reject") {
27878
- throw new ScriptViolationError(field, locale, allowed, sample);
27879
- }
27880
- warnings.push({ field, locale, expected: allowed, sample });
27881
- if (mode === "filter") {
27882
- if (out === value) out = { ...value };
27883
- out[locale] = stripDisallowed(raw, allowed);
27884
- }
27885
- }
27886
- return { value: out, warnings };
27887
- }
27888
-
27889
29090
  // src/index.ts
29091
+ init_policy();
29092
+ init_script();
27890
29093
  init_errors();
27891
29094
 
27892
29095
  // src/team/sync-credentials.ts
@@ -28553,6 +29756,7 @@ function shortJSON(value) {
28553
29756
  DICT_COLLECTION_PREFIX,
28554
29757
  DIRECTORY_RECORD_ID,
28555
29758
  DanglingReferenceError,
29759
+ DataResidencyError,
28556
29760
  DecryptionError,
28557
29761
  DelegationTargetMissingError,
28558
29762
  DerivationCapExceededError,
@@ -28578,6 +29782,7 @@ function shortJSON(value) {
28578
29782
  GroupedQuery,
28579
29783
  GroupedQueryN,
28580
29784
  INDEXED_STORE_POLICY,
29785
+ IllegalTransitionError,
28581
29786
  ImportCapabilityError,
28582
29787
  IndexRequiredError,
28583
29788
  IndexWriteFailureError,
@@ -28590,6 +29795,8 @@ function shortJSON(value) {
28590
29795
  LEDGER_DELTAS_COLLECTION,
28591
29796
  LedgerContentionError,
28592
29797
  LedgerStore,
29798
+ LinkEndpointError,
29799
+ LinkIntegrityError,
28593
29800
  LocaleNotSpecifiedError,
28594
29801
  Lru,
28595
29802
  MAGIC_LINK_CONTENT_INFO_PREFIX,
@@ -28721,6 +29928,7 @@ function shortJSON(value) {
28721
29928
  canonicalJson,
28722
29929
  checkGate,
28723
29930
  clearDevUnlock,
29931
+ compileSequenceFormat,
28724
29932
  computePatch,
28725
29933
  coordinatedCutover,
28726
29934
  count,
@@ -28783,11 +29991,13 @@ function shortJSON(value) {
28783
29991
  isDictKeyDescriptor,
28784
29992
  isDiscriminant,
28785
29993
  isI18nTextDescriptor,
29994
+ isLinkCollectionName,
28786
29995
  isMagicLinkGrantExpired,
28787
29996
  isMoneyDescriptor,
28788
29997
  isMoneyString,
28789
29998
  isPreCompressed,
28790
29999
  isPublicEnvelope,
30000
+ isRefArray,
28791
30001
  isSessionAlive,
28792
30002
  isStaticDictDescriptor,
28793
30003
  isULID,
@@ -28841,6 +30051,7 @@ function shortJSON(value) {
28841
30051
  recoverUser,
28842
30052
  reduceRecords,
28843
30053
  ref,
30054
+ refArray,
28844
30055
  removeAuthenticator,
28845
30056
  resetBrotliSupportCache,
28846
30057
  resetJoinWarnings,
@@ -28868,6 +30079,8 @@ function shortJSON(value) {
28868
30079
  sha256Hex,
28869
30080
  staticDict,
28870
30081
  sum,
30082
+ tokenize,
30083
+ transitionGuard,
28871
30084
  unwrapDeksFromBlob,
28872
30085
  unwrapDeksFromPaperEntry,
28873
30086
  unwrapDeksFromShamirEntry,
@@ -28891,6 +30104,7 @@ function shortJSON(value) {
28891
30104
  withMetrics,
28892
30105
  withOverlayedView,
28893
30106
  withRetry,
30107
+ withRollup,
28894
30108
  wrapBundleStore,
28895
30109
  wrapStore,
28896
30110
  writeMagicLinkGrant,