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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (307) hide show
  1. package/dist/{api-NAY63UIE.js → api-FRFEJ6WR.js} +6 -6
  2. package/dist/attestation/index.js +12 -12
  3. package/dist/{backup-M2VND2VS.js → backup-OLBRFYY7.js} +10 -10
  4. package/dist/{backup-M2VND2VS.js.map → backup-OLBRFYY7.js.map} +1 -1
  5. package/dist/blobs/index.js +8 -8
  6. package/dist/broker/index.js +6 -6
  7. package/dist/cargo/index.js +26 -26
  8. package/dist/{chunk-4CSG6KBX.js → chunk-2ION5ZRH.js} +17 -14
  9. package/dist/chunk-2ION5ZRH.js.map +1 -0
  10. package/dist/{chunk-466KGOQB.js → chunk-2NE7ILO7.js} +2 -2
  11. package/dist/{chunk-CZV57HHC.js → chunk-3KXIGMZ4.js} +2 -2
  12. package/dist/{chunk-5CH4DWQA.js → chunk-3ODOWQMV.js} +13 -14
  13. package/dist/chunk-3ODOWQMV.js.map +1 -0
  14. package/dist/{chunk-3OEWD45V.js → chunk-3RZ7QLJ4.js} +5 -5
  15. package/dist/{chunk-SFY6NT7D.js → chunk-3Y4OFWQJ.js} +1 -1
  16. package/dist/chunk-3Y4OFWQJ.js.map +1 -0
  17. package/dist/{chunk-FZ2SSI5R.js → chunk-6AL6O4CS.js} +2 -2
  18. package/dist/{chunk-4XEUSZWN.js → chunk-6D3CN2JR.js} +2 -2
  19. package/dist/{chunk-6UAVKAG6.js → chunk-6QCXMCLZ.js} +3 -3
  20. package/dist/{chunk-B4G3JPH5.js → chunk-7EH322GJ.js} +14 -7
  21. package/dist/chunk-7EH322GJ.js.map +1 -0
  22. package/dist/{chunk-AMKCY454.js → chunk-7WD2G6K4.js} +7 -2
  23. package/dist/{chunk-AMKCY454.js.map → chunk-7WD2G6K4.js.map} +1 -1
  24. package/dist/{chunk-VUUUIKRG.js → chunk-A5QQKWCK.js} +14 -23
  25. package/dist/chunk-A5QQKWCK.js.map +1 -0
  26. package/dist/{chunk-RRMXMJUP.js → chunk-AA2WWTS3.js} +9 -7
  27. package/dist/chunk-AA2WWTS3.js.map +1 -0
  28. package/dist/{chunk-WMUN7ZUD.js → chunk-AA7LM2A3.js} +2 -2
  29. package/dist/{chunk-PJGMBELR.js → chunk-AIZNLHKN.js} +4 -4
  30. package/dist/chunk-AIZNLHKN.js.map +1 -0
  31. package/dist/{chunk-MKSTUVMJ.js → chunk-AMMYIT7F.js} +35 -31
  32. package/dist/chunk-AMMYIT7F.js.map +1 -0
  33. package/dist/{chunk-TWPTRJEU.js → chunk-BBU3OD3D.js} +2 -2
  34. package/dist/{chunk-OJEDR25M.js → chunk-BVIGGV6D.js} +33 -45
  35. package/dist/chunk-BVIGGV6D.js.map +1 -0
  36. package/dist/{chunk-IQGIPRSE.js → chunk-CFTPERGL.js} +14 -11
  37. package/dist/chunk-CFTPERGL.js.map +1 -0
  38. package/dist/{chunk-T6CCRXD3.js → chunk-CGQUSSW7.js} +26 -142
  39. package/dist/chunk-CGQUSSW7.js.map +1 -0
  40. package/dist/{chunk-4JDTLWUZ.js → chunk-DFFSCYIW.js} +9 -9
  41. package/dist/{chunk-JZVXGOJE.js → chunk-DKYEVHZH.js} +7 -10
  42. package/dist/chunk-DKYEVHZH.js.map +1 -0
  43. package/dist/{chunk-YVOW35HH.js → chunk-DL4GDOWL.js} +4 -6
  44. package/dist/chunk-DL4GDOWL.js.map +1 -0
  45. package/dist/{chunk-LGGLPURL.js → chunk-DWO55ZWE.js} +13 -22
  46. package/dist/chunk-DWO55ZWE.js.map +1 -0
  47. package/dist/{chunk-MKV2KYRV.js → chunk-ER3BQCCE.js} +2 -2
  48. package/dist/{chunk-JU3JKOY5.js → chunk-EWAGI3EM.js} +2 -2
  49. package/dist/{chunk-WMZQCXZ5.js → chunk-EZ3UHHND.js} +13 -15
  50. package/dist/chunk-EZ3UHHND.js.map +1 -0
  51. package/dist/{chunk-5TF5DUQ5.js → chunk-FDIZHZ3C.js} +29 -47
  52. package/dist/chunk-FDIZHZ3C.js.map +1 -0
  53. package/dist/{chunk-56W3T5O2.js → chunk-FOABWV7L.js} +45 -18
  54. package/dist/chunk-FOABWV7L.js.map +1 -0
  55. package/dist/{chunk-H5U4T6JK.js → chunk-FRGF56PD.js} +11 -14
  56. package/dist/chunk-FRGF56PD.js.map +1 -0
  57. package/dist/{chunk-53PWELSC.js → chunk-FY33DKPH.js} +2 -2
  58. package/dist/{chunk-KKRFL5E2.js → chunk-GSRW5MTU.js} +220 -169
  59. package/dist/chunk-GSRW5MTU.js.map +1 -0
  60. package/dist/{chunk-4VRZDYRE.js → chunk-HJRTFXAA.js} +7 -10
  61. package/dist/chunk-HJRTFXAA.js.map +1 -0
  62. package/dist/chunk-HK67PX4J.js +27 -0
  63. package/dist/chunk-HK67PX4J.js.map +1 -0
  64. package/dist/{chunk-A3WHDOWI.js → chunk-IN5EZWYQ.js} +13 -14
  65. package/dist/chunk-IN5EZWYQ.js.map +1 -0
  66. package/dist/{chunk-PZLIA5R3.js → chunk-ITXPKEDP.js} +23 -29
  67. package/dist/chunk-ITXPKEDP.js.map +1 -0
  68. package/dist/{chunk-343FH4S5.js → chunk-JYFJNSTW.js} +9 -9
  69. package/dist/chunk-JYFJNSTW.js.map +1 -0
  70. package/dist/{chunk-35RIRCGW.js → chunk-KLVGF2D4.js} +11 -9
  71. package/dist/{chunk-35RIRCGW.js.map → chunk-KLVGF2D4.js.map} +1 -1
  72. package/dist/{chunk-7LLKPODL.js → chunk-LPAGIMXJ.js} +2 -2
  73. package/dist/{chunk-3JQVA527.js → chunk-LU6NPJWN.js} +3 -3
  74. package/dist/{chunk-UVFO25FJ.js → chunk-NAAPSNCS.js} +90 -33
  75. package/dist/chunk-NAAPSNCS.js.map +1 -0
  76. package/dist/{chunk-OPGKH5RD.js → chunk-NFPSC2HL.js} +3 -3
  77. package/dist/{chunk-55B3KHZW.js → chunk-NRNTXBHT.js} +7 -10
  78. package/dist/chunk-NRNTXBHT.js.map +1 -0
  79. package/dist/{chunk-I5OLA3H6.js → chunk-O4QDBAB6.js} +7 -10
  80. package/dist/{chunk-I5OLA3H6.js.map → chunk-O4QDBAB6.js.map} +1 -1
  81. package/dist/{chunk-4DJQU3PH.js → chunk-OCCNYGKG.js} +7 -7
  82. package/dist/{chunk-76FBDG6M.js → chunk-OZD3D2DZ.js} +10 -13
  83. package/dist/chunk-OZD3D2DZ.js.map +1 -0
  84. package/dist/{chunk-YX6J3Z3Z.js → chunk-PBPYQZDW.js} +2 -2
  85. package/dist/{chunk-TTAND7MR.js → chunk-PBRIHUQ5.js} +5 -5
  86. package/dist/{chunk-TTAND7MR.js.map → chunk-PBRIHUQ5.js.map} +1 -1
  87. package/dist/{chunk-NBLLQOMR.js → chunk-Q67P7S2I.js} +14 -15
  88. package/dist/chunk-Q67P7S2I.js.map +1 -0
  89. package/dist/{chunk-NY6RQBUN.js → chunk-RAYGDC4A.js} +50 -63
  90. package/dist/chunk-RAYGDC4A.js.map +1 -0
  91. package/dist/{chunk-RVTOCAHQ.js → chunk-SQSZRCYT.js} +1 -1
  92. package/dist/chunk-SQSZRCYT.js.map +1 -0
  93. package/dist/{chunk-JN64WH3U.js → chunk-T7IR4N2S.js} +1 -1
  94. package/dist/{chunk-JN64WH3U.js.map → chunk-T7IR4N2S.js.map} +1 -1
  95. package/dist/{chunk-THUITYH7.js → chunk-TNOQNU3R.js} +5 -4
  96. package/dist/{chunk-THUITYH7.js.map → chunk-TNOQNU3R.js.map} +1 -1
  97. package/dist/{chunk-TY6N3PAC.js → chunk-TXDOMADN.js} +9 -6
  98. package/dist/chunk-TXDOMADN.js.map +1 -0
  99. package/dist/{chunk-6EF3N2MH.js → chunk-U6D5XTI5.js} +13 -13
  100. package/dist/{chunk-6EF3N2MH.js.map → chunk-U6D5XTI5.js.map} +1 -1
  101. package/dist/{chunk-HNX47VTC.js → chunk-UE72AQDZ.js} +6 -6
  102. package/dist/chunk-UIYZ5SOU.js +190 -0
  103. package/dist/chunk-UIYZ5SOU.js.map +1 -0
  104. package/dist/{chunk-IGE6XWKK.js → chunk-VXVKEWYC.js} +3 -3
  105. package/dist/{chunk-LTVETWDH.js → chunk-X3AXSYBL.js} +217 -123
  106. package/dist/chunk-X3AXSYBL.js.map +1 -0
  107. package/dist/{chunk-AD2PJGSS.js → chunk-ZDATYUYG.js} +2 -2
  108. package/dist/{chunk-LJDW6LQJ.js → chunk-ZSOTZ2RZ.js} +41 -24
  109. package/dist/chunk-ZSOTZ2RZ.js.map +1 -0
  110. package/dist/{chunk-6E2JZRBD.js → chunk-ZVAFJQZB.js} +25 -19
  111. package/dist/chunk-ZVAFJQZB.js.map +1 -0
  112. package/dist/classified/index.js +4 -4
  113. package/dist/{classified-marker-L7LXKS6Q.js → classified-marker-GWQTNDSL.js} +3 -3
  114. package/dist/{collection-facade-KN3UFKZ4.js → collection-facade-56RTPO4G.js} +12 -12
  115. package/dist/consent/index.js +4 -4
  116. package/dist/cover/index.js +9 -2
  117. package/dist/custody/index.js +6 -6
  118. package/dist/{dead-filter-6HG5FP3I.js → dead-filter-XP7MPHCJ.js} +2 -2
  119. package/dist/{delegation-7EGBYBRN.js → delegation-Q3HEAVLA.js} +18 -19
  120. package/dist/delegation-Q3HEAVLA.js.map +1 -0
  121. package/dist/derivations/index.js +3 -3
  122. package/dist/{derive-XKAYJ2OA.js → derive-SVDY36ER.js} +4 -4
  123. package/dist/directory/index.js +5 -5
  124. package/dist/{dispatch-56T7VPGE.js → dispatch-MG5KNMWG.js} +5 -5
  125. package/dist/{dispatch-BM5GNCE2.js → dispatch-RVJQ4BOL.js} +11 -11
  126. package/dist/{enclave-KDLGVX5Z.js → enclave-AOZTWFZK.js} +16 -4
  127. package/dist/{executor-LFHRV6JH.js → executor-FRUVM36Q.js} +12 -12
  128. package/dist/{export-accessible-HFBJ4JJY.js → export-accessible-HDDHZYPM.js} +7 -7
  129. package/dist/{extract-partition-7ZRFN6QP.js → extract-partition-TRADP234.js} +12 -12
  130. package/dist/{fanout-sidecar-MT2OBMZB.js → fanout-sidecar-NZA6J7QB.js} +13 -22
  131. package/dist/fanout-sidecar-NZA6J7QB.js.map +1 -0
  132. package/dist/forget/index.js +4 -4
  133. package/dist/history/index.js +18 -15
  134. package/dist/history/index.js.map +1 -1
  135. package/dist/i18n/index.js +9 -9
  136. package/dist/index.d.ts +16 -0
  137. package/dist/index.js +129 -127
  138. package/dist/index.js.map +1 -1
  139. package/dist/introspection/index.js +5 -5
  140. package/dist/{issue-LCOOEH45.js → issue-GZQDW5TQ.js} +6 -6
  141. package/dist/kernel/enclave/index.d.ts +7 -5
  142. package/dist/kernel/enclave/record-aad.d.ts +79 -7
  143. package/dist/kernel/enclave/record-envelope.d.ts +54 -7
  144. package/dist/kernel/enclave/record-keys/envelope-body.d.ts +34 -2
  145. package/dist/kernel/enclave/record-keys/lifecycle.d.ts +22 -5
  146. package/dist/kernel/enclave/record-keys/record-codec.d.ts +47 -14
  147. package/dist/kernel/enclave/record-keys/rekey.d.ts +68 -0
  148. package/dist/kernel/enclave/record-keys/tombstone.d.ts +3 -3
  149. package/dist/kernel/merge-authority.d.ts +25 -0
  150. package/dist/kernel/tier-visibility.d.ts +15 -0
  151. package/dist/kernel/types.d.ts +23 -0
  152. package/dist/kernel/vault.d.ts +1 -1
  153. package/dist/kernel/via/index.d.ts +11 -2
  154. package/dist/{ledger-6IHXJHDD.js → ledger-KQAJPEMZ.js} +6 -6
  155. package/dist/liberate-Z2EXJY2S.js +24 -0
  156. package/dist/{link-set-XRVHGYWC.js → link-set-XKN5V7VM.js} +5 -5
  157. package/dist/{managed-secret-IDAOXSSX.js → managed-secret-7WIIVMJ5.js} +11 -3
  158. package/dist/materialized-views/index.js +14 -14
  159. package/dist/money/index.js +3 -3
  160. package/dist/{noydb-M5WII7RP.js → noydb-NPW3C2EX.js} +39 -38
  161. package/dist/periods/index.js +6 -6
  162. package/dist/pod/index.js +42 -41
  163. package/dist/{pod-handle-BYUSBXCT.js → pod-handle-SKDKRPOR.js} +6 -6
  164. package/dist/{pod-handle-BYUSBXCT.js.map → pod-handle-SKDKRPOR.js.map} +1 -1
  165. package/dist/policy/index.js +10 -3
  166. package/dist/port/with/merge-authority.d.ts +52 -0
  167. package/dist/port/with/strategies.d.ts +2 -0
  168. package/dist/portability/index.js +6 -6
  169. package/dist/{post-register-O3YW6CGS.js → post-register-CVS7CCJE.js} +4 -4
  170. package/dist/reduce/index.js +3 -3
  171. package/dist/{register-XHOOX37G.js → register-GJ6PW4KP.js} +10 -10
  172. package/dist/{registry-OFMM4WAC.js → registry-6LUJ5IJW.js} +4 -4
  173. package/dist/{registry-CYAFB35H.js → registry-KI77RVK7.js} +4 -4
  174. package/dist/{request-withdrawal-GCE2SLNW.js → request-withdrawal-LICOUGXG.js} +9 -9
  175. package/dist/{reveal-UHBP5IH3.js → reveal-PWM47FQN.js} +3 -3
  176. package/dist/{revoke-6KY4VGZY.js → revoke-AWZKZLZB.js} +6 -6
  177. package/dist/sealed-record/index.js +14 -7
  178. package/dist/sealed-record/index.js.map +1 -1
  179. package/dist/search/index.js +3 -3
  180. package/dist/{seed-DGLQTCIO.js → seed-LNTLU2A3.js} +14 -17
  181. package/dist/seed-LNTLU2A3.js.map +1 -0
  182. package/dist/sequence/index.js +4 -4
  183. package/dist/session/index.js +3 -3
  184. package/dist/{signer-SLOMB6HS.js → signer-JE6NEURY.js} +5 -5
  185. package/dist/snapshots/index.js +5 -5
  186. package/dist/{stale-26ZJLNLG.js → stale-ZWTKUVZX.js} +5 -5
  187. package/dist/{storage-N3IMFNGS.js → storage-WCZOCA46.js} +5 -5
  188. package/dist/{store-coordination-provider-RGHCIRWV.js → store-coordination-provider-7H62VO2C.js} +18 -12
  189. package/dist/store-coordination-provider-7H62VO2C.js.map +1 -0
  190. package/dist/sync/index.js +5 -5
  191. package/dist/team/index.js +10 -9
  192. package/dist/tiers/index.js +6 -5
  193. package/dist/vault-head/index.js +111 -0
  194. package/dist/vault-head/index.js.map +1 -0
  195. package/dist/{walk-B4YFJJ7S.js → walk-HSS3NLQ3.js} +6 -6
  196. package/dist/with-audit/periods/periods.d.ts +1 -1
  197. package/dist/with-audit/periods/strategy.d.ts +1 -1
  198. package/dist/with-audit/sealed-record/index.d.ts +13 -0
  199. package/dist/with-audit/tiers/index.d.ts +17 -2
  200. package/dist/with-commit/history/history.d.ts +26 -0
  201. package/dist/with-commit/history/index.d.ts +2 -1
  202. package/dist/with-commit/history/ledger/store.d.ts +7 -1
  203. package/dist/with-commit/history/strategy.d.ts +11 -1
  204. package/dist/with-commit/vault-head/active.d.ts +2 -0
  205. package/dist/with-commit/vault-head/head.d.ts +28 -0
  206. package/dist/with-commit/vault-head/index.d.ts +10 -0
  207. package/dist/with-commit/vault-head/strategy.d.ts +127 -0
  208. package/dist/with-commit/vault-head/verify.d.ts +50 -0
  209. package/dist/with-party/directory/cover/storage.d.ts +0 -8
  210. package/dist/with-party/directory/storage.d.ts +0 -11
  211. package/dist/with-party/directory/visibility.d.ts +0 -21
  212. package/dist/with-party/policy/storage.d.ts +0 -10
  213. package/dist/with-party/team/keyring.d.ts +5 -0
  214. package/dist/with-party/team/tiers.d.ts +2 -1
  215. package/dist/with-sync/engine.d.ts +33 -5
  216. package/dist/with-sync/strategy.d.ts +3 -0
  217. package/dist/{withdraw-accessible-OYG7YZUR.js → withdraw-accessible-GHBA4DAA.js} +8 -8
  218. package/package.json +7 -3
  219. package/dist/chunk-343FH4S5.js.map +0 -1
  220. package/dist/chunk-4CSG6KBX.js.map +0 -1
  221. package/dist/chunk-4VRZDYRE.js.map +0 -1
  222. package/dist/chunk-55B3KHZW.js.map +0 -1
  223. package/dist/chunk-56W3T5O2.js.map +0 -1
  224. package/dist/chunk-5CH4DWQA.js.map +0 -1
  225. package/dist/chunk-5TF5DUQ5.js.map +0 -1
  226. package/dist/chunk-6E2JZRBD.js.map +0 -1
  227. package/dist/chunk-76FBDG6M.js.map +0 -1
  228. package/dist/chunk-A3WHDOWI.js.map +0 -1
  229. package/dist/chunk-AFHQATY4.js +0 -47
  230. package/dist/chunk-AFHQATY4.js.map +0 -1
  231. package/dist/chunk-B4G3JPH5.js.map +0 -1
  232. package/dist/chunk-H5U4T6JK.js.map +0 -1
  233. package/dist/chunk-IQGIPRSE.js.map +0 -1
  234. package/dist/chunk-JZVXGOJE.js.map +0 -1
  235. package/dist/chunk-KKRFL5E2.js.map +0 -1
  236. package/dist/chunk-LGGLPURL.js.map +0 -1
  237. package/dist/chunk-LJDW6LQJ.js.map +0 -1
  238. package/dist/chunk-LTVETWDH.js.map +0 -1
  239. package/dist/chunk-MKSTUVMJ.js.map +0 -1
  240. package/dist/chunk-NBLLQOMR.js.map +0 -1
  241. package/dist/chunk-NY6RQBUN.js.map +0 -1
  242. package/dist/chunk-OJEDR25M.js.map +0 -1
  243. package/dist/chunk-PJGMBELR.js.map +0 -1
  244. package/dist/chunk-PZLIA5R3.js.map +0 -1
  245. package/dist/chunk-RRMXMJUP.js.map +0 -1
  246. package/dist/chunk-RVTOCAHQ.js.map +0 -1
  247. package/dist/chunk-SFY6NT7D.js.map +0 -1
  248. package/dist/chunk-T6CCRXD3.js.map +0 -1
  249. package/dist/chunk-TY6N3PAC.js.map +0 -1
  250. package/dist/chunk-UVFO25FJ.js.map +0 -1
  251. package/dist/chunk-VUUUIKRG.js.map +0 -1
  252. package/dist/chunk-WMZQCXZ5.js.map +0 -1
  253. package/dist/chunk-YVOW35HH.js.map +0 -1
  254. package/dist/delegation-7EGBYBRN.js.map +0 -1
  255. package/dist/fanout-sidecar-MT2OBMZB.js.map +0 -1
  256. package/dist/liberate-UEYC4QLJ.js +0 -24
  257. package/dist/seed-DGLQTCIO.js.map +0 -1
  258. package/dist/store-coordination-provider-RGHCIRWV.js.map +0 -1
  259. /package/dist/{api-NAY63UIE.js.map → api-FRFEJ6WR.js.map} +0 -0
  260. /package/dist/{chunk-466KGOQB.js.map → chunk-2NE7ILO7.js.map} +0 -0
  261. /package/dist/{chunk-CZV57HHC.js.map → chunk-3KXIGMZ4.js.map} +0 -0
  262. /package/dist/{chunk-3OEWD45V.js.map → chunk-3RZ7QLJ4.js.map} +0 -0
  263. /package/dist/{chunk-FZ2SSI5R.js.map → chunk-6AL6O4CS.js.map} +0 -0
  264. /package/dist/{chunk-4XEUSZWN.js.map → chunk-6D3CN2JR.js.map} +0 -0
  265. /package/dist/{chunk-6UAVKAG6.js.map → chunk-6QCXMCLZ.js.map} +0 -0
  266. /package/dist/{chunk-WMUN7ZUD.js.map → chunk-AA7LM2A3.js.map} +0 -0
  267. /package/dist/{chunk-TWPTRJEU.js.map → chunk-BBU3OD3D.js.map} +0 -0
  268. /package/dist/{chunk-4JDTLWUZ.js.map → chunk-DFFSCYIW.js.map} +0 -0
  269. /package/dist/{chunk-MKV2KYRV.js.map → chunk-ER3BQCCE.js.map} +0 -0
  270. /package/dist/{chunk-JU3JKOY5.js.map → chunk-EWAGI3EM.js.map} +0 -0
  271. /package/dist/{chunk-53PWELSC.js.map → chunk-FY33DKPH.js.map} +0 -0
  272. /package/dist/{chunk-7LLKPODL.js.map → chunk-LPAGIMXJ.js.map} +0 -0
  273. /package/dist/{chunk-3JQVA527.js.map → chunk-LU6NPJWN.js.map} +0 -0
  274. /package/dist/{chunk-OPGKH5RD.js.map → chunk-NFPSC2HL.js.map} +0 -0
  275. /package/dist/{chunk-4DJQU3PH.js.map → chunk-OCCNYGKG.js.map} +0 -0
  276. /package/dist/{chunk-YX6J3Z3Z.js.map → chunk-PBPYQZDW.js.map} +0 -0
  277. /package/dist/{chunk-HNX47VTC.js.map → chunk-UE72AQDZ.js.map} +0 -0
  278. /package/dist/{chunk-IGE6XWKK.js.map → chunk-VXVKEWYC.js.map} +0 -0
  279. /package/dist/{chunk-AD2PJGSS.js.map → chunk-ZDATYUYG.js.map} +0 -0
  280. /package/dist/{classified-marker-L7LXKS6Q.js.map → classified-marker-GWQTNDSL.js.map} +0 -0
  281. /package/dist/{collection-facade-KN3UFKZ4.js.map → collection-facade-56RTPO4G.js.map} +0 -0
  282. /package/dist/{dead-filter-6HG5FP3I.js.map → dead-filter-XP7MPHCJ.js.map} +0 -0
  283. /package/dist/{derive-XKAYJ2OA.js.map → derive-SVDY36ER.js.map} +0 -0
  284. /package/dist/{dispatch-56T7VPGE.js.map → dispatch-MG5KNMWG.js.map} +0 -0
  285. /package/dist/{dispatch-BM5GNCE2.js.map → dispatch-RVJQ4BOL.js.map} +0 -0
  286. /package/dist/{enclave-KDLGVX5Z.js.map → enclave-AOZTWFZK.js.map} +0 -0
  287. /package/dist/{executor-LFHRV6JH.js.map → executor-FRUVM36Q.js.map} +0 -0
  288. /package/dist/{export-accessible-HFBJ4JJY.js.map → export-accessible-HDDHZYPM.js.map} +0 -0
  289. /package/dist/{extract-partition-7ZRFN6QP.js.map → extract-partition-TRADP234.js.map} +0 -0
  290. /package/dist/{issue-LCOOEH45.js.map → issue-GZQDW5TQ.js.map} +0 -0
  291. /package/dist/{ledger-6IHXJHDD.js.map → ledger-KQAJPEMZ.js.map} +0 -0
  292. /package/dist/{liberate-UEYC4QLJ.js.map → liberate-Z2EXJY2S.js.map} +0 -0
  293. /package/dist/{link-set-XRVHGYWC.js.map → link-set-XKN5V7VM.js.map} +0 -0
  294. /package/dist/{managed-secret-IDAOXSSX.js.map → managed-secret-7WIIVMJ5.js.map} +0 -0
  295. /package/dist/{noydb-M5WII7RP.js.map → noydb-NPW3C2EX.js.map} +0 -0
  296. /package/dist/{post-register-O3YW6CGS.js.map → post-register-CVS7CCJE.js.map} +0 -0
  297. /package/dist/{register-XHOOX37G.js.map → register-GJ6PW4KP.js.map} +0 -0
  298. /package/dist/{registry-CYAFB35H.js.map → registry-6LUJ5IJW.js.map} +0 -0
  299. /package/dist/{registry-OFMM4WAC.js.map → registry-KI77RVK7.js.map} +0 -0
  300. /package/dist/{request-withdrawal-GCE2SLNW.js.map → request-withdrawal-LICOUGXG.js.map} +0 -0
  301. /package/dist/{reveal-UHBP5IH3.js.map → reveal-PWM47FQN.js.map} +0 -0
  302. /package/dist/{revoke-6KY4VGZY.js.map → revoke-AWZKZLZB.js.map} +0 -0
  303. /package/dist/{signer-SLOMB6HS.js.map → signer-JE6NEURY.js.map} +0 -0
  304. /package/dist/{stale-26ZJLNLG.js.map → stale-ZWTKUVZX.js.map} +0 -0
  305. /package/dist/{storage-N3IMFNGS.js.map → storage-WCZOCA46.js.map} +0 -0
  306. /package/dist/{walk-B4YFJJ7S.js.map → walk-HSS3NLQ3.js.map} +0 -0
  307. /package/dist/{withdraw-accessible-OYG7YZUR.js.map → withdraw-accessible-GHBA4DAA.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/with-party/team/managed-secret.ts"],"sourcesContent":["/**\n * Managed-secret mode — rubber-hose-resistant vaults.\n *\n * A vault mode where the secret is machine-generated and never\n * exposed to the user, sealed under a developer-provided\n * {@link SealingKeyProvider} (macOS Keychain, Windows Credential\n * Manager, libsecret, AWS KMS, …). The user has no secret to give\n * up to coercion — they can't reveal what they don't know.\n *\n * ## Components in this file\n *\n * - {@link SealingKeyProvider} — the interface concrete providers\n * implement. Provider implementations live OUTSIDE hub (per-\n * platform packages).\n * - {@link MemorySealingKeyProvider} — in-memory test provider; uses\n * a deterministic per-instance \"key\" so two providers with\n * different ids cannot unseal each other's outputs.\n * - {@link RecipientHint} — public material a sender uses to seal\n * plaintext for a specific recipient; published by\n * {@link RecipientSealer.publishRecipientHint} and transported\n * out-of-band to the sender before bundle writes.\n * - {@link RecipientSealer} — interface for asymmetric/granted\n * providers that support recipient-target sealing (RSA-OAEP,\n * cloud-KMS asymmetric, etc.); distinct from self-only\n * {@link SealingKeyProvider} (macOS Keychain, WebAuthn-PRF).\n * - {@link MemoryRecipientSealer} — in-process reference\n * implementation of both `RecipientSealer` and\n * `SealingKeyProvider` using real WebCrypto RSA-OAEP + AES-GCM;\n * safe for tests and same-process sender/recipient scenarios.\n * - {@link loadSealedSecret} / {@link saveSealedSecret} —\n * plaintext envelope storage at `_meta/sealed-secret`.\n * Mirrors the `_meta/handle` and `_meta/public-envelope` AES-\n * GCM-bypassed patterns. The sealing layer (provider's job)\n * is the security boundary; hub doesn't have a key to encrypt\n * with at this layer — that's the whole point of the design.\n * - {@link resolveManagedSecret} — orchestrates the \"generate +\n * seal + persist on first open; unseal on reopen\" flow.\n * Returns the plaintext secret string that the rest of the\n * `createNoydb` keyring path consumes.\n *\n * Deferred to follow-ups:\n * - Block `rotate-secret` policy gate under managed mode.\n * - Mandatory strong-recovery enforcement.\n * - Recovery flow under managed mode (generates fresh sealed phrase).\n *\n * @see https://github.com/vLannaAi/noy-db-docs/blob/main/content/docs/services/session-tiers.md → Managed-secret mode\n *\n * @module\n */\n\nimport { buildRecordEnvelope } from '../../kernel/enclave/index.js'\nimport type { NoydbStore, RecipientSealer } from '../../kernel/types.js'\n\n/**\n * The contract concrete providers (per-platform key stores) implement\n * to seal and unseal a hub-generated random secret. The plaintext\n * secret NEVER leaves hub-controlled memory in unsealed form —\n * the provider receives the bytes, returns opaque sealed bytes, and\n * later reverses the operation. Hub treats the sealed bytes as\n * fully opaque.\n *\n * Implementations live OUTSIDE `@noy-db/hub` (separate packages\n * per the issue's \"Concrete providers (live outside hub)\" note):\n *\n * | Platform | Package (TBD) | Backing |\n * |---|---|---|\n * | macOS | `@noy-db/seal-macos-keychain` | Security.framework |\n * | Windows | `@noy-db/seal-wincred` | Credential Manager |\n * | Linux | `@noy-db/seal-libsecret` | libsecret / secret-service |\n * | Cloud / server | `@noy-db/seal-aws-kms` | AWS KMS Decrypt |\n */\nexport interface SealingKeyProvider {\n /**\n * Non-sensitive identifier disclosed in the persisted envelope.\n * Surfaced to consumers via `loadSealedSecret().providerId` so\n * a vault opened with the wrong provider class can detect the\n * mismatch and surface a clear error. NOT secret — fine to log.\n *\n * Suggested format: `<family>:<scope>` — e.g. `macos-keychain:com.acme.app`,\n * `aws-kms:arn:aws:kms:us-east-1:123:key/abc`. The hub never\n * parses this; it's purely audit metadata.\n */\n readonly id: string\n\n /** Seal raw secret bytes. Output bytes are opaque to hub. */\n seal(secret: Uint8Array): Promise<Uint8Array>\n\n /**\n * Reverse {@link seal}. MUST throw on tamper, wrong-provider, or\n * any other failure — hub treats a thrown error as \"this provider\n * cannot unlock this vault\" and surfaces it to the caller.\n */\n unseal(sealed: Uint8Array): Promise<Uint8Array>\n}\n\n/**\n * In-memory test provider. NOT secure — uses a deterministic\n * per-instance \"key\" (16-byte SHA-256 of `id`) XOR'd over the\n * secret plus a 4-byte provider-id fingerprint prefix. The XOR is\n * sufficient to make different `id` values produce mutually-unsealable\n * outputs (the contract tests for that), but offers ZERO real\n * confidentiality — never use outside tests.\n *\n * Replace with a real platform provider in production.\n */\nexport class MemorySealingKeyProvider implements SealingKeyProvider {\n readonly id: string\n private readonly fingerprint: Uint8Array\n private readonly keyBytes: Uint8Array\n\n constructor(opts: { id: string }) {\n this.id = opts.id\n // Deterministic 4-byte fingerprint of the provider id, prepended\n // to every sealed output so we can detect \"wrong provider\" at\n // unseal time without leaking anything sensitive about either\n // provider's actual key material.\n const encoded = new TextEncoder().encode(opts.id)\n let h = 0\n for (let i = 0; i < encoded.length; i++) {\n h = (h * 31 + encoded[i]!) >>> 0\n }\n this.fingerprint = new Uint8Array([\n (h >>> 24) & 0xff, (h >>> 16) & 0xff, (h >>> 8) & 0xff, h & 0xff,\n ])\n // Deterministic 16-byte \"key\" derived from the id by repeating\n // the fingerprint with offsets. Good enough for the XOR-stream\n // test cipher; never confuse this with real key derivation.\n this.keyBytes = new Uint8Array(16)\n for (let i = 0; i < 16; i++) {\n this.keyBytes[i] = this.fingerprint[i % 4]! ^ (i * 17)\n }\n }\n\n async seal(secret: Uint8Array): Promise<Uint8Array> {\n const out = new Uint8Array(4 + secret.length)\n out.set(this.fingerprint, 0)\n for (let i = 0; i < secret.length; i++) {\n out[4 + i] = secret[i]! ^ this.keyBytes[i % 16]!\n }\n return out\n }\n\n async unseal(sealed: Uint8Array): Promise<Uint8Array> {\n if (sealed.length < 4) {\n throw new Error('MemorySealingKeyProvider: sealed input too short')\n }\n for (let i = 0; i < 4; i++) {\n if (sealed[i] !== this.fingerprint[i]) {\n throw new Error(\n `MemorySealingKeyProvider(\"${this.id}\"): provider-id mismatch on unseal `\n + '(sealed bytes were produced by a different provider)',\n )\n }\n }\n const body = sealed.subarray(4)\n const out = new Uint8Array(body.length)\n for (let i = 0; i < body.length; i++) {\n out[i] = body[i]! ^ this.keyBytes[i % 16]!\n }\n return out\n }\n}\n\n/**\n * Public material a sender uses to seal-for-this-recipient. Published by\n * a recipient's RecipientSealer; transported to the sender out-of-band\n * (email, S3, in-app message). The sender obtains the hint, supplies it\n * to writePod's sealedCredentials.perUser[userId].hint, and the\n * hub seals each user's credential against it. Per foundation §11.4.\n */\nexport type RecipientHint = {\n readonly v: 1\n /** Recipient's provider id; matches the SealedAutoUnlockEntry.pid they'll unseal under. */\n readonly pid: string\n /** Algorithm the sender uses to produce the seal. Slice 1 ships RSA-OAEP-SHA256 only. */\n readonly alg: 'rsa-oaep-sha256'\n /** Public material — alg-specific. For 'rsa-oaep-sha256': { publicKeyPem: string }. */\n readonly material: Readonly<Record<string, unknown>>\n}\n\n// Hoisted to kernel/types.ts (C3 — enclave self-containment: sealing.ts\n// consumes this as a spine-owned contract type). Re-exported here so existing\n// importers of this module are unaffected.\nexport type { RecipientSealer } from '../../kernel/types.js'\n\n/**\n * Shared RSA-OAEP-SHA256 + AES-GCM seal in the canonical recipient-target\n * TLV wire format. Mints a fresh 32-byte CEK, AES-GCM-encrypts `plaintext`\n * under it, RSA-OAEP-SHA256-wraps the CEK to `publicKeyPem`, and packs:\n *\n * byte 0 : version (0x01)\n * bytes 1..256 : RSA-OAEP-wrapped CEK (fixed 256 bytes at RSA-2048)\n * bytes 257..268: AES-GCM IV (12 bytes)\n * bytes 269.. : AES-GCM ciphertext ‖ 16-byte tag\n *\n * This is the single source of truth for the wire format — both\n * {@link MemoryRecipientSealer} and external sealers (e.g. `@noy-db/at-aws-kms`'s\n * asymmetric-KMS recipient sealer) call it so a blob sealed by one unseals\n * by the other. WebCrypto RSA-OAEP/SHA-256 here is wire-compatible with\n * AWS KMS `RSAES_OAEP_SHA_256` (both RSAES-OAEP, SHA-256 hash, MGF1-SHA256,\n * empty label).\n *\n * @public — re-exported from the hub barrel for external sealer packages.\n */\nexport async function sealRsaOaepTlv(plaintext: Uint8Array, publicKeyPem: string): Promise<Uint8Array> {\n // Parse PEM → SPKI bytes.\n const b64 = publicKeyPem.replace(/-----BEGIN PUBLIC KEY-----/, '').replace(/-----END PUBLIC KEY-----/, '').replace(/\\s+/g, '')\n const spki = base64ToBytes(b64)\n const recipientPub = await crypto.subtle.importKey(\n 'spki', spki as BufferSource,\n { name: 'RSA-OAEP', hash: 'SHA-256' },\n false, ['encrypt'],\n )\n // Mint fresh CEK + IV, AES-GCM encrypt plaintext.\n const cekBytes = crypto.getRandomValues(new Uint8Array(32))\n const cek = await crypto.subtle.importKey('raw', cekBytes as BufferSource, 'AES-GCM', false, ['encrypt'])\n const iv = crypto.getRandomValues(new Uint8Array(12))\n const ct = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv as BufferSource }, cek, plaintext as BufferSource))\n // RSA-OAEP-wrap the CEK bytes.\n const wrapped = new Uint8Array(await crypto.subtle.encrypt({ name: 'RSA-OAEP' }, recipientPub, cekBytes as BufferSource))\n cekBytes.fill(0)\n if (wrapped.length !== 256) {\n throw new Error(`sealRsaOaepTlv: expected 256-byte RSA-OAEP wrap, got ${wrapped.length}`)\n }\n // TLV layout.\n const out = new Uint8Array(1 + 256 + 12 + ct.length)\n out[0] = 0x01\n out.set(wrapped, 1)\n out.set(iv, 1 + 256)\n out.set(ct, 1 + 256 + 12)\n return out\n}\n\n/**\n * Parse a {@link sealRsaOaepTlv} blob into its three segments without\n * decrypting. The `wrapped` CEK is RSA-OAEP-SHA256 ciphertext over a\n * 32-byte CEK — the unwrap step is pluggable: {@link MemoryRecipientSealer}\n * decrypts it with a local RSA private key; `@noy-db/at-aws-kms` hands it to\n * KMS `Decrypt`. After unwrapping, pass the CEK + `iv` + `ct` to\n * {@link aesGcmOpen}.\n *\n * @public — re-exported from the hub barrel for external sealer packages.\n */\nexport function parseRsaOaepTlv(bytes: Uint8Array): { wrapped: Uint8Array; iv: Uint8Array; ct: Uint8Array } {\n if (bytes.length < 1 + 256 + 12 + 16) {\n throw new Error('parseRsaOaepTlv: sealed input too short')\n }\n if (bytes[0] !== 0x01) {\n throw new Error(`parseRsaOaepTlv: unknown TLV version ${bytes[0]}`)\n }\n return {\n wrapped: bytes.subarray(1, 1 + 256),\n iv: bytes.subarray(1 + 256, 1 + 256 + 12),\n ct: bytes.subarray(1 + 256 + 12),\n }\n}\n\n/**\n * AES-GCM-decrypt the `ct` segment of a {@link parseRsaOaepTlv} result under\n * the unwrapped 32-byte CEK and its `iv`. Throws on a bad tag (tamper) — the\n * same authenticated-decryption guarantee the TLV relies on.\n *\n * @public — re-exported from the hub barrel for external sealer packages.\n */\nexport async function aesGcmOpen(cekBytes: Uint8Array, iv: Uint8Array, ct: Uint8Array): Promise<Uint8Array> {\n const cek = await crypto.subtle.importKey('raw', cekBytes as BufferSource, 'AES-GCM', false, ['decrypt'])\n return new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv as BufferSource }, cek, ct as BufferSource))\n}\n\n/**\n * Reference implementation of `RecipientSealer` + `SealingKeyProvider`.\n * Uses WebCrypto RSA-OAEP-SHA256 (2048-bit) to wrap a fresh 32-byte\n * AES-GCM CEK, AES-GCM-encrypts plaintext under it, and packs the\n * result into a self-describing TLV:\n *\n * byte 0 : version (0x01)\n * bytes 1..256 : RSA-OAEP-wrapped CEK (fixed 256 bytes at RSA-2048)\n * bytes 257..268: AES-GCM IV (12 bytes)\n * bytes 269.. : AES-GCM ciphertext ‖ 16-byte tag\n *\n * Implements BOTH interfaces. `seal(plaintext)` (self-target) is just\n * `sealForRecipient(plaintext, this own hint)` — same TLV. Convenient\n * for tests where one provider plays both ends. Real cloud providers\n * (`at-aws-kms`, etc.) will pick their own internal layouts; the only\n * contract is round-trip identity.\n *\n * SAFE for production within its scope — the cryptography is real\n * (RSA-OAEP + AES-GCM via WebCrypto), but the keypair lives in-process\n * and is regenerated on every construction. Not suitable as a managed\n * keychain; use it for tests and for shipping bundles where the\n * recipient instance lives in the same process as the sender (rare).\n */\nexport class MemoryRecipientSealer implements SealingKeyProvider, RecipientSealer {\n readonly id: string\n private readonly keypair: Promise<CryptoKeyPair>\n\n constructor(opts: { id: string }) {\n this.id = opts.id\n this.keypair = crypto.subtle.generateKey(\n { name: 'RSA-OAEP', modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' },\n true,\n ['encrypt', 'decrypt'],\n )\n }\n\n async publishRecipientHint(): Promise<RecipientHint> {\n const { publicKey } = await this.keypair\n const spki = await crypto.subtle.exportKey('spki', publicKey)\n const pem = '-----BEGIN PUBLIC KEY-----\\n'\n + bytesToBase64(new Uint8Array(spki)).match(/.{1,64}/g)!.join('\\n')\n + '\\n-----END PUBLIC KEY-----\\n'\n return { v: 1, pid: this.id, alg: 'rsa-oaep-sha256', material: { publicKeyPem: pem } }\n }\n\n async sealForRecipient(plaintext: Uint8Array, hint: RecipientHint): Promise<Uint8Array> {\n if (hint.v !== 1) {\n throw new Error(`MemoryRecipientSealer.sealForRecipient: unsupported hint.v ${String(hint.v)} (expected 1)`)\n }\n if (hint.alg !== 'rsa-oaep-sha256') {\n throw new Error(`MemoryRecipientSealer.sealForRecipient: unsupported hint.alg '${String(hint.alg)}' (expected 'rsa-oaep-sha256')`)\n }\n const pem = hint.material['publicKeyPem']\n if (typeof pem !== 'string') {\n throw new Error('MemoryRecipientSealer.sealForRecipient: hint.material.publicKeyPem missing or not a string')\n }\n return sealRsaOaepTlv(plaintext, pem)\n }\n\n async seal(plaintext: Uint8Array): Promise<Uint8Array> {\n const hint = await this.publishRecipientHint()\n return this.sealForRecipient(plaintext, hint)\n }\n\n async unseal(bytes: Uint8Array): Promise<Uint8Array> {\n const { wrapped, iv, ct } = parseRsaOaepTlv(bytes)\n const { privateKey } = await this.keypair\n // Local RSA-OAEP-SHA256 unwrap of the CEK — the pluggable step external\n // sealers replace with KMS Decrypt.\n const cekBytes = new Uint8Array(await crypto.subtle.decrypt({ name: 'RSA-OAEP' }, privateKey, wrapped as BufferSource))\n const pt = await aesGcmOpen(cekBytes, iv, ct)\n cekBytes.fill(0)\n return pt\n }\n}\n\n// ─── Persisted envelope ────────────────────────────────────────────────\n\n/** Reserved id for the managed-secret envelope under `_meta`. */\nexport const SEALED_SECRET_RECORD_ID = 'sealed-secret' as const\n\n/** Plaintext payload stored inside the `_meta/sealed-secret` envelope. */\nexport interface SealedSecret {\n readonly _noydb_sealed: 1\n readonly providerId: string\n /** Sealed bytes. Base64-encoded on the wire; decoded on load. */\n readonly sealed: Uint8Array\n}\n\n/**\n * Wire-format envelope persisted at `_meta/sealed-secret` for\n * managed-mode vaults. The provider produces raw sealed bytes via\n * {@link SealingKeyProvider.seal}; this wrapper carries the dispatch\n * metadata hub needs to pick the right provider on the unseal path.\n *\n * Stability boundary: once shipped, the wire format only grows by\n * adding optional fields. See the at-* sealing dimension foundation\n * doc, §11.9.1.\n *\n * v1 shape (this release): `{ v: 1, _noydb_sealed: 1, pid, payload }`.\n *\n * Legacy shape (earlier releases): `{ _noydb_sealed: 1, providerId, sealed }`\n * — accepted on read for backwards compatibility; never produced on\n * write going forward.\n */\nexport interface SealedEnvelope {\n /** Envelope schema version. v1 is the current shape. */\n readonly v: 1\n /** Magic marker for forensics + legacy-shape detection. */\n readonly _noydb_sealed: 1\n /** Matches the producing provider's `.id`. Dispatch key on unseal. */\n readonly pid: string\n /** Sealed bytes from the provider, base64-encoded on the wire. */\n readonly payload: string\n}\n\nfunction bytesToBase64(bytes: Uint8Array): string {\n let binary = ''\n for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]!)\n return btoa(binary)\n}\n\nfunction base64ToBytes(b64: string): Uint8Array {\n const binary = atob(b64)\n const out = new Uint8Array(binary.length)\n for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i)\n return out\n}\n\n/**\n * Parse a `_meta/sealed-secret` `_data` JSON string into the\n * in-memory {@link SealedSecret} representation. Accepts both:\n *\n * 1. v1 wire format `{ v: 1, _noydb_sealed: 1, pid, payload }` —\n * the current shape.\n * 2. Legacy wire format `{ _noydb_sealed: 1, providerId, sealed }` —\n * read-only; never written\n * going forward.\n *\n * Returns `undefined` for any input that doesn't match either shape,\n * so callers can fall back to \"no managed-mode envelope present.\"\n *\n * @internal — exported only for the migration safety-net test suite.\n */\nexport function parseSealedEnvelope(raw: unknown): SealedSecret | undefined {\n if (typeof raw !== 'object' || raw === null) return undefined\n const r = raw as Record<string, unknown>\n if (r._noydb_sealed !== 1) return undefined\n\n // v1 shape — preferred.\n if (\n r.v === 1\n && typeof r.pid === 'string'\n && typeof r.payload === 'string'\n ) {\n return {\n _noydb_sealed: 1,\n providerId: r.pid,\n sealed: base64ToBytes(r.payload),\n }\n }\n\n // Legacy shape — earlier releases. Accept on read for compat.\n if (\n typeof r.providerId === 'string'\n && typeof r.sealed === 'string'\n ) {\n return {\n _noydb_sealed: 1,\n providerId: r.providerId,\n sealed: base64ToBytes(r.sealed),\n }\n }\n\n return undefined\n}\n\nexport async function saveSealedSecret(\n store: NoydbStore,\n vault: string,\n payload: { readonly providerId: string; readonly sealed: Uint8Array },\n): Promise<void> {\n const persisted: SealedEnvelope = {\n v: 1,\n _noydb_sealed: 1,\n pid: payload.providerId,\n payload: bytesToBase64(payload.sealed),\n }\n const prior = await store.get(vault, '_meta', SEALED_SECRET_RECORD_ID)\n const env = buildRecordEnvelope(\n { collection: '_meta', id: SEALED_SECRET_RECORD_ID, version: (prior?._v ?? 0) + 1 },\n // AES-GCM bypassed — the sealing layer is the security boundary.\n { iv: '', data: JSON.stringify(persisted) },\n )\n await store.put(vault, '_meta', SEALED_SECRET_RECORD_ID, env)\n}\n\nexport async function loadSealedSecret(\n store: NoydbStore,\n vault: string,\n): Promise<SealedSecret | undefined> {\n const envelope = await store.get(vault, '_meta', SEALED_SECRET_RECORD_ID)\n if (!envelope) return undefined\n try {\n return parseSealedEnvelope(JSON.parse(envelope._data))\n } catch {\n return undefined\n }\n}\n\n// ─── createNoydb orchestration ─────────────────────────────────────────\n\n/**\n * Resolve the effective plaintext secret string for a managed-mode\n * vault. Two paths:\n *\n * 1. **First open (no envelope persisted):** generate a 256-bit random\n * via `crypto.getRandomValues`, base64-encode for use as a\n * secret string, seal the underlying bytes under the\n * provider, persist `_meta/sealed-secret`, return the\n * base64 string.\n *\n * 2. **Reopen (envelope exists):** read + unseal + decode → return.\n * A different provider whose `seal` output disagrees on the\n * stored bytes throws here, surfaced as a clear error.\n *\n * The returned string is the same shape that `secret:` would take in\n * standard mode — the rest of the keyring path consumes it\n * unchanged.\n *\n * @internal — called from `createNoydb` / `getKeyringInternal`.\n */\nexport async function resolveManagedSecret(\n store: NoydbStore,\n vault: string,\n provider: SealingKeyProvider,\n): Promise<string> {\n const existing = await loadSealedSecret(store, vault)\n if (existing) {\n if (existing.providerId !== provider.id) {\n throw new Error(\n `Managed-mode vault \"${vault}\" was sealed under provider id `\n + `\"${existing.providerId}\" but the current SealingKeyProvider is `\n + `\"${provider.id}\". Pass the same provider that originally enrolled `\n + 'the vault, or treat this as a fresh enrollment and clear '\n + '`_meta/sealed-secret` first.',\n )\n }\n const plaintext = await provider.unseal(existing.sealed)\n return bytesToBase64(plaintext)\n }\n\n // First open: mint a 256-bit random, seal, persist.\n const random = new Uint8Array(32)\n globalThis.crypto.getRandomValues(random)\n const sealed = await provider.seal(random)\n await saveSealedSecret(store, vault, { providerId: provider.id, sealed })\n return bytesToBase64(random)\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
- NOYDB_FORMAT_VERSION
3
- } from "./chunk-SFY6NT7D.js";
2
+ buildRecordEnvelope
3
+ } from "./chunk-ZVAFJQZB.js";
4
4
  import {
5
5
  ValidationError
6
6
  } from "./chunk-I64WQSN3.js";
@@ -187,13 +187,10 @@ async function loadCover(store, vault) {
187
187
  }
188
188
  }
189
189
  async function saveCover(store, vault, cover) {
190
- const wireEnvelope = {
191
- _noydb: NOYDB_FORMAT_VERSION,
192
- _v: 1,
193
- _ts: (/* @__PURE__ */ new Date()).toISOString(),
194
- _iv: "",
195
- _data: JSON.stringify(cover)
196
- };
190
+ const wireEnvelope = buildRecordEnvelope(
191
+ { collection: "_meta", id: COVER_RECORD_ID, version: 1 },
192
+ { iv: "", data: JSON.stringify(cover) }
193
+ );
197
194
  await store.put(vault, "_meta", COVER_RECORD_ID, wireEnvelope);
198
195
  }
199
196
  async function readCover(store, vault, opts = {}) {
@@ -233,4 +230,4 @@ export {
233
230
  resolveLocale,
234
231
  pickLocale
235
232
  };
236
- //# sourceMappingURL=chunk-I5OLA3H6.js.map
233
+ //# sourceMappingURL=chunk-O4QDBAB6.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/with-party/directory/cover/custom.ts","../src/with-party/directory/cover/schema.ts","../src/with-party/directory/cover/storage.ts"],"sourcesContent":["/**\n * The cover's namespaced `custom` extension slot (#800) — input\n * validation, the namespace-level patch merge, and the post-merge\n * size caps.\n *\n * Like everything on the cover, `custom` is plaintext, public by\n * design, and unauthenticated — payloads are hints (display defaults,\n * routing suggestions, registry pointers), never an input to\n * security, authorization, or data-integrity decisions.\n *\n * @see https://github.com/vLannaAi/noy-db-docs/blob/main/content/docs/services/public-envelope.md\n *\n * @module\n */\nimport { ValidationError } from '../../../kernel/errors.js'\nimport type { Cover, JsonValue, ResolvedCoverSchema } from './types.js'\n\n/**\n * Namespace keys must be reverse-DNS or package-style identifiers —\n * at least one dot/dash-separated segment (`noydb.viewer`,\n * `com.acme.registry` pass; a bare `config` fails), so two frameworks\n * can't collide on a bare word.\n */\nconst CUSTOM_KEY_PATTERN = /^[a-z0-9]+([.-][a-z0-9]+)+$/i\n\n/** Maximum nesting depth of a namespace payload (containers, not leaves). */\nconst MAX_CUSTOM_DEPTH = 8\n\n/**\n * Validate the owner-supplied `custom` input: the field must be a\n * plain object, every key must match {@link CUSTOM_KEY_PATTERN}, and\n * every value must be JSON-serializable (no functions, `undefined`,\n * symbols, bigints, or cycles) within the depth cap. `null` is\n * accepted — it is the delete-this-namespace directive and never\n * persists. Throws `ValidationError` on the first violation.\n */\nexport function validateCustomInput(custom: Record<string, JsonValue>): void {\n if (custom === null || typeof custom !== 'object' || Array.isArray(custom)) {\n throw new ValidationError(\n `setCover: custom must be a { [namespace]: value } object, got ${\n Array.isArray(custom) ? 'array' : typeof custom\n }.`,\n )\n }\n for (const [ns, value] of Object.entries(custom)) {\n if (!CUSTOM_KEY_PATTERN.test(ns)) {\n throw new ValidationError(\n `setCover: custom namespace \"${ns}\" is invalid. Keys must be ` +\n 'reverse-DNS or package-style identifiers with at least one ' +\n 'dot/dash-separated segment (e.g. \"noydb.viewer\", \"com.acme.registry\").',\n )\n }\n assertJsonValue(value, ns, 1, new Set())\n }\n}\n\nfunction assertJsonValue(\n value: unknown,\n ns: string,\n depth: number,\n seen: Set<object>,\n): void {\n if (value === null) return\n const t = typeof value\n if (t === 'string' || t === 'number' || t === 'boolean') return\n if (t !== 'object') {\n throw new ValidationError(\n `setCover: custom[\"${ns}\"] contains a non-JSON value (${t}). ` +\n 'Only strings, numbers, booleans, null, arrays, and plain objects are allowed.',\n )\n }\n const container = value as object\n if (seen.has(container)) {\n throw new ValidationError(\n `setCover: custom[\"${ns}\"] contains a circular reference.`,\n )\n }\n if (depth > MAX_CUSTOM_DEPTH) {\n throw new ValidationError(\n `setCover: custom[\"${ns}\"] nests deeper than the ${MAX_CUSTOM_DEPTH}-level cap.`,\n )\n }\n seen.add(container)\n const children = Array.isArray(container) ? container : Object.values(container)\n for (const child of children) {\n assertJsonValue(child, ns, depth + 1, seen)\n }\n seen.delete(container)\n}\n\n/**\n * Namespace-level patch merge for `setCover`: provided namespaces\n * replace their previous value, absent namespaces are preserved, and\n * an explicit `null` deletes that namespace (so `null` never persists\n * as a namespace value). A whole-`custom`-absent patch preserves the\n * previous custom, consistent with the other cover fields. Rationale:\n * framework A must never need read-modify-write of framework B's data.\n *\n * Returns a spreadable fragment — `{ custom }` when the merge leaves\n * at least one namespace, `{}` when it leaves none (the field is\n * dropped from the persisted document rather than stored empty).\n */\nexport function mergeCustom(\n existing: Record<string, JsonValue> | undefined,\n patch: Record<string, JsonValue> | undefined,\n): { custom: Record<string, JsonValue> } | Record<string, never> {\n if (patch === undefined) {\n return existing !== undefined ? { custom: existing } : {}\n }\n const merged: Record<string, JsonValue> = { ...existing }\n for (const [ns, value] of Object.entries(patch)) {\n if (value === null) {\n delete merged[ns]\n } else {\n merged[ns] = value\n }\n }\n return Object.keys(merged).length > 0 ? { custom: merged } : {}\n}\n\n/**\n * Post-merge size caps — validates the WOULD-BE-PERSISTED document\n * (after the namespace merge), so a small patch can't tip the stored\n * cover over a cap unnoticed. `maxCoverBytes` bounds the entire\n * serialized document, which also closes the unbounded locale-map\n * key-count hole for `name` / `description`. Sizes are measured as\n * `JSON.stringify` length. Throws `ValidationError` on violation.\n */\nexport function validateCoverSize(\n cover: Cover,\n schema: ResolvedCoverSchema,\n): void {\n if (cover.custom !== undefined) {\n const customSize = JSON.stringify(cover.custom).length\n if (customSize > schema.maxCustomBytes) {\n throw new ValidationError(\n `setCover: custom exceeds the ${schema.maxCustomBytes}-byte cap ` +\n `(got ${customSize} bytes serialized, after merging with the stored namespaces).`,\n )\n }\n }\n const totalSize = JSON.stringify(cover).length\n if (totalSize > schema.maxCoverBytes) {\n throw new ValidationError(\n `setCover: the cover document exceeds the ${schema.maxCoverBytes}-byte cap ` +\n `(got ${totalSize} bytes serialized).`,\n )\n }\n}\n","/**\n * Validate-on-write for the cover. Runs at every `setCover` call;\n * the developer's schema decides which fields are allowed and the\n * size caps that apply.\n *\n * @module\n */\nimport { ValidationError } from '../../../kernel/errors.js'\nimport { validateCustomInput } from './custom.js'\nimport type {\n Cover,\n CoverText,\n JsonValue,\n ResolvedCoverSchema,\n CoverField,\n} from './types.js'\n\n/** Owner-supplied input — the subset of {@link Cover} the owner can set. */\nexport interface SetCoverInput {\n readonly name?: CoverText\n readonly description?: CoverText\n readonly icon?: string\n readonly defaultLocale?: string\n /**\n * Namespace-level patch for the cover's `custom` slot (#800):\n * provided namespaces replace their previous value, absent\n * namespaces are preserved, an explicit `null` deletes that\n * namespace. Requires `'custom'` in the schema's `fields` list —\n * the `cover: true` shorthand does NOT enable it.\n */\n readonly custom?: Record<string, JsonValue>\n}\n\nconst DATA_URL_PREFIX = /^data:([a-zA-Z0-9.+-]+\\/[a-zA-Z0-9.+-]+);base64,/\n\n/**\n * Validate an owner-supplied cover input against the developer's\n * resolved schema. Throws `ValidationError` on the first violation;\n * returns void on success.\n *\n * The validator is deliberately strict: every fail mode is a hard\n * error rather than a silent drop, so the owner finds out immediately\n * which field they oversized rather than discovering a truncated\n * label months later.\n */\nexport function validateCoverInput(\n input: SetCoverInput,\n schema: ResolvedCoverSchema,\n): void {\n const allowed = new Set<CoverField>(schema.fields)\n\n // Reject any key not in the schema's allowed-field list.\n for (const key of Object.keys(input)) {\n const known: CoverField | undefined =\n key === 'name' || key === 'description' || key === 'icon' || key === 'defaultLocale' || key === 'custom'\n ? key\n : undefined\n if (!known) {\n throw new ValidationError(\n `setCover: unknown field \"${key}\". ` +\n `Allowed fields: ${[...allowed].join(', ')}.`,\n )\n }\n if (!allowed.has(known)) {\n throw new ValidationError(\n `setCover: field \"${known}\" is not enabled in this vault's schema. ` +\n `Allowed fields: ${[...allowed].join(', ')}.`,\n )\n }\n }\n\n if (input.name !== undefined) {\n validateText(input.name, 'name', schema.maxStringChars)\n }\n if (input.description !== undefined) {\n validateText(input.description, 'description', schema.maxStringChars)\n }\n if (input.icon !== undefined) {\n validateIcon(input.icon, schema)\n }\n if (input.defaultLocale !== undefined && typeof input.defaultLocale !== 'string') {\n throw new ValidationError(\n `setCover: defaultLocale must be a string (BCP-47), got ${typeof input.defaultLocale}.`,\n )\n }\n if (input.custom !== undefined) {\n validateCustomInput(input.custom)\n }\n}\n\nfunction validateText(\n value: CoverText,\n field: string,\n maxChars: number,\n): void {\n if (typeof value === 'string') {\n if (value.length > maxChars) {\n throw new ValidationError(\n `setCover: ${field} exceeds the ${maxChars}-character cap (got ${value.length}).`,\n )\n }\n return\n }\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new ValidationError(\n `setCover: ${field} must be a string or { [locale]: string } map, got ${typeof value}.`,\n )\n }\n // Locale map. Each value must be a non-empty string within the cap.\n for (const [locale, str] of Object.entries(value)) {\n if (typeof str !== 'string') {\n throw new ValidationError(\n `setCover: ${field}[${locale}] must be a string, got ${typeof str}.`,\n )\n }\n if (str.length > maxChars) {\n throw new ValidationError(\n `setCover: ${field}[${locale}] exceeds the ${maxChars}-character cap (got ${str.length}).`,\n )\n }\n }\n}\n\nfunction validateIcon(icon: string, schema: ResolvedCoverSchema): void {\n if (typeof icon !== 'string') {\n throw new ValidationError(\n `setCover: icon must be a data: URL string, got ${typeof icon}.`,\n )\n }\n if (icon.length > schema.maxIconBytes) {\n throw new ValidationError(\n `setCover: icon exceeds the ${schema.maxIconBytes}-byte cap (got ${icon.length}).`,\n )\n }\n const m = DATA_URL_PREFIX.exec(icon)\n if (!m) {\n throw new ValidationError(\n 'setCover: icon must be a base64 data URL ' +\n '(`data:image/png;base64,…` or `data:image/svg+xml;base64,…`). ' +\n 'External URLs are not supported in v1.',\n )\n }\n const mime = m[1]!\n if (!schema.iconMimeTypes.includes(mime)) {\n throw new ValidationError(\n `setCover: icon MIME type \"${mime}\" is not allowed. ` +\n `Permitted types: ${schema.iconMimeTypes.join(', ')}.`,\n )\n }\n}\n\n/**\n * Lightweight runtime predicate — used by the bundle header\n * validator to recognise a cover without requiring it. (The wire\n * discriminator keeps its frozen name: `_noydb_public: 1`.)\n */\nexport function isCover(x: unknown): x is Cover {\n if (x === null || typeof x !== 'object' || Array.isArray(x)) return false\n const obj = x as Record<string, unknown>\n return obj['_noydb_public'] === 1 && typeof obj['version'] === 'number'\n}\n","/**\n * Persistence helpers for `_meta/public-envelope` — the cover's\n * frozen wire location. Mirrors the bypass-AES pattern used by\n * `_meta/handle` and `_meta/policy` — the document is plaintext\n * JSON, the envelope's `_iv` field is left empty.\n *\n * @module\n */\nimport type { NoydbStore, EncryptedEnvelope } from '../../../kernel/types.js'\nimport { NOYDB_FORMAT_VERSION } from '../../../kernel/types.js'\nimport type { Cover } from './types.js'\nimport { isCover } from './schema.js'\n\n/**\n * Reserved id for the vault-level cover record. The VALUE is frozen\n * wire format — the record predates the #799 rename and stays at\n * `_meta/public-envelope` so existing vaults need no migration.\n */\nexport const COVER_RECORD_ID = 'public-envelope'\n\n/**\n * Read the cover from `_meta/public-envelope`. Returns `undefined`\n * when no cover has been persisted (fresh vault, or a vault written\n * before the feature was enabled). Tolerates corrupted documents —\n * a JSON parse failure surfaces as `undefined`, not a thrown error,\n * mirroring `_meta/handle`'s contract.\n */\nexport async function loadCover(\n store: NoydbStore,\n vault: string,\n): Promise<Cover | undefined> {\n const envelope = await store.get(vault, '_meta', COVER_RECORD_ID)\n if (!envelope) return undefined\n try {\n const parsed = JSON.parse(envelope._data) as unknown\n if (!isCover(parsed)) return undefined\n return parsed\n } catch {\n return undefined\n }\n}\n\n/** Persist the cover. Idempotent — overwrites any prior write. */\nexport async function saveCover(\n store: NoydbStore,\n vault: string,\n cover: Cover,\n): Promise<void> {\n const wireEnvelope: EncryptedEnvelope = {\n _noydb: NOYDB_FORMAT_VERSION,\n _v: 1,\n _ts: new Date().toISOString(),\n _iv: '',\n _data: JSON.stringify(cover),\n }\n await store.put(vault, '_meta', COVER_RECORD_ID, wireEnvelope)\n}\n\n/**\n * Public, no-key reader. Plain function, not a method on `Noydb` —\n * the whole point is it works without an authenticated session, so\n * a UI can show \"Acme 2026 Tax Records\" before unlocking.\n *\n * Resolves any `name` / `description` locale map through the supplied\n * `locale` (when provided). Omitting `locale` returns the raw\n * cover, which a multilingual picker can render as it pleases.\n */\nexport async function readCover(\n store: NoydbStore,\n vault: string,\n opts: { readonly locale?: string } = {},\n): Promise<Cover | undefined> {\n const raw = await loadCover(store, vault)\n if (!raw) return undefined\n if (opts.locale === undefined) return raw\n return resolveLocale(raw, opts.locale)\n}\n\n/**\n * Resolve the locale-map fields (`name`, `description`) of a cover\n * to plain strings for the requested locale, falling back through\n * `defaultLocale` and then any available translation.\n *\n * @internal\n */\nexport function resolveLocale(\n cover: Cover,\n locale: string,\n): Cover {\n return {\n ...cover,\n ...(cover.name !== undefined ? { name: pickLocale(cover.name, locale, cover.defaultLocale) } : {}),\n ...(cover.description !== undefined ? { description: pickLocale(cover.description, locale, cover.defaultLocale) } : {}),\n }\n}\n\n/**\n * Resolve a `string | { [locale]: string }` value to a string for a\n * given locale. Exported so the bundle reader can reuse it without\n * duplicating the fallback chain.\n *\n * **Looser than `resolveI18nText`** — the hub's record-field resolver\n * throws `LocaleNotSpecifiedError` when no translation matches, but\n * label semantics deliberately prefer \"show *something*\" over\n * throwing. The fallback chain is:\n * 1. Exact `locale` match.\n * 2. `defaultLocale` (when supplied on the cover).\n * 3. First non-empty value in the map.\n * 4. Empty string (only if every translation is empty — pathological).\n *\n * This deviation is documented in `https://github.com/vLannaAi/noy-db-docs/blob/main/content/docs/services/public-envelope.md`.\n */\nexport function pickLocale(\n value: string | Record<string, string>,\n locale: string,\n defaultLocale: string | undefined,\n): string {\n if (typeof value === 'string') return value\n if (value[locale] !== undefined && value[locale] !== '') return value[locale]\n if (defaultLocale && value[defaultLocale] !== undefined && value[defaultLocale] !== '') {\n return value[defaultLocale]\n }\n for (const v of Object.values(value)) {\n if (v !== '') return v\n }\n return ''\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/with-party/directory/cover/custom.ts","../src/with-party/directory/cover/schema.ts","../src/with-party/directory/cover/storage.ts"],"sourcesContent":["/**\n * The cover's namespaced `custom` extension slot (#800) — input\n * validation, the namespace-level patch merge, and the post-merge\n * size caps.\n *\n * Like everything on the cover, `custom` is plaintext, public by\n * design, and unauthenticated — payloads are hints (display defaults,\n * routing suggestions, registry pointers), never an input to\n * security, authorization, or data-integrity decisions.\n *\n * @see https://github.com/vLannaAi/noy-db-docs/blob/main/content/docs/services/public-envelope.md\n *\n * @module\n */\nimport { ValidationError } from '../../../kernel/errors.js'\nimport type { Cover, JsonValue, ResolvedCoverSchema } from './types.js'\n\n/**\n * Namespace keys must be reverse-DNS or package-style identifiers —\n * at least one dot/dash-separated segment (`noydb.viewer`,\n * `com.acme.registry` pass; a bare `config` fails), so two frameworks\n * can't collide on a bare word.\n */\nconst CUSTOM_KEY_PATTERN = /^[a-z0-9]+([.-][a-z0-9]+)+$/i\n\n/** Maximum nesting depth of a namespace payload (containers, not leaves). */\nconst MAX_CUSTOM_DEPTH = 8\n\n/**\n * Validate the owner-supplied `custom` input: the field must be a\n * plain object, every key must match {@link CUSTOM_KEY_PATTERN}, and\n * every value must be JSON-serializable (no functions, `undefined`,\n * symbols, bigints, or cycles) within the depth cap. `null` is\n * accepted — it is the delete-this-namespace directive and never\n * persists. Throws `ValidationError` on the first violation.\n */\nexport function validateCustomInput(custom: Record<string, JsonValue>): void {\n if (custom === null || typeof custom !== 'object' || Array.isArray(custom)) {\n throw new ValidationError(\n `setCover: custom must be a { [namespace]: value } object, got ${\n Array.isArray(custom) ? 'array' : typeof custom\n }.`,\n )\n }\n for (const [ns, value] of Object.entries(custom)) {\n if (!CUSTOM_KEY_PATTERN.test(ns)) {\n throw new ValidationError(\n `setCover: custom namespace \"${ns}\" is invalid. Keys must be ` +\n 'reverse-DNS or package-style identifiers with at least one ' +\n 'dot/dash-separated segment (e.g. \"noydb.viewer\", \"com.acme.registry\").',\n )\n }\n assertJsonValue(value, ns, 1, new Set())\n }\n}\n\nfunction assertJsonValue(\n value: unknown,\n ns: string,\n depth: number,\n seen: Set<object>,\n): void {\n if (value === null) return\n const t = typeof value\n if (t === 'string' || t === 'number' || t === 'boolean') return\n if (t !== 'object') {\n throw new ValidationError(\n `setCover: custom[\"${ns}\"] contains a non-JSON value (${t}). ` +\n 'Only strings, numbers, booleans, null, arrays, and plain objects are allowed.',\n )\n }\n const container = value as object\n if (seen.has(container)) {\n throw new ValidationError(\n `setCover: custom[\"${ns}\"] contains a circular reference.`,\n )\n }\n if (depth > MAX_CUSTOM_DEPTH) {\n throw new ValidationError(\n `setCover: custom[\"${ns}\"] nests deeper than the ${MAX_CUSTOM_DEPTH}-level cap.`,\n )\n }\n seen.add(container)\n const children = Array.isArray(container) ? container : Object.values(container)\n for (const child of children) {\n assertJsonValue(child, ns, depth + 1, seen)\n }\n seen.delete(container)\n}\n\n/**\n * Namespace-level patch merge for `setCover`: provided namespaces\n * replace their previous value, absent namespaces are preserved, and\n * an explicit `null` deletes that namespace (so `null` never persists\n * as a namespace value). A whole-`custom`-absent patch preserves the\n * previous custom, consistent with the other cover fields. Rationale:\n * framework A must never need read-modify-write of framework B's data.\n *\n * Returns a spreadable fragment — `{ custom }` when the merge leaves\n * at least one namespace, `{}` when it leaves none (the field is\n * dropped from the persisted document rather than stored empty).\n */\nexport function mergeCustom(\n existing: Record<string, JsonValue> | undefined,\n patch: Record<string, JsonValue> | undefined,\n): { custom: Record<string, JsonValue> } | Record<string, never> {\n if (patch === undefined) {\n return existing !== undefined ? { custom: existing } : {}\n }\n const merged: Record<string, JsonValue> = { ...existing }\n for (const [ns, value] of Object.entries(patch)) {\n if (value === null) {\n delete merged[ns]\n } else {\n merged[ns] = value\n }\n }\n return Object.keys(merged).length > 0 ? { custom: merged } : {}\n}\n\n/**\n * Post-merge size caps — validates the WOULD-BE-PERSISTED document\n * (after the namespace merge), so a small patch can't tip the stored\n * cover over a cap unnoticed. `maxCoverBytes` bounds the entire\n * serialized document, which also closes the unbounded locale-map\n * key-count hole for `name` / `description`. Sizes are measured as\n * `JSON.stringify` length. Throws `ValidationError` on violation.\n */\nexport function validateCoverSize(\n cover: Cover,\n schema: ResolvedCoverSchema,\n): void {\n if (cover.custom !== undefined) {\n const customSize = JSON.stringify(cover.custom).length\n if (customSize > schema.maxCustomBytes) {\n throw new ValidationError(\n `setCover: custom exceeds the ${schema.maxCustomBytes}-byte cap ` +\n `(got ${customSize} bytes serialized, after merging with the stored namespaces).`,\n )\n }\n }\n const totalSize = JSON.stringify(cover).length\n if (totalSize > schema.maxCoverBytes) {\n throw new ValidationError(\n `setCover: the cover document exceeds the ${schema.maxCoverBytes}-byte cap ` +\n `(got ${totalSize} bytes serialized).`,\n )\n }\n}\n","/**\n * Validate-on-write for the cover. Runs at every `setCover` call;\n * the developer's schema decides which fields are allowed and the\n * size caps that apply.\n *\n * @module\n */\nimport { ValidationError } from '../../../kernel/errors.js'\nimport { validateCustomInput } from './custom.js'\nimport type {\n Cover,\n CoverText,\n JsonValue,\n ResolvedCoverSchema,\n CoverField,\n} from './types.js'\n\n/** Owner-supplied input — the subset of {@link Cover} the owner can set. */\nexport interface SetCoverInput {\n readonly name?: CoverText\n readonly description?: CoverText\n readonly icon?: string\n readonly defaultLocale?: string\n /**\n * Namespace-level patch for the cover's `custom` slot (#800):\n * provided namespaces replace their previous value, absent\n * namespaces are preserved, an explicit `null` deletes that\n * namespace. Requires `'custom'` in the schema's `fields` list —\n * the `cover: true` shorthand does NOT enable it.\n */\n readonly custom?: Record<string, JsonValue>\n}\n\nconst DATA_URL_PREFIX = /^data:([a-zA-Z0-9.+-]+\\/[a-zA-Z0-9.+-]+);base64,/\n\n/**\n * Validate an owner-supplied cover input against the developer's\n * resolved schema. Throws `ValidationError` on the first violation;\n * returns void on success.\n *\n * The validator is deliberately strict: every fail mode is a hard\n * error rather than a silent drop, so the owner finds out immediately\n * which field they oversized rather than discovering a truncated\n * label months later.\n */\nexport function validateCoverInput(\n input: SetCoverInput,\n schema: ResolvedCoverSchema,\n): void {\n const allowed = new Set<CoverField>(schema.fields)\n\n // Reject any key not in the schema's allowed-field list.\n for (const key of Object.keys(input)) {\n const known: CoverField | undefined =\n key === 'name' || key === 'description' || key === 'icon' || key === 'defaultLocale' || key === 'custom'\n ? key\n : undefined\n if (!known) {\n throw new ValidationError(\n `setCover: unknown field \"${key}\". ` +\n `Allowed fields: ${[...allowed].join(', ')}.`,\n )\n }\n if (!allowed.has(known)) {\n throw new ValidationError(\n `setCover: field \"${known}\" is not enabled in this vault's schema. ` +\n `Allowed fields: ${[...allowed].join(', ')}.`,\n )\n }\n }\n\n if (input.name !== undefined) {\n validateText(input.name, 'name', schema.maxStringChars)\n }\n if (input.description !== undefined) {\n validateText(input.description, 'description', schema.maxStringChars)\n }\n if (input.icon !== undefined) {\n validateIcon(input.icon, schema)\n }\n if (input.defaultLocale !== undefined && typeof input.defaultLocale !== 'string') {\n throw new ValidationError(\n `setCover: defaultLocale must be a string (BCP-47), got ${typeof input.defaultLocale}.`,\n )\n }\n if (input.custom !== undefined) {\n validateCustomInput(input.custom)\n }\n}\n\nfunction validateText(\n value: CoverText,\n field: string,\n maxChars: number,\n): void {\n if (typeof value === 'string') {\n if (value.length > maxChars) {\n throw new ValidationError(\n `setCover: ${field} exceeds the ${maxChars}-character cap (got ${value.length}).`,\n )\n }\n return\n }\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new ValidationError(\n `setCover: ${field} must be a string or { [locale]: string } map, got ${typeof value}.`,\n )\n }\n // Locale map. Each value must be a non-empty string within the cap.\n for (const [locale, str] of Object.entries(value)) {\n if (typeof str !== 'string') {\n throw new ValidationError(\n `setCover: ${field}[${locale}] must be a string, got ${typeof str}.`,\n )\n }\n if (str.length > maxChars) {\n throw new ValidationError(\n `setCover: ${field}[${locale}] exceeds the ${maxChars}-character cap (got ${str.length}).`,\n )\n }\n }\n}\n\nfunction validateIcon(icon: string, schema: ResolvedCoverSchema): void {\n if (typeof icon !== 'string') {\n throw new ValidationError(\n `setCover: icon must be a data: URL string, got ${typeof icon}.`,\n )\n }\n if (icon.length > schema.maxIconBytes) {\n throw new ValidationError(\n `setCover: icon exceeds the ${schema.maxIconBytes}-byte cap (got ${icon.length}).`,\n )\n }\n const m = DATA_URL_PREFIX.exec(icon)\n if (!m) {\n throw new ValidationError(\n 'setCover: icon must be a base64 data URL ' +\n '(`data:image/png;base64,…` or `data:image/svg+xml;base64,…`). ' +\n 'External URLs are not supported in v1.',\n )\n }\n const mime = m[1]!\n if (!schema.iconMimeTypes.includes(mime)) {\n throw new ValidationError(\n `setCover: icon MIME type \"${mime}\" is not allowed. ` +\n `Permitted types: ${schema.iconMimeTypes.join(', ')}.`,\n )\n }\n}\n\n/**\n * Lightweight runtime predicate — used by the bundle header\n * validator to recognise a cover without requiring it. (The wire\n * discriminator keeps its frozen name: `_noydb_public: 1`.)\n */\nexport function isCover(x: unknown): x is Cover {\n if (x === null || typeof x !== 'object' || Array.isArray(x)) return false\n const obj = x as Record<string, unknown>\n return obj['_noydb_public'] === 1 && typeof obj['version'] === 'number'\n}\n","/**\n * Persistence helpers for `_meta/public-envelope` — the cover's\n * frozen wire location. Mirrors the bypass-AES pattern used by\n * `_meta/handle` and `_meta/policy` — the document is plaintext\n * JSON, the envelope's `_iv` field is left empty.\n *\n * @module\n */\nimport { buildRecordEnvelope } from '../../../kernel/enclave/index.js'\nimport type { NoydbStore } from '../../../kernel/types.js'\nimport type { Cover } from './types.js'\nimport { isCover } from './schema.js'\n\n/**\n * Reserved id for the vault-level cover record. The VALUE is frozen\n * wire format — the record predates the #799 rename and stays at\n * `_meta/public-envelope` so existing vaults need no migration.\n */\nexport const COVER_RECORD_ID = 'public-envelope'\n\n/**\n * Read the cover from `_meta/public-envelope`. Returns `undefined`\n * when no cover has been persisted (fresh vault, or a vault written\n * before the feature was enabled). Tolerates corrupted documents —\n * a JSON parse failure surfaces as `undefined`, not a thrown error,\n * mirroring `_meta/handle`'s contract.\n */\nexport async function loadCover(\n store: NoydbStore,\n vault: string,\n): Promise<Cover | undefined> {\n const envelope = await store.get(vault, '_meta', COVER_RECORD_ID)\n if (!envelope) return undefined\n try {\n const parsed = JSON.parse(envelope._data) as unknown\n if (!isCover(parsed)) return undefined\n return parsed\n } catch {\n return undefined\n }\n}\n\n/** Persist the cover. Idempotent — overwrites any prior write. */\nexport async function saveCover(\n store: NoydbStore,\n vault: string,\n cover: Cover,\n): Promise<void> {\n const wireEnvelope = buildRecordEnvelope(\n { collection: '_meta', id: COVER_RECORD_ID, version: 1 },\n { iv: '', data: JSON.stringify(cover) },\n )\n await store.put(vault, '_meta', COVER_RECORD_ID, wireEnvelope)\n}\n\n/**\n * Public, no-key reader. Plain function, not a method on `Noydb` —\n * the whole point is it works without an authenticated session, so\n * a UI can show \"Acme 2026 Tax Records\" before unlocking.\n *\n * Resolves any `name` / `description` locale map through the supplied\n * `locale` (when provided). Omitting `locale` returns the raw\n * cover, which a multilingual picker can render as it pleases.\n */\nexport async function readCover(\n store: NoydbStore,\n vault: string,\n opts: { readonly locale?: string } = {},\n): Promise<Cover | undefined> {\n const raw = await loadCover(store, vault)\n if (!raw) return undefined\n if (opts.locale === undefined) return raw\n return resolveLocale(raw, opts.locale)\n}\n\n/**\n * Resolve the locale-map fields (`name`, `description`) of a cover\n * to plain strings for the requested locale, falling back through\n * `defaultLocale` and then any available translation.\n *\n * @internal\n */\nexport function resolveLocale(\n cover: Cover,\n locale: string,\n): Cover {\n return {\n ...cover,\n ...(cover.name !== undefined ? { name: pickLocale(cover.name, locale, cover.defaultLocale) } : {}),\n ...(cover.description !== undefined ? { description: pickLocale(cover.description, locale, cover.defaultLocale) } : {}),\n }\n}\n\n/**\n * Resolve a `string | { [locale]: string }` value to a string for a\n * given locale. Exported so the bundle reader can reuse it without\n * duplicating the fallback chain.\n *\n * **Looser than `resolveI18nText`** — the hub's record-field resolver\n * throws `LocaleNotSpecifiedError` when no translation matches, but\n * label semantics deliberately prefer \"show *something*\" over\n * throwing. The fallback chain is:\n * 1. Exact `locale` match.\n * 2. `defaultLocale` (when supplied on the cover).\n * 3. First non-empty value in the map.\n * 4. Empty string (only if every translation is empty — pathological).\n *\n * This deviation is documented in `https://github.com/vLannaAi/noy-db-docs/blob/main/content/docs/services/public-envelope.md`.\n */\nexport function pickLocale(\n value: string | Record<string, string>,\n locale: string,\n defaultLocale: string | undefined,\n): string {\n if (typeof value === 'string') return value\n if (value[locale] !== undefined && value[locale] !== '') return value[locale]\n if (defaultLocale && value[defaultLocale] !== undefined && value[defaultLocale] !== '') {\n return value[defaultLocale]\n }\n for (const v of Object.values(value)) {\n if (v !== '') return v\n }\n return ''\n}\n"],"mappings":";;;;;;;;AAuBA,IAAM,qBAAqB;AAG3B,IAAM,mBAAmB;AAUlB,SAAS,oBAAoB,QAAyC;AAC3E,MAAI,WAAW,QAAQ,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GAAG;AAC1E,UAAM,IAAI;AAAA,MACR,iEACE,MAAM,QAAQ,MAAM,IAAI,UAAU,OAAO,MAC3C;AAAA,IACF;AAAA,EACF;AACA,aAAW,CAAC,IAAI,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AAChD,QAAI,CAAC,mBAAmB,KAAK,EAAE,GAAG;AAChC,YAAM,IAAI;AAAA,QACR,+BAA+B,EAAE;AAAA,MAGnC;AAAA,IACF;AACA,oBAAgB,OAAO,IAAI,GAAG,oBAAI,IAAI,CAAC;AAAA,EACzC;AACF;AAEA,SAAS,gBACP,OACA,IACA,OACA,MACM;AACN,MAAI,UAAU,KAAM;AACpB,QAAM,IAAI,OAAO;AACjB,MAAI,MAAM,YAAY,MAAM,YAAY,MAAM,UAAW;AACzD,MAAI,MAAM,UAAU;AAClB,UAAM,IAAI;AAAA,MACR,qBAAqB,EAAE,iCAAiC,CAAC;AAAA,IAE3D;AAAA,EACF;AACA,QAAM,YAAY;AAClB,MAAI,KAAK,IAAI,SAAS,GAAG;AACvB,UAAM,IAAI;AAAA,MACR,qBAAqB,EAAE;AAAA,IACzB;AAAA,EACF;AACA,MAAI,QAAQ,kBAAkB;AAC5B,UAAM,IAAI;AAAA,MACR,qBAAqB,EAAE,4BAA4B,gBAAgB;AAAA,IACrE;AAAA,EACF;AACA,OAAK,IAAI,SAAS;AAClB,QAAM,WAAW,MAAM,QAAQ,SAAS,IAAI,YAAY,OAAO,OAAO,SAAS;AAC/E,aAAW,SAAS,UAAU;AAC5B,oBAAgB,OAAO,IAAI,QAAQ,GAAG,IAAI;AAAA,EAC5C;AACA,OAAK,OAAO,SAAS;AACvB;AAcO,SAAS,YACd,UACA,OAC+D;AAC/D,MAAI,UAAU,QAAW;AACvB,WAAO,aAAa,SAAY,EAAE,QAAQ,SAAS,IAAI,CAAC;AAAA,EAC1D;AACA,QAAM,SAAoC,EAAE,GAAG,SAAS;AACxD,aAAW,CAAC,IAAI,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAC/C,QAAI,UAAU,MAAM;AAClB,aAAO,OAAO,EAAE;AAAA,IAClB,OAAO;AACL,aAAO,EAAE,IAAI;AAAA,IACf;AAAA,EACF;AACA,SAAO,OAAO,KAAK,MAAM,EAAE,SAAS,IAAI,EAAE,QAAQ,OAAO,IAAI,CAAC;AAChE;AAUO,SAAS,kBACd,OACA,QACM;AACN,MAAI,MAAM,WAAW,QAAW;AAC9B,UAAM,aAAa,KAAK,UAAU,MAAM,MAAM,EAAE;AAChD,QAAI,aAAa,OAAO,gBAAgB;AACtC,YAAM,IAAI;AAAA,QACR,gCAAgC,OAAO,cAAc,kBAC3C,UAAU;AAAA,MACtB;AAAA,IACF;AAAA,EACF;AACA,QAAM,YAAY,KAAK,UAAU,KAAK,EAAE;AACxC,MAAI,YAAY,OAAO,eAAe;AACpC,UAAM,IAAI;AAAA,MACR,4CAA4C,OAAO,aAAa,kBACtD,SAAS;AAAA,IACrB;AAAA,EACF;AACF;;;ACnHA,IAAM,kBAAkB;AAYjB,SAAS,mBACd,OACA,QACM;AACN,QAAM,UAAU,IAAI,IAAgB,OAAO,MAAM;AAGjD,aAAW,OAAO,OAAO,KAAK,KAAK,GAAG;AACpC,UAAM,QACJ,QAAQ,UAAU,QAAQ,iBAAiB,QAAQ,UAAU,QAAQ,mBAAmB,QAAQ,WAC5F,MACA;AACN,QAAI,CAAC,OAAO;AACV,YAAM,IAAI;AAAA,QACR,4BAA4B,GAAG,sBACV,CAAC,GAAG,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,MAC9C;AAAA,IACF;AACA,QAAI,CAAC,QAAQ,IAAI,KAAK,GAAG;AACvB,YAAM,IAAI;AAAA,QACR,oBAAoB,KAAK,4DACJ,CAAC,GAAG,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,MAC9C;AAAA,IACF;AAAA,EACF;AAEA,MAAI,MAAM,SAAS,QAAW;AAC5B,iBAAa,MAAM,MAAM,QAAQ,OAAO,cAAc;AAAA,EACxD;AACA,MAAI,MAAM,gBAAgB,QAAW;AACnC,iBAAa,MAAM,aAAa,eAAe,OAAO,cAAc;AAAA,EACtE;AACA,MAAI,MAAM,SAAS,QAAW;AAC5B,iBAAa,MAAM,MAAM,MAAM;AAAA,EACjC;AACA,MAAI,MAAM,kBAAkB,UAAa,OAAO,MAAM,kBAAkB,UAAU;AAChF,UAAM,IAAI;AAAA,MACR,0DAA0D,OAAO,MAAM,aAAa;AAAA,IACtF;AAAA,EACF;AACA,MAAI,MAAM,WAAW,QAAW;AAC9B,wBAAoB,MAAM,MAAM;AAAA,EAClC;AACF;AAEA,SAAS,aACP,OACA,OACA,UACM;AACN,MAAI,OAAO,UAAU,UAAU;AAC7B,QAAI,MAAM,SAAS,UAAU;AAC3B,YAAM,IAAI;AAAA,QACR,aAAa,KAAK,gBAAgB,QAAQ,uBAAuB,MAAM,MAAM;AAAA,MAC/E;AAAA,IACF;AACA;AAAA,EACF;AACA,MAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GAAG;AACvE,UAAM,IAAI;AAAA,MACR,aAAa,KAAK,sDAAsD,OAAO,KAAK;AAAA,IACtF;AAAA,EACF;AAEA,aAAW,CAAC,QAAQ,GAAG,KAAK,OAAO,QAAQ,KAAK,GAAG;AACjD,QAAI,OAAO,QAAQ,UAAU;AAC3B,YAAM,IAAI;AAAA,QACR,aAAa,KAAK,IAAI,MAAM,2BAA2B,OAAO,GAAG;AAAA,MACnE;AAAA,IACF;AACA,QAAI,IAAI,SAAS,UAAU;AACzB,YAAM,IAAI;AAAA,QACR,aAAa,KAAK,IAAI,MAAM,iBAAiB,QAAQ,uBAAuB,IAAI,MAAM;AAAA,MACxF;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,aAAa,MAAc,QAAmC;AACrE,MAAI,OAAO,SAAS,UAAU;AAC5B,UAAM,IAAI;AAAA,MACR,kDAAkD,OAAO,IAAI;AAAA,IAC/D;AAAA,EACF;AACA,MAAI,KAAK,SAAS,OAAO,cAAc;AACrC,UAAM,IAAI;AAAA,MACR,8BAA8B,OAAO,YAAY,kBAAkB,KAAK,MAAM;AAAA,IAChF;AAAA,EACF;AACA,QAAM,IAAI,gBAAgB,KAAK,IAAI;AACnC,MAAI,CAAC,GAAG;AACN,UAAM,IAAI;AAAA,MACR;AAAA,IAGF;AAAA,EACF;AACA,QAAM,OAAO,EAAE,CAAC;AAChB,MAAI,CAAC,OAAO,cAAc,SAAS,IAAI,GAAG;AACxC,UAAM,IAAI;AAAA,MACR,6BAA6B,IAAI,sCACX,OAAO,cAAc,KAAK,IAAI,CAAC;AAAA,IACvD;AAAA,EACF;AACF;AAOO,SAAS,QAAQ,GAAwB;AAC9C,MAAI,MAAM,QAAQ,OAAO,MAAM,YAAY,MAAM,QAAQ,CAAC,EAAG,QAAO;AACpE,QAAM,MAAM;AACZ,SAAO,IAAI,eAAe,MAAM,KAAK,OAAO,IAAI,SAAS,MAAM;AACjE;;;AC9IO,IAAM,kBAAkB;AAS/B,eAAsB,UACpB,OACA,OAC4B;AAC5B,QAAM,WAAW,MAAM,MAAM,IAAI,OAAO,SAAS,eAAe;AAChE,MAAI,CAAC,SAAU,QAAO;AACtB,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,SAAS,KAAK;AACxC,QAAI,CAAC,QAAQ,MAAM,EAAG,QAAO;AAC7B,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,eAAsB,UACpB,OACA,OACA,OACe;AACf,QAAM,eAAe;AAAA,IACnB,EAAE,YAAY,SAAS,IAAI,iBAAiB,SAAS,EAAE;AAAA,IACvD,EAAE,IAAI,IAAI,MAAM,KAAK,UAAU,KAAK,EAAE;AAAA,EACxC;AACA,QAAM,MAAM,IAAI,OAAO,SAAS,iBAAiB,YAAY;AAC/D;AAWA,eAAsB,UACpB,OACA,OACA,OAAqC,CAAC,GACV;AAC5B,QAAM,MAAM,MAAM,UAAU,OAAO,KAAK;AACxC,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI,KAAK,WAAW,OAAW,QAAO;AACtC,SAAO,cAAc,KAAK,KAAK,MAAM;AACvC;AASO,SAAS,cACd,OACA,QACO;AACP,SAAO;AAAA,IACL,GAAG;AAAA,IACH,GAAI,MAAM,SAAS,SAAY,EAAE,MAAM,WAAW,MAAM,MAAM,QAAQ,MAAM,aAAa,EAAE,IAAI,CAAC;AAAA,IAChG,GAAI,MAAM,gBAAgB,SAAY,EAAE,aAAa,WAAW,MAAM,aAAa,QAAQ,MAAM,aAAa,EAAE,IAAI,CAAC;AAAA,EACvH;AACF;AAkBO,SAAS,WACd,OACA,QACA,eACQ;AACR,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI,MAAM,MAAM,MAAM,UAAa,MAAM,MAAM,MAAM,GAAI,QAAO,MAAM,MAAM;AAC5E,MAAI,iBAAiB,MAAM,aAAa,MAAM,UAAa,MAAM,aAAa,MAAM,IAAI;AACtF,WAAO,MAAM,aAAa;AAAA,EAC5B;AACA,aAAW,KAAK,OAAO,OAAO,KAAK,GAAG;AACpC,QAAI,MAAM,GAAI,QAAO;AAAA,EACvB;AACA,SAAO;AACT;","names":[]}
@@ -5,22 +5,22 @@ import {
5
5
  canonicalizeMoneyFieldsAsDecimal,
6
6
  decodeMoneyFields,
7
7
  exactMath
8
- } from "./chunk-JU3JKOY5.js";
9
- import {
10
- groupAndReduce
11
- } from "./chunk-3JQVA527.js";
8
+ } from "./chunk-EWAGI3EM.js";
12
9
  import {
13
10
  putDerivedOutput
14
- } from "./chunk-OPGKH5RD.js";
11
+ } from "./chunk-NFPSC2HL.js";
15
12
  import {
16
13
  DEFAULT_JOIN_MAX_ROWS
17
14
  } from "./chunk-QI2KXQOM.js";
15
+ import {
16
+ groupAndReduce
17
+ } from "./chunk-LU6NPJWN.js";
18
18
  import {
19
19
  canonicalGroupKey
20
20
  } from "./chunk-XZHEOSQ2.js";
21
21
  import {
22
22
  applyI18nLocale
23
- } from "./chunk-7LLKPODL.js";
23
+ } from "./chunk-LPAGIMXJ.js";
24
24
  import {
25
25
  JoinTooLargeError,
26
26
  LocaleNotSpecifiedError,
@@ -307,4 +307,4 @@ async function listOutputIds(outputColl) {
307
307
  export {
308
308
  MaterializedViewExecutor
309
309
  };
310
- //# sourceMappingURL=chunk-4DJQU3PH.js.map
310
+ //# sourceMappingURL=chunk-OCCNYGKG.js.map
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  freezeAndDeleteClosure,
3
3
  randomId
4
- } from "./chunk-TY6N3PAC.js";
4
+ } from "./chunk-TXDOMADN.js";
5
5
  import {
6
6
  buildAccessibleBundle,
7
7
  resolveAccessibleCollections
8
- } from "./chunk-TWPTRJEU.js";
8
+ } from "./chunk-BBU3OD3D.js";
9
9
  import {
10
- NOYDB_FORMAT_VERSION
11
- } from "./chunk-SFY6NT7D.js";
10
+ buildRecordEnvelope
11
+ } from "./chunk-ZVAFJQZB.js";
12
12
  import {
13
13
  NoydbError
14
14
  } from "./chunk-I64WQSN3.js";
@@ -24,14 +24,11 @@ var WithdrawalRequestError = class extends NoydbError {
24
24
  function writeRequest(vault, req, expectedVersion) {
25
25
  const { name: vaultName, adapter } = vault._introspectState();
26
26
  const body = JSON.stringify(req);
27
- const env = {
28
- _noydb: NOYDB_FORMAT_VERSION,
29
- _v: expectedVersion + 1,
30
- _ts: req.decidedAt ?? req.requestedAt,
31
- _iv: "",
32
- _data: body,
33
- _by: req.decidedBy ?? req.requester
34
- };
27
+ const actor = req.decidedBy ?? req.requester;
28
+ const env = buildRecordEnvelope(
29
+ { collection: WITHDRAWAL_REQUESTS_COLLECTION, id: req.requestId, by: actor, version: expectedVersion + 1 },
30
+ { ts: req.decidedAt ?? req.requestedAt, iv: "", data: body }
31
+ );
35
32
  return adapter.put(vaultName, WITHDRAWAL_REQUESTS_COLLECTION, req.requestId, env, expectedVersion);
36
33
  }
37
34
  async function readRequest(vault, requestId) {
@@ -161,4 +158,4 @@ export {
161
158
  approveWithdrawal,
162
159
  rejectWithdrawal
163
160
  };
164
- //# sourceMappingURL=chunk-76FBDG6M.js.map
161
+ //# sourceMappingURL=chunk-OZD3D2DZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/with-audit/portability/request-withdrawal.ts"],"sourcesContent":["/**\n * Two-party withdrawal ceremony.\n *\n * The conservative counterpart to `unilateralWithdrawal`: a non-owner —\n * including a read-only `client`/`viewer` who cannot self-serve a deletion —\n * files a durable REQUEST; an owner/admin reviews it and either APPROVES\n * (extract-and-dispose under firm authority) or REJECTS it. Every step is\n * audited in the tamper-evident ledger.\n *\n * requester: vault.user.requestWithdrawal({ scope, disposition?, legalBasis? })\n * owner: vault.user.listWithdrawalRequests()\n * vault.user.approveWithdrawal(requestId, { reKey }) → { bundle, snapshot? }\n * vault.user.rejectWithdrawal(requestId, { reason })\n *\n * Requests live in the reserved `_user_withdrawal_requests` namespace. The\n * record body is plaintext metadata (collection names + disposition + legal\n * basis — none secret in this trust model; the owner sees the data anyway) and\n * carries NO secret: the re-key secret is supplied by the approver at\n * approval time and conveyed to the requester out-of-band, so no secret is\n * stored at rest.\n */\nimport type { Vault } from '../../kernel/vault.js'\nimport { buildRecordEnvelope } from '../../kernel/enclave/index.js'\nimport { NoydbError } from '../../kernel/errors.js'\nimport { resolveAccessibleCollections, buildAccessibleBundle } from './export-accessible.js'\nimport { freezeAndDeleteClosure, randomId, type FrozenSnapshotRef, type WithdrawResult } from './withdraw-accessible.js'\n\nexport const WITHDRAWAL_REQUESTS_COLLECTION = '_user_withdrawal_requests'\n\n/** Raised when a request is missing, already decided, or expired. */\nexport class WithdrawalRequestError extends NoydbError {\n constructor(message: string) {\n super('WITHDRAWAL_REQUEST', message)\n this.name = 'WithdrawalRequestError'\n }\n}\n\nexport type WithdrawalRequestStatus = 'pending' | 'approved' | 'rejected'\n\nexport interface WithdrawalRequest {\n readonly requestId: string\n readonly requester: string\n readonly collections: readonly string[]\n readonly disposition: 'delete' | 'freeze'\n readonly legalBasis?: string\n readonly status: WithdrawalRequestStatus\n readonly requestedAt: string\n readonly expiresAt?: string\n readonly decidedAt?: string\n readonly decidedBy?: string\n readonly rejectReason?: string\n readonly snapshotSha256?: string\n}\n\nexport interface RequestWithdrawalOptions {\n readonly scope?: { readonly collections?: readonly string[] }\n readonly disposition?: 'delete' | 'freeze'\n readonly legalBasis?: string\n /** Time-to-live in ms; after this the request can no longer be approved. */\n readonly expiresInMs?: number\n}\n\nexport interface RequestWithdrawalResult {\n readonly requestId: string\n readonly status: WithdrawalRequestStatus\n readonly expiresAt?: string\n}\n\nfunction writeRequest(vault: Vault, req: WithdrawalRequest, expectedVersion: number): Promise<void> {\n const { name: vaultName, adapter } = vault._introspectState()\n const body = JSON.stringify(req)\n const actor = req.decidedBy ?? req.requester\n const env = buildRecordEnvelope(\n { collection: WITHDRAWAL_REQUESTS_COLLECTION, id: req.requestId, by: actor, version: expectedVersion + 1 },\n { ts: req.decidedAt ?? req.requestedAt, iv: '', data: body},\n )\n return adapter.put(vaultName, WITHDRAWAL_REQUESTS_COLLECTION, req.requestId, env, expectedVersion)\n}\n\nasync function readRequest(vault: Vault, requestId: string): Promise<{ req: WithdrawalRequest; version: number }> {\n const { name: vaultName, adapter } = vault._introspectState()\n const env = await adapter.get(vaultName, WITHDRAWAL_REQUESTS_COLLECTION, requestId)\n if (!env) throw new WithdrawalRequestError(`withdrawal request \"${requestId}\" not found`)\n return { req: JSON.parse(env._data) as WithdrawalRequest, version: env._v }\n}\n\n/**\n * Requester side. Files a durable request for the caller's accessible scope.\n * Gated by `user-request-withdrawal` (checked in the UserApi wrapper).\n */\nexport async function requestWithdrawal(\n vault: Vault,\n opts: RequestWithdrawalOptions = {},\n): Promise<RequestWithdrawalResult> {\n const { keyring } = vault._introspectState()\n // Resolve the requester's accessible collections (read access defines \"theirs\";\n // the owner — who can delete anything — executes the disposition on approval).\n const collections = resolveAccessibleCollections(keyring, opts.scope, false)\n if (!collections || collections.length === 0) {\n throw new WithdrawalRequestError(\n 'requestWithdrawal needs a concrete scope.collections (the caller has all-collection access — name the collections to withdraw)',\n )\n }\n const requestId = `wr-${randomId()}`\n const requestedAt = new Date().toISOString()\n const req: WithdrawalRequest = {\n requestId, requester: keyring.userId, collections, disposition: opts.disposition ?? 'delete',\n status: 'pending', requestedAt,\n ...(opts.legalBasis ? { legalBasis: opts.legalBasis } : {}),\n ...(opts.expiresInMs ? { expiresAt: new Date(Date.parse(requestedAt) + opts.expiresInMs).toISOString() } : {}),\n }\n await writeRequest(vault, req, 0) // create-only\n await vault._getLedgerOrNull()?.append({\n op: 'lifecycle', collection: '', id: '', version: 0, actor: keyring.userId, payloadHash: '',\n reason: `user-withdrawal-request:${requestId}:${keyring.userId}`,\n })\n return { requestId, status: 'pending', ...(req.expiresAt ? { expiresAt: req.expiresAt } : {}) }\n}\n\n/** Owner side. List filed requests (optionally by status). */\nexport async function listWithdrawalRequests(\n vault: Vault,\n opts: { status?: WithdrawalRequestStatus } = {},\n): Promise<WithdrawalRequest[]> {\n const { name: vaultName, adapter } = vault._introspectState()\n const ids = await adapter.list(vaultName, WITHDRAWAL_REQUESTS_COLLECTION)\n const out: WithdrawalRequest[] = []\n for (const id of ids) {\n const env = await adapter.get(vaultName, WITHDRAWAL_REQUESTS_COLLECTION, id)\n if (!env) continue\n const req = JSON.parse(env._data) as WithdrawalRequest\n if (!opts.status || req.status === opts.status) out.push(req)\n }\n return out\n}\n\nfunction assertApprover(vault: Vault): string {\n const { keyring } = vault._introspectState()\n // FR-6: custodian is INTENTIONALLY excluded (SAFER default — review flag).\n // Approving a two-party withdrawal is a destructive extract-and-dispose\n // exercised under FIRM authority — a governance act adjacent to grant/revoke,\n // which a non-owning custodian cannot do. It stays owner/admin-only.\n if (keyring.role !== 'owner' && keyring.role !== 'admin') {\n throw new WithdrawalRequestError('approveWithdrawal / rejectWithdrawal require an owner or admin')\n }\n return keyring.userId\n}\n\nfunction assertPending(req: WithdrawalRequest): void {\n if (req.status !== 'pending') {\n throw new WithdrawalRequestError(`withdrawal request \"${req.requestId}\" is already ${req.status}`)\n }\n if (req.expiresAt && Date.now() > Date.parse(req.expiresAt)) {\n throw new WithdrawalRequestError(`withdrawal request \"${req.requestId}\" expired at ${req.expiresAt}`)\n }\n}\n\nexport interface ApproveWithdrawalOptions {\n /** Re-key the handed-back bundle to a secret the requester will use. */\n readonly reKey?: { readonly secret: string }\n}\n\n/**\n * Owner side. Extract the requester's recorded scope under firm authority,\n * dispose of the source per the request's disposition, mark the request\n * approved, and return the re-keyed bundle to hand back. Gated by\n * `approve-user-withdrawal` (checked in the UserApi wrapper) + owner/admin role.\n */\nexport async function approveWithdrawal(\n vault: Vault,\n requestId: string,\n opts: ApproveWithdrawalOptions = {},\n): Promise<WithdrawResult> {\n const approver = assertApprover(vault)\n const { req, version } = await readRequest(vault, requestId)\n assertPending(req)\n\n // 1. Produce the requester's re-keyed portable copy FIRST (owner authority\n // holds every DEK, so the recorded collections are all readable).\n const bundle = await buildAccessibleBundle(vault, [...req.collections], opts.reKey)\n\n // 2. + 3. dispose of the source (freeze snapshot, optional → delete-closure).\n const snapshot: FrozenSnapshotRef | undefined = await freezeAndDeleteClosure(vault, req.collections, {\n disposition: req.disposition, actorUserId: approver, withdrawalId: `wd-${requestId}`,\n })\n\n // 4. mark approved + audit (OCC against the version we read).\n const decidedAt = new Date().toISOString()\n await writeRequest(vault, {\n ...req, status: 'approved', decidedAt, decidedBy: approver,\n ...(snapshot ? { snapshotSha256: snapshot.sha256 } : {}),\n }, version)\n await vault._getLedgerOrNull()?.append({\n op: 'lifecycle', collection: '', id: '', version: 0, actor: approver, payloadHash: '',\n reason: `user-withdrawal-approved:${requestId}:${req.requester}:${req.disposition}`,\n })\n\n return snapshot ? { bundle, snapshot } : { bundle }\n}\n\nexport interface RejectWithdrawalOptions {\n readonly reason?: string\n}\n\n/** Owner side. Decline a pending request (no data is touched). */\nexport async function rejectWithdrawal(\n vault: Vault,\n requestId: string,\n opts: RejectWithdrawalOptions = {},\n): Promise<WithdrawalRequest> {\n const approver = assertApprover(vault)\n const { req, version } = await readRequest(vault, requestId)\n assertPending(req)\n const decidedAt = new Date().toISOString()\n const updated: WithdrawalRequest = {\n ...req, status: 'rejected', decidedAt, decidedBy: approver,\n ...(opts.reason ? { rejectReason: opts.reason } : {}),\n }\n await writeRequest(vault, updated, version)\n await vault._getLedgerOrNull()?.append({\n op: 'lifecycle', collection: '', id: '', version: 0, actor: approver, payloadHash: '',\n reason: `user-withdrawal-rejected:${requestId}:${req.requester}`,\n })\n return updated\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  LookupHandle
3
- } from "./chunk-IQGIPRSE.js";
3
+ } from "./chunk-CFTPERGL.js";
4
4
 
5
5
  // src/via/lookup/active.ts
6
6
  function withLookup() {
@@ -33,4 +33,4 @@ function withLookup() {
33
33
  export {
34
34
  withLookup
35
35
  };
36
- //# sourceMappingURL=chunk-YX6J3Z3Z.js.map
36
+ //# sourceMappingURL=chunk-PBPYQZDW.js.map
@@ -4,10 +4,10 @@ import {
4
4
  import {
5
5
  canonicalJson,
6
6
  sha256Hex
7
- } from "./chunk-53PWELSC.js";
7
+ } from "./chunk-FY33DKPH.js";
8
8
  import {
9
9
  isDeleteMarker
10
- } from "./chunk-6E2JZRBD.js";
10
+ } from "./chunk-ZVAFJQZB.js";
11
11
  import {
12
12
  PeriodClosedError,
13
13
  ValidationError
@@ -68,7 +68,7 @@ async function loadPeriods(adapter, vault, decrypt) {
68
68
  const records = [];
69
69
  for (const id of ids) {
70
70
  const env = await adapter.get(vault, PERIODS_COLLECTION, id);
71
- if (env) records.push(await decrypt(env));
71
+ if (env) records.push(await decrypt(id, env));
72
72
  }
73
73
  records.sort((a, b) => a.closedAt.localeCompare(b.closedAt));
74
74
  return records;
@@ -124,7 +124,7 @@ function validatePeriodName(name, existing, partition) {
124
124
  }
125
125
  async function appendPeriodLedgerEntry(ledger, actor, envelope, name, collection = PERIODS_COLLECTION) {
126
126
  if (!ledger) return;
127
- const { envelopePayloadHash } = await import("./ledger-6IHXJHDD.js");
127
+ const { envelopePayloadHash } = await import("./ledger-KQAJPEMZ.js");
128
128
  await ledger.append({
129
129
  op: "put",
130
130
  collection,
@@ -198,4 +198,4 @@ export {
198
198
  purgeMarkersOn,
199
199
  withPeriods
200
200
  };
201
- //# sourceMappingURL=chunk-TTAND7MR.js.map
201
+ //# sourceMappingURL=chunk-PBRIHUQ5.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/with-audit/periods/periods.ts","../src/with-audit/periods/active.ts","../src/with-audit/periods/strategy.ts"],"sourcesContent":["/**\n * Accounting-period closure + opening.\n *\n * A closed period seals every record whose envelope `_ts` is at or\n * before the period's `endDate`: further writes (`put` / `delete`)\n * against such records throw {@link PeriodClosedError}. The period\n * itself is stored as a record in the reserved `_periods` collection\n * and written through the normal ledger-instrumented path, so every\n * closure appends a tamper-evident entry to the vault's hash chain.\n *\n * ## Closure model\n *\n * ```\n * vault.closePeriod({ name: 'FY2026-Q1', endDate: '2026-03-31' })\n * └─► PeriodRecord written to _periods/<name>\n * ├─ priorPeriodName / priorPeriodHash — chain to last close\n * ├─ closedAt / closedBy — provenance\n * └─ normal ledger append fires (LedgerStore.append)\n * ```\n *\n * Enforcement (`assertTsWritable`) is vault-local: the Vault caches\n * the list of closed periods on first read and consults that cache in\n * the `Collection.put` / `.delete` path via the `periodGuard` hook.\n *\n * ## Opening model\n *\n * ```\n * vault.openPeriod({\n * name: 'FY2026-Q2',\n * startDate: '2026-04-01',\n * fromPeriod: 'FY2026-Q1',\n * carryForward: async (priorView) => Record<string, Record<string, unknown>>,\n * })\n * ```\n *\n * `carryForward` receives a read-only `VaultInstant` anchored at the\n * prior period's `endDate` (built via `vault.at(endDate)`) so the\n * callback can compute closing aggregates from the sealed state. The\n * returned `{ [collectionName]: { [id]: record } }` map is written\n * before the new `PeriodRecord` lands — opening balances materialise\n * as normal records with fresh timestamps that fall outside every\n * closed period.\n *\n * ## Freeze\n *\n * ```\n * vault.freezePeriod('FY2026-Q1')\n * └─► physically purges delete markers whose write-time falls inside\n * the closed period's window (via the #589 `_purgeDeleteMarkers`\n * seam), then records the fact:\n * ├─ PeriodFreezeRecord written to _period_freezes/<name>\n * └─ normal ledger append fires (LedgerStore.append)\n * ```\n *\n * The chained `_periods/<name>` record is never mutated — `frozenAt` /\n * `frozenBy` / `purgedMarkerCount` are merged onto the returned\n * `PeriodRecord` at read time from the companion, so a tamper with the\n * freeze can never break the inter-period hash chain. Freezing is\n * terminal (a closed period, once frozen, stays frozen) and idempotent\n * (a second call is a no-op that returns the same merged record without\n * re-purging or re-appending a ledger entry). Freeze does NOT purge\n * forget-tombstones (GDPR crypto-shred erasure evidence), `_history`\n * versions, or live records — the delete-markers-only seam leaves all\n * three untouched by construction.\n *\n * Freeze purges the LOCAL adapter only. On a synced vault, markers already\n * pushed to sync targets survive there, and a later pull re-imports them\n * (benign — they still read deleted, but the space isn't reclaimed). A\n * re-imported marker keeps its original `_ts` (inside the already-frozen\n * period's window), so — like any late-booked delete — it is reclaimed by\n * the NEXT period's freeze, whose window covers it; freeze stays terminal\n * and does NOT re-purge an already-frozen period (#611). Sweeping the sync\n * targets themselves is a cross-target-purge concern deferred to the\n * cold-archival spec. Purging re-opens the #589 resurrection window for a\n * peer offline since before the cutoff, which is why the closed period is\n * the operator-asserted safe-point that gates the call.\n *\n * A period whose purge window has not fully elapsed cannot be frozen —\n * `freezePeriod` throws rather than purge markers for deletes that may not\n * have converged yet (#610).\n *\n * ## Archive\n *\n * ```\n * vault.archivePeriod('FY2026-Q1')\n * └─► relocates the closed period's in-window records (those with\n * `_ts < periodExclusiveUpperBound(endDate)`) from the hot store to\n * the configured cold tier (routeStore's `cold` route), then records:\n * ├─ PeriodArchiveRecord written to _period_archives/<name>\n * └─ a ledger entry attributed to _period_archives\n * ```\n *\n * Archival is NON-DESTRUCTIVE: routeStore reads fall through to the cold\n * tier on a hot miss, so an archived record still reads normally. It is\n * therefore gated only on `closed` (not `frozen`) — it does not re-open the\n * #589 resurrection window and needs no convergence safe-point. Freeze\n * (purge markers) and archive (relocate records) are independent and compose\n * in either order. Like freeze, archival keeps the chained `_periods/<name>`\n * record byte-immutable (state lives in the companion) and is idempotent.\n *\n * Bounds by write-time `_ts`, NOT business date: the store tier sees only\n * encrypted envelopes. A record with an in-period business date but a later\n * `_ts` (late-booked) archives at the NEXT period's archive — the same rule\n * freeze uses for late-booked delete markers. Requires a `routeStore` with a\n * cold route (`age: { cold }`); throws otherwise.\n *\n * Read cost: with `withLazy()` (per-id reads) archived records are truly\n * cold — fetched from cold only on access. In the default hydrated mode,\n * `loadAll` merges the cold store, so archived records still load into RAM\n * on vault open (hot-tier STORAGE is reclaimed; RAM is not). Summaries\n * (`_`-prefixed) always stay hot.\n *\n * ## Target-purge\n *\n * ```\n * vault.purgePeriodTargets('FY2026-Q1')\n * └─► sweeps delete markers (`_ts < periodExclusiveUpperBound(endDate)`) off\n * the vault's PUSH-ONLY sync targets (backup/archive roles), then records:\n * ├─ PeriodTargetPurgeRecord written to _period_target_purges/<name>\n * └─ a ledger entry attributed to _period_target_purges\n * ```\n *\n * Extends freeze's local marker purge to the vault's own remote sinks.\n * `sync-peer` (bidirectional) targets are SKIPPED: purging a marker there\n * re-opens the #589 resurrection window for a client offline before the\n * cutoff, an assertion no single vault can verify. Backup/archive targets are\n * push-only — never pulled from into convergence — so sweeping their markers\n * is safe. Requires the period be frozen first (closed → frozen →\n * target-purged) so the local safe-point is already established. Idempotent\n * once run; a vault with no push-only targets writes no companion and is\n * re-runnable (so a target added later is still swept). Single-vault only —\n * fleet-wide purge across sovereign vaults is klum's concern over\n * `@noy-db/hub/cargo`.\n *\n * ## Not covered\n *\n * - Partial re-opening of a closed period. If an auditor needs to\n * make a correction inside a sealed period, the sanctioned path is\n * a compensating entry in the NEW period, not an unlock of the\n * old one.\n * - Automatic period rollover. `closePeriod` / `openPeriod` are\n * deliberately explicit operator calls so the caller decides when\n * the boundary lands.\n *\n * @module\n */\n\nimport type { NoydbStore, EncryptedEnvelope } from '../../kernel/types.js'\nimport type { LedgerStore } from '../../with-commit/history/ledger/index.js'\nimport { sha256Hex, canonicalJson } from '../../with-commit/history/ledger/index.js'\nimport { isDeleteMarker } from '../../kernel/enclave/index.js'\nimport { PeriodClosedError, ValidationError } from '../../kernel/errors.js'\n\n// The reserved collection names + `periodExclusiveUpperBound` moved to the\n// dependency-light `window.ts` so the sync engine's period-scoped pull (#807)\n// can import them without this module's ledger imports; re-exported here so\n// every existing import path keeps working.\nexport {\n PERIODS_COLLECTION,\n PERIOD_FREEZES_COLLECTION,\n PERIOD_ARCHIVES_COLLECTION,\n PERIOD_TARGET_PURGES_COLLECTION,\n PERIOD_REOPENS_COLLECTION,\n periodExclusiveUpperBound,\n} from './window.js'\nimport { PERIODS_COLLECTION } from './window.js'\n\n/**\n * Companion record recording that a closed period was frozen (its delete\n * markers physically purged). Stored in {@link PERIOD_FREEZES_COLLECTION},\n * keyed by period name — kept OFF the hash-chained `_periods/<name>` record so\n * freeze never alters the inter-period chain.\n */\nexport interface PeriodFreezeRecord {\n readonly period: string\n readonly frozenAt: string\n readonly frozenBy: string\n readonly purgedMarkerCount: number\n}\n\n/**\n * Companion record noting that a closed period was cold-archived (its\n * in-window records physically relocated hot → cold). Stored in\n * {@link PERIOD_ARCHIVES_COLLECTION}, keyed by period name — kept OFF the\n * hash-chained `_periods/<name>` record so archive never alters the chain.\n */\nexport interface PeriodArchiveRecord {\n readonly period: string\n readonly archivedAt: string\n readonly archivedBy: string\n readonly archivedRecordCount: number\n}\n\n/** Per-target count of delete markers purged off one push-only sync target (#615). */\nexport interface TargetPurgeCount {\n readonly label?: string\n readonly role: 'backup' | 'archive'\n readonly purgedCount: number\n}\n\n/**\n * One entry in a period's append-only reopen/reclose log (#1022).\n *\n * Real accounting close is a three-state lifecycle — open / closed / reopened\n * — not a one-way door. A month gets closed, a missing invoice arrives or a\n * filing is rejected, the month is reopened, corrected, and reclosed. The audit\n * value is not the ability to write again; it is the chain being able to say\n * *closed at T1, reopened at T2 by U, reclosed at T3*.\n */\nexport interface PeriodReopenEvent {\n readonly op: 'reopen' | 'reclose'\n /** ISO timestamp the event was recorded. */\n readonly at: string\n /** userId of the keyring that performed it. */\n readonly by: string\n /**\n * `reopen` only — ISO instant after which the period re-seals on its own,\n * with nobody acting. Absent means the window stays open until an explicit\n * `reclosePeriod`.\n */\n readonly until?: string\n /** Free-text justification, carried verbatim into the audit trail. */\n readonly reason?: string\n}\n\n/**\n * Companion holding a period's reopen/reclose history (#1022). Stored in\n * {@link PERIOD_REOPENS_COLLECTION}, keyed by the period's storage key — kept\n * OFF the hash-chained `_periods/<name>` record for the same reason freeze and\n * archive are: reopening must never rewrite the close, or the chain that proves\n * the close happened is the very thing the reopen destroys.\n *\n * `events` is APPEND-ONLY. Where the other companions are single-shot and\n * idempotent, this one accumulates, because the cycle repeats.\n */\nexport interface PeriodReopenRecord {\n readonly period: string\n readonly partition?: PeriodPartition\n readonly events: readonly PeriodReopenEvent[]\n}\n\n/**\n * Collapse an append-only reopen log into the return-only fields merged onto a\n * {@link PeriodRecord} on read.\n *\n * Expiry is deliberately NOT resolved here: `reopenedUntil` is carried through\n * verbatim and compared against the clock at write-guard time, so a bounded\n * window re-seals on its own without anything having to run.\n *\n * @internal\n */\nexport function resolveReopenState(events: readonly PeriodReopenEvent[]): {\n reopenedAt?: string\n reopenedBy?: string\n reopenedUntil?: string\n reopenReason?: string\n reclosedAt?: string\n reopenCount: number\n} {\n let lastReopen: PeriodReopenEvent | undefined\n let reclosedAfter: string | undefined\n let reopenCount = 0\n for (const e of events) {\n if (e.op === 'reopen') {\n lastReopen = e\n reclosedAfter = undefined\n reopenCount++\n } else if (lastReopen !== undefined) {\n reclosedAfter = e.at\n }\n }\n if (!lastReopen) return { reopenCount }\n return {\n reopenedAt: lastReopen.at,\n reopenedBy: lastReopen.by,\n ...(lastReopen.until !== undefined && { reopenedUntil: lastReopen.until }),\n ...(lastReopen.reason !== undefined && { reopenReason: lastReopen.reason }),\n ...(reclosedAfter !== undefined && { reclosedAt: reclosedAfter }),\n reopenCount,\n }\n}\n\n/**\n * Is this period writable right now on account of a reopen? (#1022)\n *\n * Three ways to be sealed again: never reopened, explicitly reclosed after the\n * last reopen, or a bounded window that has elapsed. The clock is read by the\n * caller and passed in, so the guard and any diagnostic agree on one instant.\n *\n * @internal\n */\nexport function isEffectivelyReopened(period: PeriodRecord, nowIso: string): boolean {\n if (period.reopenedAt === undefined) return false\n if (period.reclosedAt !== undefined && period.reclosedAt >= period.reopenedAt) return false\n if (period.reopenedUntil !== undefined && nowIso > period.reopenedUntil) return false\n return true\n}\n\n/**\n * Companion record noting that a closed+frozen period's delete markers were\n * swept off the vault's push-only sync targets (#615). Stored in\n * {@link PERIOD_TARGET_PURGES_COLLECTION}, keyed by period name — kept OFF the\n * hash-chained `_periods/<name>` record so target-purge never alters the chain.\n */\nexport interface PeriodTargetPurgeRecord {\n readonly period: string\n readonly purgedAt: string\n readonly purgedBy: string\n readonly targets: readonly TargetPurgeCount[]\n}\n\n/**\n * Scope tuple for a period timeline (#1005).\n *\n * Identical in shape and semantics to `SequenceOptions.partition`: a\n * partitioned timeline is always disjoint from any unpartitioned one, and from\n * every other tuple. `['acme', 'vat']` and `['acme', 'wht']` are two\n * independent close calendars for the same subject — which is the whole point,\n * since sub-ledgers for one legal entity and one month routinely close on\n * different statutory deadlines.\n */\nexport type PeriodPartition = readonly (string | number)[]\n\n/**\n * Resolve the `_periods` storage key for a (name, partition) pair.\n *\n * Deliberately the same encoding as `resolveSequenceKey`: `name` verbatim when\n * unpartitioned, else `${name}\\x00${parts}` with each component\n * `encodeURIComponent`d and `'/'`-joined. The null-byte separator cannot occur\n * in a period name, so a partitioned key never collides with an unpartitioned\n * one; URI-encoding keeps `['a/b']` distinct from `['a','b']`.\n *\n * @throws {ValidationError} on an empty component or a non-finite number.\n * @internal\n */\nexport function resolvePeriodKey(name: string, partition?: PeriodPartition): string {\n if (!partition || partition.length === 0) return name\n const parts = partition.map((p) => {\n if (typeof p === 'number' && !Number.isFinite(p)) {\n throw new ValidationError(`period partition component must be a finite number, got ${p}`)\n }\n const s = String(p)\n if (s === '') {\n throw new ValidationError('period partition component must not be empty')\n }\n return encodeURIComponent(s)\n })\n return `${name}\\x00${parts.join('/')}`\n}\n\n/**\n * Do two partitions denote the same timeline? Absent and empty both mean \"the\n * unpartitioned timeline\", so they compare equal.\n *\n * @internal\n */\nexport function samePartition(a?: PeriodPartition, b?: PeriodPartition): boolean {\n const x = a ?? []\n const y = b ?? []\n if (x.length !== y.length) return false\n return x.every((v, i) => String(v) === String(y[i]))\n}\n\n/**\n * Resolves a record to the timeline that governs it. Supplied by\n * `withPeriods({ subjects })`; returns `undefined` for any collection with no\n * mapping, which is what keeps an unconfigured vault on the single vault-wide\n * timeline it has always had.\n *\n * @internal\n */\nexport type PartitionResolver = (\n collection: string,\n record: Record<string, unknown>,\n) => PeriodPartition | undefined\n\n/**\n * Stored record for one closed or opened accounting period. One entry\n * per period, keyed by `name` in the reserved `_periods` collection.\n *\n * The hash chain between periods is computed at read time by\n * `loadPeriods()` — each record carries the name + hash of its\n * predecessor so a tamper with any period's record breaks the chain\n * into the next one, the same way the ledger's `prevHash` works.\n */\nexport interface PeriodRecord {\n /**\n * Human-readable name (e.g., `'FY2026-Q1'`). Unique per PARTITION — two\n * timelines may each carry a `'2026-06'`, which is the normal case when one\n * vault serves several subjects (#1005). Unique per vault when unpartitioned.\n */\n readonly name: string\n /**\n * The timeline this period belongs to. Absent = the vault-wide timeline.\n * Two periods with the same `name` and different `partition` are unrelated:\n * separate hash chains, separate close state, and the write guard applies\n * each only to records that resolve to its own tuple.\n */\n readonly partition?: PeriodPartition\n /**\n * Role discriminator. A period is `'closed'` from the moment its\n * `closedAt` is recorded; `'opened'` marks a period whose opening\n * entries have been carried forward via {@link openPeriod}. Many\n * workflows will produce one opened period per closed period (the\n * opened one is the SUCCESSOR — its `startDate` equals the prior\n * `endDate + 1 day`).\n */\n readonly kind: 'closed' | 'opened'\n /** ISO date — inclusive upper bound for records belonging to this period. */\n readonly endDate: string\n /** ISO date — lower bound (present on opened periods only). */\n readonly startDate?: string\n /**\n * Record field carrying the business date (e.g. `'date'` on an\n * invoice, `'paidAt'` on a payment). The guard compares\n * `record[dateField]` against `endDate` — NOT the envelope `_ts`.\n * Accounting entries booked late (business date `2026-01-15`,\n * write-time `2026-04-22`) still get sealed when Q1 closes at\n * `2026-03-31` because the comparison uses the business date.\n *\n * Optional for backwards compat. When absent, the guard falls back\n * to envelope `_ts` — that's a write-time seal, appropriate for\n * content that doesn't carry a logical business date (e.g. system\n * settings) but almost never right for accounting ledgers.\n */\n readonly dateField?: string\n /** ISO timestamp recorded at `closePeriod()` / `openPeriod()` call time. */\n readonly closedAt: string\n /** userId of the keyring that invoked the close/open. */\n readonly closedBy: string\n /** Name of the prior period this one chains to, if any. */\n readonly priorPeriodName?: string\n /** sha256(canonicalJson(priorPeriod)) — empty for the first period. */\n readonly priorPeriodHash: string\n /**\n * Opened periods only — the names of the collections whose\n * carry-forward aggregates were written by {@link openPeriod}.\n * Recorded for auditability so a future `verifyPeriodChain()` can\n * cross-check the opening balances against the closing snapshot.\n */\n readonly openingCollections?: readonly string[]\n /** #604 return-only — merged from the `_period_freezes/<name>` companion on\n * read; NEVER written into the stored `_periods/<name>` record (would break\n * the hash chain). Absent = not yet frozen. */\n readonly frozenAt?: string\n readonly frozenBy?: string\n readonly purgedMarkerCount?: number\n /** #613 return-only — merged from the `_period_archives/<name>` companion on\n * read; NEVER written into the stored `_periods/<name>` record. Absent = not\n * yet archived. */\n readonly archivedAt?: string\n readonly archivedBy?: string\n readonly archivedRecordCount?: number\n /** #615 return-only — merged from the `_period_target_purges/<name>` companion\n * on read; NEVER written into the stored `_periods/<name>` record. Absent =\n * target-purge not yet run (or the vault has no push-only targets). */\n readonly targetsPurgedAt?: string\n readonly targetsPurgedBy?: string\n readonly targetsPurged?: readonly TargetPurgeCount[]\n /** #1022 return-only — collapsed from the `_period_reopens/<key>` append-only\n * log on read; NEVER written into the stored `_periods/<name>` record, so a\n * reopen cannot disturb the inter-period hash chain. Absent = never reopened.\n * `reclosedAt` present (and >= `reopenedAt`) means the window was closed\n * again explicitly; `reopenedUntil` in the past means it lapsed on its own.\n * Use {@link isEffectivelyReopened} rather than reading these directly. */\n readonly reopenedAt?: string\n readonly reopenedBy?: string\n readonly reopenedUntil?: string\n readonly reopenReason?: string\n readonly reclosedAt?: string\n /** How many times this period has been reopened, ever. */\n readonly reopenCount?: number\n}\n\n/** Options for `vault.closePeriod()`. */\nexport interface ClosePeriodOptions {\n /** Human-readable name. Must not collide with an existing period. */\n readonly name: string\n /**\n * Inclusive upper cutoff. A record is sealed when its\n * `record[dateField]` (or, if absent, the envelope `_ts`) is at or\n * before this ISO timestamp.\n */\n readonly endDate: string\n /**\n * Record field carrying the business date used for period\n * membership. Recommended for accounting workflows — e.g. an\n * invoice booked late (write-time after close) is still sealed\n * when its `invoice.date` falls inside the closed period.\n *\n * Omit to use envelope `_ts` (write-time seal). This fallback\n * rarely matches real-world accounting semantics; prefer passing\n * an explicit `dateField`.\n */\n readonly dateField?: string\n /**\n * Close only this timeline (#1005). Omit for the vault-wide timeline.\n *\n * ```ts\n * vault.closePeriod({\n * name: '2026-06', endDate: '2026-06-30', dateField: 'issuedAt',\n * partition: [clientId, 'vat'],\n * })\n * ```\n *\n * Which records the resulting seal applies to is decided by the\n * `subjects` map passed to `withPeriods()` — without one, no record ever\n * resolves to a partition and a partitioned close seals nothing.\n */\n readonly partition?: PeriodPartition\n}\n\n/** Options for `vault.openPeriod()`. */\nexport interface OpenPeriodOptions<TCollections = Record<string, Record<string, unknown>>> {\n /** Human-readable name for the new period. Must be unique. */\n readonly name: string\n /**\n * The timeline to open in. Must match the partition of `fromPeriod` — a\n * period cannot chain across timelines, since each carries its own hash\n * chain (#1005).\n */\n readonly partition?: PeriodPartition\n /** ISO lower bound of the new period (usually prior `endDate + 1 day`). */\n readonly startDate: string\n /**\n * Name of the prior CLOSED period this one chains from. The prior\n * period's record is verified to exist and to be `kind: 'closed'`;\n * its `endDate` is made available to the `carryForward` callback.\n */\n readonly fromPeriod: string\n /**\n * Receives a read-only facade over the vault's CURRENT state,\n * plus the prior period's `endDate`. Accounting semantics: after\n * a period closes, records with `record[dateField] <= endDate`\n * are frozen — current state equals closing state, so a caller\n * can compute closing balances by querying the live collection\n * with a `where('date', '<=', priorEndDate)` filter.\n *\n * Returns opening-balance records keyed by collection name.\n * Example:\n *\n * ```ts\n * carryForward: async (ctx) => {\n * const closing = await ctx.collection<Journal>('journal')\n * .query().where('date', '<=', ctx.priorEndDate).toArray()\n * const opening: Record<string, Journal> = {}\n * for (const entry of closing) {\n * opening[`OB-${entry.id}`] = { ...entry, date: '2026-04-01' }\n * }\n * return { journal: opening }\n * }\n * ```\n */\n readonly carryForward: (\n ctx: CarryForwardContext,\n ) => Promise<TCollections> | TCollections\n}\n\n/**\n * Context passed to `OpenPeriodOptions.carryForward`. Exposes a\n * read-only subset of the live vault (`collection(name).get/list`)\n * plus the prior period's `endDate` so business-date filters can\n * be built by the caller.\n *\n * Writes go via the return value, not via the facade — the\n * `collection()` here is deliberately restricted to reads.\n */\nexport interface CarryForwardContext {\n /** The prior period's `endDate` — the boundary of the closing snapshot. */\n readonly priorEndDate: string\n /** Read-only collection facade over current vault state. */\n collection<T = unknown>(name: string): ReadOnlyCollection<T>\n}\n\n/** Minimum read surface exposed to `carryForward`. */\nexport interface ReadOnlyCollection<T> {\n get(id: string): Promise<T | null>\n list(): Promise<T[]>\n}\n\n/**\n * Load every period record currently stored on the adapter.\n * Decrypting is the caller's responsibility (we return plain records\n * so the vault can use its own `_periods` DEK).\n *\n * @internal — called by Vault methods that need the closed-period\n * cache. Not part of the public API surface.\n */\nexport async function loadPeriods(\n adapter: NoydbStore,\n vault: string,\n decrypt: (envelope: EncryptedEnvelope) => Promise<PeriodRecord>,\n): Promise<PeriodRecord[]> {\n const ids = await adapter.list(vault, PERIODS_COLLECTION)\n const records: PeriodRecord[] = []\n for (const id of ids) {\n const env = await adapter.get(vault, PERIODS_COLLECTION, id)\n if (env) records.push(await decrypt(env))\n }\n // Stable order by closedAt so chain verification is reproducible.\n records.sort((a, b) => a.closedAt.localeCompare(b.closedAt))\n return records\n}\n\n/**\n * Given the current ordered period list, pick the last entry that\n * belongs to the hash chain — used as the `priorPeriodHash` anchor\n * for the next closure/opening.\n *\n * @internal\n */\nexport async function chainAnchor(\n records: readonly PeriodRecord[],\n partition?: PeriodPartition,\n): Promise<{ priorPeriodName?: string; priorPeriodHash: string }> {\n // #1005 — each timeline carries its OWN chain. Anchoring a partitioned close\n // to whatever happened to be written last vault-wide would interleave\n // unrelated subjects into one chain, so verifying client A's June would\n // depend on client B never having closed in between.\n const inTimeline = records.filter((p) => samePartition(p.partition, partition))\n const last = inTimeline[inTimeline.length - 1]\n if (!last) return { priorPeriodHash: '' }\n const hash = await sha256Hex(canonicalJson(last as unknown as Record<string, unknown>))\n return { priorPeriodName: last.name, priorPeriodHash: hash }\n}\n\n/**\n * Throw `PeriodClosedError` if the record being touched falls within\n * any closed period.\n *\n * Three signals, evaluated per period:\n *\n * 1. If the period declares a `dateField`, the guard reads\n * `record[dateField]` on BOTH the existing (prior) record AND the\n * incoming (new) record. Either comparing `<= endDate` triggers\n * the error — callers cannot slide a record into a closed period\n * by editing its date field.\n * 2. If the period has no `dateField`, the guard falls back to the\n * envelope `_ts` of the existing record. Fresh inserts (no\n * existing envelope) pass.\n * 3. For a delete, only the existing side is checked.\n *\n * @internal\n */\nexport function assertTsWritable(\n existing: { ts: string | null; record: Record<string, unknown> | null } | null,\n incomingRecord: Record<string, unknown> | null,\n closedPeriods: readonly PeriodRecord[],\n scope?: { collection: string; resolve?: PartitionResolver },\n): void {\n // #1005 — a period only governs records that resolve to ITS timeline. With no\n // resolver (the default `withPeriods()`), nothing resolves to a partition, so\n // every record sits on the vault-wide timeline exactly as before and a\n // partitioned period governs nothing.\n const partitionOf = (r: Record<string, unknown> | null): PeriodPartition | undefined => {\n if (!r || !scope?.resolve) return undefined\n return scope.resolve(scope.collection, r)\n }\n const existingRecord = existing?.record ?? null\n const existingPartition = partitionOf(existingRecord)\n const incomingPartition = partitionOf(incomingRecord)\n // One instant for the whole check, so a bounded reopen window cannot expire\n // between two periods in the same loop and seal a write half-way.\n const now = new Date().toISOString()\n\n for (const p of closedPeriods) {\n if (p.kind !== 'closed') continue\n // #1022 — a reopened period is writable again. This is the ONLY thing a\n // reopen does: it withdraws the period's veto. It cannot grant a write that\n // some other gate forbids, because the guard bus ANDs every handler and\n // record-level guards are registered ahead of this one.\n if (isEffectivelyReopened(p, now)) continue\n if (p.dateField) {\n const checkRecord = (\n label: string,\n r: Record<string, unknown> | null,\n recordPartition: PeriodPartition | undefined,\n ): void => {\n if (!r) return\n // Both sides are checked under their OWN partition, which is what stops\n // a write from sliding a record either INTO or OUT OF a sealed\n // timeline by rewriting the fields the subject mapping reads.\n if (!samePartition(recordPartition, p.partition)) return\n const v = r[p.dateField!]\n if (typeof v === 'string' && v <= p.endDate) {\n throw new PeriodClosedError(p.name, p.endDate, `${label}[${p.dateField}]=${v}`)\n }\n }\n checkRecord('existing', existingRecord, existingPartition)\n checkRecord('incoming', incomingRecord, incomingPartition)\n continue\n }\n // Fallback: write-time seal via envelope _ts. Scoped by the EXISTING\n // record's partition — `_ts` belongs to the stored envelope, so the\n // incoming side has no write-time of its own to compare.\n if (!samePartition(existingPartition, p.partition)) continue\n const existingTs = existing?.ts ?? null\n if (existingTs !== null && existingTs <= p.endDate) {\n throw new PeriodClosedError(p.name, p.endDate, existingTs)\n }\n }\n}\n\n/**\n * Sanity-check a proposed period name + endDate against existing\n * records. Shared by closePeriod / openPeriod so the two pathways\n * produce identical diagnostics.\n *\n * @internal\n */\nexport function validatePeriodName(\n name: string,\n existing: readonly PeriodRecord[],\n partition?: PeriodPartition,\n): void {\n if (name.length === 0) {\n throw new ValidationError('Period name cannot be empty.')\n }\n // Validates the components as a side effect — an empty or non-finite\n // component must be rejected at the call, not encoded into a storage key.\n resolvePeriodKey(name, partition)\n // #1005 — uniqueness is per TIMELINE. `'2026-06'` in `['A','vat']` does not\n // collide with `'2026-06'` in `['B','vat']`.\n if (existing.some((p) => p.name === name && samePartition(p.partition, partition))) {\n const where = partition && partition.length > 0\n ? ` in partition [${partition.join(', ')}]`\n : ''\n throw new ValidationError(`Period \"${name}\" already exists${where}.`)\n }\n}\n\n/**\n * Wire a reserved-collection ledger append for a period record. The\n * period itself is stored via the adapter as an encrypted envelope;\n * the ledger entry is a normal `put` with the period's payloadHash,\n * so period closures inherit the chain's tamper-evidence.\n *\n * @internal\n */\nexport async function appendPeriodLedgerEntry(\n ledger: LedgerStore | null,\n actor: string,\n envelope: EncryptedEnvelope,\n name: string,\n collection: string = PERIODS_COLLECTION,\n): Promise<void> {\n if (!ledger) return\n const { envelopePayloadHash } = await import('../../with-commit/history/ledger/index.js')\n await ledger.append({\n op: 'put',\n collection,\n id: name,\n version: envelope._v,\n actor,\n payloadHash: await envelopePayloadHash(envelope),\n })\n}\n\n/**\n * @internal #615. Sweep delete markers with `_ts < before` off ANY store\n * (the vault's local adapter, or a push-only sync target). Returns the count\n * removed. Shared by `vault._purgeDeleteMarkers` (local) and\n * `vault._purgePeriodTargets` (push-only targets).\n */\nexport async function purgeMarkersOn(\n store: NoydbStore,\n vault: string,\n before: string,\n collections?: string[],\n): Promise<number> {\n const snapshot = await store.loadAll(vault)\n let removed = 0\n for (const [coll, records] of Object.entries(snapshot)) {\n if (collections && !collections.includes(coll)) continue\n for (const [id, env] of Object.entries(records)) {\n if (isDeleteMarker(env) && env._ts < before) {\n await store.delete(vault, coll, id)\n removed++\n }\n }\n }\n return removed\n}\n","/**\n * Active periods strategy factory. Only reachable through the\n * `@noy-db/hub/periods` subpath.\n */\n\nimport {\n loadPeriods,\n chainAnchor,\n assertTsWritable,\n validatePeriodName,\n appendPeriodLedgerEntry,\n} from './periods.js'\nimport type { PeriodPartition, PartitionResolver } from './periods.js'\nimport type { PeriodsStrategy } from './strategy.js'\n\n/** Options for {@link withPeriods}. */\nexport interface WithPeriodsOptions {\n /**\n * Maps a collection to the timeline each of its records belongs to (#1005) —\n * the answer to \"which close calendar governs THIS record\".\n *\n * ```ts\n * withPeriods({\n * subjects: { receipts: (r) => [r.clientId, layerOf(r)] },\n * })\n * ```\n *\n * Same shape as `withForget({ subjects })`, which answers the same question\n * for erasure. A collection with no entry — and every collection when\n * `subjects` is omitted entirely — stays on the vault-wide timeline, so an\n * existing vault behaves exactly as it did before partitions existed.\n *\n * Return `undefined` from a mapper to put an individual record back on the\n * vault-wide timeline (e.g. a record that predates the field the mapping\n * reads).\n */\n readonly subjects?: Readonly<\n Record<string, (record: Record<string, unknown>) => PeriodPartition | undefined>\n >\n}\n\n/**\n * Build the default periods strategy. Pass into\n * `createNoydb({ periodsStrategy: withPeriods() })` to enable\n * `vault.closePeriod()` / `vault.openPeriod()` / write-guards.\n *\n * Pass `subjects` to run more than one close calendar in a single vault — see\n * {@link WithPeriodsOptions.subjects}.\n */\nexport function withPeriods(options?: WithPeriodsOptions): PeriodsStrategy {\n const subjects = options?.subjects\n const partitionOf: PartitionResolver | undefined = subjects\n ? (collection, record) => subjects[collection]?.(record)\n : undefined\n\n return {\n loadPeriods,\n chainAnchor,\n assertTsWritable,\n validatePeriodName,\n appendPeriodLedgerEntry,\n ...(partitionOf !== undefined && { partitionOf }),\n }\n}\n","/**\n * Strategy seam for the optional accounting-periods service. Core\n * imports `PeriodsStrategy` type-only + `NO_PERIODS` stub; the real\n * `loadPeriods` / `chainAnchor` / `assertTsWritable` /\n * `validatePeriodName` / `appendPeriodLedgerEntry` functions are\n * only reachable via `withPeriods()` in `./active.ts`.\n *\n * Applications that never call `vault.closePeriod()` /\n * `vault.openPeriod()` ship none of the ~363 LOC.\n *\n * @internal\n */\n\nimport type { EncryptedEnvelope, NoydbStore } from '../../kernel/types.js'\nimport type { LedgerStore } from '../../with-commit/history/ledger/store.js'\nimport type { PeriodRecord, PeriodPartition, PartitionResolver } from './periods.js'\n\n/**\n * @internal\n */\nexport interface PeriodsStrategy {\n loadPeriods(\n adapter: NoydbStore,\n vault: string,\n decrypt: (envelope: EncryptedEnvelope) => Promise<PeriodRecord>,\n ): Promise<PeriodRecord[]>\n chainAnchor(records: readonly PeriodRecord[], partition?: PeriodPartition): Promise<{\n priorPeriodName?: string\n priorPeriodHash: string\n }>\n assertTsWritable(\n existing: { ts: string | null; record: Record<string, unknown> | null } | null,\n incoming: Record<string, unknown> | null,\n periods: readonly PeriodRecord[],\n scope?: { collection: string; resolve?: PartitionResolver },\n ): void\n validatePeriodName(\n name: string,\n existing: readonly PeriodRecord[],\n partition?: PeriodPartition,\n ): void\n /**\n * Record → timeline resolver built from `withPeriods({ subjects })` (#1005).\n * `undefined` when the caller configured no subjects, which keeps every\n * record on the vault-wide timeline.\n */\n readonly partitionOf?: PartitionResolver\n appendPeriodLedgerEntry(\n ledger: LedgerStore | null,\n actor: string,\n envelope: EncryptedEnvelope,\n periodName: string,\n collection?: string,\n ): Promise<void>\n}\n\n/**\n * No-periods stub. `loadPeriods` returns `[]`; the write-guards do\n * nothing (vaults without closed periods never reject writes);\n * `validatePeriodName` / `appendPeriodLedgerEntry` throw because\n * those paths are only reached when the user explicitly called\n * `closePeriod()` / `openPeriod()` — if they did that without the\n * strategy, they need to wire it.\n *\n * @internal\n */\nconst NOT_ENABLED = new Error(\n 'Accounting periods require the periods strategy. Import ' +\n '`{ withPeriods }` from \"@noy-db/hub/periods\" and pass it to ' +\n '`createNoydb({ periodsStrategy: withPeriods() })`.',\n)\n\nexport const NO_PERIODS: PeriodsStrategy = {\n async loadPeriods() { return [] },\n async chainAnchor() { return { priorPeriodHash: '' } },\n assertTsWritable() {},\n validatePeriodName() { throw NOT_ENABLED },\n async appendPeriodLedgerEntry() { throw NOT_ENABLED },\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/with-audit/periods/periods.ts","../src/with-audit/periods/active.ts","../src/with-audit/periods/strategy.ts"],"sourcesContent":["/**\n * Accounting-period closure + opening.\n *\n * A closed period seals every record whose envelope `_ts` is at or\n * before the period's `endDate`: further writes (`put` / `delete`)\n * against such records throw {@link PeriodClosedError}. The period\n * itself is stored as a record in the reserved `_periods` collection\n * and written through the normal ledger-instrumented path, so every\n * closure appends a tamper-evident entry to the vault's hash chain.\n *\n * ## Closure model\n *\n * ```\n * vault.closePeriod({ name: 'FY2026-Q1', endDate: '2026-03-31' })\n * └─► PeriodRecord written to _periods/<name>\n * ├─ priorPeriodName / priorPeriodHash — chain to last close\n * ├─ closedAt / closedBy — provenance\n * └─ normal ledger append fires (LedgerStore.append)\n * ```\n *\n * Enforcement (`assertTsWritable`) is vault-local: the Vault caches\n * the list of closed periods on first read and consults that cache in\n * the `Collection.put` / `.delete` path via the `periodGuard` hook.\n *\n * ## Opening model\n *\n * ```\n * vault.openPeriod({\n * name: 'FY2026-Q2',\n * startDate: '2026-04-01',\n * fromPeriod: 'FY2026-Q1',\n * carryForward: async (priorView) => Record<string, Record<string, unknown>>,\n * })\n * ```\n *\n * `carryForward` receives a read-only `VaultInstant` anchored at the\n * prior period's `endDate` (built via `vault.at(endDate)`) so the\n * callback can compute closing aggregates from the sealed state. The\n * returned `{ [collectionName]: { [id]: record } }` map is written\n * before the new `PeriodRecord` lands — opening balances materialise\n * as normal records with fresh timestamps that fall outside every\n * closed period.\n *\n * ## Freeze\n *\n * ```\n * vault.freezePeriod('FY2026-Q1')\n * └─► physically purges delete markers whose write-time falls inside\n * the closed period's window (via the #589 `_purgeDeleteMarkers`\n * seam), then records the fact:\n * ├─ PeriodFreezeRecord written to _period_freezes/<name>\n * └─ normal ledger append fires (LedgerStore.append)\n * ```\n *\n * The chained `_periods/<name>` record is never mutated — `frozenAt` /\n * `frozenBy` / `purgedMarkerCount` are merged onto the returned\n * `PeriodRecord` at read time from the companion, so a tamper with the\n * freeze can never break the inter-period hash chain. Freezing is\n * terminal (a closed period, once frozen, stays frozen) and idempotent\n * (a second call is a no-op that returns the same merged record without\n * re-purging or re-appending a ledger entry). Freeze does NOT purge\n * forget-tombstones (GDPR crypto-shred erasure evidence), `_history`\n * versions, or live records — the delete-markers-only seam leaves all\n * three untouched by construction.\n *\n * Freeze purges the LOCAL adapter only. On a synced vault, markers already\n * pushed to sync targets survive there, and a later pull re-imports them\n * (benign — they still read deleted, but the space isn't reclaimed). A\n * re-imported marker keeps its original `_ts` (inside the already-frozen\n * period's window), so — like any late-booked delete — it is reclaimed by\n * the NEXT period's freeze, whose window covers it; freeze stays terminal\n * and does NOT re-purge an already-frozen period (#611). Sweeping the sync\n * targets themselves is a cross-target-purge concern deferred to the\n * cold-archival spec. Purging re-opens the #589 resurrection window for a\n * peer offline since before the cutoff, which is why the closed period is\n * the operator-asserted safe-point that gates the call.\n *\n * A period whose purge window has not fully elapsed cannot be frozen —\n * `freezePeriod` throws rather than purge markers for deletes that may not\n * have converged yet (#610).\n *\n * ## Archive\n *\n * ```\n * vault.archivePeriod('FY2026-Q1')\n * └─► relocates the closed period's in-window records (those with\n * `_ts < periodExclusiveUpperBound(endDate)`) from the hot store to\n * the configured cold tier (routeStore's `cold` route), then records:\n * ├─ PeriodArchiveRecord written to _period_archives/<name>\n * └─ a ledger entry attributed to _period_archives\n * ```\n *\n * Archival is NON-DESTRUCTIVE: routeStore reads fall through to the cold\n * tier on a hot miss, so an archived record still reads normally. It is\n * therefore gated only on `closed` (not `frozen`) — it does not re-open the\n * #589 resurrection window and needs no convergence safe-point. Freeze\n * (purge markers) and archive (relocate records) are independent and compose\n * in either order. Like freeze, archival keeps the chained `_periods/<name>`\n * record byte-immutable (state lives in the companion) and is idempotent.\n *\n * Bounds by write-time `_ts`, NOT business date: the store tier sees only\n * encrypted envelopes. A record with an in-period business date but a later\n * `_ts` (late-booked) archives at the NEXT period's archive — the same rule\n * freeze uses for late-booked delete markers. Requires a `routeStore` with a\n * cold route (`age: { cold }`); throws otherwise.\n *\n * Read cost: with `withLazy()` (per-id reads) archived records are truly\n * cold — fetched from cold only on access. In the default hydrated mode,\n * `loadAll` merges the cold store, so archived records still load into RAM\n * on vault open (hot-tier STORAGE is reclaimed; RAM is not). Summaries\n * (`_`-prefixed) always stay hot.\n *\n * ## Target-purge\n *\n * ```\n * vault.purgePeriodTargets('FY2026-Q1')\n * └─► sweeps delete markers (`_ts < periodExclusiveUpperBound(endDate)`) off\n * the vault's PUSH-ONLY sync targets (backup/archive roles), then records:\n * ├─ PeriodTargetPurgeRecord written to _period_target_purges/<name>\n * └─ a ledger entry attributed to _period_target_purges\n * ```\n *\n * Extends freeze's local marker purge to the vault's own remote sinks.\n * `sync-peer` (bidirectional) targets are SKIPPED: purging a marker there\n * re-opens the #589 resurrection window for a client offline before the\n * cutoff, an assertion no single vault can verify. Backup/archive targets are\n * push-only — never pulled from into convergence — so sweeping their markers\n * is safe. Requires the period be frozen first (closed → frozen →\n * target-purged) so the local safe-point is already established. Idempotent\n * once run; a vault with no push-only targets writes no companion and is\n * re-runnable (so a target added later is still swept). Single-vault only —\n * fleet-wide purge across sovereign vaults is klum's concern over\n * `@noy-db/hub/cargo`.\n *\n * ## Not covered\n *\n * - Partial re-opening of a closed period. If an auditor needs to\n * make a correction inside a sealed period, the sanctioned path is\n * a compensating entry in the NEW period, not an unlock of the\n * old one.\n * - Automatic period rollover. `closePeriod` / `openPeriod` are\n * deliberately explicit operator calls so the caller decides when\n * the boundary lands.\n *\n * @module\n */\n\nimport type { NoydbStore, EncryptedEnvelope } from '../../kernel/types.js'\nimport type { LedgerStore } from '../../with-commit/history/ledger/index.js'\nimport { sha256Hex, canonicalJson } from '../../with-commit/history/ledger/index.js'\nimport { isDeleteMarker } from '../../kernel/enclave/index.js'\nimport { PeriodClosedError, ValidationError } from '../../kernel/errors.js'\n\n// The reserved collection names + `periodExclusiveUpperBound` moved to the\n// dependency-light `window.ts` so the sync engine's period-scoped pull (#807)\n// can import them without this module's ledger imports; re-exported here so\n// every existing import path keeps working.\nexport {\n PERIODS_COLLECTION,\n PERIOD_FREEZES_COLLECTION,\n PERIOD_ARCHIVES_COLLECTION,\n PERIOD_TARGET_PURGES_COLLECTION,\n PERIOD_REOPENS_COLLECTION,\n periodExclusiveUpperBound,\n} from './window.js'\nimport { PERIODS_COLLECTION } from './window.js'\n\n/**\n * Companion record recording that a closed period was frozen (its delete\n * markers physically purged). Stored in {@link PERIOD_FREEZES_COLLECTION},\n * keyed by period name — kept OFF the hash-chained `_periods/<name>` record so\n * freeze never alters the inter-period chain.\n */\nexport interface PeriodFreezeRecord {\n readonly period: string\n readonly frozenAt: string\n readonly frozenBy: string\n readonly purgedMarkerCount: number\n}\n\n/**\n * Companion record noting that a closed period was cold-archived (its\n * in-window records physically relocated hot → cold). Stored in\n * {@link PERIOD_ARCHIVES_COLLECTION}, keyed by period name — kept OFF the\n * hash-chained `_periods/<name>` record so archive never alters the chain.\n */\nexport interface PeriodArchiveRecord {\n readonly period: string\n readonly archivedAt: string\n readonly archivedBy: string\n readonly archivedRecordCount: number\n}\n\n/** Per-target count of delete markers purged off one push-only sync target (#615). */\nexport interface TargetPurgeCount {\n readonly label?: string\n readonly role: 'backup' | 'archive'\n readonly purgedCount: number\n}\n\n/**\n * One entry in a period's append-only reopen/reclose log (#1022).\n *\n * Real accounting close is a three-state lifecycle — open / closed / reopened\n * — not a one-way door. A month gets closed, a missing invoice arrives or a\n * filing is rejected, the month is reopened, corrected, and reclosed. The audit\n * value is not the ability to write again; it is the chain being able to say\n * *closed at T1, reopened at T2 by U, reclosed at T3*.\n */\nexport interface PeriodReopenEvent {\n readonly op: 'reopen' | 'reclose'\n /** ISO timestamp the event was recorded. */\n readonly at: string\n /** userId of the keyring that performed it. */\n readonly by: string\n /**\n * `reopen` only — ISO instant after which the period re-seals on its own,\n * with nobody acting. Absent means the window stays open until an explicit\n * `reclosePeriod`.\n */\n readonly until?: string\n /** Free-text justification, carried verbatim into the audit trail. */\n readonly reason?: string\n}\n\n/**\n * Companion holding a period's reopen/reclose history (#1022). Stored in\n * {@link PERIOD_REOPENS_COLLECTION}, keyed by the period's storage key — kept\n * OFF the hash-chained `_periods/<name>` record for the same reason freeze and\n * archive are: reopening must never rewrite the close, or the chain that proves\n * the close happened is the very thing the reopen destroys.\n *\n * `events` is APPEND-ONLY. Where the other companions are single-shot and\n * idempotent, this one accumulates, because the cycle repeats.\n */\nexport interface PeriodReopenRecord {\n readonly period: string\n readonly partition?: PeriodPartition\n readonly events: readonly PeriodReopenEvent[]\n}\n\n/**\n * Collapse an append-only reopen log into the return-only fields merged onto a\n * {@link PeriodRecord} on read.\n *\n * Expiry is deliberately NOT resolved here: `reopenedUntil` is carried through\n * verbatim and compared against the clock at write-guard time, so a bounded\n * window re-seals on its own without anything having to run.\n *\n * @internal\n */\nexport function resolveReopenState(events: readonly PeriodReopenEvent[]): {\n reopenedAt?: string\n reopenedBy?: string\n reopenedUntil?: string\n reopenReason?: string\n reclosedAt?: string\n reopenCount: number\n} {\n let lastReopen: PeriodReopenEvent | undefined\n let reclosedAfter: string | undefined\n let reopenCount = 0\n for (const e of events) {\n if (e.op === 'reopen') {\n lastReopen = e\n reclosedAfter = undefined\n reopenCount++\n } else if (lastReopen !== undefined) {\n reclosedAfter = e.at\n }\n }\n if (!lastReopen) return { reopenCount }\n return {\n reopenedAt: lastReopen.at,\n reopenedBy: lastReopen.by,\n ...(lastReopen.until !== undefined && { reopenedUntil: lastReopen.until }),\n ...(lastReopen.reason !== undefined && { reopenReason: lastReopen.reason }),\n ...(reclosedAfter !== undefined && { reclosedAt: reclosedAfter }),\n reopenCount,\n }\n}\n\n/**\n * Is this period writable right now on account of a reopen? (#1022)\n *\n * Three ways to be sealed again: never reopened, explicitly reclosed after the\n * last reopen, or a bounded window that has elapsed. The clock is read by the\n * caller and passed in, so the guard and any diagnostic agree on one instant.\n *\n * @internal\n */\nexport function isEffectivelyReopened(period: PeriodRecord, nowIso: string): boolean {\n if (period.reopenedAt === undefined) return false\n if (period.reclosedAt !== undefined && period.reclosedAt >= period.reopenedAt) return false\n if (period.reopenedUntil !== undefined && nowIso > period.reopenedUntil) return false\n return true\n}\n\n/**\n * Companion record noting that a closed+frozen period's delete markers were\n * swept off the vault's push-only sync targets (#615). Stored in\n * {@link PERIOD_TARGET_PURGES_COLLECTION}, keyed by period name — kept OFF the\n * hash-chained `_periods/<name>` record so target-purge never alters the chain.\n */\nexport interface PeriodTargetPurgeRecord {\n readonly period: string\n readonly purgedAt: string\n readonly purgedBy: string\n readonly targets: readonly TargetPurgeCount[]\n}\n\n/**\n * Scope tuple for a period timeline (#1005).\n *\n * Identical in shape and semantics to `SequenceOptions.partition`: a\n * partitioned timeline is always disjoint from any unpartitioned one, and from\n * every other tuple. `['acme', 'vat']` and `['acme', 'wht']` are two\n * independent close calendars for the same subject — which is the whole point,\n * since sub-ledgers for one legal entity and one month routinely close on\n * different statutory deadlines.\n */\nexport type PeriodPartition = readonly (string | number)[]\n\n/**\n * Resolve the `_periods` storage key for a (name, partition) pair.\n *\n * Deliberately the same encoding as `resolveSequenceKey`: `name` verbatim when\n * unpartitioned, else `${name}\\x00${parts}` with each component\n * `encodeURIComponent`d and `'/'`-joined. The null-byte separator cannot occur\n * in a period name, so a partitioned key never collides with an unpartitioned\n * one; URI-encoding keeps `['a/b']` distinct from `['a','b']`.\n *\n * @throws {ValidationError} on an empty component or a non-finite number.\n * @internal\n */\nexport function resolvePeriodKey(name: string, partition?: PeriodPartition): string {\n if (!partition || partition.length === 0) return name\n const parts = partition.map((p) => {\n if (typeof p === 'number' && !Number.isFinite(p)) {\n throw new ValidationError(`period partition component must be a finite number, got ${p}`)\n }\n const s = String(p)\n if (s === '') {\n throw new ValidationError('period partition component must not be empty')\n }\n return encodeURIComponent(s)\n })\n return `${name}\\x00${parts.join('/')}`\n}\n\n/**\n * Do two partitions denote the same timeline? Absent and empty both mean \"the\n * unpartitioned timeline\", so they compare equal.\n *\n * @internal\n */\nexport function samePartition(a?: PeriodPartition, b?: PeriodPartition): boolean {\n const x = a ?? []\n const y = b ?? []\n if (x.length !== y.length) return false\n return x.every((v, i) => String(v) === String(y[i]))\n}\n\n/**\n * Resolves a record to the timeline that governs it. Supplied by\n * `withPeriods({ subjects })`; returns `undefined` for any collection with no\n * mapping, which is what keeps an unconfigured vault on the single vault-wide\n * timeline it has always had.\n *\n * @internal\n */\nexport type PartitionResolver = (\n collection: string,\n record: Record<string, unknown>,\n) => PeriodPartition | undefined\n\n/**\n * Stored record for one closed or opened accounting period. One entry\n * per period, keyed by `name` in the reserved `_periods` collection.\n *\n * The hash chain between periods is computed at read time by\n * `loadPeriods()` — each record carries the name + hash of its\n * predecessor so a tamper with any period's record breaks the chain\n * into the next one, the same way the ledger's `prevHash` works.\n */\nexport interface PeriodRecord {\n /**\n * Human-readable name (e.g., `'FY2026-Q1'`). Unique per PARTITION — two\n * timelines may each carry a `'2026-06'`, which is the normal case when one\n * vault serves several subjects (#1005). Unique per vault when unpartitioned.\n */\n readonly name: string\n /**\n * The timeline this period belongs to. Absent = the vault-wide timeline.\n * Two periods with the same `name` and different `partition` are unrelated:\n * separate hash chains, separate close state, and the write guard applies\n * each only to records that resolve to its own tuple.\n */\n readonly partition?: PeriodPartition\n /**\n * Role discriminator. A period is `'closed'` from the moment its\n * `closedAt` is recorded; `'opened'` marks a period whose opening\n * entries have been carried forward via {@link openPeriod}. Many\n * workflows will produce one opened period per closed period (the\n * opened one is the SUCCESSOR — its `startDate` equals the prior\n * `endDate + 1 day`).\n */\n readonly kind: 'closed' | 'opened'\n /** ISO date — inclusive upper bound for records belonging to this period. */\n readonly endDate: string\n /** ISO date — lower bound (present on opened periods only). */\n readonly startDate?: string\n /**\n * Record field carrying the business date (e.g. `'date'` on an\n * invoice, `'paidAt'` on a payment). The guard compares\n * `record[dateField]` against `endDate` — NOT the envelope `_ts`.\n * Accounting entries booked late (business date `2026-01-15`,\n * write-time `2026-04-22`) still get sealed when Q1 closes at\n * `2026-03-31` because the comparison uses the business date.\n *\n * Optional for backwards compat. When absent, the guard falls back\n * to envelope `_ts` — that's a write-time seal, appropriate for\n * content that doesn't carry a logical business date (e.g. system\n * settings) but almost never right for accounting ledgers.\n */\n readonly dateField?: string\n /** ISO timestamp recorded at `closePeriod()` / `openPeriod()` call time. */\n readonly closedAt: string\n /** userId of the keyring that invoked the close/open. */\n readonly closedBy: string\n /** Name of the prior period this one chains to, if any. */\n readonly priorPeriodName?: string\n /** sha256(canonicalJson(priorPeriod)) — empty for the first period. */\n readonly priorPeriodHash: string\n /**\n * Opened periods only — the names of the collections whose\n * carry-forward aggregates were written by {@link openPeriod}.\n * Recorded for auditability so a future `verifyPeriodChain()` can\n * cross-check the opening balances against the closing snapshot.\n */\n readonly openingCollections?: readonly string[]\n /** #604 return-only — merged from the `_period_freezes/<name>` companion on\n * read; NEVER written into the stored `_periods/<name>` record (would break\n * the hash chain). Absent = not yet frozen. */\n readonly frozenAt?: string\n readonly frozenBy?: string\n readonly purgedMarkerCount?: number\n /** #613 return-only — merged from the `_period_archives/<name>` companion on\n * read; NEVER written into the stored `_periods/<name>` record. Absent = not\n * yet archived. */\n readonly archivedAt?: string\n readonly archivedBy?: string\n readonly archivedRecordCount?: number\n /** #615 return-only — merged from the `_period_target_purges/<name>` companion\n * on read; NEVER written into the stored `_periods/<name>` record. Absent =\n * target-purge not yet run (or the vault has no push-only targets). */\n readonly targetsPurgedAt?: string\n readonly targetsPurgedBy?: string\n readonly targetsPurged?: readonly TargetPurgeCount[]\n /** #1022 return-only — collapsed from the `_period_reopens/<key>` append-only\n * log on read; NEVER written into the stored `_periods/<name>` record, so a\n * reopen cannot disturb the inter-period hash chain. Absent = never reopened.\n * `reclosedAt` present (and >= `reopenedAt`) means the window was closed\n * again explicitly; `reopenedUntil` in the past means it lapsed on its own.\n * Use {@link isEffectivelyReopened} rather than reading these directly. */\n readonly reopenedAt?: string\n readonly reopenedBy?: string\n readonly reopenedUntil?: string\n readonly reopenReason?: string\n readonly reclosedAt?: string\n /** How many times this period has been reopened, ever. */\n readonly reopenCount?: number\n}\n\n/** Options for `vault.closePeriod()`. */\nexport interface ClosePeriodOptions {\n /** Human-readable name. Must not collide with an existing period. */\n readonly name: string\n /**\n * Inclusive upper cutoff. A record is sealed when its\n * `record[dateField]` (or, if absent, the envelope `_ts`) is at or\n * before this ISO timestamp.\n */\n readonly endDate: string\n /**\n * Record field carrying the business date used for period\n * membership. Recommended for accounting workflows — e.g. an\n * invoice booked late (write-time after close) is still sealed\n * when its `invoice.date` falls inside the closed period.\n *\n * Omit to use envelope `_ts` (write-time seal). This fallback\n * rarely matches real-world accounting semantics; prefer passing\n * an explicit `dateField`.\n */\n readonly dateField?: string\n /**\n * Close only this timeline (#1005). Omit for the vault-wide timeline.\n *\n * ```ts\n * vault.closePeriod({\n * name: '2026-06', endDate: '2026-06-30', dateField: 'issuedAt',\n * partition: [clientId, 'vat'],\n * })\n * ```\n *\n * Which records the resulting seal applies to is decided by the\n * `subjects` map passed to `withPeriods()` — without one, no record ever\n * resolves to a partition and a partitioned close seals nothing.\n */\n readonly partition?: PeriodPartition\n}\n\n/** Options for `vault.openPeriod()`. */\nexport interface OpenPeriodOptions<TCollections = Record<string, Record<string, unknown>>> {\n /** Human-readable name for the new period. Must be unique. */\n readonly name: string\n /**\n * The timeline to open in. Must match the partition of `fromPeriod` — a\n * period cannot chain across timelines, since each carries its own hash\n * chain (#1005).\n */\n readonly partition?: PeriodPartition\n /** ISO lower bound of the new period (usually prior `endDate + 1 day`). */\n readonly startDate: string\n /**\n * Name of the prior CLOSED period this one chains from. The prior\n * period's record is verified to exist and to be `kind: 'closed'`;\n * its `endDate` is made available to the `carryForward` callback.\n */\n readonly fromPeriod: string\n /**\n * Receives a read-only facade over the vault's CURRENT state,\n * plus the prior period's `endDate`. Accounting semantics: after\n * a period closes, records with `record[dateField] <= endDate`\n * are frozen — current state equals closing state, so a caller\n * can compute closing balances by querying the live collection\n * with a `where('date', '<=', priorEndDate)` filter.\n *\n * Returns opening-balance records keyed by collection name.\n * Example:\n *\n * ```ts\n * carryForward: async (ctx) => {\n * const closing = await ctx.collection<Journal>('journal')\n * .query().where('date', '<=', ctx.priorEndDate).toArray()\n * const opening: Record<string, Journal> = {}\n * for (const entry of closing) {\n * opening[`OB-${entry.id}`] = { ...entry, date: '2026-04-01' }\n * }\n * return { journal: opening }\n * }\n * ```\n */\n readonly carryForward: (\n ctx: CarryForwardContext,\n ) => Promise<TCollections> | TCollections\n}\n\n/**\n * Context passed to `OpenPeriodOptions.carryForward`. Exposes a\n * read-only subset of the live vault (`collection(name).get/list`)\n * plus the prior period's `endDate` so business-date filters can\n * be built by the caller.\n *\n * Writes go via the return value, not via the facade — the\n * `collection()` here is deliberately restricted to reads.\n */\nexport interface CarryForwardContext {\n /** The prior period's `endDate` — the boundary of the closing snapshot. */\n readonly priorEndDate: string\n /** Read-only collection facade over current vault state. */\n collection<T = unknown>(name: string): ReadOnlyCollection<T>\n}\n\n/** Minimum read surface exposed to `carryForward`. */\nexport interface ReadOnlyCollection<T> {\n get(id: string): Promise<T | null>\n list(): Promise<T[]>\n}\n\n/**\n * Load every period record currently stored on the adapter.\n * Decrypting is the caller's responsibility (we return plain records\n * so the vault can use its own `_periods` DEK).\n *\n * @internal — called by Vault methods that need the closed-period\n * cache. Not part of the public API surface.\n */\nexport async function loadPeriods(\n adapter: NoydbStore,\n vault: string,\n decrypt: (id: string, envelope: EncryptedEnvelope) => Promise<PeriodRecord>,\n): Promise<PeriodRecord[]> {\n const ids = await adapter.list(vault, PERIODS_COLLECTION)\n const records: PeriodRecord[] = []\n for (const id of ids) {\n const env = await adapter.get(vault, PERIODS_COLLECTION, id)\n if (env) records.push(await decrypt(id, env))\n }\n // Stable order by closedAt so chain verification is reproducible.\n records.sort((a, b) => a.closedAt.localeCompare(b.closedAt))\n return records\n}\n\n/**\n * Given the current ordered period list, pick the last entry that\n * belongs to the hash chain — used as the `priorPeriodHash` anchor\n * for the next closure/opening.\n *\n * @internal\n */\nexport async function chainAnchor(\n records: readonly PeriodRecord[],\n partition?: PeriodPartition,\n): Promise<{ priorPeriodName?: string; priorPeriodHash: string }> {\n // #1005 — each timeline carries its OWN chain. Anchoring a partitioned close\n // to whatever happened to be written last vault-wide would interleave\n // unrelated subjects into one chain, so verifying client A's June would\n // depend on client B never having closed in between.\n const inTimeline = records.filter((p) => samePartition(p.partition, partition))\n const last = inTimeline[inTimeline.length - 1]\n if (!last) return { priorPeriodHash: '' }\n const hash = await sha256Hex(canonicalJson(last as unknown as Record<string, unknown>))\n return { priorPeriodName: last.name, priorPeriodHash: hash }\n}\n\n/**\n * Throw `PeriodClosedError` if the record being touched falls within\n * any closed period.\n *\n * Three signals, evaluated per period:\n *\n * 1. If the period declares a `dateField`, the guard reads\n * `record[dateField]` on BOTH the existing (prior) record AND the\n * incoming (new) record. Either comparing `<= endDate` triggers\n * the error — callers cannot slide a record into a closed period\n * by editing its date field.\n * 2. If the period has no `dateField`, the guard falls back to the\n * envelope `_ts` of the existing record. Fresh inserts (no\n * existing envelope) pass.\n * 3. For a delete, only the existing side is checked.\n *\n * @internal\n */\nexport function assertTsWritable(\n existing: { ts: string | null; record: Record<string, unknown> | null } | null,\n incomingRecord: Record<string, unknown> | null,\n closedPeriods: readonly PeriodRecord[],\n scope?: { collection: string; resolve?: PartitionResolver },\n): void {\n // #1005 — a period only governs records that resolve to ITS timeline. With no\n // resolver (the default `withPeriods()`), nothing resolves to a partition, so\n // every record sits on the vault-wide timeline exactly as before and a\n // partitioned period governs nothing.\n const partitionOf = (r: Record<string, unknown> | null): PeriodPartition | undefined => {\n if (!r || !scope?.resolve) return undefined\n return scope.resolve(scope.collection, r)\n }\n const existingRecord = existing?.record ?? null\n const existingPartition = partitionOf(existingRecord)\n const incomingPartition = partitionOf(incomingRecord)\n // One instant for the whole check, so a bounded reopen window cannot expire\n // between two periods in the same loop and seal a write half-way.\n const now = new Date().toISOString()\n\n for (const p of closedPeriods) {\n if (p.kind !== 'closed') continue\n // #1022 — a reopened period is writable again. This is the ONLY thing a\n // reopen does: it withdraws the period's veto. It cannot grant a write that\n // some other gate forbids, because the guard bus ANDs every handler and\n // record-level guards are registered ahead of this one.\n if (isEffectivelyReopened(p, now)) continue\n if (p.dateField) {\n const checkRecord = (\n label: string,\n r: Record<string, unknown> | null,\n recordPartition: PeriodPartition | undefined,\n ): void => {\n if (!r) return\n // Both sides are checked under their OWN partition, which is what stops\n // a write from sliding a record either INTO or OUT OF a sealed\n // timeline by rewriting the fields the subject mapping reads.\n if (!samePartition(recordPartition, p.partition)) return\n const v = r[p.dateField!]\n if (typeof v === 'string' && v <= p.endDate) {\n throw new PeriodClosedError(p.name, p.endDate, `${label}[${p.dateField}]=${v}`)\n }\n }\n checkRecord('existing', existingRecord, existingPartition)\n checkRecord('incoming', incomingRecord, incomingPartition)\n continue\n }\n // Fallback: write-time seal via envelope _ts. Scoped by the EXISTING\n // record's partition — `_ts` belongs to the stored envelope, so the\n // incoming side has no write-time of its own to compare.\n if (!samePartition(existingPartition, p.partition)) continue\n const existingTs = existing?.ts ?? null\n if (existingTs !== null && existingTs <= p.endDate) {\n throw new PeriodClosedError(p.name, p.endDate, existingTs)\n }\n }\n}\n\n/**\n * Sanity-check a proposed period name + endDate against existing\n * records. Shared by closePeriod / openPeriod so the two pathways\n * produce identical diagnostics.\n *\n * @internal\n */\nexport function validatePeriodName(\n name: string,\n existing: readonly PeriodRecord[],\n partition?: PeriodPartition,\n): void {\n if (name.length === 0) {\n throw new ValidationError('Period name cannot be empty.')\n }\n // Validates the components as a side effect — an empty or non-finite\n // component must be rejected at the call, not encoded into a storage key.\n resolvePeriodKey(name, partition)\n // #1005 — uniqueness is per TIMELINE. `'2026-06'` in `['A','vat']` does not\n // collide with `'2026-06'` in `['B','vat']`.\n if (existing.some((p) => p.name === name && samePartition(p.partition, partition))) {\n const where = partition && partition.length > 0\n ? ` in partition [${partition.join(', ')}]`\n : ''\n throw new ValidationError(`Period \"${name}\" already exists${where}.`)\n }\n}\n\n/**\n * Wire a reserved-collection ledger append for a period record. The\n * period itself is stored via the adapter as an encrypted envelope;\n * the ledger entry is a normal `put` with the period's payloadHash,\n * so period closures inherit the chain's tamper-evidence.\n *\n * @internal\n */\nexport async function appendPeriodLedgerEntry(\n ledger: LedgerStore | null,\n actor: string,\n envelope: EncryptedEnvelope,\n name: string,\n collection: string = PERIODS_COLLECTION,\n): Promise<void> {\n if (!ledger) return\n const { envelopePayloadHash } = await import('../../with-commit/history/ledger/index.js')\n await ledger.append({\n op: 'put',\n collection,\n id: name,\n version: envelope._v,\n actor,\n payloadHash: await envelopePayloadHash(envelope),\n })\n}\n\n/**\n * @internal #615. Sweep delete markers with `_ts < before` off ANY store\n * (the vault's local adapter, or a push-only sync target). Returns the count\n * removed. Shared by `vault._purgeDeleteMarkers` (local) and\n * `vault._purgePeriodTargets` (push-only targets).\n */\nexport async function purgeMarkersOn(\n store: NoydbStore,\n vault: string,\n before: string,\n collections?: string[],\n): Promise<number> {\n const snapshot = await store.loadAll(vault)\n let removed = 0\n for (const [coll, records] of Object.entries(snapshot)) {\n if (collections && !collections.includes(coll)) continue\n for (const [id, env] of Object.entries(records)) {\n if (isDeleteMarker(env) && env._ts < before) {\n await store.delete(vault, coll, id)\n removed++\n }\n }\n }\n return removed\n}\n","/**\n * Active periods strategy factory. Only reachable through the\n * `@noy-db/hub/periods` subpath.\n */\n\nimport {\n loadPeriods,\n chainAnchor,\n assertTsWritable,\n validatePeriodName,\n appendPeriodLedgerEntry,\n} from './periods.js'\nimport type { PeriodPartition, PartitionResolver } from './periods.js'\nimport type { PeriodsStrategy } from './strategy.js'\n\n/** Options for {@link withPeriods}. */\nexport interface WithPeriodsOptions {\n /**\n * Maps a collection to the timeline each of its records belongs to (#1005) —\n * the answer to \"which close calendar governs THIS record\".\n *\n * ```ts\n * withPeriods({\n * subjects: { receipts: (r) => [r.clientId, layerOf(r)] },\n * })\n * ```\n *\n * Same shape as `withForget({ subjects })`, which answers the same question\n * for erasure. A collection with no entry — and every collection when\n * `subjects` is omitted entirely — stays on the vault-wide timeline, so an\n * existing vault behaves exactly as it did before partitions existed.\n *\n * Return `undefined` from a mapper to put an individual record back on the\n * vault-wide timeline (e.g. a record that predates the field the mapping\n * reads).\n */\n readonly subjects?: Readonly<\n Record<string, (record: Record<string, unknown>) => PeriodPartition | undefined>\n >\n}\n\n/**\n * Build the default periods strategy. Pass into\n * `createNoydb({ periodsStrategy: withPeriods() })` to enable\n * `vault.closePeriod()` / `vault.openPeriod()` / write-guards.\n *\n * Pass `subjects` to run more than one close calendar in a single vault — see\n * {@link WithPeriodsOptions.subjects}.\n */\nexport function withPeriods(options?: WithPeriodsOptions): PeriodsStrategy {\n const subjects = options?.subjects\n const partitionOf: PartitionResolver | undefined = subjects\n ? (collection, record) => subjects[collection]?.(record)\n : undefined\n\n return {\n loadPeriods,\n chainAnchor,\n assertTsWritable,\n validatePeriodName,\n appendPeriodLedgerEntry,\n ...(partitionOf !== undefined && { partitionOf }),\n }\n}\n","/**\n * Strategy seam for the optional accounting-periods service. Core\n * imports `PeriodsStrategy` type-only + `NO_PERIODS` stub; the real\n * `loadPeriods` / `chainAnchor` / `assertTsWritable` /\n * `validatePeriodName` / `appendPeriodLedgerEntry` functions are\n * only reachable via `withPeriods()` in `./active.ts`.\n *\n * Applications that never call `vault.closePeriod()` /\n * `vault.openPeriod()` ship none of the ~363 LOC.\n *\n * @internal\n */\n\nimport type { EncryptedEnvelope, NoydbStore } from '../../kernel/types.js'\nimport type { LedgerStore } from '../../with-commit/history/ledger/store.js'\nimport type { PeriodRecord, PeriodPartition, PartitionResolver } from './periods.js'\n\n/**\n * @internal\n */\nexport interface PeriodsStrategy {\n loadPeriods(\n adapter: NoydbStore,\n vault: string,\n decrypt: (id: string, envelope: EncryptedEnvelope) => Promise<PeriodRecord>,\n ): Promise<PeriodRecord[]>\n chainAnchor(records: readonly PeriodRecord[], partition?: PeriodPartition): Promise<{\n priorPeriodName?: string\n priorPeriodHash: string\n }>\n assertTsWritable(\n existing: { ts: string | null; record: Record<string, unknown> | null } | null,\n incoming: Record<string, unknown> | null,\n periods: readonly PeriodRecord[],\n scope?: { collection: string; resolve?: PartitionResolver },\n ): void\n validatePeriodName(\n name: string,\n existing: readonly PeriodRecord[],\n partition?: PeriodPartition,\n ): void\n /**\n * Record → timeline resolver built from `withPeriods({ subjects })` (#1005).\n * `undefined` when the caller configured no subjects, which keeps every\n * record on the vault-wide timeline.\n */\n readonly partitionOf?: PartitionResolver\n appendPeriodLedgerEntry(\n ledger: LedgerStore | null,\n actor: string,\n envelope: EncryptedEnvelope,\n periodName: string,\n collection?: string,\n ): Promise<void>\n}\n\n/**\n * No-periods stub. `loadPeriods` returns `[]`; the write-guards do\n * nothing (vaults without closed periods never reject writes);\n * `validatePeriodName` / `appendPeriodLedgerEntry` throw because\n * those paths are only reached when the user explicitly called\n * `closePeriod()` / `openPeriod()` — if they did that without the\n * strategy, they need to wire it.\n *\n * @internal\n */\nconst NOT_ENABLED = new Error(\n 'Accounting periods require the periods strategy. Import ' +\n '`{ withPeriods }` from \"@noy-db/hub/periods\" and pass it to ' +\n '`createNoydb({ periodsStrategy: withPeriods() })`.',\n)\n\nexport const NO_PERIODS: PeriodsStrategy = {\n async loadPeriods() { return [] },\n async chainAnchor() { return { priorPeriodHash: '' } },\n assertTsWritable() {},\n validatePeriodName() { throw NOT_ENABLED },\n async appendPeriodLedgerEntry() { throw NOT_ENABLED },\n}\n"],"mappings":"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","names":[]}