@noy-db/hub 0.2.0-pre.1 → 0.2.0-pre.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/dist/aggregate/index.cjs.map +1 -1
  2. package/dist/aggregate/index.js +2 -2
  3. package/dist/attestation/index.cjs +305 -0
  4. package/dist/attestation/index.cjs.map +1 -0
  5. package/dist/attestation/index.d.cts +52 -0
  6. package/dist/attestation/index.d.ts +52 -0
  7. package/dist/attestation/index.js +36 -0
  8. package/dist/attestation/index.js.map +1 -0
  9. package/dist/blobs/index.cjs.map +1 -1
  10. package/dist/blobs/index.d.cts +4 -3
  11. package/dist/blobs/index.d.ts +4 -3
  12. package/dist/blobs/index.js +10 -8
  13. package/dist/blobs/index.js.map +1 -1
  14. package/dist/bundle/index.cjs +17940 -129
  15. package/dist/bundle/index.cjs.map +1 -1
  16. package/dist/bundle/index.d.cts +172 -3
  17. package/dist/bundle/index.d.ts +172 -3
  18. package/dist/bundle/index.js +533 -5
  19. package/dist/bundle/index.js.map +1 -1
  20. package/dist/{chunk-CBAHB2BF.js → chunk-2EYC3WDT.js} +7 -70
  21. package/dist/chunk-2EYC3WDT.js.map +1 -0
  22. package/dist/{chunk-P7EQ2S5O.js → chunk-2XLVPKXG.js} +2 -2
  23. package/dist/chunk-4OQWR46B.js +79 -0
  24. package/dist/chunk-4OQWR46B.js.map +1 -0
  25. package/dist/{chunk-23TTQXVO.js → chunk-4UBOTYP5.js} +2 -2
  26. package/dist/chunk-4X2S7PBF.js +251 -0
  27. package/dist/chunk-4X2S7PBF.js.map +1 -0
  28. package/dist/{chunk-MKSA2V7A.js → chunk-5YHWBPOT.js} +2 -2
  29. package/dist/{chunk-DYBQG5PQ.js → chunk-6S3LLAQ5.js} +2 -2
  30. package/dist/{chunk-UA4RI7OT.js → chunk-74JEQFMT.js} +5 -5
  31. package/dist/chunk-75QDHSE4.js +59 -0
  32. package/dist/chunk-75QDHSE4.js.map +1 -0
  33. package/dist/chunk-A6SWRXUQ.js +118 -0
  34. package/dist/chunk-A6SWRXUQ.js.map +1 -0
  35. package/dist/{chunk-UZXLQCHP.js → chunk-BFI3RS42.js} +2 -2
  36. package/dist/{chunk-EGQYGYIU.js → chunk-EMEX37ZN.js} +2 -2
  37. package/dist/{chunk-PEULZC6M.js → chunk-EPK6A3WJ.js} +8 -1
  38. package/dist/chunk-EPK6A3WJ.js.map +1 -0
  39. package/dist/{chunk-VMIO4IXG.js → chunk-FBMXWVGP.js} +6 -229
  40. package/dist/chunk-FBMXWVGP.js.map +1 -0
  41. package/dist/{chunk-ZNOEIM6Y.js → chunk-FCDO7UAO.js} +2 -2
  42. package/dist/{chunk-5SCJ5UEF.js → chunk-FS7A4XNF.js} +3 -3
  43. package/dist/{chunk-YS3POABP.js → chunk-FXQYZNOW.js} +1 -1
  44. package/dist/chunk-FXQYZNOW.js.map +1 -0
  45. package/dist/{chunk-SIZWEV2Y.js → chunk-G7PAZ3TD.js} +4 -4
  46. package/dist/{chunk-SIZWEV2Y.js.map → chunk-G7PAZ3TD.js.map} +1 -1
  47. package/dist/{chunk-537VFZTR.js → chunk-GAUBWHAF.js} +4 -4
  48. package/dist/{chunk-FCXOFQAJ.js → chunk-GD3BGKAR.js} +2 -2
  49. package/dist/{chunk-DPMFBCV6.js → chunk-GDTCGIPX.js} +2 -2
  50. package/dist/{chunk-DPMFBCV6.js.map → chunk-GDTCGIPX.js.map} +1 -1
  51. package/dist/{chunk-6HPZY4ON.js → chunk-GVXBHCZ2.js} +8 -3
  52. package/dist/chunk-GVXBHCZ2.js.map +1 -0
  53. package/dist/{chunk-HB3Z2GCR.js → chunk-HGZ7DC5H.js} +2 -2
  54. package/dist/{chunk-MIQHZESA.js → chunk-IS5HWQO7.js} +5 -5
  55. package/dist/{chunk-MIQHZESA.js.map → chunk-IS5HWQO7.js.map} +1 -1
  56. package/dist/{chunk-5DWL3JBF.js → chunk-K5PVGKE4.js} +2 -2
  57. package/dist/{chunk-NIOHFJPJ.js → chunk-KMI2NBBF.js} +7 -119
  58. package/dist/chunk-KMI2NBBF.js.map +1 -0
  59. package/dist/{chunk-XGSOTWYX.js → chunk-KYKMKLJ6.js} +2 -2
  60. package/dist/chunk-LOL725S4.js +233 -0
  61. package/dist/chunk-LOL725S4.js.map +1 -0
  62. package/dist/{chunk-4TFSM22V.js → chunk-LS3JLEIB.js} +4 -4
  63. package/dist/{chunk-2AXFIYHT.js → chunk-NCO2JGKK.js} +1 -1
  64. package/dist/chunk-NCO2JGKK.js.map +1 -0
  65. package/dist/{chunk-Z72JH4KG.js → chunk-NGSPBLLE.js} +4 -34
  66. package/dist/chunk-NGSPBLLE.js.map +1 -0
  67. package/dist/{chunk-OMLIZL2P.js → chunk-NSLTPGEN.js} +2 -2
  68. package/dist/{chunk-7H6DOO3E.js → chunk-P6256WTJ.js} +211 -36
  69. package/dist/chunk-P6256WTJ.js.map +1 -0
  70. package/dist/{chunk-KESP7GOK.js → chunk-QAU5HM6Q.js} +3 -3
  71. package/dist/{chunk-34YSDCDP.js → chunk-SAVQ6E2O.js} +2 -2
  72. package/dist/chunk-T6HQMVML.js +9960 -0
  73. package/dist/chunk-T6HQMVML.js.map +1 -0
  74. package/dist/{chunk-PA6R5ZCI.js → chunk-TLFUDXVV.js} +4 -4
  75. package/dist/{chunk-WCA2NROQ.js → chunk-UOF74WQY.js} +2 -2
  76. package/dist/chunk-UVPGJXVO.js +83 -0
  77. package/dist/chunk-UVPGJXVO.js.map +1 -0
  78. package/dist/{chunk-DYECX3IX.js → chunk-WRLHNG6H.js} +2 -2
  79. package/dist/{chunk-ADQ5MQ54.js → chunk-YDLAFP36.js} +71 -1
  80. package/dist/chunk-YDLAFP36.js.map +1 -0
  81. package/dist/{chunk-I6MX32UC.js → chunk-YK72A4IT.js} +4 -4
  82. package/dist/chunk-YL2DR3HY.js +36 -0
  83. package/dist/chunk-YL2DR3HY.js.map +1 -0
  84. package/dist/{chunk-RD5LYKD6.js → chunk-ZC2AAE6J.js} +2 -2
  85. package/dist/chunk-ZUMGGHRB.js +57 -0
  86. package/dist/chunk-ZUMGGHRB.js.map +1 -0
  87. package/dist/consent/index.cjs.map +1 -1
  88. package/dist/consent/index.d.cts +4 -3
  89. package/dist/consent/index.d.ts +4 -3
  90. package/dist/consent/index.js +3 -3
  91. package/dist/{crypto-A7FRXYHC.js → crypto-H2Y3DDFW.js} +3 -3
  92. package/dist/{delegation-YBA4X4JN.js → delegation-QSC7G5QC.js} +5 -5
  93. package/dist/derivations/index.cjs.map +1 -1
  94. package/dist/derivations/index.d.cts +5 -4
  95. package/dist/derivations/index.d.ts +5 -4
  96. package/dist/derivations/index.js +4 -4
  97. package/dist/{dev-unlock-D9s-loPr.d.ts → dev-unlock-Cf2B7Kih.d.ts} +1 -1
  98. package/dist/{dev-unlock-DRwVSy2S.d.cts → dev-unlock-De3mjQWv.d.cts} +1 -1
  99. package/dist/executor-BZKFZVRC.js +8 -0
  100. package/dist/executor-GFZFDQXV.js +8 -0
  101. package/dist/executor-KT2IOZVP.js +11 -0
  102. package/dist/{fanout-sidecar-VJ52RIEY.js → fanout-sidecar-NRBWSLRK.js} +2 -2
  103. package/dist/guards/index.cjs +7 -0
  104. package/dist/guards/index.cjs.map +1 -1
  105. package/dist/guards/index.d.cts +5 -4
  106. package/dist/guards/index.d.ts +5 -4
  107. package/dist/guards/index.js +4 -4
  108. package/dist/{hash-DXXXusyk.d.ts → hash-gVn_uKhp.d.ts} +1 -1
  109. package/dist/{hash-DtRih9MQ.d.cts → hash-vBCB0-Ps.d.cts} +1 -1
  110. package/dist/history/index.cjs +2 -2
  111. package/dist/history/index.cjs.map +1 -1
  112. package/dist/history/index.d.cts +5 -4
  113. package/dist/history/index.d.ts +5 -4
  114. package/dist/history/index.js +6 -6
  115. package/dist/i18n/index.cjs.map +1 -1
  116. package/dist/i18n/index.d.cts +4 -3
  117. package/dist/i18n/index.d.ts +4 -3
  118. package/dist/i18n/index.js +14 -12
  119. package/dist/i18n/index.js.map +1 -1
  120. package/dist/{index-CNwA-B6-.d.ts → index-BF1B2HB9.d.ts} +53 -1
  121. package/dist/{index-CmVgTkqk.d.cts → index-DVkvrgpm.d.cts} +53 -1
  122. package/dist/index.cjs +1780 -64
  123. package/dist/index.cjs.map +1 -1
  124. package/dist/index.d.cts +34 -12
  125. package/dist/index.d.ts +34 -12
  126. package/dist/index.js +160 -8804
  127. package/dist/index.js.map +1 -1
  128. package/dist/indexing/index.cjs.map +1 -1
  129. package/dist/indexing/index.js +2 -2
  130. package/dist/issue-BAJ7ZB4S.js +12 -0
  131. package/dist/{ledger-3TXNP47J.js → ledger-WOEJUYTP.js} +6 -6
  132. package/dist/materialized-views/index.cjs.map +1 -1
  133. package/dist/materialized-views/index.d.cts +6 -5
  134. package/dist/materialized-views/index.d.ts +6 -5
  135. package/dist/materialized-views/index.js +6 -6
  136. package/dist/noydb-XNQSKXGO.js +34 -0
  137. package/dist/overlay-views/index.cjs.map +1 -1
  138. package/dist/overlay-views/index.d.cts +5 -4
  139. package/dist/overlay-views/index.d.ts +5 -4
  140. package/dist/overlay-views/index.js +6 -4
  141. package/dist/periods/index.cjs.map +1 -1
  142. package/dist/periods/index.d.cts +4 -3
  143. package/dist/periods/index.d.ts +4 -3
  144. package/dist/periods/index.js +6 -6
  145. package/dist/{public-envelope-PY6NKFLI.js → public-envelope-OHQ5UZFM.js} +4 -4
  146. package/dist/query/index.cjs.map +1 -1
  147. package/dist/query/index.d.cts +1 -1
  148. package/dist/query/index.d.ts +1 -1
  149. package/dist/query/index.js +3 -3
  150. package/dist/registry-2IEARCGT.js +7 -0
  151. package/dist/{registry-3L3N3PTG.js → registry-CDHASH73.js} +3 -3
  152. package/dist/registry-EMGLZGR6.js +8 -0
  153. package/dist/registry-NQALYR77.js +8 -0
  154. package/dist/registry-NQALYR77.js.map +1 -0
  155. package/dist/revoke-7JOVLZFD.js +17 -0
  156. package/dist/revoke-7JOVLZFD.js.map +1 -0
  157. package/dist/session/index.cjs.map +1 -1
  158. package/dist/session/index.d.cts +5 -4
  159. package/dist/session/index.d.ts +5 -4
  160. package/dist/session/index.js +3 -3
  161. package/dist/shadow/index.cjs.map +1 -1
  162. package/dist/shadow/index.d.cts +4 -3
  163. package/dist/shadow/index.d.ts +4 -3
  164. package/dist/shadow/index.js +2 -2
  165. package/dist/signer-M4K5HBLD.js +18 -0
  166. package/dist/signer-M4K5HBLD.js.map +1 -0
  167. package/dist/{stale-HSC5YO2O.js → stale-PAGCS4K5.js} +2 -2
  168. package/dist/stale-PAGCS4K5.js.map +1 -0
  169. package/dist/store/index.cjs.map +1 -1
  170. package/dist/store/index.d.cts +4 -3
  171. package/dist/store/index.d.ts +4 -3
  172. package/dist/store/index.js +2 -2
  173. package/dist/sync/index.cjs.map +1 -1
  174. package/dist/sync/index.d.cts +3 -2
  175. package/dist/sync/index.d.ts +3 -2
  176. package/dist/sync/index.js +4 -4
  177. package/dist/team/index.cjs.map +1 -1
  178. package/dist/team/index.d.cts +4 -3
  179. package/dist/team/index.d.ts +4 -3
  180. package/dist/team/index.js +13 -11
  181. package/dist/tx/index.cjs +81 -1
  182. package/dist/tx/index.cjs.map +1 -1
  183. package/dist/tx/index.d.cts +5 -4
  184. package/dist/tx/index.d.ts +5 -4
  185. package/dist/tx/index.js +56 -3
  186. package/dist/tx/index.js.map +1 -1
  187. package/dist/{types-C4lwMKKF.d.cts → types-CSLcfytP.d.cts} +644 -5
  188. package/dist/{types-DW9RGSSs.d.ts → types-D9eB0Rvh.d.ts} +644 -5
  189. package/dist/{index-4agOpzqd.d.ts → ulid-CG2YvAbg.d.cts} +51 -33
  190. package/dist/{index-hdFvZkBP.d.cts → ulid-CiM2OAeM.d.ts} +51 -33
  191. package/dist/util/index.cjs.map +1 -1
  192. package/dist/util/index.js +1 -1
  193. package/dist/{with-derivation-g-pGoMzL.d.ts → with-derivation-Bzpj6UTv.d.ts} +1 -1
  194. package/dist/{with-derivation-C8LDlV7t.d.cts → with-derivation-DWajFh4K.d.cts} +1 -1
  195. package/dist/{with-guard-jI1x9Z3k.d.cts → with-guard-DF_Ul3DT.d.cts} +1 -1
  196. package/dist/{with-guard-DWOCK4Ca.d.ts → with-guard-DR7U-l4v.d.ts} +1 -1
  197. package/dist/{with-materialized-view-DcTx4H3j.d.cts → with-materialized-view-_piodoIz.d.cts} +1 -1
  198. package/dist/{with-materialized-view-DaKR-N6J.d.ts → with-materialized-view-qtoJ3xKJ.d.ts} +1 -1
  199. package/dist/{with-overlayed-view-N7jYuNOS.d.ts → with-overlayed-view-DFaRfgMr.d.ts} +1 -1
  200. package/dist/{with-overlayed-view-D-6oWAgM.d.cts → with-overlayed-view-DwzCKxn2.d.cts} +1 -1
  201. package/package.json +15 -3
  202. package/dist/chunk-2AXFIYHT.js.map +0 -1
  203. package/dist/chunk-6HPZY4ON.js.map +0 -1
  204. package/dist/chunk-7H6DOO3E.js.map +0 -1
  205. package/dist/chunk-ADQ5MQ54.js.map +0 -1
  206. package/dist/chunk-CBAHB2BF.js.map +0 -1
  207. package/dist/chunk-NIOHFJPJ.js.map +0 -1
  208. package/dist/chunk-PEULZC6M.js.map +0 -1
  209. package/dist/chunk-VMIO4IXG.js.map +0 -1
  210. package/dist/chunk-YS3POABP.js.map +0 -1
  211. package/dist/chunk-Z72JH4KG.js.map +0 -1
  212. package/dist/executor-7E3VFGW7.js +0 -11
  213. package/dist/executor-CEWX2FQI.js +0 -8
  214. package/dist/executor-X4SQ3ZLC.js +0 -8
  215. package/dist/registry-O47PUPSY.js +0 -8
  216. package/dist/registry-RFGGMVNJ.js +0 -7
  217. package/dist/registry-WLLMODKN.js +0 -8
  218. /package/dist/{chunk-P7EQ2S5O.js.map → chunk-2XLVPKXG.js.map} +0 -0
  219. /package/dist/{chunk-23TTQXVO.js.map → chunk-4UBOTYP5.js.map} +0 -0
  220. /package/dist/{chunk-MKSA2V7A.js.map → chunk-5YHWBPOT.js.map} +0 -0
  221. /package/dist/{chunk-DYBQG5PQ.js.map → chunk-6S3LLAQ5.js.map} +0 -0
  222. /package/dist/{chunk-UA4RI7OT.js.map → chunk-74JEQFMT.js.map} +0 -0
  223. /package/dist/{chunk-UZXLQCHP.js.map → chunk-BFI3RS42.js.map} +0 -0
  224. /package/dist/{chunk-EGQYGYIU.js.map → chunk-EMEX37ZN.js.map} +0 -0
  225. /package/dist/{chunk-ZNOEIM6Y.js.map → chunk-FCDO7UAO.js.map} +0 -0
  226. /package/dist/{chunk-5SCJ5UEF.js.map → chunk-FS7A4XNF.js.map} +0 -0
  227. /package/dist/{chunk-537VFZTR.js.map → chunk-GAUBWHAF.js.map} +0 -0
  228. /package/dist/{chunk-FCXOFQAJ.js.map → chunk-GD3BGKAR.js.map} +0 -0
  229. /package/dist/{chunk-HB3Z2GCR.js.map → chunk-HGZ7DC5H.js.map} +0 -0
  230. /package/dist/{chunk-5DWL3JBF.js.map → chunk-K5PVGKE4.js.map} +0 -0
  231. /package/dist/{chunk-XGSOTWYX.js.map → chunk-KYKMKLJ6.js.map} +0 -0
  232. /package/dist/{chunk-4TFSM22V.js.map → chunk-LS3JLEIB.js.map} +0 -0
  233. /package/dist/{chunk-OMLIZL2P.js.map → chunk-NSLTPGEN.js.map} +0 -0
  234. /package/dist/{chunk-KESP7GOK.js.map → chunk-QAU5HM6Q.js.map} +0 -0
  235. /package/dist/{chunk-34YSDCDP.js.map → chunk-SAVQ6E2O.js.map} +0 -0
  236. /package/dist/{chunk-PA6R5ZCI.js.map → chunk-TLFUDXVV.js.map} +0 -0
  237. /package/dist/{chunk-WCA2NROQ.js.map → chunk-UOF74WQY.js.map} +0 -0
  238. /package/dist/{chunk-DYECX3IX.js.map → chunk-WRLHNG6H.js.map} +0 -0
  239. /package/dist/{chunk-I6MX32UC.js.map → chunk-YK72A4IT.js.map} +0 -0
  240. /package/dist/{chunk-RD5LYKD6.js.map → chunk-ZC2AAE6J.js.map} +0 -0
  241. /package/dist/{crypto-A7FRXYHC.js.map → crypto-H2Y3DDFW.js.map} +0 -0
  242. /package/dist/{delegation-YBA4X4JN.js.map → delegation-QSC7G5QC.js.map} +0 -0
  243. /package/dist/{executor-7E3VFGW7.js.map → executor-BZKFZVRC.js.map} +0 -0
  244. /package/dist/{executor-CEWX2FQI.js.map → executor-GFZFDQXV.js.map} +0 -0
  245. /package/dist/{executor-X4SQ3ZLC.js.map → executor-KT2IOZVP.js.map} +0 -0
  246. /package/dist/{fanout-sidecar-VJ52RIEY.js.map → fanout-sidecar-NRBWSLRK.js.map} +0 -0
  247. /package/dist/{ledger-3TXNP47J.js.map → issue-BAJ7ZB4S.js.map} +0 -0
  248. /package/dist/{public-envelope-PY6NKFLI.js.map → ledger-WOEJUYTP.js.map} +0 -0
  249. /package/dist/{registry-3L3N3PTG.js.map → noydb-XNQSKXGO.js.map} +0 -0
  250. /package/dist/{registry-O47PUPSY.js.map → public-envelope-OHQ5UZFM.js.map} +0 -0
  251. /package/dist/{registry-RFGGMVNJ.js.map → registry-2IEARCGT.js.map} +0 -0
  252. /package/dist/{registry-WLLMODKN.js.map → registry-CDHASH73.js.map} +0 -0
  253. /package/dist/{stale-HSC5YO2O.js.map → registry-EMGLZGR6.js.map} +0 -0
package/dist/index.cjs CHANGED
@@ -46,7 +46,7 @@ var init_types = __esm({
46
46
  });
47
47
 
48
48
  // src/errors.ts
49
- var NoydbError, DecryptionError, TamperedError, InvalidKeyError, KeyringCorruptError, NoAccessError, ReadOnlyError, ReadOnlyAtInstantError, ReadOnlyFrameError, PermissionDeniedError, ExportCapabilityError, KeyringExpiredError, ImportCapabilityError, StoreCapabilityError, PrivilegeEscalationError, PeriodClosedError, RecordLockedError, FieldFrozenError, InvariantError, AmendmentForbiddenError, DirectoryDisabledError, TierNotGrantedError, ElevationExpiredError, AlreadyElevatedError, TierDemoteDeniedError, DelegationTargetMissingError, ConflictError, LedgerContentionError, BundleVersionConflictError, NetworkError, NotFoundError, ValidationError, SchemaValidationError, GroupCardinalityError, IndexRequiredError, IndexWriteFailureError, BundleIntegrityError, BundleSealMismatchError, ReservedCollectionNameError, DictKeyMissingError, DictKeyInUseError, MissingTranslationError, LocaleNotSpecifiedError, TranslatorNotConfiguredError, BackupLedgerError, BackupCorruptedError, SessionExpiredError, SessionNotFoundError, SessionPolicyError, JoinTooLargeError, DanglingReferenceError, FilenameSanitizationError, PathEscapeError, DerivationCycleError, DerivationDepthError, DerivationOutputUnknownError, DerivationOutputShapeError, DerivationCapExceededError, MaterializedViewCycleError, MaterializedViewSourceUnknownError, MaterializedViewTooLargeError, MaterializedViewConfigError, OverlayBaseIsVirtualError, OverlayCollectionUnavailableError, OverlayNameCollisionError, OverlayIdMismatchError;
49
+ var NoydbError, DecryptionError, TamperedError, InvalidKeyError, KeyringCorruptError, NoAccessError, ReadOnlyError, ReadOnlyAtInstantError, ReadOnlyFrameError, PermissionDeniedError, ExportCapabilityError, KeyringExpiredError, ImportCapabilityError, StoreCapabilityError, PrivilegeEscalationError, PeriodClosedError, RecordLockedError, FieldFrozenError, InvariantError, AmendmentForbiddenError, DirectoryDisabledError, TierNotGrantedError, ElevationExpiredError, AlreadyElevatedError, TierDemoteDeniedError, DelegationTargetMissingError, ConflictError, LedgerContentionError, BundleVersionConflictError, NetworkError, NotFoundError, ValidationError, SchemaValidationError, SchemaUpdateError, NonAdditiveSchemaChangeError, SchemaLockedError, SchemaFenceError, MigrationRequiredError, QuiesceTimeoutError, GroupCardinalityError, IndexRequiredError, IndexWriteFailureError, BundleIntegrityError, BundleSealMismatchError, ReservedCollectionNameError, DictKeyMissingError, DictKeyInUseError, MissingTranslationError, LocaleNotSpecifiedError, TranslatorNotConfiguredError, BackupLedgerError, BackupCorruptedError, AttestationError, SessionExpiredError, SessionNotFoundError, SessionPolicyError, JoinTooLargeError, DanglingReferenceError, FilenameSanitizationError, PathEscapeError, DerivationCycleError, DerivationDepthError, DerivationOutputUnknownError, DerivationOutputShapeError, DerivationCapExceededError, MaterializedViewCycleError, MaterializedViewSourceUnknownError, MaterializedViewTooLargeError, MaterializedViewConfigError, OverlayBaseIsVirtualError, OverlayCollectionUnavailableError, OverlayNameCollisionError, OverlayIdMismatchError;
50
50
  var init_errors = __esm({
51
51
  "src/errors.ts"() {
52
52
  "use strict";
@@ -377,6 +377,42 @@ var init_errors = __esm({
377
377
  this.direction = direction;
378
378
  }
379
379
  };
380
+ SchemaUpdateError = class extends NoydbError {
381
+ constructor(code, message) {
382
+ super(code, message);
383
+ this.name = "SchemaUpdateError";
384
+ }
385
+ };
386
+ NonAdditiveSchemaChangeError = class extends SchemaUpdateError {
387
+ constructor(message) {
388
+ super("NON_ADDITIVE_SCHEMA_CHANGE", message);
389
+ this.name = "NonAdditiveSchemaChangeError";
390
+ }
391
+ };
392
+ SchemaLockedError = class extends SchemaUpdateError {
393
+ constructor(message) {
394
+ super("SCHEMA_LOCKED", message);
395
+ this.name = "SchemaLockedError";
396
+ }
397
+ };
398
+ SchemaFenceError = class extends SchemaUpdateError {
399
+ constructor(message) {
400
+ super("SCHEMA_FENCE", message);
401
+ this.name = "SchemaFenceError";
402
+ }
403
+ };
404
+ MigrationRequiredError = class extends SchemaUpdateError {
405
+ constructor(message) {
406
+ super("MIGRATION_REQUIRED", message);
407
+ this.name = "MigrationRequiredError";
408
+ }
409
+ };
410
+ QuiesceTimeoutError = class extends SchemaUpdateError {
411
+ constructor(message) {
412
+ super("QUIESCE_TIMEOUT", message);
413
+ this.name = "QuiesceTimeoutError";
414
+ }
415
+ };
380
416
  GroupCardinalityError = class extends NoydbError {
381
417
  /** The field being grouped on. */
382
418
  field;
@@ -563,6 +599,12 @@ Resolutions:
563
599
  this.id = id;
564
600
  }
565
601
  };
602
+ AttestationError = class extends NoydbError {
603
+ constructor(message) {
604
+ super("ATTESTATION", message);
605
+ this.name = "AttestationError";
606
+ }
607
+ };
566
608
  SessionExpiredError = class extends NoydbError {
567
609
  sessionId;
568
610
  constructor(sessionId) {
@@ -3298,6 +3340,185 @@ var init_fanout_sidecar = __esm({
3298
3340
  }
3299
3341
  });
3300
3342
 
3343
+ // src/attestation/signer.ts
3344
+ var signer_exports = {};
3345
+ __export(signer_exports, {
3346
+ ATTESTATIONS_COLLECTION: () => ATTESTATIONS_COLLECTION,
3347
+ REVOKED_RECORD_ID: () => REVOKED_RECORD_ID,
3348
+ SIGNER_RECORD_ID: () => SIGNER_RECORD_ID,
3349
+ loadOrCreateSigner: () => loadOrCreateSigner,
3350
+ loadSigner: () => loadSigner
3351
+ });
3352
+ async function loadSigner(store, vault, getDEK) {
3353
+ const existing = await store.get(vault, ATTESTATIONS_COLLECTION, SIGNER_RECORD_ID);
3354
+ if (!existing) return null;
3355
+ const dek = await getDEK(ATTESTATIONS_COLLECTION);
3356
+ const json = await decrypt(existing._iv, existing._data, dek);
3357
+ return JSON.parse(json);
3358
+ }
3359
+ async function loadOrCreateSigner(store, vault, getDEK) {
3360
+ const existing = await loadSigner(store, vault, getDEK);
3361
+ if (existing) return existing;
3362
+ const dek = await getDEK(ATTESTATIONS_COLLECTION);
3363
+ const signer = await (0, import_attestation.generateDocSigningKeyPair)();
3364
+ const { iv, data } = await encrypt(JSON.stringify(signer), dek);
3365
+ const env = {
3366
+ _noydb: NOYDB_FORMAT_VERSION,
3367
+ _v: 1,
3368
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
3369
+ _iv: iv,
3370
+ _data: data
3371
+ };
3372
+ try {
3373
+ await store.put(vault, ATTESTATIONS_COLLECTION, SIGNER_RECORD_ID, env, 0);
3374
+ return signer;
3375
+ } catch (e) {
3376
+ if (!(e instanceof ConflictError)) throw e;
3377
+ const winner = await loadSigner(store, vault, getDEK);
3378
+ if (!winner) {
3379
+ throw new ConflictError(0, "loadOrCreateSigner: signer mint lost a concurrent race but the winning record could not be re-read.");
3380
+ }
3381
+ return winner;
3382
+ }
3383
+ }
3384
+ var import_attestation, ATTESTATIONS_COLLECTION, SIGNER_RECORD_ID, REVOKED_RECORD_ID;
3385
+ var init_signer = __esm({
3386
+ "src/attestation/signer.ts"() {
3387
+ "use strict";
3388
+ init_types();
3389
+ init_crypto();
3390
+ init_errors();
3391
+ import_attestation = require("@noy-db/attestation");
3392
+ ATTESTATIONS_COLLECTION = "_attestations";
3393
+ SIGNER_RECORD_ID = "_signer";
3394
+ REVOKED_RECORD_ID = "_revoked";
3395
+ }
3396
+ });
3397
+
3398
+ // src/attestation/issue.ts
3399
+ var issue_exports = {};
3400
+ __export(issue_exports, {
3401
+ issueAttestationCore: () => issueAttestationCore
3402
+ });
3403
+ async function issueAttestationCore(ctx, args) {
3404
+ if (ctx.role !== "owner") {
3405
+ throw new AttestationError(`issueAttestation requires the 'owner' role; caller is '${ctx.role}'. Issuing a signed attestation is the firm's identity operation.`);
3406
+ }
3407
+ const src = await ctx.readRecord(args.collection, args.id);
3408
+ if (!src) throw new AttestationError(`issueAttestation: source record '${args.collection}/${args.id}' not found.`);
3409
+ const dek = await ctx.getDEK();
3410
+ const signer = await loadOrCreateSigner(ctx.store, ctx.vault, () => Promise.resolve(dek));
3411
+ const saltB64 = (0, import_attestation2.bytesToB64url)(crypto.getRandomValues(new Uint8Array(16)));
3412
+ let fieldHashes;
3413
+ try {
3414
+ fieldHashes = await (0, import_attestation2.computeFieldHashes)(saltB64, args.fieldSchema, src.record);
3415
+ } catch (e) {
3416
+ throw new AttestationError(`issueAttestation: ${e.message}`);
3417
+ }
3418
+ const docId = generateULID();
3419
+ const sig = await (0, import_attestation2.signPayloadCore)({ v: 1, docId, salt: saltB64, keyId: signer.keyId, fieldHashes }, signer.privateKeyPkcs8B64);
3420
+ const payload = { v: 1, docId, salt: saltB64, alg: "ed25519", keyId: signer.keyId, fieldHashes, sig };
3421
+ const index = {
3422
+ docId,
3423
+ issuedAt: (/* @__PURE__ */ new Date()).toISOString(),
3424
+ keyId: signer.keyId,
3425
+ fieldPaths: args.fieldSchema.fields.map((f) => f.path),
3426
+ sourceRefs: [{ collection: args.collection, id: args.id, version: src.version }]
3427
+ };
3428
+ const { iv, data } = await encrypt(JSON.stringify(index), dek);
3429
+ const env = { _noydb: NOYDB_FORMAT_VERSION, _v: 1, _ts: index.issuedAt, _iv: iv, _data: data };
3430
+ await ctx.store.put(ctx.vault, ATTESTATIONS_COLLECTION, docId, env);
3431
+ return { docId, qr: (0, import_attestation2.encodeQr)(payload), payload, keyId: signer.keyId, publicKeyB64: signer.publicKeyB64 };
3432
+ }
3433
+ var import_attestation2;
3434
+ var init_issue = __esm({
3435
+ "src/attestation/issue.ts"() {
3436
+ "use strict";
3437
+ init_types();
3438
+ init_crypto();
3439
+ init_errors();
3440
+ init_ulid();
3441
+ init_signer();
3442
+ import_attestation2 = require("@noy-db/attestation");
3443
+ }
3444
+ });
3445
+
3446
+ // src/attestation/revoke.ts
3447
+ var revoke_exports = {};
3448
+ __export(revoke_exports, {
3449
+ getRevokedDocIdsCore: () => getRevokedDocIdsCore,
3450
+ publishRevocationListCore: () => publishRevocationListCore,
3451
+ revokeDocCore: () => revokeDocCore,
3452
+ unrevokeDocCore: () => unrevokeDocCore
3453
+ });
3454
+ function requireOwner(ctx, op) {
3455
+ if (ctx.role !== "owner") {
3456
+ throw new AttestationError(`${op} requires the 'owner' role; caller is '${ctx.role}'. Revocation is the firm's identity operation.`);
3457
+ }
3458
+ }
3459
+ async function readSet(store, vault, dek) {
3460
+ const env = await store.get(vault, ATTESTATIONS_COLLECTION, REVOKED_RECORD_ID);
3461
+ if (!env) return { docIds: /* @__PURE__ */ new Set(), version: void 0 };
3462
+ const set = JSON.parse(await decrypt(env._iv, env._data, dek));
3463
+ return { docIds: new Set(set.docIds), version: env._v };
3464
+ }
3465
+ async function mutateSet(ctx, mutate) {
3466
+ const dek = await ctx.getDEK();
3467
+ for (let attempt = 0; attempt < 2; attempt++) {
3468
+ const { docIds, version } = await readSet(ctx.store, ctx.vault, dek);
3469
+ mutate(docIds);
3470
+ const payload = { docIds: [...docIds].sort(), updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
3471
+ const { iv, data } = await encrypt(JSON.stringify(payload), dek);
3472
+ const expectedVersion = version ?? 0;
3473
+ const env = {
3474
+ _noydb: NOYDB_FORMAT_VERSION,
3475
+ _v: expectedVersion + 1,
3476
+ _ts: payload.updatedAt,
3477
+ _iv: iv,
3478
+ _data: data
3479
+ };
3480
+ try {
3481
+ await ctx.store.put(ctx.vault, ATTESTATIONS_COLLECTION, REVOKED_RECORD_ID, env, expectedVersion);
3482
+ return;
3483
+ } catch (e) {
3484
+ if (e instanceof ConflictError && attempt === 0) continue;
3485
+ throw e;
3486
+ }
3487
+ }
3488
+ }
3489
+ async function revokeDocCore(ctx, docId) {
3490
+ requireOwner(ctx, "revokeAttestation");
3491
+ const issued = await ctx.store.get(ctx.vault, ATTESTATIONS_COLLECTION, docId);
3492
+ if (!issued) throw new AttestationError(`revokeAttestation: attestation '${docId}' not found (was it issued by this vault?).`);
3493
+ await mutateSet(ctx, (ids) => ids.add(docId));
3494
+ }
3495
+ async function unrevokeDocCore(ctx, docId) {
3496
+ requireOwner(ctx, "unrevokeAttestation");
3497
+ await mutateSet(ctx, (ids) => ids.delete(docId));
3498
+ }
3499
+ async function getRevokedDocIdsCore(ctx) {
3500
+ const dek = await ctx.getDEK();
3501
+ const { docIds } = await readSet(ctx.store, ctx.vault, dek);
3502
+ return [...docIds].sort();
3503
+ }
3504
+ async function publishRevocationListCore(ctx) {
3505
+ requireOwner(ctx, "publishRevocationList");
3506
+ const docIds = await getRevokedDocIdsCore(ctx);
3507
+ const signer = await loadOrCreateSigner(ctx.store, ctx.vault, () => ctx.getDEK());
3508
+ return (0, import_attestation3.signRevocationList)(docIds, (/* @__PURE__ */ new Date()).toISOString(), signer.keyId, signer.privateKeyPkcs8B64);
3509
+ }
3510
+ var import_attestation3;
3511
+ var init_revoke = __esm({
3512
+ "src/attestation/revoke.ts"() {
3513
+ "use strict";
3514
+ init_types();
3515
+ init_crypto();
3516
+ init_errors();
3517
+ init_signer();
3518
+ import_attestation3 = require("@noy-db/attestation");
3519
+ }
3520
+ });
3521
+
3301
3522
  // src/guards/registry.ts
3302
3523
  var registry_exports2 = {};
3303
3524
  __export(registry_exports2, {
@@ -3321,6 +3542,13 @@ var init_registry2 = __esm({
3321
3542
  guardsFor(collection) {
3322
3543
  return this._byCollection.get(collection) ?? [];
3323
3544
  }
3545
+ /** Per-collection guard counts, for introspection (#229). */
3546
+ summary() {
3547
+ return [...this._byCollection.entries()].map(([collection, guards]) => ({
3548
+ collection,
3549
+ count: guards.length
3550
+ }));
3551
+ }
3324
3552
  /**
3325
3553
  * Run every guard's `check` for this collection. First throw wins —
3326
3554
  * remaining guards are not invoked. Guards without a `check` skip.
@@ -3695,6 +3923,7 @@ __export(src_exports, {
3695
3923
  Aggregation: () => Aggregation,
3696
3924
  AlreadyElevatedError: () => AlreadyElevatedError,
3697
3925
  AmendmentForbiddenError: () => AmendmentForbiddenError,
3926
+ AttestationError: () => AttestationError,
3698
3927
  BLOB_CHUNKS_COLLECTION: () => BLOB_CHUNKS_COLLECTION,
3699
3928
  BLOB_COLLECTION: () => BLOB_COLLECTION,
3700
3929
  BLOB_INDEX_COLLECTION: () => BLOB_INDEX_COLLECTION,
@@ -3768,7 +3997,9 @@ __export(src_exports, {
3768
3997
  MaterializedViewCycleError: () => MaterializedViewCycleError,
3769
3998
  MaterializedViewSourceUnknownError: () => MaterializedViewSourceUnknownError,
3770
3999
  MaterializedViewTooLargeError: () => MaterializedViewTooLargeError,
4000
+ MemoryRecipientSealer: () => MemoryRecipientSealer,
3771
4001
  MemorySealingKeyProvider: () => MemorySealingKeyProvider,
4002
+ MigrationRequiredError: () => MigrationRequiredError,
3772
4003
  MissingTranslationError: () => MissingTranslationError,
3773
4004
  NOYDB_BACKUP_VERSION: () => NOYDB_BACKUP_VERSION,
3774
4005
  NOYDB_BUNDLE_FORMAT_VERSION: () => NOYDB_BUNDLE_FORMAT_VERSION,
@@ -3779,6 +4010,7 @@ __export(src_exports, {
3779
4010
  NOYDB_SYNC_VERSION: () => NOYDB_SYNC_VERSION,
3780
4011
  NetworkError: () => NetworkError,
3781
4012
  NoAccessError: () => NoAccessError,
4013
+ NonAdditiveSchemaChangeError: () => NonAdditiveSchemaChangeError,
3782
4014
  NotFoundError: () => NotFoundError,
3783
4015
  Noydb: () => Noydb,
3784
4016
  NoydbError: () => NoydbError,
@@ -3800,6 +4032,7 @@ __export(src_exports, {
3800
4032
  PrivilegeEscalationError: () => PrivilegeEscalationError,
3801
4033
  Query: () => Query,
3802
4034
  QuickUnlockStore: () => QuickUnlockStore,
4035
+ QuiesceTimeoutError: () => QuiesceTimeoutError,
3803
4036
  ReadOnlyAtInstantError: () => ReadOnlyAtInstantError,
3804
4037
  ReadOnlyError: () => ReadOnlyError,
3805
4038
  ReadOnlyFrameError: () => ReadOnlyFrameError,
@@ -3815,6 +4048,9 @@ __export(src_exports, {
3815
4048
  STRICT_POLICY: () => STRICT_POLICY,
3816
4049
  SYNC_CREDENTIALS_COLLECTION: () => SYNC_CREDENTIALS_COLLECTION,
3817
4050
  ScanBuilder: () => ScanBuilder,
4051
+ SchemaFenceError: () => SchemaFenceError,
4052
+ SchemaLockedError: () => SchemaLockedError,
4053
+ SchemaUpdateError: () => SchemaUpdateError,
3818
4054
  SchemaValidationError: () => SchemaValidationError,
3819
4055
  SessionExpiredError: () => SessionExpiredError,
3820
4056
  SessionNotFoundError: () => SessionNotFoundError,
@@ -3841,6 +4077,7 @@ __export(src_exports, {
3841
4077
  VaultInstant: () => VaultInstant,
3842
4078
  WeakPassphraseError: () => WeakPassphraseError,
3843
4079
  activeSessionCount: () => activeSessionCount,
4080
+ additiveOnly: () => additiveOnly,
3844
4081
  applyI18nLocale: () => applyI18nLocale,
3845
4082
  applyJoins: () => applyJoins,
3846
4083
  applyPatch: () => applyPatch,
@@ -3848,6 +4085,7 @@ __export(src_exports, {
3848
4085
  assertTierAccess: () => assertTierAccess,
3849
4086
  avg: () => avg,
3850
4087
  base64ToBuffer: () => base64ToBuffer,
4088
+ blindUpdate: () => blindUpdate,
3851
4089
  bufferToBase64: () => bufferToBase64,
3852
4090
  buildLiveQuery: () => buildLiveQuery,
3853
4091
  buildRecipientKeyringFile: () => buildRecipientKeyringFile,
@@ -3856,6 +4094,7 @@ __export(src_exports, {
3856
4094
  checkGate: () => checkGate,
3857
4095
  clearDevUnlock: () => clearDevUnlock,
3858
4096
  computePatch: () => computePatch,
4097
+ coordinatedCutover: () => coordinatedCutover,
3859
4098
  count: () => count,
3860
4099
  createBundleStore: () => createBundleStore,
3861
4100
  createEnforcer: () => createEnforcer,
@@ -3934,6 +4173,7 @@ __export(src_exports, {
3934
4173
  loadShamirRecoveryEntries: () => loadShamirRecoveryEntries,
3935
4174
  loadUserEnvelope: () => loadUserEnvelope,
3936
4175
  loadVaultPolicy: () => loadVaultPolicy,
4176
+ lockSchema: () => lockSchema,
3937
4177
  magicLinkGrantRecordId: () => magicLinkGrantRecordId,
3938
4178
  max: () => max,
3939
4179
  mergeCrdtStates: () => mergeCrdtStates,
@@ -5015,6 +5255,127 @@ function createBundleStore(factory) {
5015
5255
  return factory;
5016
5256
  }
5017
5257
 
5258
+ // src/persisted-schemas/canonicalize.ts
5259
+ function canonicalize(value) {
5260
+ if (value === null || typeof value !== "object") {
5261
+ return JSON.stringify(value);
5262
+ }
5263
+ if (Array.isArray(value)) {
5264
+ return "[" + value.map(canonicalize).join(",") + "]";
5265
+ }
5266
+ const obj = value;
5267
+ const keys = Object.keys(obj).sort();
5268
+ const parts = keys.map((k) => JSON.stringify(k) + ":" + canonicalize(obj[k]));
5269
+ return "{" + parts.join(",") + "}";
5270
+ }
5271
+
5272
+ // src/schema-update/delta.ts
5273
+ function computeSchemaDelta(stored, fresh, collection) {
5274
+ const a = stored;
5275
+ const b = fresh;
5276
+ const aProps = a.properties ?? {};
5277
+ const bProps = b.properties ?? {};
5278
+ const aReq = new Set(a.required ?? []);
5279
+ const bReq = new Set(b.required ?? []);
5280
+ const aKeys = Object.keys(aProps);
5281
+ const bKeys = Object.keys(bProps);
5282
+ const added = bKeys.filter((k) => !(k in aProps));
5283
+ const removed = aKeys.filter((k) => !(k in bProps));
5284
+ const changed = [];
5285
+ for (const k of bKeys) {
5286
+ if (!(k in aProps)) continue;
5287
+ const shapeChanged = canonicalize(aProps[k]) !== canonicalize(bProps[k]);
5288
+ const requiredChanged = aReq.has(k) !== bReq.has(k);
5289
+ if (shapeChanged || requiredChanged) {
5290
+ changed.push({ field: k, requiredChanged, shapeChanged });
5291
+ }
5292
+ }
5293
+ let kind;
5294
+ if (added.length === 0 && removed.length === 0 && changed.length === 0) {
5295
+ kind = "none";
5296
+ } else if (removed.length === 0 && changed.length === 0 && added.every((k) => !bReq.has(k))) {
5297
+ kind = "additive";
5298
+ } else {
5299
+ kind = "non-additive";
5300
+ }
5301
+ return { collection, kind, added, removed, changed };
5302
+ }
5303
+
5304
+ // src/schema-update/dispatch.ts
5305
+ async function evaluateStrategies(delta, strategies, ctx) {
5306
+ for (const strategy of strategies) {
5307
+ const decision = await strategy.onSchemaDelta(delta, ctx);
5308
+ if (decision.action !== "allow") return decision;
5309
+ }
5310
+ return { action: "allow" };
5311
+ }
5312
+
5313
+ // src/schema-update/strategies.ts
5314
+ init_errors();
5315
+ function blindUpdate() {
5316
+ return { name: "blindUpdate", onSchemaDelta: () => ({ action: "allow" }) };
5317
+ }
5318
+ function additiveOnly() {
5319
+ return {
5320
+ name: "additiveOnly",
5321
+ onSchemaDelta(delta) {
5322
+ if (delta.kind === "non-additive") {
5323
+ return {
5324
+ action: "reject",
5325
+ error: new NonAdditiveSchemaChangeError(
5326
+ `Non-additive schema change to "${delta.collection}" (added: [${delta.added.join(", ")}], removed: [${delta.removed.join(", ")}], changed: [${delta.changed.map((c) => c.field).join(", ")}]). Register a coordinatedCutover() strategy to migrate, or revert the change.`
5327
+ )
5328
+ };
5329
+ }
5330
+ return { action: "allow" };
5331
+ }
5332
+ };
5333
+ }
5334
+ function lockSchema(opts) {
5335
+ const fields = opts?.fields;
5336
+ return {
5337
+ name: "lockSchema",
5338
+ onSchemaDelta(delta) {
5339
+ if (delta.kind === "none") return { action: "allow" };
5340
+ const touched = fields ? [...delta.added, ...delta.removed, ...delta.changed.map((c) => c.field)].filter((f) => fields.includes(f)) : ["<any>"];
5341
+ if (touched.length === 0) return { action: "allow" };
5342
+ return {
5343
+ action: "reject",
5344
+ error: new SchemaLockedError(
5345
+ `Schema for "${delta.collection}" is locked` + (fields ? ` on fields [${fields.join(", ")}] (touched: [${touched.join(", ")}])` : "") + `; the change was refused.`
5346
+ )
5347
+ };
5348
+ }
5349
+ };
5350
+ }
5351
+
5352
+ // src/schema-update/cutover.ts
5353
+ function coordinatedCutover(opts) {
5354
+ return {
5355
+ name: "coordinatedCutover",
5356
+ onSchemaDelta(delta) {
5357
+ if (delta.kind === "non-additive") {
5358
+ return { action: "cutover", transform: opts.transform };
5359
+ }
5360
+ return { action: "allow" };
5361
+ }
5362
+ };
5363
+ }
5364
+
5365
+ // src/schema-update/gate.ts
5366
+ var SchemaUpdateGate = class {
5367
+ #decision;
5368
+ constructor(decision) {
5369
+ this.#decision = decision.catch(() => null);
5370
+ }
5371
+ async assertWritable() {
5372
+ const decision = await this.#decision;
5373
+ if (decision && decision.action === "reject") {
5374
+ throw decision.error;
5375
+ }
5376
+ }
5377
+ };
5378
+
5018
5379
  // src/store/sync-policy.ts
5019
5380
  var INDEXED_STORE_POLICY = {
5020
5381
  push: { mode: "on-change", minIntervalMs: 0, onUnload: true },
@@ -5589,8 +5950,8 @@ function withRetry(opts = {}) {
5589
5950
  } catch (err) {
5590
5951
  lastError = err;
5591
5952
  if (attempt >= maxRetries || !shouldRetry(err)) throw err;
5592
- const delay = backoffMs * Math.pow(2, attempt) * (1 + Math.random() * jitter);
5593
- await new Promise((r) => setTimeout(r, delay));
5953
+ const delay2 = backoffMs * Math.pow(2, attempt) * (1 + Math.random() * jitter);
5954
+ await new Promise((r) => setTimeout(r, delay2));
5594
5955
  }
5595
5956
  }
5596
5957
  throw lastError;
@@ -5878,7 +6239,9 @@ var ALLOWED_HEADER_KEYS = /* @__PURE__ */ new Set([
5878
6239
  "bodyBytes",
5879
6240
  "bodySha256",
5880
6241
  "publicEnvelope",
5881
- "autoUnlock"
6242
+ "autoUnlock",
6243
+ "bundleKind",
6244
+ "transferSeal"
5882
6245
  ]);
5883
6246
  function validateBundleHeader(parsed) {
5884
6247
  if (parsed === null || typeof parsed !== "object") {
@@ -5941,6 +6304,47 @@ function validateBundleHeader(parsed) {
5941
6304
  );
5942
6305
  }
5943
6306
  }
6307
+ if (h["bundleKind"] !== void 0) {
6308
+ if (h["bundleKind"] !== "snapshot" && h["bundleKind"] !== "extracted-partition") {
6309
+ const got = typeof h["bundleKind"] === "string" ? `"${h["bundleKind"]}"` : typeof h["bundleKind"];
6310
+ throw new Error(
6311
+ `.noydb bundle header.bundleKind must be 'snapshot' or 'extracted-partition' when present, got ${got}.`
6312
+ );
6313
+ }
6314
+ }
6315
+ if (h["transferSeal"] !== void 0) {
6316
+ const ts = h["transferSeal"];
6317
+ if (ts === null || typeof ts !== "object" || Array.isArray(ts)) {
6318
+ throw new Error(`.noydb bundle header.transferSeal must be a JSON object when present, got ${typeof ts}.`);
6319
+ }
6320
+ const t = ts;
6321
+ if (t["v"] !== 1) {
6322
+ throw new Error(`.noydb bundle header.transferSeal.v must be 1, got ${String(t["v"])}.`);
6323
+ }
6324
+ if (t["alg"] !== "aes-256-gcm-pre-shared") {
6325
+ throw new Error(`.noydb bundle header.transferSeal.alg must be 'aes-256-gcm-pre-shared', got ${String(t["alg"])}.`);
6326
+ }
6327
+ if (typeof t["sealId"] !== "string" || t["sealId"].length === 0) {
6328
+ throw new Error(`.noydb bundle header.transferSeal.sealId must be a non-empty string, got ${String(t["sealId"])}.`);
6329
+ }
6330
+ }
6331
+ const isExtracted = h["bundleKind"] === "extracted-partition";
6332
+ const hasSeal = h["transferSeal"] !== void 0;
6333
+ if (hasSeal && !isExtracted) {
6334
+ throw new Error(
6335
+ `.noydb bundle header.transferSeal requires bundleKind === 'extracted-partition'.`
6336
+ );
6337
+ }
6338
+ if (isExtracted && !hasSeal) {
6339
+ throw new Error(
6340
+ `.noydb bundle header with bundleKind === 'extracted-partition' must carry a transferSeal indicator.`
6341
+ );
6342
+ }
6343
+ if (isExtracted && h["autoUnlock"] !== void 0) {
6344
+ throw new Error(
6345
+ `.noydb bundle header cannot carry both autoUnlock and bundleKind === 'extracted-partition' \u2014 an extracted partition is unlocked via its transfer seal, not an auto-credential.`
6346
+ );
6347
+ }
5944
6348
  }
5945
6349
  function encodeBundleHeader(header) {
5946
6350
  validateBundleHeader(header);
@@ -5950,7 +6354,9 @@ function encodeBundleHeader(header) {
5950
6354
  bodyBytes: header.bodyBytes,
5951
6355
  bodySha256: header.bodySha256,
5952
6356
  ...header.publicEnvelope !== void 0 ? { publicEnvelope: header.publicEnvelope } : {},
5953
- ...header.autoUnlock !== void 0 ? { autoUnlock: header.autoUnlock } : {}
6357
+ ...header.autoUnlock !== void 0 ? { autoUnlock: header.autoUnlock } : {},
6358
+ ...header.bundleKind !== void 0 ? { bundleKind: header.bundleKind } : {},
6359
+ ...header.transferSeal !== void 0 ? { transferSeal: header.transferSeal } : {}
5954
6360
  });
5955
6361
  return new TextEncoder().encode(json);
5956
6362
  }
@@ -6012,10 +6418,19 @@ function normalizeAutoUnlock(opts) {
6012
6418
  return { mode: "unsealed", perUser: toAutoCredentials(opts.autoPassphrases.perUser) };
6013
6419
  }
6014
6420
  if (opts.sealedCredentials !== void 0) {
6015
- return { mode: "sealed", provider: opts.sealedCredentials.provider, perUser: opts.sealedCredentials.perUser };
6421
+ if (opts.sealedCredentials.mode === "recipient-target") {
6422
+ const perUser = {};
6423
+ const hints = {};
6424
+ for (const [userId, entry] of Object.entries(opts.sealedCredentials.perUser)) {
6425
+ perUser[userId] = entry.credential;
6426
+ hints[userId] = entry.hint;
6427
+ }
6428
+ return { mode: "sealed-recipient", provider: opts.sealedCredentials.provider, perUser, hints };
6429
+ }
6430
+ return { mode: "sealed-self", provider: opts.sealedCredentials.provider, perUser: opts.sealedCredentials.perUser };
6016
6431
  }
6017
6432
  return {
6018
- mode: "sealed",
6433
+ mode: "sealed-self",
6019
6434
  provider: opts.sealedPassphrases.provider,
6020
6435
  perUser: toAutoCredentials(opts.sealedPassphrases.perUser)
6021
6436
  };
@@ -6045,10 +6460,52 @@ function validateAutoUnlockOptions(opts, normalized) {
6045
6460
  }
6046
6461
  return "unsealed";
6047
6462
  }
6048
- const mode = opts.sealedCredentials?.mode ?? opts.sealedPassphrases?.mode;
6049
- if (mode !== "self-target") {
6463
+ if (normalized.mode === "sealed-recipient") {
6464
+ const provider = normalized.provider;
6465
+ if (provider === void 0 || typeof provider.publishRecipientHint !== "function" || typeof provider.sealForRecipient !== "function") {
6466
+ throw new ValidationError(
6467
+ "writeNoydbBundle: `sealedCredentials.provider` for mode 'recipient-target' must be a RecipientSealer (publishRecipientHint + sealForRecipient). Self-only providers (MemorySealingKeyProvider, at-macos-keychain, etc.) do not satisfy this contract."
6468
+ );
6469
+ }
6470
+ const hints = normalized.hints;
6471
+ if (hints === void 0) {
6472
+ throw new Error("unreachable \u2014 sealed-recipient normalization must populate hints");
6473
+ }
6474
+ for (const userId of Object.keys(normalized.perUser)) {
6475
+ const hint = hints[userId];
6476
+ if (hint === void 0) {
6477
+ throw new ValidationError(
6478
+ `writeNoydbBundle: \`sealedCredentials.perUser['${userId}']\` missing required \`hint\` for mode 'recipient-target'.`
6479
+ );
6480
+ }
6481
+ if (hint.v !== 1) {
6482
+ throw new ValidationError(
6483
+ `writeNoydbBundle: \`sealedCredentials.perUser['${userId}'].hint.v\` must be 1 (got ${String(hint.v)}).`
6484
+ );
6485
+ }
6486
+ if (typeof hint.pid !== "string" || hint.pid.length === 0) {
6487
+ throw new ValidationError(
6488
+ `writeNoydbBundle: \`sealedCredentials.perUser['${userId}'].hint.pid\` must be a non-empty string identifying the recipient.`
6489
+ );
6490
+ }
6491
+ if (hint.alg !== "rsa-oaep-sha256") {
6492
+ throw new ValidationError(
6493
+ `writeNoydbBundle: \`sealedCredentials.perUser['${userId}'].hint.alg\` must be 'rsa-oaep-sha256' in slice 1 (got '${String(hint.alg)}').`
6494
+ );
6495
+ }
6496
+ }
6497
+ const userCount2 = Object.keys(normalized.perUser).length;
6498
+ if (userCount2 === 0) {
6499
+ throw new ValidationError(
6500
+ "writeNoydbBundle: `sealedCredentials.perUser` must have at least one entry."
6501
+ );
6502
+ }
6503
+ return "sealed";
6504
+ }
6505
+ const selfTargetMode = opts.sealedCredentials?.mode ?? opts.sealedPassphrases?.mode;
6506
+ if (selfTargetMode !== "self-target") {
6050
6507
  throw new ValidationError(
6051
- `writeNoydbBundle: \`sealedCredentials.mode\` (or \`sealedPassphrases.mode\`) must be 'self-target' in slice 1 (got '${String(mode)}'). Recipient-target sealing via the RecipientSealer interface is deferred per foundation \xA711.4.`
6508
+ `writeNoydbBundle: \`sealedCredentials.mode\` (or \`sealedPassphrases.mode\`) must be 'self-target' or 'recipient-target' (got '${String(selfTargetMode)}').`
6052
6509
  );
6053
6510
  }
6054
6511
  if (normalized.provider === void 0) {
@@ -6081,14 +6538,35 @@ async function buildAutoUnlockWrapper(dumpJson, normalized) {
6081
6538
  }
6082
6539
  const sealedPerUser = {};
6083
6540
  const encoder = new TextEncoder();
6084
- for (const [userId, cred] of Object.entries(normalized.perUser)) {
6085
- const sealed = await provider.seal(encoder.encode(cred.value));
6086
- sealedPerUser[userId] = {
6087
- pid: provider.id,
6088
- sealed: bytesToBase64(sealed),
6089
- alg: "aes-256-gcm",
6090
- kind: cred.kind
6091
- };
6541
+ if (normalized.mode === "sealed-recipient") {
6542
+ const recipientSealer = provider;
6543
+ const hints = normalized.hints;
6544
+ if (hints === void 0) {
6545
+ throw new Error("unreachable \u2014 sealed-recipient normalization must populate hints");
6546
+ }
6547
+ for (const [userId, cred] of Object.entries(normalized.perUser)) {
6548
+ const hint = hints[userId];
6549
+ const sealed = await recipientSealer.sealForRecipient(encoder.encode(cred.value), hint);
6550
+ sealedPerUser[userId] = {
6551
+ pid: hint.pid,
6552
+ // use the recipient's pid, not the sender's
6553
+ sealed: bytesToBase64(sealed),
6554
+ alg: "aes-256-gcm",
6555
+ kind: cred.kind,
6556
+ hint
6557
+ };
6558
+ }
6559
+ } else {
6560
+ const selfSealer = provider;
6561
+ for (const [userId, cred] of Object.entries(normalized.perUser)) {
6562
+ const sealed = await selfSealer.seal(encoder.encode(cred.value));
6563
+ sealedPerUser[userId] = {
6564
+ pid: selfSealer.id,
6565
+ sealed: bytesToBase64(sealed),
6566
+ alg: "aes-256-gcm",
6567
+ kind: cred.kind
6568
+ };
6569
+ }
6092
6570
  }
6093
6571
  return {
6094
6572
  _noydb_bundle_body: 1,
@@ -6171,11 +6649,19 @@ async function resolveAutoUnlock(blob, opts) {
6171
6649
  }
6172
6650
  }
6173
6651
  if (opened === null) {
6652
+ if (entry.hint !== void 0) {
6653
+ unsealedMap[userId] = { kind: credKind, value: entry.sealed };
6654
+ continue;
6655
+ }
6174
6656
  throw new BundleSealMismatchError(userId, entry.pid);
6175
6657
  }
6176
6658
  unsealedMap[userId] = { kind: credKind, value: opened };
6177
6659
  continue;
6178
6660
  }
6661
+ if (entry.hint !== void 0) {
6662
+ unsealedMap[userId] = { kind: credKind, value: entry.sealed };
6663
+ continue;
6664
+ }
6179
6665
  throw new BundleSealMismatchError(userId, entry.pid);
6180
6666
  }
6181
6667
  const plaintextBytes = await provider.unseal(base64ToBytes(entry.sealed));
@@ -6343,6 +6829,29 @@ async function applyPlaintextFilters(vault, dumpJson, opts) {
6343
6829
  backup.collections = next;
6344
6830
  return JSON.stringify(backup);
6345
6831
  }
6832
+ async function assembleBundleContainer(opts) {
6833
+ const dumpBytes = new TextEncoder().encode(opts.bodyJsonStr);
6834
+ const { format, streamFormat } = selectCompression(opts.compression);
6835
+ const body = streamFormat === null ? dumpBytes : await pumpThroughStream(dumpBytes, new CompressionStream(streamFormat));
6836
+ const bodySha256 = await sha256Hex2(body);
6837
+ const header = {
6838
+ formatVersion: NOYDB_BUNDLE_FORMAT_VERSION,
6839
+ handle: opts.handle,
6840
+ bodyBytes: body.length,
6841
+ bodySha256,
6842
+ ...opts.headerExtras?.publicEnvelope !== void 0 ? { publicEnvelope: opts.headerExtras.publicEnvelope } : {},
6843
+ ...opts.headerExtras?.autoUnlock !== void 0 ? { autoUnlock: opts.headerExtras.autoUnlock } : {},
6844
+ ...opts.headerExtras?.bundleKind !== void 0 ? { bundleKind: opts.headerExtras.bundleKind } : {},
6845
+ ...opts.headerExtras?.transferSeal !== void 0 ? { transferSeal: opts.headerExtras.transferSeal } : {}
6846
+ };
6847
+ const headerBytes = encodeBundleHeader(header);
6848
+ const prefix = new Uint8Array(NOYDB_BUNDLE_PREFIX_BYTES);
6849
+ prefix.set(NOYDB_BUNDLE_MAGIC, 0);
6850
+ prefix[4] = (streamFormat === null ? 0 : FLAG_COMPRESSED) | FLAG_HAS_INTEGRITY_HASH;
6851
+ prefix[5] = format;
6852
+ writeUint32BE(prefix, 6, headerBytes.length);
6853
+ return concatBytes([prefix, headerBytes, body]);
6854
+ }
6346
6855
  async function writeNoydbBundle(vault, opts = {}) {
6347
6856
  if (opts.exportPassphrase !== void 0 && opts.recipients !== void 0) {
6348
6857
  throw new Error(
@@ -6357,26 +6866,16 @@ async function writeNoydbBundle(vault, opts = {}) {
6357
6866
  const plainFiltered = await applyPlaintextFilters(vault, rekeyed, opts);
6358
6867
  const filtered = applySliceFilters(plainFiltered, opts);
6359
6868
  const bodyJsonStr = normalizedAutoUnlock === null ? filtered : JSON.stringify(await buildAutoUnlockWrapper(filtered, normalizedAutoUnlock));
6360
- const dumpBytes = new TextEncoder().encode(bodyJsonStr);
6361
- const { format, streamFormat } = selectCompression(opts.compression);
6362
- const body = streamFormat === null ? dumpBytes : await pumpThroughStream(dumpBytes, new CompressionStream(streamFormat));
6363
- const bodySha256 = await sha256Hex2(body);
6364
6869
  const publicEnvelope = await vault.getPublicEnvelope();
6365
- const header = {
6366
- formatVersion: NOYDB_BUNDLE_FORMAT_VERSION,
6870
+ return assembleBundleContainer({
6367
6871
  handle,
6368
- bodyBytes: body.length,
6369
- bodySha256,
6370
- ...publicEnvelope !== void 0 ? { publicEnvelope } : {},
6371
- ...autoUnlockMode !== null ? { autoUnlock: autoUnlockMode } : {}
6372
- };
6373
- const headerBytes = encodeBundleHeader(header);
6374
- const prefix = new Uint8Array(NOYDB_BUNDLE_PREFIX_BYTES);
6375
- prefix.set(NOYDB_BUNDLE_MAGIC, 0);
6376
- prefix[4] = (streamFormat === null ? 0 : FLAG_COMPRESSED) | FLAG_HAS_INTEGRITY_HASH;
6377
- prefix[5] = format;
6378
- writeUint32BE(prefix, 6, headerBytes.length);
6379
- return concatBytes([prefix, headerBytes, body]);
6872
+ bodyJsonStr,
6873
+ compression: opts.compression,
6874
+ headerExtras: {
6875
+ ...publicEnvelope !== void 0 ? { publicEnvelope } : {},
6876
+ ...autoUnlockMode !== null ? { autoUnlock: autoUnlockMode } : {}
6877
+ }
6878
+ });
6380
6879
  }
6381
6880
  function parsePrefixAndHeader(bytes) {
6382
6881
  if (!hasNoydbBundleMagic(bytes)) {
@@ -6499,20 +6998,6 @@ function formatPath(path) {
6499
6998
  ).join(".");
6500
6999
  }
6501
7000
 
6502
- // src/persisted-schemas/canonicalize.ts
6503
- function canonicalize(value) {
6504
- if (value === null || typeof value !== "object") {
6505
- return JSON.stringify(value);
6506
- }
6507
- if (Array.isArray(value)) {
6508
- return "[" + value.map(canonicalize).join(",") + "]";
6509
- }
6510
- const obj = value;
6511
- const keys = Object.keys(obj).sort();
6512
- const parts = keys.map((k) => JSON.stringify(k) + ":" + canonicalize(obj[k]));
6513
- return "{" + parts.join(",") + "}";
6514
- }
6515
-
6516
7001
  // src/persisted-schemas/derive.ts
6517
7002
  init_crypto();
6518
7003
  function isZodSchema(value) {
@@ -6593,10 +7078,22 @@ async function persistSchemaIfNeeded(opts) {
6593
7078
  const fresh = await derivePersistedSchema(opts.validator);
6594
7079
  const stored = await loadPersistedSchema(opts.store, opts.vault, opts.collectionName, opts.dek);
6595
7080
  if (stored && isEquivalent(stored, fresh)) {
6596
- return { written: false, skipped: true, envelope: stored };
7081
+ return { written: false, skipped: true, envelope: stored, decision: { action: "allow" } };
7082
+ }
7083
+ let decision = { action: "allow" };
7084
+ const strategies = opts.strategies ?? [];
7085
+ if (stored && strategies.length > 0 && stored.kind === fresh.kind && isPlainObject(stored.jsonSchema) && isPlainObject(fresh.jsonSchema)) {
7086
+ const delta = computeSchemaDelta(stored.jsonSchema, fresh.jsonSchema, opts.collectionName);
7087
+ decision = await evaluateStrategies(delta, strategies, { collection: opts.collectionName });
7088
+ }
7089
+ if (decision.action !== "allow") {
7090
+ return { written: false, skipped: false, envelope: stored ?? fresh, decision };
6597
7091
  }
6598
7092
  await savePersistedSchema(opts.store, opts.vault, opts.collectionName, opts.dek, fresh);
6599
- return { written: true, skipped: false, envelope: fresh };
7093
+ return { written: true, skipped: false, envelope: fresh, decision };
7094
+ }
7095
+ function isPlainObject(v) {
7096
+ return typeof v === "object" && v !== null && !Array.isArray(v);
6600
7097
  }
6601
7098
  function isEquivalent(a, b) {
6602
7099
  if (a.kind !== b.kind) return false;
@@ -6737,8 +7234,8 @@ var CollectionInstant = class {
6737
7234
  for (const e of entries) {
6738
7235
  if (e.collection !== this.name || e.id !== id) continue;
6739
7236
  if (e.ts > this.targetTs) break;
6740
- if (e.op === "amendment") continue;
6741
- latest = { op: e.op, version: e.version };
7237
+ if (e.op === "amendment" || e.op === "lifecycle") continue;
7238
+ latest = { op: e.op === "migration" ? "put" : e.op, version: e.version };
6742
7239
  }
6743
7240
  if (!latest) return null;
6744
7241
  if (latest.op === "delete") return null;
@@ -8691,7 +9188,7 @@ var UserApi = class {
8691
9188
  }
8692
9189
  };
8693
9190
  function deepMerge(source, patch) {
8694
- if (!isPlainObject(source) || !isPlainObject(patch)) {
9191
+ if (!isPlainObject2(source) || !isPlainObject2(patch)) {
8695
9192
  return patch;
8696
9193
  }
8697
9194
  const out = { ...source };
@@ -8704,8 +9201,8 @@ function deepMerge(source, patch) {
8704
9201
  continue;
8705
9202
  }
8706
9203
  const sourceVal = source[key];
8707
- if (isPlainObject(patchVal)) {
8708
- const recurseSource = isPlainObject(sourceVal) ? sourceVal : {};
9204
+ if (isPlainObject2(patchVal)) {
9205
+ const recurseSource = isPlainObject2(sourceVal) ? sourceVal : {};
8709
9206
  out[key] = deepMerge(recurseSource, patchVal);
8710
9207
  } else {
8711
9208
  out[key] = patchVal;
@@ -8713,7 +9210,7 @@ function deepMerge(source, patch) {
8713
9210
  }
8714
9211
  return out;
8715
9212
  }
8716
- function isPlainObject(x) {
9213
+ function isPlainObject2(x) {
8717
9214
  if (x === null || typeof x !== "object") return false;
8718
9215
  if (Array.isArray(x)) return false;
8719
9216
  const proto = Object.getPrototypeOf(x);
@@ -8926,6 +9423,81 @@ var MemorySealingKeyProvider = class {
8926
9423
  return out;
8927
9424
  }
8928
9425
  };
9426
+ var MemoryRecipientSealer = class {
9427
+ id;
9428
+ keypair;
9429
+ constructor(opts) {
9430
+ this.id = opts.id;
9431
+ this.keypair = crypto.subtle.generateKey(
9432
+ { name: "RSA-OAEP", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" },
9433
+ true,
9434
+ ["encrypt", "decrypt"]
9435
+ );
9436
+ }
9437
+ async publishRecipientHint() {
9438
+ const { publicKey } = await this.keypair;
9439
+ const spki = await crypto.subtle.exportKey("spki", publicKey);
9440
+ const pem = "-----BEGIN PUBLIC KEY-----\n" + bytesToBase644(new Uint8Array(spki)).match(/.{1,64}/g).join("\n") + "\n-----END PUBLIC KEY-----\n";
9441
+ return { v: 1, pid: this.id, alg: "rsa-oaep-sha256", material: { publicKeyPem: pem } };
9442
+ }
9443
+ async sealForRecipient(plaintext, hint) {
9444
+ if (hint.v !== 1) {
9445
+ throw new Error(`MemoryRecipientSealer.sealForRecipient: unsupported hint.v ${String(hint.v)} (expected 1)`);
9446
+ }
9447
+ if (hint.alg !== "rsa-oaep-sha256") {
9448
+ throw new Error(`MemoryRecipientSealer.sealForRecipient: unsupported hint.alg '${String(hint.alg)}' (expected 'rsa-oaep-sha256')`);
9449
+ }
9450
+ const pem = hint.material["publicKeyPem"];
9451
+ if (typeof pem !== "string") {
9452
+ throw new Error("MemoryRecipientSealer.sealForRecipient: hint.material.publicKeyPem missing or not a string");
9453
+ }
9454
+ const b64 = pem.replace(/-----BEGIN PUBLIC KEY-----/, "").replace(/-----END PUBLIC KEY-----/, "").replace(/\s+/g, "");
9455
+ const spki = base64ToBytes3(b64);
9456
+ const recipientPub = await crypto.subtle.importKey(
9457
+ "spki",
9458
+ spki,
9459
+ { name: "RSA-OAEP", hash: "SHA-256" },
9460
+ false,
9461
+ ["encrypt"]
9462
+ );
9463
+ const cekBytes = crypto.getRandomValues(new Uint8Array(32));
9464
+ const cek = await crypto.subtle.importKey("raw", cekBytes, "AES-GCM", false, ["encrypt"]);
9465
+ const iv = crypto.getRandomValues(new Uint8Array(12));
9466
+ const ct = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv }, cek, plaintext));
9467
+ const wrapped = new Uint8Array(await crypto.subtle.encrypt({ name: "RSA-OAEP" }, recipientPub, cekBytes));
9468
+ cekBytes.fill(0);
9469
+ if (wrapped.length !== 256) {
9470
+ throw new Error(`MemoryRecipientSealer.sealForRecipient: expected 256-byte RSA-OAEP wrap, got ${wrapped.length}`);
9471
+ }
9472
+ const out = new Uint8Array(1 + 256 + 12 + ct.length);
9473
+ out[0] = 1;
9474
+ out.set(wrapped, 1);
9475
+ out.set(iv, 1 + 256);
9476
+ out.set(ct, 1 + 256 + 12);
9477
+ return out;
9478
+ }
9479
+ async seal(plaintext) {
9480
+ const hint = await this.publishRecipientHint();
9481
+ return this.sealForRecipient(plaintext, hint);
9482
+ }
9483
+ async unseal(bytes) {
9484
+ if (bytes.length < 1 + 256 + 12 + 16) {
9485
+ throw new Error("MemoryRecipientSealer.unseal: sealed input too short");
9486
+ }
9487
+ if (bytes[0] !== 1) {
9488
+ throw new Error(`MemoryRecipientSealer.unseal: unknown TLV version ${bytes[0]}`);
9489
+ }
9490
+ const wrapped = bytes.subarray(1, 1 + 256);
9491
+ const iv = bytes.subarray(1 + 256, 1 + 256 + 12);
9492
+ const ct = bytes.subarray(1 + 256 + 12);
9493
+ const { privateKey } = await this.keypair;
9494
+ const cekBytes = new Uint8Array(await crypto.subtle.decrypt({ name: "RSA-OAEP" }, privateKey, wrapped));
9495
+ const cek = await crypto.subtle.importKey("raw", cekBytes, "AES-GCM", false, ["decrypt"]);
9496
+ const pt = new Uint8Array(await crypto.subtle.decrypt({ name: "AES-GCM", iv }, cek, ct));
9497
+ cekBytes.fill(0);
9498
+ return pt;
9499
+ }
9500
+ };
8929
9501
  var SEALED_PASSPHRASE_RECORD_ID = "sealed-passphrase";
8930
9502
  function bytesToBase644(bytes) {
8931
9503
  let binary = "";
@@ -11121,7 +11693,10 @@ var NO_BLOBS = {
11121
11693
 
11122
11694
  // src/tx/transaction.ts
11123
11695
  init_errors();
11696
+ init_ulid();
11124
11697
  var TxContext = class {
11698
+ /** Stable id for this transaction; shared by all writes it performs (#230). */
11699
+ txId = generateULID();
11125
11700
  /** @internal */
11126
11701
  _ops = [];
11127
11702
  /**
@@ -11486,6 +12061,11 @@ var Collection = class {
11486
12061
  keyring;
11487
12062
  encrypted;
11488
12063
  emitter;
12064
+ writeQueue;
12065
+ schemaUpdateGate;
12066
+ schemaFence;
12067
+ writeHooks;
12068
+ activeTxId;
11489
12069
  getDEK;
11490
12070
  onDirty;
11491
12071
  historyConfig;
@@ -11760,6 +12340,11 @@ var Collection = class {
11760
12340
  this.keyring = opts.keyring;
11761
12341
  this.encrypted = opts.encrypted;
11762
12342
  this.emitter = opts.emitter;
12343
+ this.writeQueue = opts.writeQueue;
12344
+ this.schemaUpdateGate = opts.schemaUpdateGate;
12345
+ this.schemaFence = opts.schemaFence;
12346
+ this.writeHooks = opts.writeHooks;
12347
+ this.activeTxId = opts.activeTxId;
11763
12348
  this.blobStrategy = opts.blobStrategy ?? NO_BLOBS;
11764
12349
  this.aggregateStrategy = opts.aggregateStrategy ?? NO_AGGREGATE;
11765
12350
  this.crdtStrategy = opts.crdtStrategy ?? NO_CRDT;
@@ -11985,7 +12570,8 @@ var Collection = class {
11985
12570
  return this.syncStrategy.buildPresence(presenceOpts);
11986
12571
  }
11987
12572
  /**
11988
- * Create or update a record.
12573
+ * Create or update a record. Runs inside the hub's write-queue tracker
12574
+ * (#227) so `hub.writeQueue.pending` reflects this write.
11989
12575
  *
11990
12576
  * @param id Record identifier.
11991
12577
  * @param record The record body (validated by the collection's schema
@@ -11996,6 +12582,59 @@ var Collection = class {
11996
12582
  * `entries.filter(e => e.reason?.startsWith('import:'))`.
11997
12583
  */
11998
12584
  async put(id, record, options) {
12585
+ await this.schemaUpdateGate?.assertWritable();
12586
+ await this.schemaFence?.assertWritable(this.name);
12587
+ let event;
12588
+ if (this.#hooksActive()) {
12589
+ const prior = await this.#priorForHook(id);
12590
+ event = {
12591
+ op: prior.record === null ? "create" : "update",
12592
+ vault: this.vault,
12593
+ collection: this.name,
12594
+ docId: id,
12595
+ before: prior.record,
12596
+ after: record,
12597
+ userId: this.keyring.userId,
12598
+ timestamp: Date.now(),
12599
+ txId: this.#txIdForHook(),
12600
+ baseVersion: prior.version,
12601
+ version: prior.version + 1
12602
+ };
12603
+ await this.writeHooks.runBefore(event);
12604
+ }
12605
+ if (this.writeQueue) await this.writeQueue.track(() => this.putInternal(id, record, options));
12606
+ else await this.putInternal(id, record, options);
12607
+ if (event) await this.writeHooks.runAfter(event);
12608
+ }
12609
+ /** @internal #230 — true when hooks should fire for this write (handlers exist, not re-entrant). */
12610
+ #hooksActive() {
12611
+ return this.writeHooks !== void 0 && this.writeHooks.hasHandlers && !this.writeHooks.suppressed;
12612
+ }
12613
+ /**
12614
+ * @internal #230/#228c — resolve the prior record for a hook's `before` and
12615
+ * its version. Critically, this uses the SAME basis `putInternal` writes from
12616
+ * (the in-memory cache in eager mode; lru-then-adapter in lazy) — NOT a fresh
12617
+ * store read — so `baseVersion`/`version` match the version actually written.
12618
+ * A separate store read would diverge once another tab has advanced the shared
12619
+ * store past this tab's cache, breaking #228c conflict detection.
12620
+ */
12621
+ async #priorForHook(id) {
12622
+ if (this.lazy && this.lru) {
12623
+ const cached2 = this.lru.get(id);
12624
+ if (cached2) return { record: cached2.record, version: cached2.version };
12625
+ const env = await this.adapter.get(this.vault, this.name, id);
12626
+ if (!env) return { record: null, version: 0 };
12627
+ return { record: await this.decryptRecord(env, { skipValidation: true }), version: env._v };
12628
+ }
12629
+ await this.ensureHydrated();
12630
+ const cached = this.cache.get(id);
12631
+ return cached ? { record: cached.record, version: cached.version } : { record: null, version: 0 };
12632
+ }
12633
+ #txIdForHook() {
12634
+ return this.activeTxId?.() ?? generateULID();
12635
+ }
12636
+ /** @internal Untracked put body — call {@link put}, not this. */
12637
+ async putInternal(id, record, options) {
11999
12638
  if (!hasWritePermission(this.keyring, this.name)) {
12000
12639
  throw new ReadOnlyError();
12001
12640
  }
@@ -12372,8 +13011,71 @@ var Collection = class {
12372
13011
  }
12373
13012
  }
12374
13013
  }
12375
- /** Delete a record by ID. */
12376
- async delete(id) {
13014
+ /**
13015
+ * Delete a record by ID. Runs inside the hub's write-queue tracker
13016
+ * (#227) so `hub.writeQueue.pending` reflects this write.
13017
+ */
13018
+ async delete(id) {
13019
+ await this.schemaUpdateGate?.assertWritable();
13020
+ await this.schemaFence?.assertWritable(this.name);
13021
+ let event;
13022
+ if (this.#hooksActive()) {
13023
+ const prior = await this.#priorForHook(id);
13024
+ event = {
13025
+ op: "delete",
13026
+ vault: this.vault,
13027
+ collection: this.name,
13028
+ docId: id,
13029
+ before: prior.record,
13030
+ after: null,
13031
+ userId: this.keyring.userId,
13032
+ timestamp: Date.now(),
13033
+ txId: this.#txIdForHook(),
13034
+ baseVersion: prior.version,
13035
+ version: prior.version + 1
13036
+ };
13037
+ await this.writeHooks.runBefore(event);
13038
+ }
13039
+ if (this.writeQueue) await this.writeQueue.track(() => this.deleteInternal(id));
13040
+ else await this.deleteInternal(id);
13041
+ if (event) await this.writeHooks.runAfter(event);
13042
+ }
13043
+ /**
13044
+ * @internal #232 — bulk-rewrite every record through a cutover transform.
13045
+ * Raw adapter path (bypasses the write gate + guards — the transform is
13046
+ * trusted and runs only during the `migrating` phase). Bumps each
13047
+ * record's `_v` and appends a ledger `op:'migration'` entry.
13048
+ */
13049
+ async _applyCutoverTransform(transform) {
13050
+ const ids = await this.adapter.list(this.vault, this.name);
13051
+ let count2 = 0;
13052
+ for (const id of ids) {
13053
+ const env = await this.adapter.get(this.vault, this.name, id);
13054
+ if (!env) continue;
13055
+ const record = await this.decryptRecord(env, { skipValidation: true });
13056
+ const next = transform(record);
13057
+ const nextVersion = (env._v ?? 0) + 1;
13058
+ const newEnv = await this.encryptRecord(next, nextVersion);
13059
+ await this.adapter.put(this.vault, this.name, id, newEnv);
13060
+ await this._invalidateCacheEntry(id);
13061
+ if (this.ledger) {
13062
+ await this.ledger.append({
13063
+ op: "migration",
13064
+ collection: this.name,
13065
+ id,
13066
+ version: nextVersion,
13067
+ actor: this.keyring.userId,
13068
+ payloadHash: "",
13069
+ reason: "schema:coordinated-cutover"
13070
+ }).catch(() => {
13071
+ });
13072
+ }
13073
+ count2++;
13074
+ }
13075
+ return count2;
13076
+ }
13077
+ /** @internal Untracked delete body — call {@link delete}, not this. */
13078
+ async deleteInternal(id) {
12377
13079
  await this._doDelete(id, false);
12378
13080
  }
12379
13081
  /**
@@ -13196,6 +13898,21 @@ var Collection = class {
13196
13898
  this.cache.set(id, { record, version: envelope._v });
13197
13899
  this.indexes?.upsert(id, record, previous ? previous.record : null);
13198
13900
  }
13901
+ /**
13902
+ * #228b — apply a peer tab's committed write to THIS tab's in-memory view:
13903
+ * re-read the (already-persisted) envelope from the shared store + refresh
13904
+ * cache/indexes, then emit a `change` event so reactive consumers re-render.
13905
+ * Never writes to the store and never fires write hooks, so it cannot loop.
13906
+ */
13907
+ async _applyRemoteChange(id, action) {
13908
+ await this._invalidateCacheEntry(id);
13909
+ this.emitter.emit("change", { vault: this.vault, collection: this.name, id, action });
13910
+ }
13911
+ /** @internal #228c — the current in-memory record without a store read (for conflict capture). */
13912
+ _peekCached(id) {
13913
+ const entry = this.lazy && this.lru ? this.lru.get(id) : this.cache.get(id);
13914
+ return entry ? entry.record : null;
13915
+ }
13199
13916
  async ensureHydrated() {
13200
13917
  if (this.hydrated) return;
13201
13918
  const ids = await this.adapter.list(this.vault, this.name);
@@ -15025,6 +15742,245 @@ function isMagicLinkGrantExpired(payload, now = /* @__PURE__ */ new Date()) {
15025
15742
  return payload.until <= now.toISOString();
15026
15743
  }
15027
15744
 
15745
+ // src/schema-update/fence.ts
15746
+ init_types();
15747
+ var FENCE_RECORD_ID = "schema-fence";
15748
+ var META_COLLECTION3 = "_meta";
15749
+ var DEFAULT_FENCE = { currentSchemaVersion: 0, fenceState: "normal" };
15750
+ async function loadFence(store, vault) {
15751
+ const envelope = await store.get(vault, META_COLLECTION3, FENCE_RECORD_ID);
15752
+ if (!envelope) return DEFAULT_FENCE;
15753
+ try {
15754
+ const parsed = JSON.parse(envelope._data);
15755
+ if (!isFenceDoc(parsed)) return DEFAULT_FENCE;
15756
+ return parsed;
15757
+ } catch {
15758
+ return DEFAULT_FENCE;
15759
+ }
15760
+ }
15761
+ async function saveFence(store, vault, fence) {
15762
+ const envelope = {
15763
+ _noydb: NOYDB_FORMAT_VERSION,
15764
+ _v: 1,
15765
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
15766
+ _iv: "",
15767
+ _data: JSON.stringify(fence)
15768
+ };
15769
+ await store.put(vault, META_COLLECTION3, FENCE_RECORD_ID, envelope);
15770
+ }
15771
+ function isFenceDoc(x) {
15772
+ if (x === null || typeof x !== "object") return false;
15773
+ const o = x;
15774
+ return typeof o["currentSchemaVersion"] === "number" && (o["fenceState"] === "normal" || o["fenceState"] === "draining" || o["fenceState"] === "migrating" || o["fenceState"] === "complete");
15775
+ }
15776
+
15777
+ // src/schema-update/fence-controller.ts
15778
+ init_errors();
15779
+
15780
+ // src/schema-update/client-registry.ts
15781
+ init_types();
15782
+ var META_COLLECTION4 = "_meta";
15783
+ var CLIENT_PREFIX = "schema-fence:client:";
15784
+ async function writeClientDoc(store, vault, clientId, doc) {
15785
+ const envelope = {
15786
+ _noydb: NOYDB_FORMAT_VERSION,
15787
+ _v: 1,
15788
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
15789
+ _iv: "",
15790
+ _data: JSON.stringify({ clientId, ...doc })
15791
+ };
15792
+ await store.put(vault, META_COLLECTION4, `${CLIENT_PREFIX}${clientId}`, envelope);
15793
+ }
15794
+ async function listClientDocs(store, vault) {
15795
+ const ids = await store.list(vault, META_COLLECTION4);
15796
+ const out = [];
15797
+ for (const id of ids) {
15798
+ if (!id.startsWith(CLIENT_PREFIX)) continue;
15799
+ const env = await store.get(vault, META_COLLECTION4, id);
15800
+ if (!env) continue;
15801
+ try {
15802
+ const parsed = JSON.parse(env._data);
15803
+ if (isClientDoc(parsed)) out.push(parsed);
15804
+ } catch {
15805
+ }
15806
+ }
15807
+ return out;
15808
+ }
15809
+ async function activeQuiesced(store, vault, opts) {
15810
+ const docs = await listClientDocs(store, vault);
15811
+ const active = docs.filter(
15812
+ (d) => d.lastSeen >= opts.now - opts.staleMs && d.clientId !== opts.excludeClientId
15813
+ );
15814
+ return active.every((d) => d.quiescedAtVersion === opts.generation);
15815
+ }
15816
+ function isClientDoc(x) {
15817
+ if (x === null || typeof x !== "object") return false;
15818
+ const o = x;
15819
+ return typeof o["clientId"] === "string" && typeof o["lastSeen"] === "number" && (o["quiescedAtVersion"] === null || typeof o["quiescedAtVersion"] === "number");
15820
+ }
15821
+
15822
+ // src/schema-update/fence-controller.ts
15823
+ var SchemaFenceController = class {
15824
+ #store;
15825
+ #vault;
15826
+ #onFlush;
15827
+ #clientId;
15828
+ #now;
15829
+ #staleMs;
15830
+ #quiesceTimeoutMs;
15831
+ #emit;
15832
+ #snapshot = 0;
15833
+ #pending = /* @__PURE__ */ new Map();
15834
+ constructor(opts) {
15835
+ this.#store = opts.store;
15836
+ this.#vault = opts.vault;
15837
+ this.#onFlush = opts.onFlush;
15838
+ this.#clientId = opts.clientId ?? "migrator";
15839
+ this.#now = opts.now ?? (() => Date.now());
15840
+ this.#staleMs = opts.staleMs ?? 3e4;
15841
+ this.#quiesceTimeoutMs = opts.quiesceTimeoutMs ?? 6e4;
15842
+ this.#emit = opts.emit ?? (() => {
15843
+ });
15844
+ }
15845
+ /** Capture the generation snapshot at vault-open. */
15846
+ async init() {
15847
+ this.#snapshot = (await loadFence(this.#store, this.#vault)).currentSchemaVersion;
15848
+ }
15849
+ /** Record a per-collection pending cutover (from a registration `cutover` decision). */
15850
+ registerPendingCutover(collection, transform) {
15851
+ this.#pending.set(collection, transform);
15852
+ }
15853
+ /** Write-path gate. Throws when behind, fenced, or this collection is cutover-pending. */
15854
+ async assertWritable(collection) {
15855
+ const fence = await loadFence(this.#store, this.#vault);
15856
+ if (fence.currentSchemaVersion > this.#snapshot) {
15857
+ throw new MigrationRequiredError(
15858
+ `Vault "${this.#vault}" advanced to schema generation ${fence.currentSchemaVersion} (this client opened at ${this.#snapshot}). Reload to continue.`
15859
+ );
15860
+ }
15861
+ if (fence.fenceState === "draining" || fence.fenceState === "migrating") {
15862
+ throw new SchemaFenceError(`Vault "${this.#vault}" is mid-cutover (${fence.fenceState}); writes are paused.`);
15863
+ }
15864
+ if (this.#pending.has(collection)) {
15865
+ throw new SchemaFenceError(
15866
+ `Collection "${collection}" has a pending schema cutover; run vault.runSchemaCutover() before writing.`
15867
+ );
15868
+ }
15869
+ }
15870
+ /**
15871
+ * Admin trigger. Drain → wait for the active set to quiesce (or time out)
15872
+ * → migrate each pending transform → bump → complete → normal. The
15873
+ * migrator excludes itself from the barrier (it drained synchronously
15874
+ * here). `onPoll` (tests) advances other clients between barrier checks;
15875
+ * production falls back to a short real delay.
15876
+ */
15877
+ async runCutover(run, opts) {
15878
+ if (this.#pending.size === 0) return { migrated: 0 };
15879
+ const base = await loadFence(this.#store, this.#vault);
15880
+ const generation = base.currentSchemaVersion;
15881
+ await this.#setState(generation, "draining");
15882
+ await this.#onFlush();
15883
+ const deadline = this.#now() + this.#quiesceTimeoutMs;
15884
+ while (!await activeQuiesced(this.#store, this.#vault, {
15885
+ generation,
15886
+ now: this.#now(),
15887
+ staleMs: this.#staleMs,
15888
+ excludeClientId: this.#clientId
15889
+ })) {
15890
+ if (this.#now() >= deadline) {
15891
+ throw new QuiesceTimeoutError(
15892
+ `Cutover on "${this.#vault}" timed out waiting for clients to quiesce at generation ${generation}.`
15893
+ );
15894
+ }
15895
+ await (opts?.onPoll ? opts.onPoll() : delay(50));
15896
+ }
15897
+ await this.#setState(generation, "migrating");
15898
+ let migrated = 0;
15899
+ for (const [collection, transform] of this.#pending) {
15900
+ await run(collection, transform);
15901
+ migrated++;
15902
+ }
15903
+ const nextVersion = generation + 1;
15904
+ await this.#setState(nextVersion, "complete");
15905
+ this.#pending.clear();
15906
+ await this.#setState(nextVersion, "normal");
15907
+ this.#snapshot = nextVersion;
15908
+ return { migrated };
15909
+ }
15910
+ /** Recover a stuck drain: reset fenceState to normal at the current version (no bump). */
15911
+ async abort() {
15912
+ const fence = await loadFence(this.#store, this.#vault);
15913
+ await this.#setState(fence.currentSchemaVersion, "normal");
15914
+ }
15915
+ async #setState(currentSchemaVersion, fenceState) {
15916
+ await saveFence(this.#store, this.#vault, { currentSchemaVersion, fenceState });
15917
+ this.#emit({ currentSchemaVersion, fenceState });
15918
+ }
15919
+ };
15920
+ function delay(ms) {
15921
+ return new Promise((resolve) => setTimeout(resolve, ms));
15922
+ }
15923
+
15924
+ // src/schema-update/fence-watcher.ts
15925
+ var FenceWatcher = class {
15926
+ #store;
15927
+ #vault;
15928
+ #clientId;
15929
+ #onFlush;
15930
+ #now;
15931
+ #emit;
15932
+ #lastState = null;
15933
+ #quiescedAtVersion = null;
15934
+ #timer;
15935
+ constructor(opts) {
15936
+ this.#store = opts.store;
15937
+ this.#vault = opts.vault;
15938
+ this.#clientId = opts.clientId;
15939
+ this.#onFlush = opts.onFlush;
15940
+ this.#now = opts.now ?? (() => Date.now());
15941
+ this.#emit = opts.emit ?? (() => {
15942
+ });
15943
+ }
15944
+ /** Publish liveness (and the current ack) without changing quiesce state. */
15945
+ async beat() {
15946
+ await writeClientDoc(this.#store, this.#vault, this.#clientId, {
15947
+ lastSeen: this.#now(),
15948
+ quiescedAtVersion: this.#quiescedAtVersion
15949
+ });
15950
+ }
15951
+ /** Poll the fence; quiesce on draining; emit on transitions. */
15952
+ async check() {
15953
+ const fence = await loadFence(this.#store, this.#vault);
15954
+ if (fence.fenceState !== this.#lastState) {
15955
+ this.#lastState = fence.fenceState;
15956
+ this.#emit({ currentSchemaVersion: fence.currentSchemaVersion, fenceState: fence.fenceState });
15957
+ }
15958
+ if (fence.fenceState === "draining" && this.#quiescedAtVersion !== fence.currentSchemaVersion) {
15959
+ await this.#onFlush();
15960
+ this.#quiescedAtVersion = fence.currentSchemaVersion;
15961
+ await this.beat();
15962
+ }
15963
+ if (fence.fenceState === "normal") {
15964
+ this.#quiescedAtVersion = null;
15965
+ }
15966
+ }
15967
+ start(intervalMs) {
15968
+ if (this.#timer) return;
15969
+ this.#timer = setInterval(() => {
15970
+ void this.beat();
15971
+ void this.check();
15972
+ }, intervalMs);
15973
+ const timer = this.#timer;
15974
+ if (typeof timer.unref === "function") timer.unref();
15975
+ }
15976
+ stop() {
15977
+ if (this.#timer) {
15978
+ clearInterval(this.#timer);
15979
+ this.#timer = void 0;
15980
+ }
15981
+ }
15982
+ };
15983
+
15028
15984
  // src/introspection/fields.ts
15029
15985
  function jsonSchemaType(node) {
15030
15986
  if (Array.isArray(node.type)) {
@@ -15371,12 +16327,25 @@ var Vault = class {
15371
16327
  */
15372
16328
  reloadKeyring;
15373
16329
  collectionCache = /* @__PURE__ */ new Map();
16330
+ /** #232 — vault-level schema cutover fence/controller. */
16331
+ schemaFence;
16332
+ /** #232 — per-client heartbeat/watcher; started lazily on cutover registration. */
16333
+ #fenceWatcher;
16334
+ #fenceCoordinationStarted = false;
16335
+ /** #229 — per-collection registered schema-update strategy names. */
16336
+ #schemaUpdateNames = /* @__PURE__ */ new Map();
15374
16337
  /**
15375
16338
  * per-collection `blobFields` retention/TTL config.
15376
16339
  * Populated on `collection({ blobFields })` and read by
15377
16340
  * `vault.compact()`. Indexed by collection name.
15378
16341
  */
15379
16342
  blobFieldsRegistry = /* @__PURE__ */ new Map();
16343
+ /**
16344
+ * Per-collection attestation field-schema (issue side). Populated on
16345
+ * `collection({ attestation })` and read by `issueAttestation()`.
16346
+ * Indexed by collection name.
16347
+ */
16348
+ attestationRegistry = /* @__PURE__ */ new Map();
15380
16349
  /**
15381
16350
  * Per-vault ledger store. Lazy-initialized on first
15382
16351
  * `collection()` call (which passes it through to the Collection)
@@ -15480,6 +16449,13 @@ var Vault = class {
15480
16449
  this.noydb = opts.noydb;
15481
16450
  this.keyring = opts.keyring;
15482
16451
  this.encrypted = opts.encrypted;
16452
+ this.schemaFence = new SchemaFenceController({
16453
+ store: this.adapter,
16454
+ vault: this.name,
16455
+ onFlush: () => this.noydb._writeQueueTracker.onFlush(),
16456
+ clientId: this.noydb._clientId,
16457
+ emit: (e) => this.emitter.emit("schema:fence-changed", { vault: this.name, ...e })
16458
+ });
15483
16459
  this.emitter = opts.emitter;
15484
16460
  this.onDirty = opts.onDirty;
15485
16461
  this.onRegisterConflictResolver = opts.onRegisterConflictResolver;
@@ -15578,6 +16554,9 @@ var Vault = class {
15578
16554
  if (options?.blobFields) {
15579
16555
  this.blobFieldsRegistry.set(collectionName, options.blobFields);
15580
16556
  }
16557
+ if (options?.attestation !== void 0) {
16558
+ this.attestationRegistry.set(collectionName, options.attestation);
16559
+ }
15581
16560
  if (options?.dictKeyFields) {
15582
16561
  const dictFieldMap = {};
15583
16562
  for (const [field, desc] of Object.entries(options.dictKeyFields)) {
@@ -15585,6 +16564,35 @@ var Vault = class {
15585
16564
  }
15586
16565
  this.dictKeyFieldRegistry.set(collectionName, dictFieldMap);
15587
16566
  }
16567
+ if ((options?.schemaUpdate?.length ?? 0) > 0) {
16568
+ this.#schemaUpdateNames.set(collectionName, (options.schemaUpdate ?? []).map((s) => s.name));
16569
+ }
16570
+ let schemaUpdateGate;
16571
+ if (options?.persistJsonSchema === true && options.schema !== void 0 && (options.schemaUpdate?.length ?? 0) > 0) {
16572
+ const validator = options.schema;
16573
+ const strategies = options.schemaUpdate ?? [];
16574
+ const work = (async () => {
16575
+ const dek = await this.getDEK(collectionName);
16576
+ const result = await persistSchemaIfNeeded({
16577
+ store: this.adapter,
16578
+ vault: this.name,
16579
+ collectionName,
16580
+ validator,
16581
+ dek,
16582
+ strategies
16583
+ });
16584
+ const decision = result.decision ?? { action: "allow" };
16585
+ if (decision.action === "cutover") {
16586
+ this.schemaFence.registerPendingCutover(collectionName, decision.transform);
16587
+ this._ensureFenceCoordination();
16588
+ }
16589
+ return decision;
16590
+ })();
16591
+ this._pendingSchemaWrites.push(work.then(() => {
16592
+ }, () => {
16593
+ }));
16594
+ schemaUpdateGate = new SchemaUpdateGate(work);
16595
+ }
15588
16596
  const collOpts = {
15589
16597
  adapter: this.adapter,
15590
16598
  vault: this.name,
@@ -15592,6 +16600,11 @@ var Vault = class {
15592
16600
  keyring: this.keyring,
15593
16601
  encrypted: this.encrypted,
15594
16602
  emitter: this.emitter,
16603
+ writeQueue: this.noydb._writeQueueTracker,
16604
+ writeHooks: this.noydb._writeHooks,
16605
+ activeTxId: () => this.noydb._activeTxContextOrNull?.txId ?? null,
16606
+ schemaUpdateGate,
16607
+ schemaFence: this.schemaFence,
15595
16608
  getDEK: this.getDEK,
15596
16609
  onDirty: this.onDirty,
15597
16610
  historyConfig: this.historyConfig,
@@ -15636,7 +16649,6 @@ var Vault = class {
15636
16649
  } : {},
15637
16650
  ...this.materializedViewRegistry !== null ? {
15638
16651
  materializedViewSource: {
15639
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
15640
16652
  registry: () => this.materializedViewRegistry,
15641
16653
  getCollection: (name) => this.collection(name),
15642
16654
  getActiveTxContext: () => this.noydb._activeTxContextOrNull,
@@ -15679,7 +16691,7 @@ var Vault = class {
15679
16691
  }
15680
16692
  coll = new Collection(collOpts);
15681
16693
  this.collectionCache.set(collectionName, coll);
15682
- if (options?.persistJsonSchema === true && options.schema !== void 0) {
16694
+ if (options?.persistJsonSchema === true && options.schema !== void 0 && (options.schemaUpdate?.length ?? 0) === 0) {
15683
16695
  const validator = options.schema;
15684
16696
  const work = (async () => {
15685
16697
  try {
@@ -15712,6 +16724,87 @@ var Vault = class {
15712
16724
  this._pendingSchemaWrites = [];
15713
16725
  await Promise.allSettled(pending);
15714
16726
  }
16727
+ /**
16728
+ * Run a coordinated schema cutover (#232). Drains pending writes, waits
16729
+ * for the active client set to quiesce (the ack-barrier), applies every
16730
+ * pending collection transform in bulk, bumps the vault schema generation,
16731
+ * and clears the fence. Returns the count of collections migrated.
16732
+ * `opts.onPoll` (tests) advances other clients between barrier checks.
16733
+ */
16734
+ async runSchemaCutover(opts) {
16735
+ return this.schemaFence.runCutover(
16736
+ (collectionName, transform) => this.#runCutoverTransform(collectionName, transform),
16737
+ opts
16738
+ );
16739
+ }
16740
+ async #runCutoverTransform(collectionName, transform) {
16741
+ const coll = this.collectionCache.get(collectionName);
16742
+ if (!coll) return;
16743
+ await coll._applyCutoverTransform(transform);
16744
+ }
16745
+ /**
16746
+ * #228b — refresh a loaded collection's view of one document from a peer
16747
+ * tab's broadcast. No-op when the collection isn't loaded in this tab
16748
+ * (it will read fresh on next open). Mirrors #runCutoverTransform's guard.
16749
+ */
16750
+ async _applyRemoteWrite(collectionName, docId, action) {
16751
+ const coll = this.collectionCache.get(collectionName);
16752
+ if (!coll) return;
16753
+ await coll._applyRemoteChange(docId, action);
16754
+ }
16755
+ /**
16756
+ * #228c — for a detected conflict: capture this tab's clobbered record,
16757
+ * read the common ancestor from history, converge the cache to the store's
16758
+ * authoritative value (the (b) re-read), and return all three for the
16759
+ * WriteConflict payload. Returns null when the collection isn't loaded.
16760
+ */
16761
+ async _captureAndConverge(collectionName, docId, action, baseV) {
16762
+ const coll = this.collectionCache.get(collectionName);
16763
+ if (!coll) return null;
16764
+ const local = coll._peekCached(docId);
16765
+ let base = null;
16766
+ try {
16767
+ base = await coll.getVersion(docId, baseV);
16768
+ } catch {
16769
+ base = null;
16770
+ }
16771
+ await coll._applyRemoteChange(docId, action);
16772
+ const remote = await coll.get(docId);
16773
+ return { local, remote, base };
16774
+ }
16775
+ /** Recover a stuck cutover fence (#232) — reset to normal without bumping. */
16776
+ async abortSchemaCutover() {
16777
+ await this.schemaFence.abort();
16778
+ }
16779
+ /** Current schema-cutover fence state for this vault (#232/#233). Thin live read. */
16780
+ async schemaFenceState() {
16781
+ return loadFence(this.adapter, this.name);
16782
+ }
16783
+ /** @internal Start the per-client heartbeat + fence watcher once a cutover is registered (#232). */
16784
+ _ensureFenceCoordination() {
16785
+ if (this.#fenceCoordinationStarted) return;
16786
+ this.#fenceCoordinationStarted = true;
16787
+ this.#fenceWatcher = new FenceWatcher({
16788
+ store: this.adapter,
16789
+ vault: this.name,
16790
+ clientId: this.noydb._clientId,
16791
+ onFlush: () => this.noydb._writeQueueTracker.onFlush(),
16792
+ emit: (e) => this.emitter.emit("schema:fence-changed", { vault: this.name, ...e })
16793
+ });
16794
+ this.#fenceWatcher.start(2e3);
16795
+ }
16796
+ /** @internal Stop the heartbeat/watcher (vault lock/close). */
16797
+ _stopFenceCoordination() {
16798
+ this.#fenceWatcher?.stop();
16799
+ this.#fenceWatcher = void 0;
16800
+ this.#fenceCoordinationStarted = false;
16801
+ }
16802
+ /** @internal Drive one heartbeat + watch cycle deterministically (tests). */
16803
+ async _fenceTick() {
16804
+ this._ensureFenceCoordination();
16805
+ await this.#fenceWatcher.beat();
16806
+ await this.#fenceWatcher.check();
16807
+ }
15715
16808
  /**
15716
16809
  * Validate i18nText fields on a `put()`. Called by Collection just
15717
16810
  * before the adapter write, after schema validation. Throws
@@ -16032,6 +17125,66 @@ var Vault = class {
16032
17125
  options
16033
17126
  );
16034
17127
  }
17128
+ async issueAttestation(collectionName, id) {
17129
+ const fieldSchema = this.attestationRegistry.get(collectionName);
17130
+ if (!fieldSchema) {
17131
+ throw new AttestationError(`issueAttestation: collection '${collectionName}' has no attestation field-schema. Declare it via vault.collection('${collectionName}', { attestation: { fields: [...] } }).`);
17132
+ }
17133
+ const { issueAttestationCore: issueAttestationCore2 } = await Promise.resolve().then(() => (init_issue(), issue_exports));
17134
+ const out = await issueAttestationCore2(this.makeIssueContext(), { collection: collectionName, id, fieldSchema });
17135
+ return { docId: out.docId, qr: out.qr, keyId: out.keyId, publicKeyB64: out.publicKeyB64 };
17136
+ }
17137
+ async getDocumentSigningPublicKey() {
17138
+ const { loadSigner: loadSigner2, loadOrCreateSigner: loadOrCreateSigner2 } = await Promise.resolve().then(() => (init_signer(), signer_exports));
17139
+ const existing = await loadSigner2(this.adapter, this.name, this.getDEK);
17140
+ if (existing) return { keyId: existing.keyId, publicKeyB64: existing.publicKeyB64 };
17141
+ if (this.keyring.role !== "owner") {
17142
+ throw new AttestationError(`getDocumentSigningPublicKey: no document-signing key exists yet; only the 'owner' may mint it. Caller is '${this.keyring.role}'. Have the owner issue an attestation (or call this) first.`);
17143
+ }
17144
+ const signer = await loadOrCreateSigner2(this.adapter, this.name, this.getDEK);
17145
+ return { keyId: signer.keyId, publicKeyB64: signer.publicKeyB64 };
17146
+ }
17147
+ makeIssueContext() {
17148
+ const adapter = this.adapter, vaultName = this.name, getDEK = this.getDEK;
17149
+ return {
17150
+ store: adapter,
17151
+ vault: vaultName,
17152
+ role: this.keyring.role,
17153
+ getDEK: async () => getDEK("_attestations"),
17154
+ readRecord: async (collection, recId) => {
17155
+ const env = await adapter.get(vaultName, collection, recId);
17156
+ if (!env) return null;
17157
+ const record = await this.collection(collection).get(recId, { locale: "raw" });
17158
+ if (record === null) return null;
17159
+ return { record, version: env._v };
17160
+ }
17161
+ };
17162
+ }
17163
+ async revokeAttestation(docId) {
17164
+ const { revokeDocCore: revokeDocCore2 } = await Promise.resolve().then(() => (init_revoke(), revoke_exports));
17165
+ await revokeDocCore2(this.makeRevokeContext(), docId);
17166
+ }
17167
+ async unrevokeAttestation(docId) {
17168
+ const { unrevokeDocCore: unrevokeDocCore2 } = await Promise.resolve().then(() => (init_revoke(), revoke_exports));
17169
+ await unrevokeDocCore2(this.makeRevokeContext(), docId);
17170
+ }
17171
+ async getRevokedDocIds() {
17172
+ const { getRevokedDocIdsCore: getRevokedDocIdsCore2 } = await Promise.resolve().then(() => (init_revoke(), revoke_exports));
17173
+ return getRevokedDocIdsCore2(this.makeRevokeContext());
17174
+ }
17175
+ async publishRevocationList() {
17176
+ const { publishRevocationListCore: publishRevocationListCore2 } = await Promise.resolve().then(() => (init_revoke(), revoke_exports));
17177
+ return publishRevocationListCore2(this.makeRevokeContext());
17178
+ }
17179
+ makeRevokeContext() {
17180
+ const adapter = this.adapter, vaultName = this.name, getDEK = this.getDEK;
17181
+ return {
17182
+ store: adapter,
17183
+ vault: vaultName,
17184
+ role: this.keyring.role,
17185
+ getDEK: async () => getDEK("_attestations")
17186
+ };
17187
+ }
16035
17188
  async writeExportAudit(entry) {
16036
17189
  const json = JSON.stringify(entry);
16037
17190
  const envelope = this.encrypted ? await (async () => {
@@ -17080,6 +18233,27 @@ var Vault = class {
17080
18233
  async dumpSchema(opts = {}) {
17081
18234
  return dumpVaultSchema(this, opts);
17082
18235
  }
18236
+ /**
18237
+ * Lightweight read of the vault's registered schema (#229): collections
18238
+ * (+ doc counts), guards, materialized views, schema-update strategies,
18239
+ * and the unlocked user's grants. Cheap — one `adapter.list` per
18240
+ * collection, no decryption. For a full snapshot + stats use dumpSchema().
18241
+ * Post-unlock by construction (a Vault only exists with an unlocked keyring).
18242
+ */
18243
+ async introspect() {
18244
+ const byCol = (a, b) => a.collection.localeCompare(b.collection);
18245
+ const names = [.../* @__PURE__ */ new Set([...this.collectionCache.keys(), ...await this.collections()])].filter((n) => !n.startsWith("_")).sort((a, b) => a.localeCompare(b));
18246
+ const collections = [];
18247
+ for (const name of names) {
18248
+ const ids = await this.adapter.list(this.name, name);
18249
+ collections.push({ name, docCount: ids.length });
18250
+ }
18251
+ const guards = (this._getGuardRegistry()?.summary() ?? []).slice().sort(byCol);
18252
+ const materializedViews = (this._getMaterializedViewRegistry()?.all() ?? []).map((mv) => ({ name: mv.spec.name, sourceCollections: [...mv.dependencies].sort() })).sort((a, b) => a.name.localeCompare(b.name));
18253
+ const schemaUpdate = [...this.#schemaUpdateNames.entries()].map(([collection, strategies]) => ({ collection, strategies })).sort(byCol);
18254
+ const grants = [...this.keyring.deks.keys()].filter((collection) => !collection.startsWith("_")).map((collection) => ({ collection, permission: this.keyring.permissions[collection] ?? "rw" })).sort(byCol);
18255
+ return { collections, guards, materializedViews, schemaUpdate, grants };
18256
+ }
17083
18257
  /**
17084
18258
  * Internal accessor for {@link dumpVaultSchema}. Exposes the structural
17085
18259
  * state the walker needs (collection cache, registries, ref registry,
@@ -17355,7 +18529,7 @@ var Vault = class {
17355
18529
  for (let i = allEntries.length - 1; i >= 0; i--) {
17356
18530
  const entry = allEntries[i];
17357
18531
  if (!entry) continue;
17358
- if (entry.op === "amendment") continue;
18532
+ if (entry.op === "amendment" || entry.op === "lifecycle") continue;
17359
18533
  const key = `${entry.collection}/${entry.id}`;
17360
18534
  if (seen.has(key)) continue;
17361
18535
  seen.add(key);
@@ -17719,6 +18893,387 @@ var NoydbEventEmitter = class {
17719
18893
  }
17720
18894
  };
17721
18895
 
18896
+ // src/write-queue.ts
18897
+ var WriteQueueTracker = class {
18898
+ #depth = 0;
18899
+ #error = null;
18900
+ #changeHandlers = /* @__PURE__ */ new Set();
18901
+ #flushWaiters = [];
18902
+ get pending() {
18903
+ return this.#depth > 0;
18904
+ }
18905
+ get depth() {
18906
+ return this.#depth;
18907
+ }
18908
+ /** Mark one write as started. */
18909
+ begin() {
18910
+ this.#depth++;
18911
+ this.#emitChange();
18912
+ }
18913
+ /** Mark one write as finished. Pass the error if it failed. */
18914
+ settle(error) {
18915
+ this.#depth = Math.max(0, this.#depth - 1);
18916
+ if (error) this.#error = error;
18917
+ this.#emitChange();
18918
+ if (this.#depth === 0) this.#drainFlush();
18919
+ }
18920
+ onChange(handler) {
18921
+ this.#changeHandlers.add(handler);
18922
+ return () => {
18923
+ this.#changeHandlers.delete(handler);
18924
+ };
18925
+ }
18926
+ onFlush() {
18927
+ if (this.#depth === 0) {
18928
+ const error = this.#error;
18929
+ this.#error = null;
18930
+ return error ? Promise.reject(error) : Promise.resolve();
18931
+ }
18932
+ return new Promise((resolve, reject) => {
18933
+ this.#flushWaiters.push({ resolve, reject });
18934
+ });
18935
+ }
18936
+ /**
18937
+ * Run `fn` as a tracked write: depth++ on entry, depth-- on settle
18938
+ * (success or failure). The fn's resolved value is returned; a thrown
18939
+ * error is re-thrown after the queue is decremented.
18940
+ */
18941
+ async track(fn) {
18942
+ this.begin();
18943
+ try {
18944
+ const value = await fn();
18945
+ this.settle();
18946
+ return value;
18947
+ } catch (error) {
18948
+ this.settle(error);
18949
+ throw error;
18950
+ }
18951
+ }
18952
+ #emitChange() {
18953
+ for (const handler of this.#changeHandlers) handler();
18954
+ }
18955
+ #drainFlush() {
18956
+ const waiters = this.#flushWaiters;
18957
+ this.#flushWaiters = [];
18958
+ const error = this.#error;
18959
+ this.#error = null;
18960
+ for (const waiter of waiters) {
18961
+ if (error) waiter.reject(error);
18962
+ else waiter.resolve();
18963
+ }
18964
+ }
18965
+ };
18966
+
18967
+ // src/write-hooks.ts
18968
+ var WriteHookRegistry = class {
18969
+ #before = [];
18970
+ #after = [];
18971
+ #suppressed = false;
18972
+ /** True while handlers are running — used by the write path to skip nested firing. */
18973
+ get suppressed() {
18974
+ return this.#suppressed;
18975
+ }
18976
+ /** True when any hook is registered (cheap gate for the write path). */
18977
+ get hasHandlers() {
18978
+ return this.#before.length > 0 || this.#after.length > 0;
18979
+ }
18980
+ onBeforeWrite(handler) {
18981
+ this.#before.push(handler);
18982
+ return () => {
18983
+ const i = this.#before.indexOf(handler);
18984
+ if (i >= 0) this.#before.splice(i, 1);
18985
+ };
18986
+ }
18987
+ onAfterWrite(handler) {
18988
+ this.#after.push(handler);
18989
+ return () => {
18990
+ const i = this.#after.indexOf(handler);
18991
+ if (i >= 0) this.#after.splice(i, 1);
18992
+ };
18993
+ }
18994
+ /** Run before-hooks (awaited, in order). A throw propagates and aborts the write. */
18995
+ async runBefore(event) {
18996
+ if (this.#before.length === 0) return;
18997
+ this.#suppressed = true;
18998
+ try {
18999
+ for (const h of this.#before.slice()) await h(event);
19000
+ } finally {
19001
+ this.#suppressed = false;
19002
+ }
19003
+ }
19004
+ /** Run after-hooks (awaited, in order). Per-handler errors are warned, not thrown. */
19005
+ async runAfter(event) {
19006
+ if (this.#after.length === 0) return;
19007
+ this.#suppressed = true;
19008
+ try {
19009
+ for (const h of this.#after.slice()) {
19010
+ try {
19011
+ await h(event);
19012
+ } catch (err) {
19013
+ console.warn(
19014
+ `[noy-db] onAfterWrite handler failed for ${event.collection}/${event.docId}: ` + (err instanceof Error ? err.message : String(err))
19015
+ );
19016
+ }
19017
+ }
19018
+ } finally {
19019
+ this.#suppressed = false;
19020
+ }
19021
+ }
19022
+ };
19023
+
19024
+ // src/tab-coordination.ts
19025
+ var TabCoordinator = class {
19026
+ tabId;
19027
+ role = "unknown";
19028
+ #lockManager;
19029
+ #channel;
19030
+ #lockName;
19031
+ #heartbeatMs;
19032
+ #staleMs;
19033
+ #now;
19034
+ #peers = /* @__PURE__ */ new Map();
19035
+ #roleHandlers = /* @__PURE__ */ new Set();
19036
+ #tabsHandlers = /* @__PURE__ */ new Set();
19037
+ #ac;
19038
+ #releaseLock;
19039
+ #unsub;
19040
+ #closeUnsub;
19041
+ #timer;
19042
+ #ownsChannel;
19043
+ #started = false;
19044
+ #disposed = false;
19045
+ #lastTabsSig = "";
19046
+ constructor(opts = {}) {
19047
+ this.tabId = opts.tabId ?? `tab-${Math.trunc((opts.now ?? (() => 0))())}-${cheapRand()}`;
19048
+ this.#lockManager = opts.lockManager;
19049
+ this.#channel = opts.channel;
19050
+ this.#lockName = opts.lockName ?? "noydb:tab-primary";
19051
+ this.#heartbeatMs = opts.heartbeatMs ?? 2e3;
19052
+ this.#staleMs = opts.staleMs ?? 6e3;
19053
+ this.#now = opts.now ?? (() => Date.now());
19054
+ this.#ownsChannel = opts.closeChannelOnDispose ?? false;
19055
+ }
19056
+ start() {
19057
+ if (this.#disposed || this.#started) return;
19058
+ this.#started = true;
19059
+ if (this.#channel) {
19060
+ this.#unsub = this.#channel.on("message", (p) => this.#onMessage(p));
19061
+ this.#closeUnsub = this.#channel.on("close", () => this.#onChannelClose());
19062
+ this.#beat();
19063
+ this.#timer = setInterval(() => this.#tick(), this.#heartbeatMs);
19064
+ const t = this.#timer;
19065
+ if (typeof t.unref === "function") t.unref();
19066
+ }
19067
+ if (this.#lockManager) {
19068
+ this.#ac = new AbortController();
19069
+ this.#setRole("secondary");
19070
+ void this.#lockManager.request(this.#lockName, { mode: "exclusive", signal: this.#ac.signal }, () => {
19071
+ this.#setRole("primary");
19072
+ return new Promise((resolve) => {
19073
+ this.#releaseLock = resolve;
19074
+ });
19075
+ }).catch(() => {
19076
+ });
19077
+ }
19078
+ }
19079
+ activeTabs() {
19080
+ if (!this.#channel) return [];
19081
+ const cutoff = this.#now() - this.#staleMs;
19082
+ const self = { tabId: this.tabId, lastSeen: this.#now(), role: this.role };
19083
+ const out = [self, ...[...this.#peers.values()].filter((p) => p.lastSeen >= cutoff)];
19084
+ return out.sort((a, b) => a.tabId.localeCompare(b.tabId));
19085
+ }
19086
+ onTabRoleChange(fn) {
19087
+ this.#roleHandlers.add(fn);
19088
+ return () => this.#roleHandlers.delete(fn);
19089
+ }
19090
+ onActiveTabsChange(fn) {
19091
+ this.#tabsHandlers.add(fn);
19092
+ return () => this.#tabsHandlers.delete(fn);
19093
+ }
19094
+ dispose() {
19095
+ if (this.#disposed) return;
19096
+ this.#disposed = true;
19097
+ this.#releaseLock?.();
19098
+ this.#ac?.abort();
19099
+ if (this.#timer) {
19100
+ clearInterval(this.#timer);
19101
+ this.#timer = void 0;
19102
+ }
19103
+ this.#unsub?.();
19104
+ this.#closeUnsub?.();
19105
+ if (this.#ownsChannel) this.#channel?.close();
19106
+ this.#setRole("unknown");
19107
+ }
19108
+ /** @internal test seam — broadcast one heartbeat now. */
19109
+ _beat() {
19110
+ this.#beat();
19111
+ }
19112
+ #tick() {
19113
+ this.#prune();
19114
+ this.#emitTabs();
19115
+ this.#beat();
19116
+ }
19117
+ #beat() {
19118
+ if (this.#disposed) return;
19119
+ if (!this.#channel || !this.#channel.isOpen) return;
19120
+ const msg = { kind: "tab-presence", tabId: this.tabId, lastSeen: this.#now(), role: this.role };
19121
+ this.#channel.send(JSON.stringify(msg));
19122
+ }
19123
+ #onChannelClose() {
19124
+ if (this.#timer) {
19125
+ clearInterval(this.#timer);
19126
+ this.#timer = void 0;
19127
+ }
19128
+ this.#setRole("unknown");
19129
+ }
19130
+ #onMessage(payload) {
19131
+ let msg;
19132
+ try {
19133
+ msg = JSON.parse(payload);
19134
+ } catch {
19135
+ return;
19136
+ }
19137
+ if (!isPresenceMsg(msg) || msg.tabId === this.tabId) return;
19138
+ this.#peers.set(msg.tabId, { tabId: msg.tabId, lastSeen: msg.lastSeen, role: msg.role });
19139
+ this.#prune();
19140
+ this.#emitTabs();
19141
+ }
19142
+ #prune() {
19143
+ const cutoff = this.#now() - this.#staleMs;
19144
+ for (const [id, p] of this.#peers) if (p.lastSeen < cutoff) this.#peers.delete(id);
19145
+ }
19146
+ #setRole(role) {
19147
+ if (this.role === role) return;
19148
+ this.role = role;
19149
+ for (const h of this.#roleHandlers) h(role);
19150
+ this.#beat();
19151
+ this.#emitTabs();
19152
+ }
19153
+ #emitTabs() {
19154
+ const tabs = this.activeTabs();
19155
+ const sig = tabs.map((t) => `${t.tabId}:${t.role}`).join("|");
19156
+ if (sig === this.#lastTabsSig) return;
19157
+ this.#lastTabsSig = sig;
19158
+ for (const h of this.#tabsHandlers) h(tabs);
19159
+ }
19160
+ };
19161
+ function isPresenceMsg(x) {
19162
+ if (x === null || typeof x !== "object") return false;
19163
+ const o = x;
19164
+ return o["kind"] === "tab-presence" && typeof o["tabId"] === "string" && typeof o["lastSeen"] === "number" && (o["role"] === "primary" || o["role"] === "secondary" || o["role"] === "unknown");
19165
+ }
19166
+ function cheapRand() {
19167
+ const g = globalThis;
19168
+ return g.crypto?.randomUUID ? g.crypto.randomUUID().slice(0, 8) : "anon";
19169
+ }
19170
+ function defaultLockManager() {
19171
+ const nav = globalThis.navigator;
19172
+ return nav?.locks;
19173
+ }
19174
+ function defaultChannel(name = "noydb:tabs") {
19175
+ if (typeof globalThis.window === "undefined") return void 0;
19176
+ const Bc = globalThis.BroadcastChannel;
19177
+ if (!Bc) return void 0;
19178
+ const bc = new Bc(name);
19179
+ const msgListeners = /* @__PURE__ */ new Set();
19180
+ bc.onmessage = (e) => {
19181
+ for (const l of msgListeners) l(String(e.data));
19182
+ };
19183
+ return {
19184
+ isOpen: true,
19185
+ send(payload) {
19186
+ bc.postMessage(payload);
19187
+ },
19188
+ on(event, listener) {
19189
+ if (event === "message") {
19190
+ const l = listener;
19191
+ msgListeners.add(l);
19192
+ return () => msgListeners.delete(l);
19193
+ }
19194
+ return () => {
19195
+ };
19196
+ },
19197
+ close() {
19198
+ msgListeners.clear();
19199
+ bc.close();
19200
+ }
19201
+ };
19202
+ }
19203
+
19204
+ // src/tab-write-relay.ts
19205
+ var CrossTabWriteRelay = class {
19206
+ #channel;
19207
+ #writerId;
19208
+ #subscribeAfterWrite;
19209
+ #applyRemoteWrite;
19210
+ #reportConflict;
19211
+ #ledger = /* @__PURE__ */ new Map();
19212
+ #ownsChannel;
19213
+ #unsubMsg;
19214
+ #unsubWrite;
19215
+ #started = false;
19216
+ #disposed = false;
19217
+ constructor(opts) {
19218
+ this.#channel = opts.channel;
19219
+ this.#writerId = opts.writerId;
19220
+ this.#subscribeAfterWrite = opts.subscribeAfterWrite;
19221
+ this.#applyRemoteWrite = opts.applyRemoteWrite;
19222
+ this.#reportConflict = opts.reportConflict;
19223
+ this.#ownsChannel = opts.closeChannelOnDispose ?? false;
19224
+ }
19225
+ start() {
19226
+ if (this.#started || this.#disposed) return;
19227
+ this.#started = true;
19228
+ this.#unsubMsg = this.#channel.on("message", (p) => this.#onMessage(p));
19229
+ this.#unsubWrite = this.#subscribeAfterWrite((e) => this.#onLocalWrite(e));
19230
+ }
19231
+ dispose() {
19232
+ if (this.#disposed) return;
19233
+ this.#disposed = true;
19234
+ this.#unsubWrite?.();
19235
+ this.#unsubMsg?.();
19236
+ if (this.#ownsChannel) this.#channel.close();
19237
+ }
19238
+ #onLocalWrite(e) {
19239
+ if (this.#disposed || !this.#channel.isOpen) return;
19240
+ this.#ledger.set(ledgerKey(e.vault, e.collection, e.docId), e.version);
19241
+ const action = e.op === "delete" ? "delete" : "put";
19242
+ const msg = { kind: "tab-write", writerId: this.#writerId, vault: e.vault, collection: e.collection, docId: e.docId, action, baseV: e.baseVersion, v: e.version };
19243
+ this.#channel.send(JSON.stringify(msg));
19244
+ }
19245
+ #onMessage(payload) {
19246
+ if (this.#disposed) return;
19247
+ let msg;
19248
+ try {
19249
+ msg = JSON.parse(payload);
19250
+ } catch {
19251
+ return;
19252
+ }
19253
+ if (!isTabWriteMsg(msg) || msg.writerId === this.#writerId) return;
19254
+ const key = ledgerKey(msg.vault, msg.collection, msg.docId);
19255
+ const ownV = this.#ledger.get(key);
19256
+ if (ownV !== void 0 && msg.baseV < ownV && this.#reportConflict) {
19257
+ void Promise.resolve(this.#reportConflict(msg.vault, msg.collection, msg.docId, msg.action, msg.baseV, msg.v, ownV)).catch((err) => {
19258
+ console.warn(`[noy-db] cross-tab conflict report failed for ${msg.collection}/${msg.docId}: ` + (err instanceof Error ? err.message : String(err)));
19259
+ });
19260
+ return;
19261
+ }
19262
+ if (ownV !== void 0 && msg.baseV >= ownV) this.#ledger.set(key, msg.v);
19263
+ void Promise.resolve(this.#applyRemoteWrite(msg.vault, msg.collection, msg.docId, msg.action)).catch((err) => {
19264
+ console.warn(`[noy-db] cross-tab apply failed for ${msg.collection}/${msg.docId}: ` + (err instanceof Error ? err.message : String(err)));
19265
+ });
19266
+ }
19267
+ };
19268
+ function ledgerKey(vault, collection, docId) {
19269
+ return `${vault}\0${collection}\0${docId}`;
19270
+ }
19271
+ function isTabWriteMsg(x) {
19272
+ if (x === null || typeof x !== "object") return false;
19273
+ const o = x;
19274
+ return o["kind"] === "tab-write" && typeof o["writerId"] === "string" && typeof o["vault"] === "string" && typeof o["collection"] === "string" && typeof o["docId"] === "string" && (o["action"] === "put" || o["action"] === "delete") && typeof o["baseV"] === "number" && typeof o["v"] === "number";
19275
+ }
19276
+
17722
19277
  // src/tx/strategy.ts
17723
19278
  var NOT_ENABLED5 = new Error(
17724
19279
  'Multi-record transactions require the tx strategy. Import `{ withTransactions }` from "@noy-db/hub/tx" and pass it to `createNoydb({ txStrategy: withTransactions() })`.'
@@ -17726,6 +19281,9 @@ var NOT_ENABLED5 = new Error(
17726
19281
  var NO_TX = {
17727
19282
  async runTransaction() {
17728
19283
  throw NOT_ENABLED5;
19284
+ },
19285
+ async runDryRun() {
19286
+ throw NOT_ENABLED5;
17729
19287
  }
17730
19288
  };
17731
19289
 
@@ -18023,6 +19581,9 @@ function createPlaintextKeyring(userId) {
18023
19581
  var Noydb = class {
18024
19582
  options;
18025
19583
  emitter = new NoydbEventEmitter();
19584
+ writeQueueTracker = new WriteQueueTracker();
19585
+ writeHooks = new WriteHookRegistry();
19586
+ clientId = generateULID();
18026
19587
  vaultCache = /* @__PURE__ */ new Map();
18027
19588
  keyringCache = /* @__PURE__ */ new Map();
18028
19589
  syncEngines = /* @__PURE__ */ new Map();
@@ -18055,6 +19616,10 @@ var Noydb = class {
18055
19616
  publicEnvelopeSchema;
18056
19617
  closed = false;
18057
19618
  sessionTimer = null;
19619
+ /** Same-device multi-tab coordinator (#228); created on `enableTabCoordination()`. */
19620
+ tabCoordinator;
19621
+ /** Cross-tab write relay (#228b); created on `enableTabCoordination()`. */
19622
+ writeRelay;
18058
19623
  /** Per-vault policy enforcers. */
18059
19624
  policyEnforcers = /* @__PURE__ */ new Map();
18060
19625
  txStrategy;
@@ -18247,6 +19812,7 @@ var Noydb = class {
18247
19812
  await comp._initDerivations(this.options.derivationStrategies ?? []);
18248
19813
  await comp._initMaterializedViews(this.options.materializedViewStrategies ?? []);
18249
19814
  await comp._initOverlayedViews(this.options.overlayedViewStrategies ?? []);
19815
+ await comp.schemaFence.init();
18250
19816
  this.vaultCache.set(name, comp);
18251
19817
  return comp;
18252
19818
  }
@@ -18674,6 +20240,14 @@ var Noydb = class {
18674
20240
  if (typeof arg === "function") {
18675
20241
  return this.txStrategy.runTransaction(this, arg);
18676
20242
  }
20243
+ if (typeof arg === "object" && arg !== null && arg.dryRun === true) {
20244
+ if (typeof maybeFn !== "function") {
20245
+ throw new ValidationError(
20246
+ "db.transaction({ dryRun: true }, fn) requires the callback as the second argument."
20247
+ );
20248
+ }
20249
+ return this.txStrategy.runDryRun(this, maybeFn);
20250
+ }
18677
20251
  if (typeof arg === "object" && arg !== null && arg.amendment === true) {
18678
20252
  if (typeof maybeFn !== "function") {
18679
20253
  throw new ValidationError(
@@ -18786,6 +20360,133 @@ var Noydb = class {
18786
20360
  off(event, handler) {
18787
20361
  this.emitter.off(event, handler);
18788
20362
  }
20363
+ /**
20364
+ * Observable write-queue for this hub instance. Reflects outstanding
20365
+ * in-flight writes across all collections. See {@link WriteQueue}.
20366
+ *
20367
+ * @example
20368
+ * window.addEventListener('beforeunload', (e) => {
20369
+ * if (db.writeQueue.pending) { e.preventDefault(); e.returnValue = '' }
20370
+ * })
20371
+ */
20372
+ get writeQueue() {
20373
+ return this.writeQueueTracker;
20374
+ }
20375
+ /**
20376
+ * @internal Mutable tracker behind {@link writeQueue}. Threaded into
20377
+ * each Collection (via Vault) so `put`/`delete` can `track()` writes.
20378
+ * Not part of the public surface — consumers use `writeQueue`.
20379
+ */
20380
+ get _writeQueueTracker() {
20381
+ return this.writeQueueTracker;
20382
+ }
20383
+ /**
20384
+ * Register a hook that runs before each write (#230). Awaited; a throw
20385
+ * aborts the write. Returns an unsubscribe function.
20386
+ */
20387
+ onBeforeWrite(handler) {
20388
+ return this.writeHooks.onBeforeWrite(handler);
20389
+ }
20390
+ /**
20391
+ * Register a hook that runs after each committed write (#230). Awaited;
20392
+ * a handler error is warned, never rolled back. Returns an unsubscribe fn.
20393
+ */
20394
+ onAfterWrite(handler) {
20395
+ return this.writeHooks.onAfterWrite(handler);
20396
+ }
20397
+ /** Subscribe to cross-tab write conflicts (#228c). Returns an unsubscribe. */
20398
+ onWriteConflict(fn) {
20399
+ this.on("write:conflict", fn);
20400
+ return () => this.off("write:conflict", fn);
20401
+ }
20402
+ /**
20403
+ * Enable same-device multi-tab coordination (#228): primary/secondary
20404
+ * election + presence. Browser-only — a graceful no-op (role 'unknown')
20405
+ * when Web Locks / BroadcastChannel are unavailable and nothing is
20406
+ * injected. Idempotent; returns a disposer.
20407
+ */
20408
+ enableTabCoordination(opts = {}) {
20409
+ if (this.tabCoordinator) return { dispose: () => this.disableTabCoordination() };
20410
+ const lockManager = opts.lockManager ?? defaultLockManager();
20411
+ const channel = opts.channel ?? defaultChannel();
20412
+ const c = new TabCoordinator({
20413
+ ...opts,
20414
+ ...lockManager ? { lockManager } : {},
20415
+ ...channel ? { channel } : {},
20416
+ // We own the channel only when we created the default; never close a caller-injected one.
20417
+ closeChannelOnDispose: opts.channel === void 0 && channel !== void 0
20418
+ });
20419
+ this.tabCoordinator = c;
20420
+ c.start();
20421
+ if (opts.propagateWrites !== false) {
20422
+ const writeChannel = opts.writeChannel ?? defaultChannel("noydb:tab-writes");
20423
+ if (writeChannel) {
20424
+ const relay = new CrossTabWriteRelay({
20425
+ channel: writeChannel,
20426
+ writerId: c.tabId,
20427
+ subscribeAfterWrite: (h) => this.onAfterWrite(h),
20428
+ applyRemoteWrite: (vault, collection, docId, action) => this.#applyRemoteWrite(vault, collection, docId, action),
20429
+ reportConflict: (vault, collection, docId, action, baseV, v, ownV) => this.#reportWriteConflict(vault, collection, docId, action, baseV, v, ownV),
20430
+ // Own the channel only when we created the default (mirrors the presence channel).
20431
+ closeChannelOnDispose: opts.writeChannel === void 0 && writeChannel !== void 0
20432
+ });
20433
+ this.writeRelay = relay;
20434
+ relay.start();
20435
+ }
20436
+ }
20437
+ return { dispose: () => this.disableTabCoordination() };
20438
+ }
20439
+ #applyRemoteWrite(vaultName, collectionName, docId, action) {
20440
+ const v = this.vaultCache.get(vaultName);
20441
+ if (!v) return Promise.resolve();
20442
+ return v._applyRemoteWrite(collectionName, docId, action);
20443
+ }
20444
+ async #reportWriteConflict(vaultName, collectionName, docId, action, baseV, v, ownV) {
20445
+ const vault = this.vaultCache.get(vaultName);
20446
+ if (!vault) return;
20447
+ const cap = await vault._captureAndConverge(collectionName, docId, action, baseV);
20448
+ if (!cap) return;
20449
+ const conflict = {
20450
+ vault: vaultName,
20451
+ collection: collectionName,
20452
+ docId,
20453
+ local: cap.local,
20454
+ remote: cap.remote,
20455
+ base: cap.base,
20456
+ localVersion: ownV,
20457
+ remoteVersion: v,
20458
+ baseVersion: baseV
20459
+ };
20460
+ this.emitter.emit("write:conflict", conflict);
20461
+ }
20462
+ disableTabCoordination() {
20463
+ this.tabCoordinator?.dispose();
20464
+ this.tabCoordinator = void 0;
20465
+ this.writeRelay?.dispose();
20466
+ this.writeRelay = void 0;
20467
+ }
20468
+ get tabRole() {
20469
+ return this.tabCoordinator?.role ?? "unknown";
20470
+ }
20471
+ activeTabs() {
20472
+ return this.tabCoordinator?.activeTabs() ?? [];
20473
+ }
20474
+ onTabRoleChange(fn) {
20475
+ return this.tabCoordinator?.onTabRoleChange(fn) ?? (() => {
20476
+ });
20477
+ }
20478
+ onActiveTabsChange(fn) {
20479
+ return this.tabCoordinator?.onActiveTabsChange(fn) ?? (() => {
20480
+ });
20481
+ }
20482
+ /** @internal The write-hook registry, threaded into each Collection. */
20483
+ get _writeHooks() {
20484
+ return this.writeHooks;
20485
+ }
20486
+ /** @internal Stable per-instance id for schema-cutover coordination (#232). */
20487
+ get _clientId() {
20488
+ return this.clientId;
20489
+ }
18789
20490
  /**
18790
20491
  * Soft-lock a single vault: clear its in-memory keyring, DEKs, vault
18791
20492
  * instance, sync engine, policy enforcer, and active-tier entry —
@@ -18812,6 +20513,7 @@ var Noydb = class {
18812
20513
  this.syncEngines.delete(vault);
18813
20514
  this.policyEnforcers.get(vault)?.destroy();
18814
20515
  this.policyEnforcers.delete(vault);
20516
+ this.vaultCache.get(vault)?._stopFenceCoordination();
18815
20517
  this.keyringCache.delete(vault);
18816
20518
  this.vaultCache.delete(vault);
18817
20519
  this.activeTier.delete(vault);
@@ -18831,6 +20533,8 @@ var Noydb = class {
18831
20533
  engine.stopAutoSync();
18832
20534
  }
18833
20535
  this.syncEngines.clear();
20536
+ for (const v of this.vaultCache.values()) v._stopFenceCoordination();
20537
+ this.disableTabCoordination();
18834
20538
  this.keyringCache.clear();
18835
20539
  this.vaultCache.clear();
18836
20540
  this.activeTier.clear();
@@ -21557,6 +23261,7 @@ function shortJSON(value) {
21557
23261
  Aggregation,
21558
23262
  AlreadyElevatedError,
21559
23263
  AmendmentForbiddenError,
23264
+ AttestationError,
21560
23265
  BLOB_CHUNKS_COLLECTION,
21561
23266
  BLOB_COLLECTION,
21562
23267
  BLOB_INDEX_COLLECTION,
@@ -21630,7 +23335,9 @@ function shortJSON(value) {
21630
23335
  MaterializedViewCycleError,
21631
23336
  MaterializedViewSourceUnknownError,
21632
23337
  MaterializedViewTooLargeError,
23338
+ MemoryRecipientSealer,
21633
23339
  MemorySealingKeyProvider,
23340
+ MigrationRequiredError,
21634
23341
  MissingTranslationError,
21635
23342
  NOYDB_BACKUP_VERSION,
21636
23343
  NOYDB_BUNDLE_FORMAT_VERSION,
@@ -21641,6 +23348,7 @@ function shortJSON(value) {
21641
23348
  NOYDB_SYNC_VERSION,
21642
23349
  NetworkError,
21643
23350
  NoAccessError,
23351
+ NonAdditiveSchemaChangeError,
21644
23352
  NotFoundError,
21645
23353
  Noydb,
21646
23354
  NoydbError,
@@ -21662,6 +23370,7 @@ function shortJSON(value) {
21662
23370
  PrivilegeEscalationError,
21663
23371
  Query,
21664
23372
  QuickUnlockStore,
23373
+ QuiesceTimeoutError,
21665
23374
  ReadOnlyAtInstantError,
21666
23375
  ReadOnlyError,
21667
23376
  ReadOnlyFrameError,
@@ -21677,6 +23386,9 @@ function shortJSON(value) {
21677
23386
  STRICT_POLICY,
21678
23387
  SYNC_CREDENTIALS_COLLECTION,
21679
23388
  ScanBuilder,
23389
+ SchemaFenceError,
23390
+ SchemaLockedError,
23391
+ SchemaUpdateError,
21680
23392
  SchemaValidationError,
21681
23393
  SessionExpiredError,
21682
23394
  SessionNotFoundError,
@@ -21703,6 +23415,7 @@ function shortJSON(value) {
21703
23415
  VaultInstant,
21704
23416
  WeakPassphraseError,
21705
23417
  activeSessionCount,
23418
+ additiveOnly,
21706
23419
  applyI18nLocale,
21707
23420
  applyJoins,
21708
23421
  applyPatch,
@@ -21710,6 +23423,7 @@ function shortJSON(value) {
21710
23423
  assertTierAccess,
21711
23424
  avg,
21712
23425
  base64ToBuffer,
23426
+ blindUpdate,
21713
23427
  bufferToBase64,
21714
23428
  buildLiveQuery,
21715
23429
  buildRecipientKeyringFile,
@@ -21718,6 +23432,7 @@ function shortJSON(value) {
21718
23432
  checkGate,
21719
23433
  clearDevUnlock,
21720
23434
  computePatch,
23435
+ coordinatedCutover,
21721
23436
  count,
21722
23437
  createBundleStore,
21723
23438
  createEnforcer,
@@ -21796,6 +23511,7 @@ function shortJSON(value) {
21796
23511
  loadShamirRecoveryEntries,
21797
23512
  loadUserEnvelope,
21798
23513
  loadVaultPolicy,
23514
+ lockSchema,
21799
23515
  magicLinkGrantRecordId,
21800
23516
  max,
21801
23517
  mergeCrdtStates,