@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.
- package/dist/aggregate/index.cjs.map +1 -1
- package/dist/aggregate/index.js +2 -2
- package/dist/attestation/index.cjs +305 -0
- package/dist/attestation/index.cjs.map +1 -0
- package/dist/attestation/index.d.cts +52 -0
- package/dist/attestation/index.d.ts +52 -0
- package/dist/attestation/index.js +36 -0
- package/dist/attestation/index.js.map +1 -0
- package/dist/blobs/index.cjs.map +1 -1
- package/dist/blobs/index.d.cts +4 -3
- package/dist/blobs/index.d.ts +4 -3
- package/dist/blobs/index.js +10 -8
- package/dist/blobs/index.js.map +1 -1
- package/dist/bundle/index.cjs +17940 -129
- package/dist/bundle/index.cjs.map +1 -1
- package/dist/bundle/index.d.cts +172 -3
- package/dist/bundle/index.d.ts +172 -3
- package/dist/bundle/index.js +533 -5
- package/dist/bundle/index.js.map +1 -1
- package/dist/{chunk-CBAHB2BF.js → chunk-2EYC3WDT.js} +7 -70
- package/dist/chunk-2EYC3WDT.js.map +1 -0
- package/dist/{chunk-P7EQ2S5O.js → chunk-2XLVPKXG.js} +2 -2
- package/dist/chunk-4OQWR46B.js +79 -0
- package/dist/chunk-4OQWR46B.js.map +1 -0
- package/dist/{chunk-23TTQXVO.js → chunk-4UBOTYP5.js} +2 -2
- package/dist/chunk-4X2S7PBF.js +251 -0
- package/dist/chunk-4X2S7PBF.js.map +1 -0
- package/dist/{chunk-MKSA2V7A.js → chunk-5YHWBPOT.js} +2 -2
- package/dist/{chunk-DYBQG5PQ.js → chunk-6S3LLAQ5.js} +2 -2
- package/dist/{chunk-UA4RI7OT.js → chunk-74JEQFMT.js} +5 -5
- package/dist/chunk-75QDHSE4.js +59 -0
- package/dist/chunk-75QDHSE4.js.map +1 -0
- package/dist/chunk-A6SWRXUQ.js +118 -0
- package/dist/chunk-A6SWRXUQ.js.map +1 -0
- package/dist/{chunk-UZXLQCHP.js → chunk-BFI3RS42.js} +2 -2
- package/dist/{chunk-EGQYGYIU.js → chunk-EMEX37ZN.js} +2 -2
- package/dist/{chunk-PEULZC6M.js → chunk-EPK6A3WJ.js} +8 -1
- package/dist/chunk-EPK6A3WJ.js.map +1 -0
- package/dist/{chunk-VMIO4IXG.js → chunk-FBMXWVGP.js} +6 -229
- package/dist/chunk-FBMXWVGP.js.map +1 -0
- package/dist/{chunk-ZNOEIM6Y.js → chunk-FCDO7UAO.js} +2 -2
- package/dist/{chunk-5SCJ5UEF.js → chunk-FS7A4XNF.js} +3 -3
- package/dist/{chunk-YS3POABP.js → chunk-FXQYZNOW.js} +1 -1
- package/dist/chunk-FXQYZNOW.js.map +1 -0
- package/dist/{chunk-SIZWEV2Y.js → chunk-G7PAZ3TD.js} +4 -4
- package/dist/{chunk-SIZWEV2Y.js.map → chunk-G7PAZ3TD.js.map} +1 -1
- package/dist/{chunk-537VFZTR.js → chunk-GAUBWHAF.js} +4 -4
- package/dist/{chunk-FCXOFQAJ.js → chunk-GD3BGKAR.js} +2 -2
- package/dist/{chunk-DPMFBCV6.js → chunk-GDTCGIPX.js} +2 -2
- package/dist/{chunk-DPMFBCV6.js.map → chunk-GDTCGIPX.js.map} +1 -1
- package/dist/{chunk-6HPZY4ON.js → chunk-GVXBHCZ2.js} +8 -3
- package/dist/chunk-GVXBHCZ2.js.map +1 -0
- package/dist/{chunk-HB3Z2GCR.js → chunk-HGZ7DC5H.js} +2 -2
- package/dist/{chunk-MIQHZESA.js → chunk-IS5HWQO7.js} +5 -5
- package/dist/{chunk-MIQHZESA.js.map → chunk-IS5HWQO7.js.map} +1 -1
- package/dist/{chunk-5DWL3JBF.js → chunk-K5PVGKE4.js} +2 -2
- package/dist/{chunk-NIOHFJPJ.js → chunk-KMI2NBBF.js} +7 -119
- package/dist/chunk-KMI2NBBF.js.map +1 -0
- package/dist/{chunk-XGSOTWYX.js → chunk-KYKMKLJ6.js} +2 -2
- package/dist/chunk-LOL725S4.js +233 -0
- package/dist/chunk-LOL725S4.js.map +1 -0
- package/dist/{chunk-4TFSM22V.js → chunk-LS3JLEIB.js} +4 -4
- package/dist/{chunk-2AXFIYHT.js → chunk-NCO2JGKK.js} +1 -1
- package/dist/chunk-NCO2JGKK.js.map +1 -0
- package/dist/{chunk-Z72JH4KG.js → chunk-NGSPBLLE.js} +4 -34
- package/dist/chunk-NGSPBLLE.js.map +1 -0
- package/dist/{chunk-OMLIZL2P.js → chunk-NSLTPGEN.js} +2 -2
- package/dist/{chunk-7H6DOO3E.js → chunk-P6256WTJ.js} +211 -36
- package/dist/chunk-P6256WTJ.js.map +1 -0
- package/dist/{chunk-KESP7GOK.js → chunk-QAU5HM6Q.js} +3 -3
- package/dist/{chunk-34YSDCDP.js → chunk-SAVQ6E2O.js} +2 -2
- package/dist/chunk-T6HQMVML.js +9960 -0
- package/dist/chunk-T6HQMVML.js.map +1 -0
- package/dist/{chunk-PA6R5ZCI.js → chunk-TLFUDXVV.js} +4 -4
- package/dist/{chunk-WCA2NROQ.js → chunk-UOF74WQY.js} +2 -2
- package/dist/chunk-UVPGJXVO.js +83 -0
- package/dist/chunk-UVPGJXVO.js.map +1 -0
- package/dist/{chunk-DYECX3IX.js → chunk-WRLHNG6H.js} +2 -2
- package/dist/{chunk-ADQ5MQ54.js → chunk-YDLAFP36.js} +71 -1
- package/dist/chunk-YDLAFP36.js.map +1 -0
- package/dist/{chunk-I6MX32UC.js → chunk-YK72A4IT.js} +4 -4
- package/dist/chunk-YL2DR3HY.js +36 -0
- package/dist/chunk-YL2DR3HY.js.map +1 -0
- package/dist/{chunk-RD5LYKD6.js → chunk-ZC2AAE6J.js} +2 -2
- package/dist/chunk-ZUMGGHRB.js +57 -0
- package/dist/chunk-ZUMGGHRB.js.map +1 -0
- package/dist/consent/index.cjs.map +1 -1
- package/dist/consent/index.d.cts +4 -3
- package/dist/consent/index.d.ts +4 -3
- package/dist/consent/index.js +3 -3
- package/dist/{crypto-A7FRXYHC.js → crypto-H2Y3DDFW.js} +3 -3
- package/dist/{delegation-YBA4X4JN.js → delegation-QSC7G5QC.js} +5 -5
- package/dist/derivations/index.cjs.map +1 -1
- package/dist/derivations/index.d.cts +5 -4
- package/dist/derivations/index.d.ts +5 -4
- package/dist/derivations/index.js +4 -4
- package/dist/{dev-unlock-D9s-loPr.d.ts → dev-unlock-Cf2B7Kih.d.ts} +1 -1
- package/dist/{dev-unlock-DRwVSy2S.d.cts → dev-unlock-De3mjQWv.d.cts} +1 -1
- package/dist/executor-BZKFZVRC.js +8 -0
- package/dist/executor-GFZFDQXV.js +8 -0
- package/dist/executor-KT2IOZVP.js +11 -0
- package/dist/{fanout-sidecar-VJ52RIEY.js → fanout-sidecar-NRBWSLRK.js} +2 -2
- package/dist/guards/index.cjs +7 -0
- package/dist/guards/index.cjs.map +1 -1
- package/dist/guards/index.d.cts +5 -4
- package/dist/guards/index.d.ts +5 -4
- package/dist/guards/index.js +4 -4
- package/dist/{hash-DXXXusyk.d.ts → hash-gVn_uKhp.d.ts} +1 -1
- package/dist/{hash-DtRih9MQ.d.cts → hash-vBCB0-Ps.d.cts} +1 -1
- package/dist/history/index.cjs +2 -2
- package/dist/history/index.cjs.map +1 -1
- package/dist/history/index.d.cts +5 -4
- package/dist/history/index.d.ts +5 -4
- package/dist/history/index.js +6 -6
- package/dist/i18n/index.cjs.map +1 -1
- package/dist/i18n/index.d.cts +4 -3
- package/dist/i18n/index.d.ts +4 -3
- package/dist/i18n/index.js +14 -12
- package/dist/i18n/index.js.map +1 -1
- package/dist/{index-CNwA-B6-.d.ts → index-BF1B2HB9.d.ts} +53 -1
- package/dist/{index-CmVgTkqk.d.cts → index-DVkvrgpm.d.cts} +53 -1
- package/dist/index.cjs +1780 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -12
- package/dist/index.d.ts +34 -12
- package/dist/index.js +160 -8804
- package/dist/index.js.map +1 -1
- package/dist/indexing/index.cjs.map +1 -1
- package/dist/indexing/index.js +2 -2
- package/dist/issue-BAJ7ZB4S.js +12 -0
- package/dist/{ledger-3TXNP47J.js → ledger-WOEJUYTP.js} +6 -6
- package/dist/materialized-views/index.cjs.map +1 -1
- package/dist/materialized-views/index.d.cts +6 -5
- package/dist/materialized-views/index.d.ts +6 -5
- package/dist/materialized-views/index.js +6 -6
- package/dist/noydb-XNQSKXGO.js +34 -0
- package/dist/overlay-views/index.cjs.map +1 -1
- package/dist/overlay-views/index.d.cts +5 -4
- package/dist/overlay-views/index.d.ts +5 -4
- package/dist/overlay-views/index.js +6 -4
- package/dist/periods/index.cjs.map +1 -1
- package/dist/periods/index.d.cts +4 -3
- package/dist/periods/index.d.ts +4 -3
- package/dist/periods/index.js +6 -6
- package/dist/{public-envelope-PY6NKFLI.js → public-envelope-OHQ5UZFM.js} +4 -4
- package/dist/query/index.cjs.map +1 -1
- package/dist/query/index.d.cts +1 -1
- package/dist/query/index.d.ts +1 -1
- package/dist/query/index.js +3 -3
- package/dist/registry-2IEARCGT.js +7 -0
- package/dist/{registry-3L3N3PTG.js → registry-CDHASH73.js} +3 -3
- package/dist/registry-EMGLZGR6.js +8 -0
- package/dist/registry-NQALYR77.js +8 -0
- package/dist/registry-NQALYR77.js.map +1 -0
- package/dist/revoke-7JOVLZFD.js +17 -0
- package/dist/revoke-7JOVLZFD.js.map +1 -0
- package/dist/session/index.cjs.map +1 -1
- package/dist/session/index.d.cts +5 -4
- package/dist/session/index.d.ts +5 -4
- package/dist/session/index.js +3 -3
- package/dist/shadow/index.cjs.map +1 -1
- package/dist/shadow/index.d.cts +4 -3
- package/dist/shadow/index.d.ts +4 -3
- package/dist/shadow/index.js +2 -2
- package/dist/signer-M4K5HBLD.js +18 -0
- package/dist/signer-M4K5HBLD.js.map +1 -0
- package/dist/{stale-HSC5YO2O.js → stale-PAGCS4K5.js} +2 -2
- package/dist/stale-PAGCS4K5.js.map +1 -0
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +4 -3
- package/dist/store/index.d.ts +4 -3
- package/dist/store/index.js +2 -2
- package/dist/sync/index.cjs.map +1 -1
- package/dist/sync/index.d.cts +3 -2
- package/dist/sync/index.d.ts +3 -2
- package/dist/sync/index.js +4 -4
- package/dist/team/index.cjs.map +1 -1
- package/dist/team/index.d.cts +4 -3
- package/dist/team/index.d.ts +4 -3
- package/dist/team/index.js +13 -11
- package/dist/tx/index.cjs +81 -1
- package/dist/tx/index.cjs.map +1 -1
- package/dist/tx/index.d.cts +5 -4
- package/dist/tx/index.d.ts +5 -4
- package/dist/tx/index.js +56 -3
- package/dist/tx/index.js.map +1 -1
- package/dist/{types-C4lwMKKF.d.cts → types-CSLcfytP.d.cts} +644 -5
- package/dist/{types-DW9RGSSs.d.ts → types-D9eB0Rvh.d.ts} +644 -5
- package/dist/{index-4agOpzqd.d.ts → ulid-CG2YvAbg.d.cts} +51 -33
- package/dist/{index-hdFvZkBP.d.cts → ulid-CiM2OAeM.d.ts} +51 -33
- package/dist/util/index.cjs.map +1 -1
- package/dist/util/index.js +1 -1
- package/dist/{with-derivation-g-pGoMzL.d.ts → with-derivation-Bzpj6UTv.d.ts} +1 -1
- package/dist/{with-derivation-C8LDlV7t.d.cts → with-derivation-DWajFh4K.d.cts} +1 -1
- package/dist/{with-guard-jI1x9Z3k.d.cts → with-guard-DF_Ul3DT.d.cts} +1 -1
- package/dist/{with-guard-DWOCK4Ca.d.ts → with-guard-DR7U-l4v.d.ts} +1 -1
- package/dist/{with-materialized-view-DcTx4H3j.d.cts → with-materialized-view-_piodoIz.d.cts} +1 -1
- package/dist/{with-materialized-view-DaKR-N6J.d.ts → with-materialized-view-qtoJ3xKJ.d.ts} +1 -1
- package/dist/{with-overlayed-view-N7jYuNOS.d.ts → with-overlayed-view-DFaRfgMr.d.ts} +1 -1
- package/dist/{with-overlayed-view-D-6oWAgM.d.cts → with-overlayed-view-DwzCKxn2.d.cts} +1 -1
- package/package.json +15 -3
- package/dist/chunk-2AXFIYHT.js.map +0 -1
- package/dist/chunk-6HPZY4ON.js.map +0 -1
- package/dist/chunk-7H6DOO3E.js.map +0 -1
- package/dist/chunk-ADQ5MQ54.js.map +0 -1
- package/dist/chunk-CBAHB2BF.js.map +0 -1
- package/dist/chunk-NIOHFJPJ.js.map +0 -1
- package/dist/chunk-PEULZC6M.js.map +0 -1
- package/dist/chunk-VMIO4IXG.js.map +0 -1
- package/dist/chunk-YS3POABP.js.map +0 -1
- package/dist/chunk-Z72JH4KG.js.map +0 -1
- package/dist/executor-7E3VFGW7.js +0 -11
- package/dist/executor-CEWX2FQI.js +0 -8
- package/dist/executor-X4SQ3ZLC.js +0 -8
- package/dist/registry-O47PUPSY.js +0 -8
- package/dist/registry-RFGGMVNJ.js +0 -7
- package/dist/registry-WLLMODKN.js +0 -8
- /package/dist/{chunk-P7EQ2S5O.js.map → chunk-2XLVPKXG.js.map} +0 -0
- /package/dist/{chunk-23TTQXVO.js.map → chunk-4UBOTYP5.js.map} +0 -0
- /package/dist/{chunk-MKSA2V7A.js.map → chunk-5YHWBPOT.js.map} +0 -0
- /package/dist/{chunk-DYBQG5PQ.js.map → chunk-6S3LLAQ5.js.map} +0 -0
- /package/dist/{chunk-UA4RI7OT.js.map → chunk-74JEQFMT.js.map} +0 -0
- /package/dist/{chunk-UZXLQCHP.js.map → chunk-BFI3RS42.js.map} +0 -0
- /package/dist/{chunk-EGQYGYIU.js.map → chunk-EMEX37ZN.js.map} +0 -0
- /package/dist/{chunk-ZNOEIM6Y.js.map → chunk-FCDO7UAO.js.map} +0 -0
- /package/dist/{chunk-5SCJ5UEF.js.map → chunk-FS7A4XNF.js.map} +0 -0
- /package/dist/{chunk-537VFZTR.js.map → chunk-GAUBWHAF.js.map} +0 -0
- /package/dist/{chunk-FCXOFQAJ.js.map → chunk-GD3BGKAR.js.map} +0 -0
- /package/dist/{chunk-HB3Z2GCR.js.map → chunk-HGZ7DC5H.js.map} +0 -0
- /package/dist/{chunk-5DWL3JBF.js.map → chunk-K5PVGKE4.js.map} +0 -0
- /package/dist/{chunk-XGSOTWYX.js.map → chunk-KYKMKLJ6.js.map} +0 -0
- /package/dist/{chunk-4TFSM22V.js.map → chunk-LS3JLEIB.js.map} +0 -0
- /package/dist/{chunk-OMLIZL2P.js.map → chunk-NSLTPGEN.js.map} +0 -0
- /package/dist/{chunk-KESP7GOK.js.map → chunk-QAU5HM6Q.js.map} +0 -0
- /package/dist/{chunk-34YSDCDP.js.map → chunk-SAVQ6E2O.js.map} +0 -0
- /package/dist/{chunk-PA6R5ZCI.js.map → chunk-TLFUDXVV.js.map} +0 -0
- /package/dist/{chunk-WCA2NROQ.js.map → chunk-UOF74WQY.js.map} +0 -0
- /package/dist/{chunk-DYECX3IX.js.map → chunk-WRLHNG6H.js.map} +0 -0
- /package/dist/{chunk-I6MX32UC.js.map → chunk-YK72A4IT.js.map} +0 -0
- /package/dist/{chunk-RD5LYKD6.js.map → chunk-ZC2AAE6J.js.map} +0 -0
- /package/dist/{crypto-A7FRXYHC.js.map → crypto-H2Y3DDFW.js.map} +0 -0
- /package/dist/{delegation-YBA4X4JN.js.map → delegation-QSC7G5QC.js.map} +0 -0
- /package/dist/{executor-7E3VFGW7.js.map → executor-BZKFZVRC.js.map} +0 -0
- /package/dist/{executor-CEWX2FQI.js.map → executor-GFZFDQXV.js.map} +0 -0
- /package/dist/{executor-X4SQ3ZLC.js.map → executor-KT2IOZVP.js.map} +0 -0
- /package/dist/{fanout-sidecar-VJ52RIEY.js.map → fanout-sidecar-NRBWSLRK.js.map} +0 -0
- /package/dist/{ledger-3TXNP47J.js.map → issue-BAJ7ZB4S.js.map} +0 -0
- /package/dist/{public-envelope-PY6NKFLI.js.map → ledger-WOEJUYTP.js.map} +0 -0
- /package/dist/{registry-3L3N3PTG.js.map → noydb-XNQSKXGO.js.map} +0 -0
- /package/dist/{registry-O47PUPSY.js.map → public-envelope-OHQ5UZFM.js.map} +0 -0
- /package/dist/{registry-RFGGMVNJ.js.map → registry-2IEARCGT.js.map} +0 -0
- /package/dist/{registry-WLLMODKN.js.map → registry-CDHASH73.js.map} +0 -0
- /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
|
|
5593
|
-
await new Promise((r) => setTimeout(r,
|
|
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
|
-
|
|
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
|
-
|
|
6049
|
-
|
|
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'
|
|
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
|
-
|
|
6085
|
-
const
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
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
|
-
|
|
6366
|
-
formatVersion: NOYDB_BUNDLE_FORMAT_VERSION,
|
|
6870
|
+
return assembleBundleContainer({
|
|
6367
6871
|
handle,
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
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 (!
|
|
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 (
|
|
8708
|
-
const recurseSource =
|
|
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
|
|
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
|
-
/**
|
|
12376
|
-
|
|
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,
|