@noy-db/hub 0.2.0-pre.16 → 0.2.0-pre.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aggregate/index.cjs.map +1 -1
- package/dist/aggregate/index.d.cts +3 -2
- package/dist/aggregate/index.d.ts +3 -2
- package/dist/aggregate/index.js +4 -4
- package/dist/attestation/index.cjs.map +1 -1
- package/dist/attestation/index.d.cts +5 -3
- package/dist/attestation/index.d.ts +5 -3
- package/dist/attestation/index.js +6 -6
- package/dist/blobs/index.cjs +226 -11
- package/dist/blobs/index.cjs.map +1 -1
- package/dist/blobs/index.d.cts +6 -4
- package/dist/blobs/index.d.ts +6 -4
- package/dist/blobs/index.js +6 -5
- package/dist/blobs/index.js.map +1 -1
- package/dist/bundle/index.cjs +2065 -352
- package/dist/bundle/index.cjs.map +1 -1
- package/dist/bundle/index.d.cts +7 -5
- package/dist/bundle/index.d.ts +7 -5
- package/dist/bundle/index.js +21 -10
- package/dist/bundle/index.js.map +1 -1
- package/dist/{chunk-6RR3MNMG.js → chunk-2U226RDC.js} +3 -3
- package/dist/{chunk-L2BNJ6HM.js → chunk-32XVU2LT.js} +3 -3
- package/dist/{chunk-X73VS74Y.js → chunk-33DAO2XG.js} +2 -2
- package/dist/chunk-45643PAU.js +151 -0
- package/dist/chunk-45643PAU.js.map +1 -0
- package/dist/{chunk-QSUK7YWK.js → chunk-4UI5T3K7.js} +4 -4
- package/dist/{chunk-G4SCICH5.js → chunk-5KKNBDCT.js} +2 -2
- package/dist/{chunk-DUREQF5W.js → chunk-647TFNYL.js} +34 -8
- package/dist/chunk-647TFNYL.js.map +1 -0
- package/dist/{chunk-E2CDVKMH.js → chunk-6FHCU3QO.js} +5 -5
- package/dist/{chunk-F4OJZIWQ.js → chunk-6Q5XRLKG.js} +4 -4
- package/dist/{chunk-HOR4R722.js → chunk-6XEGHIBA.js} +30 -4
- package/dist/chunk-6XEGHIBA.js.map +1 -0
- package/dist/{chunk-4TBBMHVC.js → chunk-6YEC7LLO.js} +2 -2
- package/dist/{chunk-ZNQYHJXX.js → chunk-AB7JF2KF.js} +2 -2
- package/dist/{chunk-UMLVJTYV.js → chunk-ADB7GPM3.js} +7 -4
- package/dist/chunk-ADB7GPM3.js.map +1 -0
- package/dist/{chunk-XL35NSEN.js → chunk-BUBJYIZ7.js} +3 -3
- package/dist/chunk-C2OYWD5S.js +125 -0
- package/dist/chunk-C2OYWD5S.js.map +1 -0
- package/dist/{chunk-KABJXG2F.js → chunk-CMISAJAE.js} +195 -17
- package/dist/chunk-CMISAJAE.js.map +1 -0
- package/dist/{chunk-3YWP3WBP.js → chunk-DKMPR76W.js} +5 -5
- package/dist/{chunk-BI6ETQPF.js → chunk-DR5I7Q6N.js} +4 -4
- package/dist/{chunk-667MB6AH.js → chunk-F2IJ2HGD.js} +1370 -232
- package/dist/chunk-F2IJ2HGD.js.map +1 -0
- package/dist/{chunk-6H2ZUNR7.js → chunk-FQRAYDS4.js} +4 -4
- package/dist/{chunk-535SSHBS.js → chunk-HMFC6M2G.js} +99 -2
- package/dist/chunk-HMFC6M2G.js.map +1 -0
- package/dist/{chunk-TS26M2SB.js → chunk-HOO5I3VG.js} +2 -2
- package/dist/{chunk-OMAMZKKD.js → chunk-HWK75CYX.js} +2 -2
- package/dist/{chunk-TKIY625R.js → chunk-HZOEBM67.js} +2 -2
- package/dist/{chunk-DLZ2ONOD.js → chunk-IQ4GMEYZ.js} +6 -6
- package/dist/{chunk-XWH4MXIU.js → chunk-K3NYRK7U.js} +2 -2
- package/dist/{chunk-7BQ4QWYX.js → chunk-KOURQXIU.js} +23 -6
- package/dist/chunk-KOURQXIU.js.map +1 -0
- package/dist/{chunk-7Z7KSVA5.js → chunk-KQ523X3A.js} +15 -2
- package/dist/chunk-KQ523X3A.js.map +1 -0
- package/dist/{chunk-JD3OZAI4.js → chunk-KTZ2MHQK.js} +2 -2
- package/dist/{chunk-F3BPIPLS.js → chunk-LGPSCKWZ.js} +1 -1
- package/dist/chunk-LGPSCKWZ.js.map +1 -0
- package/dist/{chunk-SCJPI4Z5.js → chunk-LQ3GD5LL.js} +5 -5
- package/dist/{chunk-AAVWKNZW.js → chunk-M3H7VSRV.js} +2 -2
- package/dist/{chunk-BR3AMFGS.js → chunk-MGB67HKX.js} +5 -5
- package/dist/{chunk-GNI5STXQ.js → chunk-P57D4KBG.js} +52 -38
- package/dist/chunk-P57D4KBG.js.map +1 -0
- package/dist/{chunk-Z6FNBOTC.js → chunk-PDVP3C2I.js} +1 -1
- package/dist/{chunk-Z6FNBOTC.js.map → chunk-PDVP3C2I.js.map} +1 -1
- package/dist/{chunk-OB2ZJQ2D.js → chunk-PGVEL5IZ.js} +3 -3
- package/dist/{chunk-YULZKK4F.js → chunk-QJKZ5WUP.js} +37 -2
- package/dist/chunk-QJKZ5WUP.js.map +1 -0
- package/dist/{chunk-BQ65SS5A.js → chunk-QPJ7Z4L3.js} +2 -2
- package/dist/{chunk-CZI2A4MQ.js → chunk-RQFG2YSV.js} +3 -3
- package/dist/{chunk-CJORTUJ2.js → chunk-RZWQNMMP.js} +2 -2
- package/dist/{chunk-FFXM3ZIF.js → chunk-T4T5I5L6.js} +3 -3
- package/dist/{chunk-QVIEAYTP.js → chunk-TFAN3NFD.js} +3 -3
- package/dist/{chunk-Z4DO7YSI.js → chunk-TPOHMOGX.js} +2 -2
- package/dist/{chunk-VLMPU56Q.js → chunk-TTS3RWL5.js} +2 -2
- package/dist/{chunk-IXBIFDEW.js → chunk-VVDSDOVV.js} +4 -4
- package/dist/{chunk-FWPKCXTN.js → chunk-WZCG3EZ6.js} +2 -2
- package/dist/{chunk-HBXJ37ZY.js → chunk-Y5XVB75E.js} +4 -4
- package/dist/chunk-YWYW2YNO.js +129 -0
- package/dist/chunk-YWYW2YNO.js.map +1 -0
- package/dist/{chunk-IQLVUT37.js → chunk-Z3BE5BRK.js} +2 -2
- package/dist/{chunk-42FEUPZQ.js → chunk-Z3I2WNGF.js} +58 -3
- package/dist/chunk-Z3I2WNGF.js.map +1 -0
- package/dist/{state-vault-TMXZRTY5.js → chunk-ZJ67TB4S.js} +24 -7
- package/dist/chunk-ZJ67TB4S.js.map +1 -0
- package/dist/consent/index.cjs.map +1 -1
- package/dist/consent/index.d.cts +6 -4
- package/dist/consent/index.d.ts +6 -4
- package/dist/consent/index.js +3 -3
- package/dist/{crypto-QXQOHMHF.js → crypto-FNK3XPCS.js} +7 -3
- package/dist/{delegation-NIQ43IPU.js → delegation-FMXNUWE6.js} +5 -5
- package/dist/derivations/index.cjs +82 -2
- package/dist/derivations/index.cjs.map +1 -1
- package/dist/derivations/index.d.cts +7 -5
- package/dist/derivations/index.d.ts +7 -5
- package/dist/derivations/index.js +8 -6
- package/dist/{dev-unlock-8XzcD2Z4.d.cts → dev-unlock-3_2b_vo6.d.cts} +1 -1
- package/dist/{dev-unlock-DR3upLd1.d.ts → dev-unlock-BMvwPr_E.d.ts} +1 -1
- package/dist/{strategy-BtW8fAjz.d.cts → errors-DUTlAt3Y.d.cts} +113 -727
- package/dist/{strategy-BtW8fAjz.d.ts → errors-DUTlAt3Y.d.ts} +113 -727
- package/dist/executor-IZ2NVXCY.js +11 -0
- package/dist/executor-THSEYEJG.js +8 -0
- package/dist/executor-WLFDUTOM.js +8 -0
- package/dist/{fanout-sidecar-67CMI3UT.js → fanout-sidecar-JGHXAJO5.js} +2 -2
- package/dist/forget/index.cjs +43 -0
- package/dist/forget/index.cjs.map +1 -0
- package/dist/forget/index.d.cts +1 -0
- package/dist/forget/index.d.ts +1 -0
- package/dist/forget/index.js +14 -0
- package/dist/guards/index.cjs +80 -3
- package/dist/guards/index.cjs.map +1 -1
- package/dist/guards/index.d.cts +7 -5
- package/dist/guards/index.d.ts +7 -5
- package/dist/guards/index.js +10 -6
- package/dist/{hash-CDjye9KV.d.ts → hash-BThBJFO1.d.ts} +1 -1
- package/dist/{hash-DuQ88_5W.d.cts → hash-BnWnL9bQ.d.cts} +1 -1
- package/dist/history/index.cjs +27 -4
- package/dist/history/index.cjs.map +1 -1
- package/dist/history/index.d.cts +7 -5
- package/dist/history/index.d.ts +7 -5
- package/dist/history/index.js +9 -7
- package/dist/history/index.js.map +1 -1
- package/dist/i18n/index.cjs +53 -0
- package/dist/i18n/index.cjs.map +1 -1
- package/dist/i18n/index.d.cts +6 -4
- package/dist/i18n/index.d.ts +6 -4
- package/dist/i18n/index.js +16 -8
- package/dist/i18n/index.js.map +1 -1
- package/dist/index-C-SSRIxP.d.cts +348 -0
- package/dist/index-C-SSRIxP.d.ts +348 -0
- package/dist/{index-C8Bk3-VF.d.cts → index-C6lgoUhK.d.cts} +47 -2
- package/dist/{index-nP99bXLg.d.ts → index-DP1JTWHZ.d.ts} +47 -2
- package/dist/index.cjs +3280 -1208
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -12
- package/dist/index.d.ts +15 -12
- package/dist/index.js +149 -107
- package/dist/index.js.map +1 -1
- package/dist/indexing/index.cjs.map +1 -1
- package/dist/indexing/index.js +4 -4
- package/dist/issue-R2MWQO6K.js +12 -0
- package/dist/{ledger-A3LL253R.js → ledger-GXC2YA3A.js} +6 -6
- package/dist/materialized-views/index.cjs.map +1 -1
- package/dist/materialized-views/index.d.cts +7 -5
- package/dist/materialized-views/index.d.ts +7 -5
- package/dist/materialized-views/index.js +12 -12
- package/dist/noydb-RJL6FQ4B.js +37 -0
- package/dist/overlay-views/index.cjs.map +1 -1
- package/dist/overlay-views/index.d.cts +7 -5
- package/dist/overlay-views/index.d.ts +7 -5
- package/dist/overlay-views/index.js +4 -4
- package/dist/periods/index.cjs.map +1 -1
- package/dist/periods/index.d.cts +6 -4
- package/dist/periods/index.d.ts +6 -4
- package/dist/periods/index.js +6 -6
- package/dist/{public-envelope-YP2UWMLG.js → public-envelope-HXOFHY4N.js} +4 -4
- package/dist/query/index.cjs +30 -4
- package/dist/query/index.cjs.map +1 -1
- package/dist/query/index.d.cts +3 -2
- package/dist/query/index.d.ts +3 -2
- package/dist/query/index.js +6 -6
- package/dist/read-only-facade-EX6WZZBP.js +7 -0
- package/dist/registry-3T2RZC5A.js +8 -0
- package/dist/registry-DMS7OKBM.js +8 -0
- package/dist/{registry-UTA4CLQS.js → registry-WVXO6NH5.js} +3 -3
- package/dist/{revoke-HNMQZSCL.js → revoke-7LCWE2AH.js} +6 -6
- package/dist/sealed-record/index.cjs +139 -0
- package/dist/sealed-record/index.cjs.map +1 -0
- package/dist/sealed-record/index.d.cts +123 -0
- package/dist/sealed-record/index.d.ts +123 -0
- package/dist/sealed-record/index.js +42 -0
- package/dist/sealed-record/index.js.map +1 -0
- package/dist/session/index.cjs.map +1 -1
- package/dist/session/index.d.cts +7 -5
- package/dist/session/index.d.ts +7 -5
- package/dist/session/index.js +3 -3
- package/dist/shadow/index.cjs.map +1 -1
- package/dist/shadow/index.d.cts +6 -4
- package/dist/shadow/index.d.ts +6 -4
- package/dist/shadow/index.js +2 -2
- package/dist/{signer-DCMNKXSF.js → signer-HAVDLGOK.js} +5 -5
- package/dist/snapshots/index.cjs.map +1 -1
- package/dist/snapshots/index.d.cts +6 -4
- package/dist/snapshots/index.d.ts +6 -4
- package/dist/snapshots/index.js +4 -4
- package/dist/{stale-W5PQTRYH.js → stale-PGTEGJDI.js} +2 -2
- package/dist/stale-PGTEGJDI.js.map +1 -0
- package/dist/state-vault-QKQKN3H3.js +14 -0
- package/dist/state-vault-QKQKN3H3.js.map +1 -0
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +6 -4
- package/dist/store/index.d.ts +6 -4
- package/dist/store/index.js +2 -2
- package/dist/strategy-Diwh5lzS.d.ts +739 -0
- package/dist/strategy-nuyN8K5N.d.cts +739 -0
- package/dist/sync/index.cjs.map +1 -1
- package/dist/sync/index.d.cts +5 -3
- package/dist/sync/index.d.ts +5 -3
- package/dist/sync/index.js +4 -4
- package/dist/team/index.cjs.map +1 -1
- package/dist/team/index.d.cts +6 -4
- package/dist/team/index.d.ts +6 -4
- package/dist/team/index.js +8 -8
- package/dist/transition-guard--t3exQHF.d.cts +165 -0
- package/dist/transition-guard-BlI9Oy5K.d.ts +165 -0
- package/dist/tx/index.cjs.map +1 -1
- package/dist/tx/index.d.cts +6 -4
- package/dist/tx/index.d.ts +6 -4
- package/dist/tx/index.js +3 -3
- package/dist/{types-Bze6vkwm.d.cts → types-BpLPqyaO.d.cts} +1264 -513
- package/dist/{types-DrmBTscX.d.ts → types-Diqc2caK.d.ts} +1264 -513
- package/dist/{ulid-DbBVrNSt.d.ts → ulid-B1zNV8r9.d.ts} +1 -1
- package/dist/{ulid-DfZlAh0u.d.cts → ulid-DNiRB4Mx.d.cts} +1 -1
- package/dist/util/index.cjs.map +1 -1
- package/dist/util/index.js +1 -1
- package/dist/{vault-group-DX2HFQMX.js → vault-group-DPZVFRI5.js} +182 -6
- package/dist/vault-group-DPZVFRI5.js.map +1 -0
- package/dist/{with-materialized-view--4PsvMDu.d.cts → with-materialized-view-BdH_A_r6.d.cts} +1 -1
- package/dist/{with-materialized-view-QT1Tp7NO.d.ts → with-materialized-view-CzAgp_HJ.d.ts} +1 -1
- package/dist/{with-overlayed-view-BEXfpzSb.d.ts → with-overlayed-view-BJbqQnsR.d.ts} +1 -1
- package/dist/{with-overlayed-view-DlH5qmeB.d.cts → with-overlayed-view-C40rDPlu.d.cts} +1 -1
- package/dist/with-rollup-Bopu5UDZ.d.cts +47 -0
- package/dist/with-rollup-DrlGkxiE.d.ts +47 -0
- package/package.json +23 -3
- package/dist/chunk-42FEUPZQ.js.map +0 -1
- package/dist/chunk-535SSHBS.js.map +0 -1
- package/dist/chunk-667MB6AH.js.map +0 -1
- package/dist/chunk-7BQ4QWYX.js.map +0 -1
- package/dist/chunk-7Z7KSVA5.js.map +0 -1
- package/dist/chunk-DUREQF5W.js.map +0 -1
- package/dist/chunk-F3BPIPLS.js.map +0 -1
- package/dist/chunk-GNI5STXQ.js.map +0 -1
- package/dist/chunk-HOR4R722.js.map +0 -1
- package/dist/chunk-KABJXG2F.js.map +0 -1
- package/dist/chunk-OQSRJG6A.js +0 -63
- package/dist/chunk-OQSRJG6A.js.map +0 -1
- package/dist/chunk-UMLVJTYV.js.map +0 -1
- package/dist/chunk-YULZKK4F.js.map +0 -1
- package/dist/executor-6ZDSDZ6V.js +0 -8
- package/dist/executor-AZLS3KBK.js +0 -11
- package/dist/executor-IDZDAFNH.js +0 -8
- package/dist/immutable-guard-CRPvu24K.d.cts +0 -82
- package/dist/immutable-guard-Dov3WvwF.d.ts +0 -82
- package/dist/issue-RZP3VI6O.js +0 -12
- package/dist/noydb-WCMY2ZOW.js +0 -35
- package/dist/read-only-facade-ITU6L7BL.js +0 -7
- package/dist/registry-EB6SISTA.js +0 -8
- package/dist/registry-IUZQVVBB.js +0 -8
- package/dist/state-vault-TMXZRTY5.js.map +0 -1
- package/dist/vault-group-DX2HFQMX.js.map +0 -1
- package/dist/with-derivation-CCqAchD5.d.cts +0 -13
- package/dist/with-derivation-_lySGdlm.d.ts +0 -13
- /package/dist/{chunk-6RR3MNMG.js.map → chunk-2U226RDC.js.map} +0 -0
- /package/dist/{chunk-L2BNJ6HM.js.map → chunk-32XVU2LT.js.map} +0 -0
- /package/dist/{chunk-X73VS74Y.js.map → chunk-33DAO2XG.js.map} +0 -0
- /package/dist/{chunk-QSUK7YWK.js.map → chunk-4UI5T3K7.js.map} +0 -0
- /package/dist/{chunk-G4SCICH5.js.map → chunk-5KKNBDCT.js.map} +0 -0
- /package/dist/{chunk-E2CDVKMH.js.map → chunk-6FHCU3QO.js.map} +0 -0
- /package/dist/{chunk-F4OJZIWQ.js.map → chunk-6Q5XRLKG.js.map} +0 -0
- /package/dist/{chunk-4TBBMHVC.js.map → chunk-6YEC7LLO.js.map} +0 -0
- /package/dist/{chunk-ZNQYHJXX.js.map → chunk-AB7JF2KF.js.map} +0 -0
- /package/dist/{chunk-XL35NSEN.js.map → chunk-BUBJYIZ7.js.map} +0 -0
- /package/dist/{chunk-3YWP3WBP.js.map → chunk-DKMPR76W.js.map} +0 -0
- /package/dist/{chunk-BI6ETQPF.js.map → chunk-DR5I7Q6N.js.map} +0 -0
- /package/dist/{chunk-6H2ZUNR7.js.map → chunk-FQRAYDS4.js.map} +0 -0
- /package/dist/{chunk-TS26M2SB.js.map → chunk-HOO5I3VG.js.map} +0 -0
- /package/dist/{chunk-OMAMZKKD.js.map → chunk-HWK75CYX.js.map} +0 -0
- /package/dist/{chunk-TKIY625R.js.map → chunk-HZOEBM67.js.map} +0 -0
- /package/dist/{chunk-DLZ2ONOD.js.map → chunk-IQ4GMEYZ.js.map} +0 -0
- /package/dist/{chunk-XWH4MXIU.js.map → chunk-K3NYRK7U.js.map} +0 -0
- /package/dist/{chunk-JD3OZAI4.js.map → chunk-KTZ2MHQK.js.map} +0 -0
- /package/dist/{chunk-SCJPI4Z5.js.map → chunk-LQ3GD5LL.js.map} +0 -0
- /package/dist/{chunk-AAVWKNZW.js.map → chunk-M3H7VSRV.js.map} +0 -0
- /package/dist/{chunk-BR3AMFGS.js.map → chunk-MGB67HKX.js.map} +0 -0
- /package/dist/{chunk-OB2ZJQ2D.js.map → chunk-PGVEL5IZ.js.map} +0 -0
- /package/dist/{chunk-BQ65SS5A.js.map → chunk-QPJ7Z4L3.js.map} +0 -0
- /package/dist/{chunk-CZI2A4MQ.js.map → chunk-RQFG2YSV.js.map} +0 -0
- /package/dist/{chunk-CJORTUJ2.js.map → chunk-RZWQNMMP.js.map} +0 -0
- /package/dist/{chunk-FFXM3ZIF.js.map → chunk-T4T5I5L6.js.map} +0 -0
- /package/dist/{chunk-QVIEAYTP.js.map → chunk-TFAN3NFD.js.map} +0 -0
- /package/dist/{chunk-Z4DO7YSI.js.map → chunk-TPOHMOGX.js.map} +0 -0
- /package/dist/{chunk-VLMPU56Q.js.map → chunk-TTS3RWL5.js.map} +0 -0
- /package/dist/{chunk-IXBIFDEW.js.map → chunk-VVDSDOVV.js.map} +0 -0
- /package/dist/{chunk-FWPKCXTN.js.map → chunk-WZCG3EZ6.js.map} +0 -0
- /package/dist/{chunk-HBXJ37ZY.js.map → chunk-Y5XVB75E.js.map} +0 -0
- /package/dist/{chunk-IQLVUT37.js.map → chunk-Z3BE5BRK.js.map} +0 -0
- /package/dist/{crypto-QXQOHMHF.js.map → crypto-FNK3XPCS.js.map} +0 -0
- /package/dist/{delegation-NIQ43IPU.js.map → delegation-FMXNUWE6.js.map} +0 -0
- /package/dist/{executor-6ZDSDZ6V.js.map → executor-IZ2NVXCY.js.map} +0 -0
- /package/dist/{executor-AZLS3KBK.js.map → executor-THSEYEJG.js.map} +0 -0
- /package/dist/{executor-IDZDAFNH.js.map → executor-WLFDUTOM.js.map} +0 -0
- /package/dist/{fanout-sidecar-67CMI3UT.js.map → fanout-sidecar-JGHXAJO5.js.map} +0 -0
- /package/dist/{issue-RZP3VI6O.js.map → forget/index.js.map} +0 -0
- /package/dist/{ledger-A3LL253R.js.map → issue-R2MWQO6K.js.map} +0 -0
- /package/dist/{noydb-WCMY2ZOW.js.map → ledger-GXC2YA3A.js.map} +0 -0
- /package/dist/{public-envelope-YP2UWMLG.js.map → noydb-RJL6FQ4B.js.map} +0 -0
- /package/dist/{read-only-facade-ITU6L7BL.js.map → public-envelope-HXOFHY4N.js.map} +0 -0
- /package/dist/{registry-EB6SISTA.js.map → read-only-facade-EX6WZZBP.js.map} +0 -0
- /package/dist/{registry-IUZQVVBB.js.map → registry-3T2RZC5A.js.map} +0 -0
- /package/dist/{registry-UTA4CLQS.js.map → registry-DMS7OKBM.js.map} +0 -0
- /package/dist/{revoke-HNMQZSCL.js.map → registry-WVXO6NH5.js.map} +0 -0
- /package/dist/{signer-DCMNKXSF.js.map → revoke-7LCWE2AH.js.map} +0 -0
- /package/dist/{stale-W5PQTRYH.js.map → signer-HAVDLGOK.js.map} +0 -0
|
@@ -7,37 +7,68 @@ import {
|
|
|
7
7
|
import {
|
|
8
8
|
TxContext,
|
|
9
9
|
revertExecuted
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-TFAN3NFD.js";
|
|
11
11
|
import {
|
|
12
12
|
OverlayedCollection
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-TTS3RWL5.js";
|
|
14
|
+
import {
|
|
15
|
+
NO_AGGREGATE,
|
|
16
|
+
Query,
|
|
17
|
+
ScanBuilder,
|
|
18
|
+
canonicalizeIncomingMoney,
|
|
19
|
+
canonicalizeStoredMoney,
|
|
20
|
+
decodeMoneyFields,
|
|
21
|
+
quantizeMoneyFields,
|
|
22
|
+
validateMoneyFieldPaths
|
|
23
|
+
} from "./chunk-647TFNYL.js";
|
|
24
|
+
import {
|
|
25
|
+
EXPORT_AUDIT_COLLECTION,
|
|
26
|
+
createExportBlobsHandle,
|
|
27
|
+
runCompaction
|
|
28
|
+
} from "./chunk-BUBJYIZ7.js";
|
|
14
29
|
import {
|
|
15
30
|
LazyQuery,
|
|
16
31
|
decodeIdxId,
|
|
17
32
|
encodeIdxId
|
|
18
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-RQFG2YSV.js";
|
|
34
|
+
import {
|
|
35
|
+
canonicalGroupKey
|
|
36
|
+
} from "./chunk-32XVU2LT.js";
|
|
37
|
+
import {
|
|
38
|
+
readPath
|
|
39
|
+
} from "./chunk-RZWQNMMP.js";
|
|
19
40
|
import {
|
|
20
41
|
SCHEMAS_COLLECTION,
|
|
21
42
|
loadPersistedSchema,
|
|
22
43
|
resolveManagedSecret,
|
|
23
44
|
savePersistedSchema,
|
|
24
45
|
saveSealedPassphrase
|
|
25
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-P57D4KBG.js";
|
|
26
47
|
import {
|
|
27
48
|
loadPublicEnvelope,
|
|
28
49
|
readPublicEnvelope,
|
|
29
50
|
savePublicEnvelope,
|
|
30
51
|
validatePublicEnvelopeInput
|
|
31
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-PGVEL5IZ.js";
|
|
53
|
+
import {
|
|
54
|
+
buildTombstone,
|
|
55
|
+
isTombstone,
|
|
56
|
+
resolveStableCek,
|
|
57
|
+
revokeSealedRecord,
|
|
58
|
+
rewrapBodyToDek,
|
|
59
|
+
rotateRecordCek,
|
|
60
|
+
sealRecordToHost
|
|
61
|
+
} from "./chunk-45643PAU.js";
|
|
32
62
|
import {
|
|
33
63
|
PERIODS_COLLECTION
|
|
34
|
-
} from "./chunk-
|
|
64
|
+
} from "./chunk-4UI5T3K7.js";
|
|
35
65
|
import {
|
|
36
66
|
getAtPath,
|
|
37
67
|
isDictCollectionName,
|
|
68
|
+
isStaticDictDescriptor,
|
|
38
69
|
resolvePolicy,
|
|
39
70
|
setAtPathInPlace
|
|
40
|
-
} from "./chunk-
|
|
71
|
+
} from "./chunk-KOURQXIU.js";
|
|
41
72
|
import {
|
|
42
73
|
ManagedRecoveryNotEnrolledError,
|
|
43
74
|
PolicyDeniedError,
|
|
@@ -59,11 +90,11 @@ import {
|
|
|
59
90
|
saveShamirRecoveryEntries,
|
|
60
91
|
updateAuthenticator,
|
|
61
92
|
writeMagicLinkGrant
|
|
62
|
-
} from "./chunk-
|
|
93
|
+
} from "./chunk-IQ4GMEYZ.js";
|
|
63
94
|
import {
|
|
64
95
|
assertTierAccess,
|
|
65
96
|
dekKey
|
|
66
|
-
} from "./chunk-
|
|
97
|
+
} from "./chunk-6YEC7LLO.js";
|
|
67
98
|
import {
|
|
68
99
|
USER_ENVELOPE_COLLECTION,
|
|
69
100
|
assertKeyringOpenAllowed,
|
|
@@ -88,7 +119,7 @@ import {
|
|
|
88
119
|
rotateKeys,
|
|
89
120
|
saveUserEnvelope,
|
|
90
121
|
updateKeyringIdentity
|
|
91
|
-
} from "./chunk-
|
|
122
|
+
} from "./chunk-FQRAYDS4.js";
|
|
92
123
|
import {
|
|
93
124
|
INDEXED_STORE_POLICY
|
|
94
125
|
} from "./chunk-2QR2PQTT.js";
|
|
@@ -98,49 +129,41 @@ import {
|
|
|
98
129
|
import {
|
|
99
130
|
LEDGER_COLLECTION,
|
|
100
131
|
LEDGER_DELTAS_COLLECTION
|
|
101
|
-
} from "./chunk-
|
|
132
|
+
} from "./chunk-MGB67HKX.js";
|
|
102
133
|
import {
|
|
103
134
|
sha256Hex as sha256Hex2
|
|
104
|
-
} from "./chunk-
|
|
105
|
-
import {
|
|
106
|
-
NO_AGGREGATE,
|
|
107
|
-
Query,
|
|
108
|
-
ScanBuilder,
|
|
109
|
-
canonicalizeIncomingMoney,
|
|
110
|
-
canonicalizeStoredMoney,
|
|
111
|
-
decodeMoneyFields,
|
|
112
|
-
quantizeMoneyFields,
|
|
113
|
-
validateMoneyFieldPaths
|
|
114
|
-
} from "./chunk-DUREQF5W.js";
|
|
115
|
-
import {
|
|
116
|
-
canonicalGroupKey
|
|
117
|
-
} from "./chunk-L2BNJ6HM.js";
|
|
135
|
+
} from "./chunk-PDVP3C2I.js";
|
|
118
136
|
import {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
137
|
+
NO_FORGET,
|
|
138
|
+
addSubjectRef,
|
|
139
|
+
coerceSubjectId,
|
|
140
|
+
lookupSubject,
|
|
141
|
+
readDottedPath,
|
|
142
|
+
rebuildSubjectIndex,
|
|
143
|
+
removeSubjectRef
|
|
144
|
+
} from "./chunk-C2OYWD5S.js";
|
|
126
145
|
import {
|
|
127
146
|
NOYDB_BACKUP_VERSION,
|
|
128
147
|
NOYDB_FORMAT_VERSION
|
|
129
|
-
} from "./chunk-
|
|
148
|
+
} from "./chunk-LGPSCKWZ.js";
|
|
130
149
|
import {
|
|
131
150
|
decrypt,
|
|
132
151
|
encrypt,
|
|
133
152
|
encryptDeterministic,
|
|
134
|
-
sha256Hex
|
|
135
|
-
|
|
153
|
+
sha256Hex,
|
|
154
|
+
unwrapCek,
|
|
155
|
+
wrapCek
|
|
156
|
+
} from "./chunk-QJKZ5WUP.js";
|
|
136
157
|
import {
|
|
137
158
|
AlreadyElevatedError,
|
|
138
159
|
AttestationError,
|
|
139
160
|
BackupCorruptedError,
|
|
140
161
|
BackupLedgerError,
|
|
141
162
|
ConflictError,
|
|
163
|
+
DerivationCapExceededError,
|
|
142
164
|
ElevationExpiredError,
|
|
143
165
|
ExportCapabilityError,
|
|
166
|
+
ForgetStrategyNotConfiguredError,
|
|
144
167
|
ImportCapabilityError,
|
|
145
168
|
IndexWriteFailureError,
|
|
146
169
|
InvalidKeyError,
|
|
@@ -159,15 +182,17 @@ import {
|
|
|
159
182
|
SchemaValidationError,
|
|
160
183
|
SequenceContentionError,
|
|
161
184
|
SequenceOfflineError,
|
|
185
|
+
StaticDictReadonlyError,
|
|
162
186
|
StoreCapabilityError,
|
|
163
187
|
TierDemoteDeniedError,
|
|
164
188
|
TierNotGrantedError,
|
|
165
189
|
TranslatorNotConfiguredError,
|
|
166
190
|
UniqueConstraintError,
|
|
191
|
+
UnknownDictCodeError,
|
|
167
192
|
UnsupportedIndexOptionError,
|
|
168
193
|
ValidationError,
|
|
169
194
|
VaultTemplateNotFoundError
|
|
170
|
-
} from "./chunk-
|
|
195
|
+
} from "./chunk-HMFC6M2G.js";
|
|
171
196
|
|
|
172
197
|
// src/policy/storage.ts
|
|
173
198
|
var META_COLLECTION = "_meta";
|
|
@@ -450,6 +475,9 @@ var NO_HISTORY = {
|
|
|
450
475
|
async clearHistory() {
|
|
451
476
|
return 0;
|
|
452
477
|
},
|
|
478
|
+
async tombstoneHistory() {
|
|
479
|
+
return 0;
|
|
480
|
+
},
|
|
453
481
|
async envelopePayloadHash() {
|
|
454
482
|
return "";
|
|
455
483
|
},
|
|
@@ -810,7 +838,7 @@ async function resolveStaleOnRead(accessor, outputCollection, id) {
|
|
|
810
838
|
}
|
|
811
839
|
const sourceWithId = { ...source, id };
|
|
812
840
|
if (DerivationExecutor === null) {
|
|
813
|
-
({ DerivationExecutor } = await import("./executor-
|
|
841
|
+
({ DerivationExecutor } = await import("./executor-WLFDUTOM.js"));
|
|
814
842
|
}
|
|
815
843
|
const ctx = { vault: accessor.getReadOnlyFacade() };
|
|
816
844
|
const result = await DerivationExecutor.run(spec, sourceWithId, 0, strategyHash, ctx);
|
|
@@ -848,6 +876,15 @@ async function resolveStaleOnRead(accessor, outputCollection, id) {
|
|
|
848
876
|
}
|
|
849
877
|
|
|
850
878
|
// src/collection.ts
|
|
879
|
+
function selfWriteFieldEqual(a, b) {
|
|
880
|
+
if (a === b) return true;
|
|
881
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
882
|
+
try {
|
|
883
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
884
|
+
} catch {
|
|
885
|
+
return false;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
851
888
|
var fallbackWarned = /* @__PURE__ */ new Set();
|
|
852
889
|
function warnOnceFallback(adapterName) {
|
|
853
890
|
if (fallbackWarned.has(adapterName)) return;
|
|
@@ -1035,6 +1072,25 @@ var Collection = class {
|
|
|
1035
1072
|
* is inactive for this collection; a frozen `Set` otherwise.
|
|
1036
1073
|
*/
|
|
1037
1074
|
deterministicFields;
|
|
1075
|
+
/**
|
|
1076
|
+
* Per-record CEK opt-in (`perRecordKeys: true`). When set, writes mint /
|
|
1077
|
+
* reuse a per-record content-encryption key and stamp `_cek` on the
|
|
1078
|
+
* envelope (see {@link EncryptedEnvelope._cek}). OFF by default — a
|
|
1079
|
+
* non-adopting collection takes the byte-identical legacy path. The READ
|
|
1080
|
+
* path does not consult this flag: `_cek` presence on the envelope is the
|
|
1081
|
+
* format discriminant, so a mixed vault (and a recipient that never set the
|
|
1082
|
+
* flag) still decrypts CEK records.
|
|
1083
|
+
*/
|
|
1084
|
+
perRecordCek;
|
|
1085
|
+
/**
|
|
1086
|
+
* Session-scoped `(id) → CEK` cache for this collection. Lets updates
|
|
1087
|
+
* reuse a record's stable CEK and lets repeated reads skip the AES-KW
|
|
1088
|
+
* unwrap. Bounded by LRU; never persisted. Dropped when the owning
|
|
1089
|
+
* collection instance is discarded — `vault.load()` clears the
|
|
1090
|
+
* collectionCache, so a keyring refresh drops every CEK alongside the
|
|
1091
|
+
* DEK cache. `null` unless `perRecordCek` is set.
|
|
1092
|
+
*/
|
|
1093
|
+
cekCache;
|
|
1038
1094
|
/**
|
|
1039
1095
|
* declared tiers for this collection. `null` when
|
|
1040
1096
|
* tier-aware methods are disabled. Tier 0 is implicit and never
|
|
@@ -1181,19 +1237,24 @@ var Collection = class {
|
|
|
1181
1237
|
} else {
|
|
1182
1238
|
this.deterministicFields = null;
|
|
1183
1239
|
}
|
|
1240
|
+
this.perRecordCek = opts.perRecordKeys === true;
|
|
1241
|
+
this.cekCache = this.perRecordCek ? new Lru({ maxRecords: 4096 }) : null;
|
|
1184
1242
|
if (opts.crdt && opts.onRegisterConflictResolver) {
|
|
1185
1243
|
const crdtMode = opts.crdt;
|
|
1186
|
-
const crdtResolver = async (
|
|
1244
|
+
const crdtResolver = async (id, local, remote) => {
|
|
1187
1245
|
if (crdtMode === "yjs") {
|
|
1188
1246
|
return local._v >= remote._v ? local : remote;
|
|
1189
1247
|
}
|
|
1190
|
-
const localJson = await this.decryptJsonString(local);
|
|
1191
|
-
const remoteJson = await this.decryptJsonString(remote);
|
|
1248
|
+
const localJson = await this.decryptJsonString(local, id);
|
|
1249
|
+
const remoteJson = await this.decryptJsonString(remote, id);
|
|
1250
|
+
if (localJson === null) return local;
|
|
1251
|
+
if (remoteJson === null) return remote;
|
|
1192
1252
|
const localState = JSON.parse(localJson);
|
|
1193
1253
|
const remoteState = JSON.parse(remoteJson);
|
|
1194
1254
|
const merged = this.crdtStrategy.mergeCrdtStates(localState, remoteState);
|
|
1195
1255
|
const mergedVersion = Math.max(local._v, remote._v) + 1;
|
|
1196
|
-
|
|
1256
|
+
const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
|
|
1257
|
+
return this.encryptJsonString(JSON.stringify(merged), mergedVersion, cek);
|
|
1197
1258
|
};
|
|
1198
1259
|
opts.onRegisterConflictResolver(this.name, crdtResolver);
|
|
1199
1260
|
}
|
|
@@ -1233,12 +1294,15 @@ var Collection = class {
|
|
|
1233
1294
|
});
|
|
1234
1295
|
} else {
|
|
1235
1296
|
const mergeFn = policy;
|
|
1236
|
-
resolver = async (
|
|
1237
|
-
const localRecord = await this.decryptRecord(local, { skipValidation: true });
|
|
1238
|
-
const remoteRecord = await this.decryptRecord(remote, { skipValidation: true });
|
|
1297
|
+
resolver = async (id, local, remote) => {
|
|
1298
|
+
const localRecord = await this.decryptRecord(local, { skipValidation: true, id });
|
|
1299
|
+
const remoteRecord = await this.decryptRecord(remote, { skipValidation: true, id });
|
|
1300
|
+
if (localRecord === null) return local;
|
|
1301
|
+
if (remoteRecord === null) return remote;
|
|
1239
1302
|
const merged = mergeFn(localRecord, remoteRecord);
|
|
1240
1303
|
const mergedVersion = Math.max(local._v, remote._v) + 1;
|
|
1241
|
-
|
|
1304
|
+
const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
|
|
1305
|
+
return this.encryptRecord(merged, mergedVersion, cek);
|
|
1242
1306
|
};
|
|
1243
1307
|
}
|
|
1244
1308
|
opts.onRegisterConflictResolver(collectionName, resolver);
|
|
@@ -1319,7 +1383,7 @@ var Collection = class {
|
|
|
1319
1383
|
}
|
|
1320
1384
|
}
|
|
1321
1385
|
if (this.materializedViewSource !== void 0) {
|
|
1322
|
-
const { resolveStaleMVOnRead } = await import("./stale-
|
|
1386
|
+
const { resolveStaleMVOnRead } = await import("./stale-PGTEGJDI.js");
|
|
1323
1387
|
await resolveStaleMVOnRead(this.materializedViewSource, this.name);
|
|
1324
1388
|
}
|
|
1325
1389
|
let record;
|
|
@@ -1330,7 +1394,9 @@ var Collection = class {
|
|
|
1330
1394
|
} else {
|
|
1331
1395
|
const envelope = await this.adapter.get(this.vault, this.name, id);
|
|
1332
1396
|
if (!envelope) return null;
|
|
1333
|
-
|
|
1397
|
+
if (isTombstone(envelope, this.encrypted)) return null;
|
|
1398
|
+
record = await this.decryptRecord(envelope, { id });
|
|
1399
|
+
if (record === null) return null;
|
|
1334
1400
|
this.lru.set(id, { record, version: envelope._v }, estimateRecordBytes(record));
|
|
1335
1401
|
}
|
|
1336
1402
|
} else {
|
|
@@ -1357,6 +1423,7 @@ var Collection = class {
|
|
|
1357
1423
|
const envelope = await this.adapter.get(this.vault, this.name, id);
|
|
1358
1424
|
if (!envelope) return null;
|
|
1359
1425
|
const json = await this.decryptJsonString(envelope);
|
|
1426
|
+
if (json === null) return null;
|
|
1360
1427
|
return JSON.parse(json);
|
|
1361
1428
|
}
|
|
1362
1429
|
/**
|
|
@@ -1445,7 +1512,7 @@ var Collection = class {
|
|
|
1445
1512
|
if (cached2) return { record: cached2.record, version: cached2.version };
|
|
1446
1513
|
const env = await this.adapter.get(this.vault, this.name, id);
|
|
1447
1514
|
if (!env) return { record: null, version: 0 };
|
|
1448
|
-
return { record: await this.decryptRecord(env, { skipValidation: true }), version: env._v };
|
|
1515
|
+
return { record: await this.decryptRecord(env, { skipValidation: true }) ?? null, version: env._v };
|
|
1449
1516
|
}
|
|
1450
1517
|
await this.ensureHydrated();
|
|
1451
1518
|
const cached = this.cache.get(id);
|
|
@@ -1558,9 +1625,11 @@ var Collection = class {
|
|
|
1558
1625
|
let existingState;
|
|
1559
1626
|
if (existingEnvelope) {
|
|
1560
1627
|
const prevJson = await this.decryptJsonString(existingEnvelope);
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1628
|
+
if (prevJson !== null) {
|
|
1629
|
+
const prevParsed = JSON.parse(prevJson);
|
|
1630
|
+
if (prevParsed !== null && typeof prevParsed === "object" && "_crdt" in prevParsed) {
|
|
1631
|
+
existingState = prevParsed;
|
|
1632
|
+
}
|
|
1564
1633
|
}
|
|
1565
1634
|
}
|
|
1566
1635
|
crdtState = this.crdtStrategy.buildLwwMapState(record, existingState, now);
|
|
@@ -1568,9 +1637,11 @@ var Collection = class {
|
|
|
1568
1637
|
let existingState;
|
|
1569
1638
|
if (existingEnvelope) {
|
|
1570
1639
|
const prevJson = await this.decryptJsonString(existingEnvelope);
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1640
|
+
if (prevJson !== null) {
|
|
1641
|
+
const prevParsed = JSON.parse(prevJson);
|
|
1642
|
+
if (prevParsed !== null && typeof prevParsed === "object" && "_crdt" in prevParsed) {
|
|
1643
|
+
existingState = prevParsed;
|
|
1644
|
+
}
|
|
1574
1645
|
}
|
|
1575
1646
|
}
|
|
1576
1647
|
const arr = Array.isArray(record) ? record : [record];
|
|
@@ -1579,12 +1650,14 @@ var Collection = class {
|
|
|
1579
1650
|
crdtState = { _crdt: "yjs", update: record };
|
|
1580
1651
|
}
|
|
1581
1652
|
const version2 = existingVersion + 1;
|
|
1582
|
-
const
|
|
1653
|
+
const cek2 = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
|
|
1654
|
+
const envelope2 = await this.encryptJsonString(JSON.stringify(crdtState), version2, cek2);
|
|
1583
1655
|
await this.adapter.put(this.vault, this.name, id, envelope2);
|
|
1584
1656
|
const resolvedRecord = this.crdtStrategy.resolveCrdtSnapshot(crdtState);
|
|
1585
|
-
const
|
|
1657
|
+
const existingResolvedRecord = existingEnvelope ? await this.decryptRecord(existingEnvelope, { skipValidation: true }) : null;
|
|
1658
|
+
const existingResolved = existingResolvedRecord !== null ? { record: existingResolvedRecord, version: existingVersion } : void 0;
|
|
1586
1659
|
if (existingResolved && this.historyConfig.enabled !== false) {
|
|
1587
|
-
const histEnvelope = await this.encryptRecord(existingResolved.record, existingResolved.version);
|
|
1660
|
+
const histEnvelope = await this.encryptRecord(existingResolved.record, existingResolved.version, cek2);
|
|
1588
1661
|
await this.historyStrategy.saveHistory(this.adapter, this.vault, this.name, id, histEnvelope);
|
|
1589
1662
|
this.emitter.emit("history:save", { vault: this.vault, collection: this.name, id, version: existingResolved.version });
|
|
1590
1663
|
if (this.historyConfig.maxVersions) {
|
|
@@ -1630,7 +1703,9 @@ var Collection = class {
|
|
|
1630
1703
|
const previousEnvelope = await this.adapter.get(this.vault, this.name, id);
|
|
1631
1704
|
if (previousEnvelope) {
|
|
1632
1705
|
const previousRecord = await this.decryptRecord(previousEnvelope);
|
|
1633
|
-
|
|
1706
|
+
if (previousRecord !== null) {
|
|
1707
|
+
existing = { record: previousRecord, version: previousEnvelope._v };
|
|
1708
|
+
}
|
|
1634
1709
|
}
|
|
1635
1710
|
}
|
|
1636
1711
|
} else {
|
|
@@ -1639,8 +1714,9 @@ var Collection = class {
|
|
|
1639
1714
|
}
|
|
1640
1715
|
const version = existing ? existing.version + 1 : 1;
|
|
1641
1716
|
this.uniqueConstraints?.check(id, record);
|
|
1717
|
+
const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
|
|
1642
1718
|
if (existing && this.historyConfig.enabled !== false) {
|
|
1643
|
-
const historyEnvelope = await this.encryptRecord(existing.record, existing.version);
|
|
1719
|
+
const historyEnvelope = await this.encryptRecord(existing.record, existing.version, cek);
|
|
1644
1720
|
await this.historyStrategy.saveHistory(this.adapter, this.vault, this.name, id, historyEnvelope);
|
|
1645
1721
|
this.emitter.emit("history:save", {
|
|
1646
1722
|
vault: this.vault,
|
|
@@ -1654,7 +1730,7 @@ var Collection = class {
|
|
|
1654
1730
|
});
|
|
1655
1731
|
}
|
|
1656
1732
|
}
|
|
1657
|
-
const envelope = await this.encryptRecord(record, version);
|
|
1733
|
+
const envelope = await this.encryptRecord(record, version, cek);
|
|
1658
1734
|
await this.adapter.put(this.vault, this.name, id, envelope);
|
|
1659
1735
|
if (this.ledger) {
|
|
1660
1736
|
const appendInput = {
|
|
@@ -1717,7 +1793,7 @@ var Collection = class {
|
|
|
1717
1793
|
if (mode === "eager") {
|
|
1718
1794
|
if (executor === null) {
|
|
1719
1795
|
;
|
|
1720
|
-
({ MaterializedViewExecutor: executor } = await import("./executor-
|
|
1796
|
+
({ MaterializedViewExecutor: executor } = await import("./executor-IZ2NVXCY.js"));
|
|
1721
1797
|
}
|
|
1722
1798
|
await executor.refresh(reg, {
|
|
1723
1799
|
getCollection: (name) => this.materializedViewSource.getCollection(name),
|
|
@@ -1726,7 +1802,7 @@ var Collection = class {
|
|
|
1726
1802
|
});
|
|
1727
1803
|
} else if (mode === "lazy") {
|
|
1728
1804
|
if (staleHelpers === null) {
|
|
1729
|
-
staleHelpers = await import("./stale-
|
|
1805
|
+
staleHelpers = await import("./stale-PGTEGJDI.js");
|
|
1730
1806
|
}
|
|
1731
1807
|
staleHelpers.markMVStale(registry, reg.spec.name);
|
|
1732
1808
|
}
|
|
@@ -1743,6 +1819,111 @@ var Collection = class {
|
|
|
1743
1819
|
* output (carries `_derivedFrom`) — defensive guard against missed
|
|
1744
1820
|
* cycle detection.
|
|
1745
1821
|
*/
|
|
1822
|
+
/**
|
|
1823
|
+
* @internal #376 — the RAW stored record (canonical-money form, i18n maps
|
|
1824
|
+
* intact), WITHOUT the locale resolution `get()` applies. Used as the
|
|
1825
|
+
* patch base for self-write reverse-denorm so writing back never clobbers
|
|
1826
|
+
* an i18n map or re-quantizes money incorrectly. Returns null for
|
|
1827
|
+
* missing / tombstoned records.
|
|
1828
|
+
*/
|
|
1829
|
+
async _getStoredRecord(id) {
|
|
1830
|
+
let raw;
|
|
1831
|
+
if (this.lazy && this.lru) {
|
|
1832
|
+
const cached = this.lru.get(id);
|
|
1833
|
+
if (cached) raw = cached.record;
|
|
1834
|
+
else {
|
|
1835
|
+
const env = await this.adapter.get(this.vault, this.name, id);
|
|
1836
|
+
if (!env || isTombstone(env, this.encrypted)) return null;
|
|
1837
|
+
raw = await this.decryptRecord(env, { id });
|
|
1838
|
+
if (raw === null) return null;
|
|
1839
|
+
this.lru.set(id, { record: raw, version: env._v }, estimateRecordBytes(raw));
|
|
1840
|
+
}
|
|
1841
|
+
} else {
|
|
1842
|
+
await this.ensureHydrated();
|
|
1843
|
+
raw = this.cache.get(id)?.record ?? null;
|
|
1844
|
+
}
|
|
1845
|
+
if (raw === null) return null;
|
|
1846
|
+
return canonicalizeStoredMoney(raw, this.moneyFields);
|
|
1847
|
+
}
|
|
1848
|
+
/**
|
|
1849
|
+
* @internal #376 — ids of records whose top-level `field` equals `value`.
|
|
1850
|
+
* Uses the FK index when the field is indexed (O(matches)); otherwise a
|
|
1851
|
+
* linear scan (O(N) — fine for small child sets; index the FK to scale).
|
|
1852
|
+
*/
|
|
1853
|
+
async _findMatchingIds(field, value) {
|
|
1854
|
+
const hit = this.getIndexes()?.lookupEqual(field, value);
|
|
1855
|
+
if (hit) return [...hit];
|
|
1856
|
+
const target = String(value);
|
|
1857
|
+
const matches = (rec) => {
|
|
1858
|
+
const fv = rec[field];
|
|
1859
|
+
return (typeof fv === "string" || typeof fv === "number") && String(fv) === target;
|
|
1860
|
+
};
|
|
1861
|
+
if (!this.lazy) {
|
|
1862
|
+
await this.ensureHydrated();
|
|
1863
|
+
const out2 = [];
|
|
1864
|
+
for (const [rid, e] of this.cache) {
|
|
1865
|
+
if (matches(e.record)) out2.push(rid);
|
|
1866
|
+
}
|
|
1867
|
+
return out2;
|
|
1868
|
+
}
|
|
1869
|
+
const ids = await this.adapter.list(this.vault, this.name);
|
|
1870
|
+
const out = [];
|
|
1871
|
+
for (const rid of ids) {
|
|
1872
|
+
const raw = await this._getStoredRecord(rid);
|
|
1873
|
+
if (raw !== null && matches(raw)) out.push(rid);
|
|
1874
|
+
}
|
|
1875
|
+
return out;
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* @internal #376 slice 2 — recompute a rollup aggregate onto the parent.
|
|
1879
|
+
* Gathers every child of `parentId`, runs `compute`, and patches only the
|
|
1880
|
+
* rollup `field` onto the parent's raw stored record (value-equality
|
|
1881
|
+
* guarded). No-op when the parent record does not exist.
|
|
1882
|
+
*/
|
|
1883
|
+
async recomputeRollup(spec, parentId) {
|
|
1884
|
+
if (this.derivationSource === void 0 || spec.rollup === void 0) return;
|
|
1885
|
+
const { from, key, field, compute } = spec.rollup;
|
|
1886
|
+
const into = spec.source;
|
|
1887
|
+
const intoColl = this.derivationSource.getCollection(into);
|
|
1888
|
+
const base = await intoColl._getStoredRecord(parentId);
|
|
1889
|
+
if (base === null) return;
|
|
1890
|
+
const fromColl = this.derivationSource.getCollection(from);
|
|
1891
|
+
const childIds = await fromColl._findMatchingIds(key, parentId);
|
|
1892
|
+
const children = [];
|
|
1893
|
+
for (const cid of childIds) {
|
|
1894
|
+
const c = await fromColl.get(cid);
|
|
1895
|
+
if (c !== null && c !== void 0) children.push(c);
|
|
1896
|
+
}
|
|
1897
|
+
const newValue = compute(children);
|
|
1898
|
+
if (selfWriteFieldEqual(base[field], newValue)) return;
|
|
1899
|
+
const patched = { ...base, [field]: newValue };
|
|
1900
|
+
const txCtx = this.derivationSource.getActiveTxContext();
|
|
1901
|
+
if (txCtx !== null) {
|
|
1902
|
+
const prior = await this.adapter.get(this.vault, into, parentId);
|
|
1903
|
+
txCtx._executed.push({
|
|
1904
|
+
op: { type: "put", vaultName: this.vault, collectionName: into, id: parentId },
|
|
1905
|
+
priorEnvelope: prior
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
await intoColl.put(parentId, patched);
|
|
1909
|
+
}
|
|
1910
|
+
/**
|
|
1911
|
+
* @internal #376 slice 2 — fire any rollups for which THIS collection is the
|
|
1912
|
+
* child `from`, recomputing the affected parent after a child delete. Called
|
|
1913
|
+
* from the delete path with the just-removed record's key value. Other
|
|
1914
|
+
* derivation kinds do not react to deletes (unchanged).
|
|
1915
|
+
*/
|
|
1916
|
+
async dispatchRollupsOnDelete(deleted) {
|
|
1917
|
+
if (this.derivationSource === void 0) return;
|
|
1918
|
+
const registry = this.derivationSource.registry();
|
|
1919
|
+
const rec = deleted;
|
|
1920
|
+
for (const { spec } of registry.strategiesForSource(this.name)) {
|
|
1921
|
+
if (!spec.rollup || spec.rollup.from !== this.name) continue;
|
|
1922
|
+
const kv = rec[spec.rollup.key];
|
|
1923
|
+
if (typeof kv !== "string" && typeof kv !== "number") continue;
|
|
1924
|
+
await this.recomputeRollup(spec, String(kv));
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1746
1927
|
async dispatchDerivations(id, record, version) {
|
|
1747
1928
|
if (this.derivationSource === void 0) return;
|
|
1748
1929
|
const incoming = canonicalizeStoredMoney(record, this.moneyFields);
|
|
@@ -1753,29 +1934,60 @@ var Collection = class {
|
|
|
1753
1934
|
let DerivationExecutor = null;
|
|
1754
1935
|
for (const { spec, strategyHash } of strategies) {
|
|
1755
1936
|
const mode = typeof spec.lifecycle === "string" ? spec.lifecycle : spec.lifecycle.mode;
|
|
1756
|
-
if (
|
|
1757
|
-
if (
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
if (spec.source === this.name) {
|
|
1763
|
-
sourceWithId = { ...incoming, id };
|
|
1937
|
+
if (spec.rollup) {
|
|
1938
|
+
if (mode !== "eager") continue;
|
|
1939
|
+
let parentId;
|
|
1940
|
+
if (this.name === spec.rollup.from) {
|
|
1941
|
+
const kv = incoming[spec.rollup.key];
|
|
1942
|
+
parentId = typeof kv === "string" || typeof kv === "number" ? String(kv) : null;
|
|
1764
1943
|
} else {
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1944
|
+
parentId = id;
|
|
1945
|
+
}
|
|
1946
|
+
if (parentId !== null) await this.recomputeRollup(spec, parentId);
|
|
1947
|
+
continue;
|
|
1948
|
+
}
|
|
1949
|
+
const isSource = spec.source === this.name;
|
|
1950
|
+
const isSibling = !isSource && (spec.sources?.includes(this.name) ?? false);
|
|
1951
|
+
const trigger = !isSource && !isSibling ? spec.triggerBy?.find((t) => t.collection === this.name) : void 0;
|
|
1952
|
+
const runs = [];
|
|
1953
|
+
if (isSource) {
|
|
1954
|
+
runs.push({ input: { ...incoming, id }, base: incoming, runId: id, version });
|
|
1955
|
+
} else if (isSibling) {
|
|
1956
|
+
const p = await this.derivationSource.getCollection(spec.source).get(id);
|
|
1957
|
+
if (p !== null && p !== void 0) {
|
|
1958
|
+
const raw = await this.derivationSource.getCollection(spec.source)._getStoredRecord(id);
|
|
1959
|
+
runs.push({ input: { ...p, id }, base: raw ?? p, runId: id, version: 0 });
|
|
1960
|
+
}
|
|
1961
|
+
} else if (trigger) {
|
|
1962
|
+
const srcColl = this.derivationSource.getCollection(spec.source);
|
|
1963
|
+
const ids = await srcColl._findMatchingIds(trigger.on, id);
|
|
1964
|
+
if (trigger.maxFanout !== void 0 && ids.length > trigger.maxFanout) {
|
|
1965
|
+
throw new DerivationCapExceededError(`triggerBy ${this.name}\u2192${spec.source}`, ids.length, trigger.maxFanout);
|
|
1966
|
+
}
|
|
1967
|
+
for (const sid of ids) {
|
|
1968
|
+
const raw = await srcColl._getStoredRecord(sid);
|
|
1969
|
+
if (raw === null) continue;
|
|
1970
|
+
runs.push({ input: { ...raw, id: sid }, base: raw, runId: sid, version: 0 });
|
|
1769
1971
|
}
|
|
1972
|
+
}
|
|
1973
|
+
if (runs.length === 0) continue;
|
|
1974
|
+
if (mode !== "eager") {
|
|
1975
|
+
for (const run of runs) await markStale(registry, spec, run.runId);
|
|
1976
|
+
continue;
|
|
1977
|
+
}
|
|
1978
|
+
if (DerivationExecutor === null) {
|
|
1979
|
+
({ DerivationExecutor } = await import("./executor-WLFDUTOM.js"));
|
|
1980
|
+
}
|
|
1981
|
+
for (const run of runs) {
|
|
1770
1982
|
const ctx = { vault: this.derivationSource.getReadOnlyFacade() };
|
|
1771
|
-
const result = await DerivationExecutor.run(spec,
|
|
1983
|
+
const result = await DerivationExecutor.run(spec, run.input, run.version, strategyHash, ctx);
|
|
1772
1984
|
for (const key of Object.keys(spec.outputs)) {
|
|
1773
1985
|
const out = result.outputs[key];
|
|
1774
1986
|
if (!out) continue;
|
|
1775
1987
|
if (out.kind === "failed") {
|
|
1776
1988
|
const err = out.error;
|
|
1777
1989
|
if (spec.strict) throw err;
|
|
1778
|
-
console.warn(`[derivation] output "${key}" for source "${spec.source}" id="${
|
|
1990
|
+
console.warn(`[derivation] output "${key}" for source "${spec.source}" id="${run.runId}" failed:`, err);
|
|
1779
1991
|
continue;
|
|
1780
1992
|
}
|
|
1781
1993
|
const outSpec = spec.outputs[key];
|
|
@@ -1783,12 +1995,12 @@ var Collection = class {
|
|
|
1783
1995
|
const outputCollection = this.derivationSource.getCollection(outSpec.collection);
|
|
1784
1996
|
const txCtx = this.derivationSource.getActiveTxContext();
|
|
1785
1997
|
if (out.kind === "array") {
|
|
1786
|
-
const { loadFanoutSidecar, saveFanoutSidecar } = await import("./fanout-sidecar-
|
|
1998
|
+
const { loadFanoutSidecar, saveFanoutSidecar } = await import("./fanout-sidecar-JGHXAJO5.js");
|
|
1787
1999
|
const prior = await loadFanoutSidecar(
|
|
1788
2000
|
this.adapter,
|
|
1789
2001
|
this.vault,
|
|
1790
2002
|
spec.source,
|
|
1791
|
-
|
|
2003
|
+
run.runId,
|
|
1792
2004
|
key
|
|
1793
2005
|
);
|
|
1794
2006
|
const prevKeys = new Set(prior?.keys ?? []);
|
|
@@ -1815,7 +2027,7 @@ var Collection = class {
|
|
|
1815
2027
|
}
|
|
1816
2028
|
await saveFanoutSidecar(this.adapter, this.vault, {
|
|
1817
2029
|
source: spec.source,
|
|
1818
|
-
sourceId:
|
|
2030
|
+
sourceId: run.runId,
|
|
1819
2031
|
outputKey: key,
|
|
1820
2032
|
outputCollection: outSpec.collection,
|
|
1821
2033
|
keys: newKeysList
|
|
@@ -1823,25 +2035,44 @@ var Collection = class {
|
|
|
1823
2035
|
continue;
|
|
1824
2036
|
}
|
|
1825
2037
|
if (out.skipped === true) {
|
|
1826
|
-
await outputCollection._internalDelete(
|
|
2038
|
+
await outputCollection._internalDelete(run.runId, txCtx);
|
|
2039
|
+
continue;
|
|
2040
|
+
}
|
|
2041
|
+
if (outSpec.shape === "record" && outSpec.denorm !== void 0 && outSpec.collection === spec.source) {
|
|
2042
|
+
const value = out.value;
|
|
2043
|
+
const patched = { ...run.base };
|
|
2044
|
+
let changed = false;
|
|
2045
|
+
for (const f of outSpec.denorm) {
|
|
2046
|
+
if (!selfWriteFieldEqual(run.base[f], value[f])) {
|
|
2047
|
+
patched[f] = value[f];
|
|
2048
|
+
changed = true;
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
if (!changed) continue;
|
|
2052
|
+
if (txCtx !== null) {
|
|
2053
|
+
const prior = await this.adapter.get(this.vault, outSpec.collection, run.runId);
|
|
2054
|
+
txCtx._executed.push({
|
|
2055
|
+
op: { type: "put", vaultName: this.vault, collectionName: outSpec.collection, id: run.runId },
|
|
2056
|
+
priorEnvelope: prior
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2059
|
+
await outputCollection.put(run.runId, patched);
|
|
1827
2060
|
continue;
|
|
1828
2061
|
}
|
|
1829
2062
|
if (txCtx !== null) {
|
|
1830
|
-
const prior = await this.adapter.get(this.vault, outSpec.collection,
|
|
2063
|
+
const prior = await this.adapter.get(this.vault, outSpec.collection, run.runId);
|
|
1831
2064
|
txCtx._executed.push({
|
|
1832
2065
|
op: {
|
|
1833
2066
|
type: "put",
|
|
1834
2067
|
vaultName: this.vault,
|
|
1835
2068
|
collectionName: outSpec.collection,
|
|
1836
|
-
id
|
|
2069
|
+
id: run.runId
|
|
1837
2070
|
},
|
|
1838
2071
|
priorEnvelope: prior
|
|
1839
2072
|
});
|
|
1840
2073
|
}
|
|
1841
|
-
await outputCollection.put(
|
|
2074
|
+
await outputCollection.put(run.runId, out.value);
|
|
1842
2075
|
}
|
|
1843
|
-
} else {
|
|
1844
|
-
await markStale(registry, spec, id);
|
|
1845
2076
|
}
|
|
1846
2077
|
}
|
|
1847
2078
|
}
|
|
@@ -1890,11 +2121,14 @@ var Collection = class {
|
|
|
1890
2121
|
let count = 0;
|
|
1891
2122
|
for (const id of ids) {
|
|
1892
2123
|
const env = await this.adapter.get(this.vault, this.name, id);
|
|
1893
|
-
if (!env) continue;
|
|
1894
|
-
const
|
|
2124
|
+
if (!env || isTombstone(env, this.encrypted)) continue;
|
|
2125
|
+
const decoded = await this.decryptRecord(env, { skipValidation: true, id });
|
|
2126
|
+
if (decoded === null) continue;
|
|
2127
|
+
const record = decoded;
|
|
1895
2128
|
const next = transform(record);
|
|
1896
2129
|
const nextVersion = (env._v ?? 0) + 1;
|
|
1897
|
-
const
|
|
2130
|
+
const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
|
|
2131
|
+
const newEnv = await this.encryptRecord(next, nextVersion, cek);
|
|
1898
2132
|
await this.adapter.put(this.vault, this.name, id, newEnv);
|
|
1899
2133
|
await this._invalidateCacheEntry(id);
|
|
1900
2134
|
if (this.ledger) {
|
|
@@ -2006,14 +2240,17 @@ var Collection = class {
|
|
|
2006
2240
|
const previousEnvelope2 = await this.adapter.get(this.vault, this.name, id);
|
|
2007
2241
|
if (previousEnvelope2) {
|
|
2008
2242
|
const previousRecord = await this.decryptRecord(previousEnvelope2);
|
|
2009
|
-
|
|
2243
|
+
if (previousRecord !== null) {
|
|
2244
|
+
existing = { record: previousRecord, version: previousEnvelope2._v };
|
|
2245
|
+
}
|
|
2010
2246
|
}
|
|
2011
2247
|
}
|
|
2012
2248
|
} else {
|
|
2013
2249
|
existing = this.cache.get(id);
|
|
2014
2250
|
}
|
|
2015
2251
|
if (existing && this.historyConfig.enabled !== false) {
|
|
2016
|
-
const
|
|
2252
|
+
const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
|
|
2253
|
+
const historyEnvelope = await this.encryptRecord(existing.record, existing.version, cek);
|
|
2017
2254
|
await this.historyStrategy.saveHistory(this.adapter, this.vault, this.name, id, historyEnvelope);
|
|
2018
2255
|
}
|
|
2019
2256
|
const previousEnvelope = await this.adapter.get(this.vault, this.name, id);
|
|
@@ -2052,8 +2289,53 @@ var Collection = class {
|
|
|
2052
2289
|
if (!internal) {
|
|
2053
2290
|
await this.dispatchMaterializedViewsOnDelete(id);
|
|
2054
2291
|
await this.dispatchArrayDerivationsOnDelete(id);
|
|
2292
|
+
if (existing) await this.dispatchRollupsOnDelete(existing.record);
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
/**
|
|
2296
|
+
* @internal — GDPR crypto-shred a LIVE record to a tombstone (#304).
|
|
2297
|
+
*
|
|
2298
|
+
* Rewrites the on-disk envelope to `{ _noydb, _v, _ts, _by, _iv:'', _data:'' }`,
|
|
2299
|
+
* dropping `_iv`/`_data`/`_cek`/`_det`. The wrapped per-record CEK is gone, so
|
|
2300
|
+
* the body — and (via {@link tombstoneHistory}) every history version under
|
|
2301
|
+
* the same CEK — is permanently undecryptable; the collection DEK and every
|
|
2302
|
+
* other record are untouched. `_det` is stripped too, so `findByDet` no
|
|
2303
|
+
* longer matches the shredded record (avoiding a post-shred TamperedError).
|
|
2304
|
+
*
|
|
2305
|
+
* Unlike `delete()`/`_internalDelete`, this:
|
|
2306
|
+
* - does NOT fire onDelete guards / MV / derivation dispatch (a shred is an
|
|
2307
|
+
* erasure, not a domain delete — re-running those would be wrong),
|
|
2308
|
+
* - does NOT append a per-record ledger entry (`vault.forget()` appends a
|
|
2309
|
+
* single `op:'forget'` summary for the whole subject),
|
|
2310
|
+
* - keeps the record KEY present (it's an overwrite, not an adapter delete)
|
|
2311
|
+
* so the version counter + "record existed" survive for audit.
|
|
2312
|
+
*
|
|
2313
|
+
* Idempotent: returns `null` when the record is absent or already a tombstone.
|
|
2314
|
+
* Otherwise returns `{ previousVersion }`. Invalidates the eager cache, the
|
|
2315
|
+
* lazy LRU, and the per-record CEK cache for this id.
|
|
2316
|
+
*/
|
|
2317
|
+
/**
|
|
2318
|
+
* @internal — decrypt an envelope to a plain record for subject-index
|
|
2319
|
+
* rebuild (#304). Returns `null` for a tombstone or unreadable envelope.
|
|
2320
|
+
* Skips schema validation — the rebuild only reads the subject field.
|
|
2321
|
+
*/
|
|
2322
|
+
async _decodeEnvelope(envelope, id) {
|
|
2323
|
+
try {
|
|
2324
|
+
const rec = await this.decryptRecord(envelope, { skipValidation: true, id });
|
|
2325
|
+
return rec === null ? null : rec;
|
|
2326
|
+
} catch {
|
|
2327
|
+
return null;
|
|
2055
2328
|
}
|
|
2056
2329
|
}
|
|
2330
|
+
async _writeTombstone(id, actor) {
|
|
2331
|
+
const live = await this.adapter.get(this.vault, this.name, id);
|
|
2332
|
+
if (!live || isTombstone(live, this.encrypted)) return null;
|
|
2333
|
+
await this.adapter.put(this.vault, this.name, id, buildTombstone(live._v, actor));
|
|
2334
|
+
this.cache.delete(id);
|
|
2335
|
+
this.lru?.remove(id);
|
|
2336
|
+
this.cekCache?.remove(id);
|
|
2337
|
+
return { previousVersion: live._v };
|
|
2338
|
+
}
|
|
2057
2339
|
/**
|
|
2058
2340
|
* Cascade deletes of array-shape derived rows when a source row is
|
|
2059
2341
|
* deleted. Reads each registered strategy's fanout sidecar
|
|
@@ -2076,7 +2358,7 @@ var Collection = class {
|
|
|
2076
2358
|
for (const [outputKey, outSpec] of Object.entries(spec.outputs)) {
|
|
2077
2359
|
if (outSpec.shape !== "array") continue;
|
|
2078
2360
|
if (helpers === null) {
|
|
2079
|
-
helpers = await import("./fanout-sidecar-
|
|
2361
|
+
helpers = await import("./fanout-sidecar-JGHXAJO5.js");
|
|
2080
2362
|
}
|
|
2081
2363
|
const sidecar = await helpers.loadFanoutSidecar(
|
|
2082
2364
|
this.adapter,
|
|
@@ -2116,7 +2398,7 @@ var Collection = class {
|
|
|
2116
2398
|
if (mode === "eager") {
|
|
2117
2399
|
if (executor === null) {
|
|
2118
2400
|
;
|
|
2119
|
-
({ MaterializedViewExecutor: executor } = await import("./executor-
|
|
2401
|
+
({ MaterializedViewExecutor: executor } = await import("./executor-IZ2NVXCY.js"));
|
|
2120
2402
|
}
|
|
2121
2403
|
await executor.refresh(reg, {
|
|
2122
2404
|
getCollection: (name) => this.materializedViewSource.getCollection(name),
|
|
@@ -2125,7 +2407,7 @@ var Collection = class {
|
|
|
2125
2407
|
});
|
|
2126
2408
|
} else if (mode === "lazy") {
|
|
2127
2409
|
if (staleHelpers === null) {
|
|
2128
|
-
staleHelpers = await import("./stale-
|
|
2410
|
+
staleHelpers = await import("./stale-PGTEGJDI.js");
|
|
2129
2411
|
}
|
|
2130
2412
|
staleHelpers.markMVStale(registry, reg.spec.name);
|
|
2131
2413
|
}
|
|
@@ -2148,7 +2430,7 @@ var Collection = class {
|
|
|
2148
2430
|
);
|
|
2149
2431
|
}
|
|
2150
2432
|
if (this.materializedViewSource !== void 0) {
|
|
2151
|
-
const { resolveStaleMVOnRead } = await import("./stale-
|
|
2433
|
+
const { resolveStaleMVOnRead } = await import("./stale-PGTEGJDI.js");
|
|
2152
2434
|
await resolveStaleMVOnRead(this.materializedViewSource, this.name);
|
|
2153
2435
|
}
|
|
2154
2436
|
await this.ensureHydrated();
|
|
@@ -2466,6 +2748,7 @@ var Collection = class {
|
|
|
2466
2748
|
const entries = [];
|
|
2467
2749
|
for (const env of envelopes) {
|
|
2468
2750
|
const record = await this.decryptRecord(env, { skipValidation: true });
|
|
2751
|
+
if (record === null) continue;
|
|
2469
2752
|
entries.push({
|
|
2470
2753
|
version: env._v,
|
|
2471
2754
|
timestamp: env._ts,
|
|
@@ -2602,6 +2885,7 @@ var Collection = class {
|
|
|
2602
2885
|
const envelope = await this.adapter.get(this.vault, this.name, id);
|
|
2603
2886
|
if (envelope) {
|
|
2604
2887
|
const record = await this.decryptRecord(envelope);
|
|
2888
|
+
if (record === null) continue;
|
|
2605
2889
|
items.push(record);
|
|
2606
2890
|
if (!this.lazy && !this.cache.has(id)) {
|
|
2607
2891
|
this.cache.set(id, { record, version: envelope._v });
|
|
@@ -2678,6 +2962,7 @@ var Collection = class {
|
|
|
2678
2962
|
const out = [];
|
|
2679
2963
|
for (const { id, envelope } of items) {
|
|
2680
2964
|
const record = await this.decryptRecord(envelope);
|
|
2965
|
+
if (record === null) continue;
|
|
2681
2966
|
out.push({ id, record, version: envelope._v });
|
|
2682
2967
|
}
|
|
2683
2968
|
return out;
|
|
@@ -2699,6 +2984,18 @@ var Collection = class {
|
|
|
2699
2984
|
* the cache entry (record still present) or deletes it (record was
|
|
2700
2985
|
* gone before the tx and the revert deleted it again).
|
|
2701
2986
|
*/
|
|
2987
|
+
/**
|
|
2988
|
+
* @internal — evict ONLY the per-record CEK cache entry for `id`. Used by
|
|
2989
|
+
* `vault.rotateRecordCek()`: after a hard CEK rotation the cached unwrapped
|
|
2990
|
+
* CEK is stale (it would decrypt the pre-rotation body and fail GCM auth on
|
|
2991
|
+
* the post-rotation body). Eviction must be synchronous with the live-envelope
|
|
2992
|
+
* rewrite so no concurrent read observes the old CEK. Paired with
|
|
2993
|
+
* {@link _invalidateCacheEntry} (which refreshes the decrypted-record cache).
|
|
2994
|
+
* No-op when the collection is not `perRecordKeys`.
|
|
2995
|
+
*/
|
|
2996
|
+
_invalidateCekCacheEntry(id) {
|
|
2997
|
+
this.cekCache?.remove(id);
|
|
2998
|
+
}
|
|
2702
2999
|
async _invalidateCacheEntry(id) {
|
|
2703
3000
|
if (this.lazy && this.lru) {
|
|
2704
3001
|
this.lru.remove(id);
|
|
@@ -2716,6 +3013,14 @@ var Collection = class {
|
|
|
2716
3013
|
return;
|
|
2717
3014
|
}
|
|
2718
3015
|
const record = await this.decryptRecord(envelope);
|
|
3016
|
+
if (record === null) {
|
|
3017
|
+
this.cache.delete(id);
|
|
3018
|
+
if (previous) {
|
|
3019
|
+
this.indexes?.remove(id, previous.record);
|
|
3020
|
+
this.uniqueConstraints?.remove(id, previous.record);
|
|
3021
|
+
}
|
|
3022
|
+
return;
|
|
3023
|
+
}
|
|
2719
3024
|
this.cache.set(id, { record, version: envelope._v });
|
|
2720
3025
|
this.indexes?.upsert(id, record, previous ? previous.record : null);
|
|
2721
3026
|
this.uniqueConstraints?.upsert(id, record, previous?.record);
|
|
@@ -2740,8 +3045,9 @@ var Collection = class {
|
|
|
2740
3045
|
const ids = await this.adapter.list(this.vault, this.name);
|
|
2741
3046
|
for (const id of ids) {
|
|
2742
3047
|
const envelope = await this.adapter.get(this.vault, this.name, id);
|
|
2743
|
-
if (envelope) {
|
|
2744
|
-
const record = await this.decryptRecord(envelope);
|
|
3048
|
+
if (envelope && !isTombstone(envelope, this.encrypted)) {
|
|
3049
|
+
const record = await this.decryptRecord(envelope, { id });
|
|
3050
|
+
if (record === null) continue;
|
|
2745
3051
|
this.cache.set(id, { record, version: envelope._v });
|
|
2746
3052
|
}
|
|
2747
3053
|
}
|
|
@@ -2752,7 +3058,9 @@ var Collection = class {
|
|
|
2752
3058
|
/** Hydrate from a pre-loaded snapshot (used by Vault). */
|
|
2753
3059
|
async hydrateFromSnapshot(records) {
|
|
2754
3060
|
for (const [id, envelope] of Object.entries(records)) {
|
|
2755
|
-
|
|
3061
|
+
if (isTombstone(envelope, this.encrypted)) continue;
|
|
3062
|
+
const record = await this.decryptRecord(envelope, { id });
|
|
3063
|
+
if (record === null) continue;
|
|
2756
3064
|
this.cache.set(id, { record, version: envelope._v });
|
|
2757
3065
|
}
|
|
2758
3066
|
this.hydrated = true;
|
|
@@ -2840,6 +3148,7 @@ var Collection = class {
|
|
|
2840
3148
|
const envelope = await this.adapter.get(this.vault, this.name, recordId);
|
|
2841
3149
|
if (!envelope) continue;
|
|
2842
3150
|
const record = await this.decryptRecord(envelope, { skipValidation: true });
|
|
3151
|
+
if (record === null) continue;
|
|
2843
3152
|
await this.maintainPersistedIndexesOnPut(recordId, record, null, envelope._v);
|
|
2844
3153
|
}
|
|
2845
3154
|
this.persistedIndexesLoaded = true;
|
|
@@ -2890,8 +3199,13 @@ var Collection = class {
|
|
|
2890
3199
|
const env = await this.adapter.get(this.vault, this.name, id);
|
|
2891
3200
|
if (!env) continue;
|
|
2892
3201
|
try {
|
|
2893
|
-
const
|
|
2894
|
-
|
|
3202
|
+
const sidecarJson = await this.decryptJsonString(env);
|
|
3203
|
+
if (sidecarJson === null) {
|
|
3204
|
+
sidecar.set(decoded.recordId, void 0);
|
|
3205
|
+
} else {
|
|
3206
|
+
const body = JSON.parse(sidecarJson);
|
|
3207
|
+
sidecar.set(decoded.recordId, body.value);
|
|
3208
|
+
}
|
|
2895
3209
|
} catch {
|
|
2896
3210
|
sidecar.set(decoded.recordId, void 0);
|
|
2897
3211
|
}
|
|
@@ -2905,6 +3219,7 @@ var Collection = class {
|
|
|
2905
3219
|
const env = await this.adapter.get(this.vault, this.name, id);
|
|
2906
3220
|
if (!env) continue;
|
|
2907
3221
|
const record = await this.decryptRecord(env, { skipValidation: true });
|
|
3222
|
+
if (record === null) continue;
|
|
2908
3223
|
const live = readPersistedValue(record, field);
|
|
2909
3224
|
const stored = sidecar.get(id);
|
|
2910
3225
|
const hasSidecar = sidecarIds.has(id);
|
|
@@ -2987,7 +3302,8 @@ var Collection = class {
|
|
|
2987
3302
|
recordId: id,
|
|
2988
3303
|
getDEK: this.getDEK,
|
|
2989
3304
|
encrypted: this.encrypted,
|
|
2990
|
-
userId: this.keyring.userId
|
|
3305
|
+
userId: this.keyring.userId,
|
|
3306
|
+
erasableBlobs: this.perRecordCek
|
|
2991
3307
|
});
|
|
2992
3308
|
}
|
|
2993
3309
|
/** Get all records as encrypted envelopes (for dump). */
|
|
@@ -2995,7 +3311,8 @@ var Collection = class {
|
|
|
2995
3311
|
await this.ensureHydrated();
|
|
2996
3312
|
const result = {};
|
|
2997
3313
|
for (const [id, entry] of this.cache) {
|
|
2998
|
-
|
|
3314
|
+
const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
|
|
3315
|
+
result[id] = await this.encryptRecord(entry.record, entry.version, cek);
|
|
2999
3316
|
}
|
|
3000
3317
|
return result;
|
|
3001
3318
|
}
|
|
@@ -3023,23 +3340,37 @@ var Collection = class {
|
|
|
3023
3340
|
if (hasMoney && this.moneyFields) {
|
|
3024
3341
|
result = decodeMoneyFields(result, this.moneyFields, typeof locale === "string" ? locale : void 0);
|
|
3025
3342
|
}
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3343
|
+
const hasStaticDisplay = hasDict && this.dictKeyFields !== void 0 && Object.values(this.dictKeyFields).some(
|
|
3344
|
+
(d) => isStaticDictDescriptor(d) && d.displayLocale !== void 0
|
|
3345
|
+
);
|
|
3346
|
+
if (!locale && !hasStaticDisplay) return result;
|
|
3347
|
+
const layer = localeOpts?._layer ?? "read";
|
|
3348
|
+
if (locale && hasI18n && this.i18nFields) {
|
|
3349
|
+
result = this.i18nStrategy.applyI18nLocale(result, this.i18nFields, locale, localeOpts?.fallback, layer);
|
|
3029
3350
|
}
|
|
3030
3351
|
if (hasDict && this.dictKeyFields && this.dictLabelResolver && locale !== "raw") {
|
|
3031
3352
|
const withLabels = { ...result };
|
|
3032
3353
|
const resolver = this.dictLabelResolver;
|
|
3033
3354
|
for (const [field, desc] of Object.entries(this.dictKeyFields)) {
|
|
3034
|
-
const policy = desc.onMissing ? resolvePolicy(desc.onMissing,
|
|
3355
|
+
const policy = desc.onMissing ? resolvePolicy(desc.onMissing, layer) : "null";
|
|
3035
3356
|
const fallback = policy === "substitute" ? localeOpts?.fallback ?? desc.substitute : localeOpts?.fallback;
|
|
3357
|
+
const effLocale = locale ?? (isStaticDictDescriptor(desc) ? desc.displayLocale : void 0);
|
|
3036
3358
|
const resolveKey = async (key) => {
|
|
3037
|
-
|
|
3359
|
+
if (!effLocale) {
|
|
3360
|
+
if (policy === "throw") {
|
|
3361
|
+
throw new LocaleNotSpecifiedError(
|
|
3362
|
+
field,
|
|
3363
|
+
`dictKey "${field}": no locale active to resolve key "${key}".`
|
|
3364
|
+
);
|
|
3365
|
+
}
|
|
3366
|
+
return null;
|
|
3367
|
+
}
|
|
3368
|
+
const label = await resolver(desc.name, key, effLocale, fallback);
|
|
3038
3369
|
if (label === void 0) {
|
|
3039
3370
|
if (policy === "throw") {
|
|
3040
3371
|
throw new LocaleNotSpecifiedError(
|
|
3041
3372
|
field,
|
|
3042
|
-
`dictKey "${field}": no label for key "${key}" in locale "${
|
|
3373
|
+
`dictKey "${field}": no label for key "${key}" in locale "${effLocale}".`
|
|
3043
3374
|
);
|
|
3044
3375
|
}
|
|
3045
3376
|
return null;
|
|
@@ -3196,6 +3527,7 @@ var Collection = class {
|
|
|
3196
3527
|
if (!envelope) continue;
|
|
3197
3528
|
try {
|
|
3198
3529
|
const json = await this.decryptJsonString(envelope);
|
|
3530
|
+
if (json === null) continue;
|
|
3199
3531
|
const body = JSON.parse(json);
|
|
3200
3532
|
if (typeof body.recordId !== "string") continue;
|
|
3201
3533
|
const rows = byField.get(decoded.field) ?? [];
|
|
@@ -3305,7 +3637,31 @@ var Collection = class {
|
|
|
3305
3637
|
};
|
|
3306
3638
|
return new LazyQuery(source);
|
|
3307
3639
|
}
|
|
3308
|
-
|
|
3640
|
+
/**
|
|
3641
|
+
* Resolve the stable CEK for a record on the WRITE path — see
|
|
3642
|
+
* {@link resolveStableCek}. Thin delegate that supplies the collection's
|
|
3643
|
+
* CEK cache, live-envelope reader, and DEK resolver.
|
|
3644
|
+
*/
|
|
3645
|
+
resolveRecordCek(id) {
|
|
3646
|
+
return resolveStableCek(
|
|
3647
|
+
{
|
|
3648
|
+
cache: this.cekCache,
|
|
3649
|
+
getLive: (rid) => this.adapter.get(this.vault, this.name, rid),
|
|
3650
|
+
getDEK: () => this.getDEK(this.name)
|
|
3651
|
+
},
|
|
3652
|
+
id
|
|
3653
|
+
);
|
|
3654
|
+
}
|
|
3655
|
+
/**
|
|
3656
|
+
* Encrypt a JSON body into an envelope.
|
|
3657
|
+
*
|
|
3658
|
+
* When `cek` is supplied (per-record CEK collections), the body is
|
|
3659
|
+
* encrypted under the CEK and the CEK is AES-KW-wrapped under the
|
|
3660
|
+
* collection DEK and stamped on `_cek`. When `cek` is omitted, the legacy
|
|
3661
|
+
* path encrypts the body directly under the collection DEK — byte-identical
|
|
3662
|
+
* to pre-CEK behaviour, so non-adopting collections pay nothing.
|
|
3663
|
+
*/
|
|
3664
|
+
async encryptJsonString(json, version, cek) {
|
|
3309
3665
|
const by = this.keyring.userId;
|
|
3310
3666
|
if (!this.encrypted) {
|
|
3311
3667
|
return {
|
|
@@ -3318,6 +3674,19 @@ var Collection = class {
|
|
|
3318
3674
|
};
|
|
3319
3675
|
}
|
|
3320
3676
|
const dek = await this.getDEK(this.name);
|
|
3677
|
+
if (cek !== void 0) {
|
|
3678
|
+
const { iv: iv2, data: data2 } = await encrypt(json, cek);
|
|
3679
|
+
const wrapped = await wrapCek(cek, dek);
|
|
3680
|
+
return {
|
|
3681
|
+
_noydb: NOYDB_FORMAT_VERSION,
|
|
3682
|
+
_v: version,
|
|
3683
|
+
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3684
|
+
_iv: iv2,
|
|
3685
|
+
_data: data2,
|
|
3686
|
+
_by: by,
|
|
3687
|
+
_cek: wrapped
|
|
3688
|
+
};
|
|
3689
|
+
}
|
|
3321
3690
|
const { iv, data } = await encrypt(json, dek);
|
|
3322
3691
|
return {
|
|
3323
3692
|
_noydb: NOYDB_FORMAT_VERSION,
|
|
@@ -3328,8 +3697,8 @@ var Collection = class {
|
|
|
3328
3697
|
_by: by
|
|
3329
3698
|
};
|
|
3330
3699
|
}
|
|
3331
|
-
async encryptRecord(record, version) {
|
|
3332
|
-
const base = await this.encryptJsonString(JSON.stringify(record), version);
|
|
3700
|
+
async encryptRecord(record, version, cek) {
|
|
3701
|
+
const base = await this.encryptJsonString(JSON.stringify(record), version, cek);
|
|
3333
3702
|
if (!this.deterministicFields || !this.encrypted) return base;
|
|
3334
3703
|
const dek = await this.getDEK(this.name);
|
|
3335
3704
|
const rec = record;
|
|
@@ -3407,7 +3776,8 @@ var Collection = class {
|
|
|
3407
3776
|
const env = await this.adapter.get(this.vault, this.name, id);
|
|
3408
3777
|
if (!env || !env._det) continue;
|
|
3409
3778
|
if (env._det[field] === target) {
|
|
3410
|
-
|
|
3779
|
+
const rec = await this.decryptRecord(env);
|
|
3780
|
+
if (rec !== null) matches.push(rec);
|
|
3411
3781
|
}
|
|
3412
3782
|
}
|
|
3413
3783
|
return matches;
|
|
@@ -3508,7 +3878,14 @@ var Collection = class {
|
|
|
3508
3878
|
return null;
|
|
3509
3879
|
}
|
|
3510
3880
|
const dek = await this.getDEK(key);
|
|
3511
|
-
|
|
3881
|
+
let plaintext;
|
|
3882
|
+
if (envelope._cek !== void 0) {
|
|
3883
|
+
const cek = await unwrapCek(envelope._cek, dek);
|
|
3884
|
+
this.cekCache?.set(id, cek, 1);
|
|
3885
|
+
plaintext = await decrypt(envelope._iv, envelope._data, cek);
|
|
3886
|
+
} else {
|
|
3887
|
+
plaintext = await decrypt(envelope._iv, envelope._data, dek);
|
|
3888
|
+
}
|
|
3512
3889
|
const record = JSON.parse(plaintext);
|
|
3513
3890
|
this.emitCrossTierEvent({
|
|
3514
3891
|
actor: this.keyring.userId,
|
|
@@ -3564,18 +3941,19 @@ var Collection = class {
|
|
|
3564
3941
|
const toKey = dekKey(this.name, toTier);
|
|
3565
3942
|
const fromDek = await this.getDEK(fromKey);
|
|
3566
3943
|
const toDek = await this.getDEK(toKey);
|
|
3567
|
-
const plaintext = await decrypt(envelope._iv, envelope._data, fromDek);
|
|
3568
|
-
const { iv, data } = await encrypt(plaintext, toDek);
|
|
3569
3944
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3945
|
+
const body = await rewrapBodyToDek(envelope, fromDek, toDek);
|
|
3946
|
+
if (body.cek) this.cekCache?.set(id, body.cek, 1);
|
|
3570
3947
|
const next = {
|
|
3571
3948
|
_noydb: NOYDB_FORMAT_VERSION,
|
|
3572
3949
|
_v: envelope._v + 1,
|
|
3573
3950
|
_ts: now,
|
|
3574
|
-
_iv:
|
|
3575
|
-
_data:
|
|
3951
|
+
_iv: body._iv,
|
|
3952
|
+
_data: body._data,
|
|
3576
3953
|
_by: this.keyring.userId,
|
|
3577
3954
|
_tier: toTier,
|
|
3578
|
-
_elevatedBy: this.keyring.userId
|
|
3955
|
+
_elevatedBy: this.keyring.userId,
|
|
3956
|
+
...body._cek !== void 0 ? { _cek: body._cek } : {}
|
|
3579
3957
|
};
|
|
3580
3958
|
await this.adapter.put(this.vault, this.name, id, next);
|
|
3581
3959
|
this.emitCrossTierEvent({
|
|
@@ -3611,17 +3989,18 @@ var Collection = class {
|
|
|
3611
3989
|
if (toTier > 0) this.assertDeclaredTier(toTier);
|
|
3612
3990
|
const fromDek = await this.getDEK(dekKey(this.name, fromTier));
|
|
3613
3991
|
const toDek = await this.getDEK(dekKey(this.name, toTier));
|
|
3614
|
-
const plaintext = await decrypt(envelope._iv, envelope._data, fromDek);
|
|
3615
|
-
const { iv, data } = await encrypt(plaintext, toDek);
|
|
3616
3992
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3993
|
+
const body = await rewrapBodyToDek(envelope, fromDek, toDek);
|
|
3994
|
+
if (body.cek) this.cekCache?.set(id, body.cek, 1);
|
|
3617
3995
|
const next = {
|
|
3618
3996
|
_noydb: NOYDB_FORMAT_VERSION,
|
|
3619
3997
|
_v: envelope._v + 1,
|
|
3620
3998
|
_ts: now,
|
|
3621
|
-
_iv:
|
|
3622
|
-
_data:
|
|
3999
|
+
_iv: body._iv,
|
|
4000
|
+
_data: body._data,
|
|
3623
4001
|
_by: this.keyring.userId,
|
|
3624
|
-
...toTier > 0 && { _tier: toTier }
|
|
4002
|
+
...toTier > 0 && { _tier: toTier },
|
|
4003
|
+
...body._cek !== void 0 ? { _cek: body._cek } : {}
|
|
3625
4004
|
};
|
|
3626
4005
|
await this.adapter.put(this.vault, this.name, id, next);
|
|
3627
4006
|
this.emitCrossTierEvent({
|
|
@@ -3643,10 +4022,30 @@ var Collection = class {
|
|
|
3643
4022
|
} catch {
|
|
3644
4023
|
}
|
|
3645
4024
|
}
|
|
3646
|
-
/**
|
|
3647
|
-
|
|
4025
|
+
/**
|
|
4026
|
+
* Low-level: decrypt an envelope and return the raw JSON string.
|
|
4027
|
+
*
|
|
4028
|
+
* `_cek` presence is the format discriminant (NOT `this.perRecordCek`),
|
|
4029
|
+
* so a mixed vault — and a recipient that never opted into
|
|
4030
|
+
* `perRecordKeys` — decrypts both legacy and CEK records:
|
|
4031
|
+
* - `_cek` present → unwrap the CEK under the collection DEK, decrypt the
|
|
4032
|
+
* body under the CEK (cache the unwrapped CEK so repeated reads skip it).
|
|
4033
|
+
* - `_cek` absent → legacy path, body decrypts directly under the
|
|
4034
|
+
* collection DEK.
|
|
4035
|
+
*
|
|
4036
|
+
* The optional `id` lets reads populate the CEK cache; it is omitted by
|
|
4037
|
+
* callers (history, conflict merge) that have only the envelope.
|
|
4038
|
+
*/
|
|
4039
|
+
async decryptJsonString(envelope, id) {
|
|
4040
|
+
if (isTombstone(envelope, this.encrypted)) return null;
|
|
3648
4041
|
if (!this.encrypted) return envelope._data;
|
|
3649
4042
|
const dek = await this.getDEK(this.name);
|
|
4043
|
+
if (envelope._cek !== void 0) {
|
|
4044
|
+
const cached = id !== void 0 ? this.cekCache?.get(id) : void 0;
|
|
4045
|
+
const cek = cached ?? await unwrapCek(envelope._cek, dek);
|
|
4046
|
+
if (cached === void 0 && id !== void 0) this.cekCache?.set(id, cek, 1);
|
|
4047
|
+
return decrypt(envelope._iv, envelope._data, cek);
|
|
4048
|
+
}
|
|
3650
4049
|
return decrypt(envelope._iv, envelope._data, dek);
|
|
3651
4050
|
}
|
|
3652
4051
|
/**
|
|
@@ -3665,7 +4064,8 @@ var Collection = class {
|
|
|
3665
4064
|
* false positive. Every non-history read leaves this flag `false`.
|
|
3666
4065
|
*/
|
|
3667
4066
|
async decryptRecord(envelope, opts = {}) {
|
|
3668
|
-
const json = await this.decryptJsonString(envelope);
|
|
4067
|
+
const json = await this.decryptJsonString(envelope, opts.id);
|
|
4068
|
+
if (json === null) return null;
|
|
3669
4069
|
let parsed = JSON.parse(json);
|
|
3670
4070
|
if (this.crdtMode && parsed !== null && typeof parsed === "object" && "_crdt" in parsed) {
|
|
3671
4071
|
parsed = this.crdtStrategy.resolveCrdtSnapshot(parsed);
|
|
@@ -3793,6 +4193,38 @@ function withArchive(opts) {
|
|
|
3793
4193
|
// src/sequence/index.ts
|
|
3794
4194
|
var SEQUENCE_COLLECTION = "_sequences";
|
|
3795
4195
|
var MAX_NEXT_ATTEMPTS = 16;
|
|
4196
|
+
var SEQ_FORMAT_TOKEN = /\{([^{}]*)\}/g;
|
|
4197
|
+
var SEQ_PAD_TOKEN = /^seq:0(\d+)$/;
|
|
4198
|
+
var SEQ_PARTITION_TOKEN = /^partition\.(\d+)$/;
|
|
4199
|
+
function compileSequenceFormat(format, series, partition) {
|
|
4200
|
+
const parts = partition ?? [];
|
|
4201
|
+
for (const m of format.matchAll(SEQ_FORMAT_TOKEN)) {
|
|
4202
|
+
const token = m[1] ?? "";
|
|
4203
|
+
if (token === "seq") continue;
|
|
4204
|
+
if (SEQ_PAD_TOKEN.test(token)) continue;
|
|
4205
|
+
const partMatch = SEQ_PARTITION_TOKEN.exec(token);
|
|
4206
|
+
if (partMatch) {
|
|
4207
|
+
const idx = Number(partMatch[1]);
|
|
4208
|
+
if (idx >= parts.length) {
|
|
4209
|
+
throw new ValidationError(
|
|
4210
|
+
`sequence("${series}"): format token "{${token}}" references partition index ${idx}, but only ${parts.length} partition component(s) were supplied.`
|
|
4211
|
+
);
|
|
4212
|
+
}
|
|
4213
|
+
continue;
|
|
4214
|
+
}
|
|
4215
|
+
throw new ValidationError(
|
|
4216
|
+
`sequence("${series}"): format contains unknown token "{${token}}". Accepted tokens: {seq}, {seq:0N}, {partition.i}.`
|
|
4217
|
+
);
|
|
4218
|
+
}
|
|
4219
|
+
return (serial) => format.replace(SEQ_FORMAT_TOKEN, (full, token) => {
|
|
4220
|
+
if (token === "seq") return String(serial);
|
|
4221
|
+
const padMatch = SEQ_PAD_TOKEN.exec(token);
|
|
4222
|
+
if (padMatch) return String(serial).padStart(Number(padMatch[1]), "0");
|
|
4223
|
+
const partMatch = SEQ_PARTITION_TOKEN.exec(token);
|
|
4224
|
+
if (partMatch) return String(parts[Number(partMatch[1])]);
|
|
4225
|
+
return full;
|
|
4226
|
+
});
|
|
4227
|
+
}
|
|
3796
4228
|
function resolveSequenceKey(series, opts) {
|
|
3797
4229
|
const partition = opts?.partition;
|
|
3798
4230
|
if (!partition || partition.length === 0) return series;
|
|
@@ -4112,6 +4544,9 @@ var NO_PERIODS = {
|
|
|
4112
4544
|
};
|
|
4113
4545
|
|
|
4114
4546
|
// src/refs.ts
|
|
4547
|
+
function isRefArray(desc) {
|
|
4548
|
+
return desc.isArray === true;
|
|
4549
|
+
}
|
|
4115
4550
|
var RefIntegrityError = class extends NoydbError {
|
|
4116
4551
|
collection;
|
|
4117
4552
|
id;
|
|
@@ -4148,6 +4583,17 @@ function ref(target, mode = "strict") {
|
|
|
4148
4583
|
}
|
|
4149
4584
|
return { target, mode };
|
|
4150
4585
|
}
|
|
4586
|
+
function refArray(target, mode = "strict") {
|
|
4587
|
+
if (target.includes("/")) {
|
|
4588
|
+
throw new RefScopeError(target);
|
|
4589
|
+
}
|
|
4590
|
+
if (!target || target.startsWith("_")) {
|
|
4591
|
+
throw new Error(
|
|
4592
|
+
`refArray(): target collection name must be non-empty and cannot start with '_' (reserved for internal collections). Got "${target}".`
|
|
4593
|
+
);
|
|
4594
|
+
}
|
|
4595
|
+
return { target, mode, isArray: true };
|
|
4596
|
+
}
|
|
4151
4597
|
var RefRegistry = class {
|
|
4152
4598
|
outbound = /* @__PURE__ */ new Map();
|
|
4153
4599
|
inbound = /* @__PURE__ */ new Map();
|
|
@@ -4172,7 +4618,7 @@ var RefRegistry = class {
|
|
|
4172
4618
|
for (const k of existingKeys) {
|
|
4173
4619
|
const a = existing[k];
|
|
4174
4620
|
const b = refs[k];
|
|
4175
|
-
if (!a || !b || a.target !== b.target || a.mode !== b.mode) {
|
|
4621
|
+
if (!a || !b || a.target !== b.target || a.mode !== b.mode || a.isArray !== b.isArray) {
|
|
4176
4622
|
throw new Error(
|
|
4177
4623
|
`RefRegistry: conflicting ref declarations for collection "${collection}" field "${k}"`
|
|
4178
4624
|
);
|
|
@@ -4183,7 +4629,7 @@ var RefRegistry = class {
|
|
|
4183
4629
|
this.outbound.set(collection, { ...refs });
|
|
4184
4630
|
for (const [field, desc] of Object.entries(refs)) {
|
|
4185
4631
|
const list = this.inbound.get(desc.target) ?? [];
|
|
4186
|
-
list.push({ collection, field, mode: desc.mode });
|
|
4632
|
+
list.push({ collection, field, mode: desc.mode, ...desc.isArray ? { isArray: true } : {} });
|
|
4187
4633
|
this.inbound.set(desc.target, list);
|
|
4188
4634
|
}
|
|
4189
4635
|
}
|
|
@@ -4211,6 +4657,141 @@ var RefRegistry = class {
|
|
|
4211
4657
|
}
|
|
4212
4658
|
};
|
|
4213
4659
|
|
|
4660
|
+
// src/links/link-set.ts
|
|
4661
|
+
var LINK_COLLECTION_PREFIX = "_links_";
|
|
4662
|
+
function linkCollectionName(name) {
|
|
4663
|
+
return `${LINK_COLLECTION_PREFIX}${name}`;
|
|
4664
|
+
}
|
|
4665
|
+
function isLinkCollectionName(name) {
|
|
4666
|
+
return name.startsWith(LINK_COLLECTION_PREFIX);
|
|
4667
|
+
}
|
|
4668
|
+
function linkRowKey(aId, bId) {
|
|
4669
|
+
return `${encodeURIComponent(aId)}|${encodeURIComponent(bId)}`;
|
|
4670
|
+
}
|
|
4671
|
+
var LinkSet = class {
|
|
4672
|
+
constructor(adapter, vault, name, spec, encrypted, getDEK, actor, emitter, endpointExists) {
|
|
4673
|
+
this.adapter = adapter;
|
|
4674
|
+
this.vault = vault;
|
|
4675
|
+
this.name = name;
|
|
4676
|
+
this.spec = spec;
|
|
4677
|
+
this.encrypted = encrypted;
|
|
4678
|
+
this.getDEK = getDEK;
|
|
4679
|
+
this.actor = actor;
|
|
4680
|
+
this.emitter = emitter;
|
|
4681
|
+
this.endpointExists = endpointExists;
|
|
4682
|
+
this.collName = linkCollectionName(name);
|
|
4683
|
+
}
|
|
4684
|
+
adapter;
|
|
4685
|
+
vault;
|
|
4686
|
+
name;
|
|
4687
|
+
spec;
|
|
4688
|
+
encrypted;
|
|
4689
|
+
getDEK;
|
|
4690
|
+
actor;
|
|
4691
|
+
emitter;
|
|
4692
|
+
endpointExists;
|
|
4693
|
+
collName;
|
|
4694
|
+
dekPromise = null;
|
|
4695
|
+
dek() {
|
|
4696
|
+
if (!this.dekPromise) this.dekPromise = this.getDEK(this.collName);
|
|
4697
|
+
return this.dekPromise;
|
|
4698
|
+
}
|
|
4699
|
+
async encryptEntry(entry, version) {
|
|
4700
|
+
const json = JSON.stringify(entry);
|
|
4701
|
+
const base = { _noydb: NOYDB_FORMAT_VERSION, _v: version, _ts: (/* @__PURE__ */ new Date()).toISOString(), _by: this.actor };
|
|
4702
|
+
if (!this.encrypted) return { ...base, _iv: "", _data: json };
|
|
4703
|
+
const { iv, data } = await encrypt(json, await this.dek());
|
|
4704
|
+
return { ...base, _iv: iv, _data: data };
|
|
4705
|
+
}
|
|
4706
|
+
async decryptEntry(env) {
|
|
4707
|
+
const json = this.encrypted ? await decrypt(env._iv, env._data, await this.dek()) : env._data;
|
|
4708
|
+
return JSON.parse(json);
|
|
4709
|
+
}
|
|
4710
|
+
async connect(aId, bId, meta) {
|
|
4711
|
+
if (!await this.endpointExists(this.spec.a, aId)) {
|
|
4712
|
+
throw new LinkEndpointError(this.name, this.spec.a, aId);
|
|
4713
|
+
}
|
|
4714
|
+
if (!await this.endpointExists(this.spec.b, bId)) {
|
|
4715
|
+
throw new LinkEndpointError(this.name, this.spec.b, bId);
|
|
4716
|
+
}
|
|
4717
|
+
const key = linkRowKey(aId, bId);
|
|
4718
|
+
const entry = meta !== void 0 ? { a: aId, b: bId, meta } : { a: aId, b: bId };
|
|
4719
|
+
const existing = await this.adapter.get(this.vault, this.collName, key);
|
|
4720
|
+
const env = await this.encryptEntry(entry, (existing?._v ?? 0) + 1);
|
|
4721
|
+
await this.adapter.put(this.vault, this.collName, key, env, existing?._v);
|
|
4722
|
+
this.emitter.emit("change", { vault: this.vault, collection: this.collName, id: key, action: "put" });
|
|
4723
|
+
}
|
|
4724
|
+
async disconnect(aId, bId) {
|
|
4725
|
+
const key = linkRowKey(aId, bId);
|
|
4726
|
+
const existing = await this.adapter.get(this.vault, this.collName, key);
|
|
4727
|
+
if (!existing) return;
|
|
4728
|
+
await this.adapter.delete(this.vault, this.collName, key);
|
|
4729
|
+
this.emitter.emit("change", { vault: this.vault, collection: this.collName, id: key, action: "delete" });
|
|
4730
|
+
}
|
|
4731
|
+
async has(aId, bId) {
|
|
4732
|
+
return await this.adapter.get(this.vault, this.collName, linkRowKey(aId, bId)) !== null;
|
|
4733
|
+
}
|
|
4734
|
+
async of(id) {
|
|
4735
|
+
const rows = await this.list();
|
|
4736
|
+
return rows.filter((r) => r.a === id || r.b === id);
|
|
4737
|
+
}
|
|
4738
|
+
async list() {
|
|
4739
|
+
const keys = await this.adapter.list(this.vault, this.collName);
|
|
4740
|
+
const out = [];
|
|
4741
|
+
for (const key of keys) {
|
|
4742
|
+
const env = await this.adapter.get(this.vault, this.collName, key);
|
|
4743
|
+
if (!env) continue;
|
|
4744
|
+
const e = await this.decryptEntry(env);
|
|
4745
|
+
out.push(e.meta !== void 0 ? { a: e.a, b: e.b, meta: e.meta } : { a: e.a, b: e.b });
|
|
4746
|
+
}
|
|
4747
|
+
return out;
|
|
4748
|
+
}
|
|
4749
|
+
// ── Vault-internal cascade helpers ──────────────────────────────────
|
|
4750
|
+
/** @internal — rows where the deleted endpoint id matches the relevant slot. */
|
|
4751
|
+
async _rowsTouchingEndpoint(collection, id) {
|
|
4752
|
+
const rows = await this.list();
|
|
4753
|
+
return rows.filter(
|
|
4754
|
+
(r) => this.spec.a === collection && r.a === id || this.spec.b === collection && r.b === id
|
|
4755
|
+
);
|
|
4756
|
+
}
|
|
4757
|
+
/** @internal — the storage collection name (for tx pre-image capture). */
|
|
4758
|
+
get _collectionName() {
|
|
4759
|
+
return this.collName;
|
|
4760
|
+
}
|
|
4761
|
+
};
|
|
4762
|
+
var LinkEndpointError = class extends NoydbError {
|
|
4763
|
+
link;
|
|
4764
|
+
endpoint;
|
|
4765
|
+
missingId;
|
|
4766
|
+
constructor(link, endpoint, missingId) {
|
|
4767
|
+
super(
|
|
4768
|
+
"LINK_ENDPOINT",
|
|
4769
|
+
`link("${link}").connect: endpoint "${endpoint}" has no record "${missingId}".`
|
|
4770
|
+
);
|
|
4771
|
+
this.name = "LinkEndpointError";
|
|
4772
|
+
this.link = link;
|
|
4773
|
+
this.endpoint = endpoint;
|
|
4774
|
+
this.missingId = missingId;
|
|
4775
|
+
}
|
|
4776
|
+
};
|
|
4777
|
+
var LinkIntegrityError = class extends NoydbError {
|
|
4778
|
+
link;
|
|
4779
|
+
endpoint;
|
|
4780
|
+
id;
|
|
4781
|
+
count;
|
|
4782
|
+
constructor(link, endpoint, id, count) {
|
|
4783
|
+
super(
|
|
4784
|
+
"LINK_INTEGRITY",
|
|
4785
|
+
`Cannot delete "${endpoint}"/"${id}": ${count} link(s) in "${link}" still reference it (onDelete: 'strict').`
|
|
4786
|
+
);
|
|
4787
|
+
this.name = "LinkIntegrityError";
|
|
4788
|
+
this.link = link;
|
|
4789
|
+
this.endpoint = endpoint;
|
|
4790
|
+
this.id = id;
|
|
4791
|
+
this.count = count;
|
|
4792
|
+
}
|
|
4793
|
+
};
|
|
4794
|
+
|
|
4214
4795
|
// src/meta/user-envelope/api.ts
|
|
4215
4796
|
var UserApi = class {
|
|
4216
4797
|
constructor(adapter, vaultName, writerKeyringId, getDek, checkGate2) {
|
|
@@ -5149,6 +5730,19 @@ function summariseAggregateOp(value) {
|
|
|
5149
5730
|
}
|
|
5150
5731
|
|
|
5151
5732
|
// src/vault.ts
|
|
5733
|
+
function resolveLabelFromMap(labels, locale, fallback) {
|
|
5734
|
+
if (labels[locale] !== void 0) return labels[locale];
|
|
5735
|
+
const chain = Array.isArray(fallback) ? fallback : fallback ? [fallback] : [];
|
|
5736
|
+
for (const fb of chain) {
|
|
5737
|
+
if (fb === "any") {
|
|
5738
|
+
const any = Object.values(labels)[0];
|
|
5739
|
+
if (any !== void 0) return any;
|
|
5740
|
+
} else if (labels[fb] !== void 0) {
|
|
5741
|
+
return labels[fb];
|
|
5742
|
+
}
|
|
5743
|
+
}
|
|
5744
|
+
return void 0;
|
|
5745
|
+
}
|
|
5152
5746
|
var Vault = class {
|
|
5153
5747
|
adapter;
|
|
5154
5748
|
/** The vault's name as passed to `openVault()`. Stable for the instance lifetime. */
|
|
@@ -5192,6 +5786,7 @@ var Vault = class {
|
|
|
5192
5786
|
periodsStrategy;
|
|
5193
5787
|
shadowStrategy;
|
|
5194
5788
|
historyStrategy;
|
|
5789
|
+
forgetStrategy;
|
|
5195
5790
|
i18nStrategy;
|
|
5196
5791
|
syncStrategy;
|
|
5197
5792
|
/**
|
|
@@ -5221,13 +5816,17 @@ var Vault = class {
|
|
|
5221
5816
|
*/
|
|
5222
5817
|
overlayedViewRegistry = null;
|
|
5223
5818
|
/**
|
|
5224
|
-
* Cached read-only
|
|
5225
|
-
* and to derivation callbacks via `derive(source, ctx)`.
|
|
5226
|
-
*
|
|
5819
|
+
* Cached read-only facades handed to guard callbacks via `ctx.vault`
|
|
5820
|
+
* and to derivation callbacks via `derive(source, ctx)`. Split by
|
|
5821
|
+
* resolution layer (#285): the guard facade reads at `layer:'guard'`,
|
|
5822
|
+
* the derivation facade at `layer:'derivation'`, so i18nText / dictKey
|
|
5823
|
+
* fields resolve under that layer's `onMissing` policy. Allocated
|
|
5824
|
+
* eagerly inside `_initGuards()` / `_initDerivations()` so read
|
|
5227
5825
|
* accessors stay synchronous (callers in `tx/transaction.ts` rely on
|
|
5228
|
-
* that).
|
|
5826
|
+
* that). Each stays `null` for vaults without that subsystem.
|
|
5229
5827
|
*/
|
|
5230
|
-
|
|
5828
|
+
guardFacade = null;
|
|
5829
|
+
derivationFacade = null;
|
|
5231
5830
|
getDEK;
|
|
5232
5831
|
/**
|
|
5233
5832
|
* Per-principal user envelope API.
|
|
@@ -5358,6 +5957,27 @@ var Vault = class {
|
|
|
5358
5957
|
* Populated by `collection()` when the `dictKeyFields` option is passed.
|
|
5359
5958
|
*/
|
|
5360
5959
|
dictKeyFieldRegistry = /* @__PURE__ */ new Map();
|
|
5960
|
+
/**
|
|
5961
|
+
* Names of dictionaries backed by a `staticDict()` descriptor (#291).
|
|
5962
|
+
* A static dict skips the `dictKeyFieldRegistry` rename machinery, but the
|
|
5963
|
+
* vault must still *know* a name is static so `vault.dictionary(name)` can
|
|
5964
|
+
* refuse mutation (`StaticDictReadonlyError`). Populated at `collection()`
|
|
5965
|
+
* config time whenever a `StaticDictDescriptor` is seen.
|
|
5966
|
+
*/
|
|
5967
|
+
staticDictNames = /* @__PURE__ */ new Set();
|
|
5968
|
+
/**
|
|
5969
|
+
* Static-dict descriptors keyed by dictionary name (#291). Backs the
|
|
5970
|
+
* read-path label resolver (resolve from the in-memory table) and the
|
|
5971
|
+
* query-seam `resolveDictSource` snapshot. Last writer wins when the same
|
|
5972
|
+
* name is registered by multiple collections (identical-across-vaults by
|
|
5973
|
+
* construction, so the tables match).
|
|
5974
|
+
*/
|
|
5975
|
+
staticByName = /* @__PURE__ */ new Map();
|
|
5976
|
+
/**
|
|
5977
|
+
* Per-collection map of field name → StaticDictDescriptor (#291). Used by
|
|
5978
|
+
* `enforceStaticDictOnPut` to validate stored codes against `desc.keys`.
|
|
5979
|
+
*/
|
|
5980
|
+
staticDescriptorByField = /* @__PURE__ */ new Map();
|
|
5361
5981
|
/**
|
|
5362
5982
|
* Registry of i18nText fields declared across all collections. Keyed
|
|
5363
5983
|
* by collection name → field name → I18nTextDescriptor. Used by
|
|
@@ -5368,6 +5988,10 @@ var Vault = class {
|
|
|
5368
5988
|
i18nFieldRegistry = /* @__PURE__ */ new Map();
|
|
5369
5989
|
/** Cache of DictionaryHandle instances, one per dictionary name. */
|
|
5370
5990
|
dictionaryCache = /* @__PURE__ */ new Map();
|
|
5991
|
+
/** Registered link specs (#377-B), keyed by link name; set by `vault.link()`. */
|
|
5992
|
+
linkRegistry = /* @__PURE__ */ new Map();
|
|
5993
|
+
/** Cache of LinkSet handles, one per link name. */
|
|
5994
|
+
linkSetCache = /* @__PURE__ */ new Map();
|
|
5371
5995
|
/** — subscribers for cross-tier access events. */
|
|
5372
5996
|
crossTierSubs = /* @__PURE__ */ new Set();
|
|
5373
5997
|
/** — currently-active elevation, or null. One per vault. */
|
|
@@ -5404,6 +6028,7 @@ var Vault = class {
|
|
|
5404
6028
|
this.periodsStrategy = opts.periodsStrategy ?? NO_PERIODS;
|
|
5405
6029
|
this.shadowStrategy = opts.shadowStrategy ?? NO_SHADOW;
|
|
5406
6030
|
this.historyStrategy = opts.historyStrategy ?? NO_HISTORY;
|
|
6031
|
+
this.forgetStrategy = opts.forgetStrategy ?? NO_FORGET;
|
|
5407
6032
|
this.i18nStrategy = opts.i18nStrategy ?? NO_I18N;
|
|
5408
6033
|
this.syncStrategy = opts.syncStrategy ?? NO_SYNC;
|
|
5409
6034
|
void opts.guardStrategies;
|
|
@@ -5483,6 +6108,9 @@ var Vault = class {
|
|
|
5483
6108
|
if (collectionName === SEQUENCE_COLLECTION) {
|
|
5484
6109
|
throw new ReservedCollectionNameError(collectionName);
|
|
5485
6110
|
}
|
|
6111
|
+
if (isLinkCollectionName(collectionName)) {
|
|
6112
|
+
throw new ReservedCollectionNameError(collectionName);
|
|
6113
|
+
}
|
|
5486
6114
|
let coll = this.collectionCache.get(collectionName);
|
|
5487
6115
|
if (coll && options?.moneyFields) {
|
|
5488
6116
|
coll._applyMoneyFields(options.moneyFields);
|
|
@@ -5508,10 +6136,22 @@ var Vault = class {
|
|
|
5508
6136
|
}
|
|
5509
6137
|
if (options?.dictKeyFields) {
|
|
5510
6138
|
const dictFieldMap = {};
|
|
6139
|
+
const staticFieldMap = {};
|
|
5511
6140
|
for (const [field, desc] of Object.entries(options.dictKeyFields)) {
|
|
5512
|
-
|
|
6141
|
+
if (isStaticDictDescriptor(desc)) {
|
|
6142
|
+
staticFieldMap[field] = desc;
|
|
6143
|
+
this.staticDictNames.add(desc.name);
|
|
6144
|
+
this.staticByName.set(desc.name, desc);
|
|
6145
|
+
} else {
|
|
6146
|
+
dictFieldMap[field] = desc.name;
|
|
6147
|
+
}
|
|
6148
|
+
}
|
|
6149
|
+
if (Object.keys(dictFieldMap).length > 0) {
|
|
6150
|
+
this.dictKeyFieldRegistry.set(collectionName, dictFieldMap);
|
|
6151
|
+
}
|
|
6152
|
+
if (Object.keys(staticFieldMap).length > 0) {
|
|
6153
|
+
this.staticDescriptorByField.set(collectionName, staticFieldMap);
|
|
5513
6154
|
}
|
|
5514
|
-
this.dictKeyFieldRegistry.set(collectionName, dictFieldMap);
|
|
5515
6155
|
}
|
|
5516
6156
|
if ((options?.schemaUpdate?.length ?? 0) > 0) {
|
|
5517
6157
|
this.#schemaUpdateNames.set(collectionName, (options.schemaUpdate ?? []).map((s) => s.name));
|
|
@@ -5542,6 +6182,7 @@ var Vault = class {
|
|
|
5542
6182
|
}));
|
|
5543
6183
|
schemaUpdateGate = new SchemaUpdateGate(work);
|
|
5544
6184
|
}
|
|
6185
|
+
const effectiveHistoryConfig = options?.historyConfig ?? this.historyConfig;
|
|
5545
6186
|
const collOpts = {
|
|
5546
6187
|
adapter: this.adapter,
|
|
5547
6188
|
vault: this.name,
|
|
@@ -5557,7 +6198,7 @@ var Vault = class {
|
|
|
5557
6198
|
schemaFence: this.schemaFence,
|
|
5558
6199
|
getDEK: this.getDEK,
|
|
5559
6200
|
onDirty: this.onDirty,
|
|
5560
|
-
historyConfig:
|
|
6201
|
+
historyConfig: effectiveHistoryConfig,
|
|
5561
6202
|
// thread the vault-wide blob strategy into every
|
|
5562
6203
|
// collection. `undefined` is intentionally preserved so the
|
|
5563
6204
|
// Collection constructor uses its NO_BLOBS default.
|
|
@@ -5568,7 +6209,11 @@ var Vault = class {
|
|
|
5568
6209
|
historyStrategy: this.historyStrategy,
|
|
5569
6210
|
i18nStrategy: this.i18nStrategy,
|
|
5570
6211
|
syncStrategy: this.syncStrategy,
|
|
5571
|
-
ledger
|
|
6212
|
+
// Per-collection ledger opt-out (#361): when this collection sets
|
|
6213
|
+
// `historyConfig.ledger: false`, withhold the ledger reference so all
|
|
6214
|
+
// four `if (this.ledger)` append sites in Collection no-op. The chain
|
|
6215
|
+
// stays valid — it simply never receives this collection's entries.
|
|
6216
|
+
ledger: effectiveHistoryConfig.ledger === false ? void 0 : this.getLedgerOrNull() ?? void 0,
|
|
5572
6217
|
refEnforcer: this,
|
|
5573
6218
|
joinResolver: this,
|
|
5574
6219
|
defaultLocale: this.locale,
|
|
@@ -5613,6 +6258,17 @@ var Vault = class {
|
|
|
5613
6258
|
if (options?.acknowledgeDeterministicRisk !== void 0) {
|
|
5614
6259
|
collOpts.acknowledgeDeterministicRisk = options.acknowledgeDeterministicRisk;
|
|
5615
6260
|
}
|
|
6261
|
+
if (options?.perRecordKeys !== void 0) {
|
|
6262
|
+
collOpts.perRecordKeys = options.perRecordKeys;
|
|
6263
|
+
}
|
|
6264
|
+
if (this.forgetStrategy.subjects[collectionName] !== void 0) {
|
|
6265
|
+
if (options?.perRecordKeys === false) {
|
|
6266
|
+
console.warn(
|
|
6267
|
+
`[noy-db] Collection "${collectionName}" is declared in withForgetCascade but opened with perRecordKeys: false. Forcing perRecordKeys: true \u2014 GDPR crypto-shred requires per-record CEKs.`
|
|
6268
|
+
);
|
|
6269
|
+
}
|
|
6270
|
+
collOpts.perRecordKeys = true;
|
|
6271
|
+
}
|
|
5616
6272
|
if (options?.tiers !== void 0) collOpts.tiers = options.tiers;
|
|
5617
6273
|
if (options?.tierMode !== void 0) collOpts.tierMode = options.tierMode;
|
|
5618
6274
|
collOpts.onCrossTierAccess = (event) => this.emitCrossTier(event);
|
|
@@ -5622,6 +6278,11 @@ var Vault = class {
|
|
|
5622
6278
|
if (options?.computed !== void 0) collOpts.computed = options.computed;
|
|
5623
6279
|
if (options?.dictKeyFields !== void 0) {
|
|
5624
6280
|
collOpts.dictLabelResolver = async (dictName, key, locale, fallback) => {
|
|
6281
|
+
const stat = this.staticByName.get(dictName);
|
|
6282
|
+
if (stat) {
|
|
6283
|
+
const labels = stat.table[key];
|
|
6284
|
+
return labels ? resolveLabelFromMap(labels, locale, fallback) : void 0;
|
|
6285
|
+
}
|
|
5625
6286
|
const handle = this.dictionary(dictName);
|
|
5626
6287
|
return handle.resolveLabel(key, locale, fallback);
|
|
5627
6288
|
};
|
|
@@ -5630,6 +6291,7 @@ var Vault = class {
|
|
|
5630
6291
|
if (options?.i18nFields !== void 0 || options?.dictKeyFields !== void 0) {
|
|
5631
6292
|
collOpts.i18nPutValidator = (record) => {
|
|
5632
6293
|
this.enforceI18nOnPut(collectionName, record);
|
|
6294
|
+
this.enforceStaticDictOnPut(collectionName, record);
|
|
5633
6295
|
};
|
|
5634
6296
|
}
|
|
5635
6297
|
if (options?.i18nFields !== void 0 && this.translateText) {
|
|
@@ -5769,6 +6431,34 @@ var Vault = class {
|
|
|
5769
6431
|
}
|
|
5770
6432
|
}
|
|
5771
6433
|
}
|
|
6434
|
+
/**
|
|
6435
|
+
* Validate staticDict codes on a `put()` (#291). For each `staticDict()`
|
|
6436
|
+
* field, every stored code must be a declared key of the descriptor's
|
|
6437
|
+
* table, else `UnknownDictCodeError`. Opt out per descriptor with
|
|
6438
|
+
* `{ validateCodes: false }`. Supports scalar, dotted, and `[].`-wildcard
|
|
6439
|
+
* field paths via `getAtPath` (same path support as i18n validation).
|
|
6440
|
+
*/
|
|
6441
|
+
enforceStaticDictOnPut(collectionName, record) {
|
|
6442
|
+
const staticFields = this.staticDescriptorByField.get(collectionName);
|
|
6443
|
+
if (!staticFields || Object.keys(staticFields).length === 0) return;
|
|
6444
|
+
if (!record || typeof record !== "object") return;
|
|
6445
|
+
const obj = record;
|
|
6446
|
+
for (const [field, desc] of Object.entries(staticFields)) {
|
|
6447
|
+
if (desc.validateCodes === false) continue;
|
|
6448
|
+
const known = new Set(desc.keys);
|
|
6449
|
+
const values = getAtPath(obj, field);
|
|
6450
|
+
for (const value of values) {
|
|
6451
|
+
if (value === void 0 || value === null) continue;
|
|
6452
|
+
const codes = Array.isArray(value) ? value : [value];
|
|
6453
|
+
for (const code of codes) {
|
|
6454
|
+
if (typeof code !== "string") continue;
|
|
6455
|
+
if (!known.has(code)) {
|
|
6456
|
+
throw new UnknownDictCodeError(desc.name, field, code);
|
|
6457
|
+
}
|
|
6458
|
+
}
|
|
6459
|
+
}
|
|
6460
|
+
}
|
|
6461
|
+
}
|
|
5772
6462
|
/**
|
|
5773
6463
|
* Apply locale resolution to a record for the given collection.
|
|
5774
6464
|
*
|
|
@@ -5777,14 +6467,18 @@ var Vault = class {
|
|
|
5777
6467
|
*/
|
|
5778
6468
|
async applyLocale(collectionName, record, localeOpts) {
|
|
5779
6469
|
const locale = localeOpts.locale ?? this.locale;
|
|
5780
|
-
|
|
6470
|
+
const staticFields = this.staticDescriptorByField.get(collectionName);
|
|
6471
|
+
const hasStaticDisplay = staticFields !== void 0 && Object.values(staticFields).some((d) => d.displayLocale !== void 0);
|
|
6472
|
+
if (!locale && !hasStaticDisplay) return record;
|
|
5781
6473
|
let result = record;
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
6474
|
+
if (locale) {
|
|
6475
|
+
const i18nFields = this.i18nFieldRegistry.get(collectionName);
|
|
6476
|
+
if (i18nFields && Object.keys(i18nFields).length > 0) {
|
|
6477
|
+
result = this.i18nStrategy.applyI18nLocale(result, i18nFields, locale, localeOpts.fallback);
|
|
6478
|
+
}
|
|
5785
6479
|
}
|
|
5786
6480
|
const dictFields = this.dictKeyFieldRegistry.get(collectionName);
|
|
5787
|
-
if (dictFields && Object.keys(dictFields).length > 0 && locale !== "raw") {
|
|
6481
|
+
if (locale && dictFields && Object.keys(dictFields).length > 0 && locale !== "raw") {
|
|
5788
6482
|
const withLabels = { ...result };
|
|
5789
6483
|
for (const [field, dictName] of Object.entries(dictFields)) {
|
|
5790
6484
|
const key = result[field];
|
|
@@ -5797,6 +6491,22 @@ var Vault = class {
|
|
|
5797
6491
|
}
|
|
5798
6492
|
result = withLabels;
|
|
5799
6493
|
}
|
|
6494
|
+
if (staticFields && Object.keys(staticFields).length > 0 && locale !== "raw") {
|
|
6495
|
+
const withLabels = { ...result };
|
|
6496
|
+
for (const [field, desc] of Object.entries(staticFields)) {
|
|
6497
|
+
const effLocale = locale ?? desc.displayLocale;
|
|
6498
|
+
if (!effLocale) continue;
|
|
6499
|
+
const key = result[field];
|
|
6500
|
+
if (typeof key !== "string") continue;
|
|
6501
|
+
const labels = desc.table[key];
|
|
6502
|
+
if (!labels) continue;
|
|
6503
|
+
const label = resolveLabelFromMap(labels, effLocale, localeOpts.fallback ?? desc.substitute);
|
|
6504
|
+
if (label !== void 0) {
|
|
6505
|
+
withLabels[`${field}Label`] = label;
|
|
6506
|
+
}
|
|
6507
|
+
}
|
|
6508
|
+
result = withLabels;
|
|
6509
|
+
}
|
|
5800
6510
|
return result;
|
|
5801
6511
|
}
|
|
5802
6512
|
/**
|
|
@@ -5818,6 +6528,9 @@ var Vault = class {
|
|
|
5818
6528
|
* ```
|
|
5819
6529
|
*/
|
|
5820
6530
|
dictionary(name, options = {}) {
|
|
6531
|
+
if (this.staticDictNames.has(name)) {
|
|
6532
|
+
throw new StaticDictReadonlyError(name);
|
|
6533
|
+
}
|
|
5821
6534
|
let handle = this.dictionaryCache.get(name);
|
|
5822
6535
|
if (!handle) {
|
|
5823
6536
|
handle = this.i18nStrategy.buildDictionaryHandle({
|
|
@@ -5861,6 +6574,68 @@ var Vault = class {
|
|
|
5861
6574
|
}
|
|
5862
6575
|
return handle;
|
|
5863
6576
|
}
|
|
6577
|
+
/**
|
|
6578
|
+
* Declare a managed many-to-many link set (#377-B). Registers a
|
|
6579
|
+
* `_links_<name>` junction between two endpoint collections; access its
|
|
6580
|
+
* rows via `vault.links(name)`. Idempotent for an identical re-declaration;
|
|
6581
|
+
* a conflicting one throws. See {@link links}.
|
|
6582
|
+
*
|
|
6583
|
+
* ```ts
|
|
6584
|
+
* vault.link('saleLineLinks', { a: ref('saleLines'), b: ref('purchaseLines'), onDelete: 'cascade' })
|
|
6585
|
+
* ```
|
|
6586
|
+
*
|
|
6587
|
+
* `a` / `b` accept either a collection name or a `ref(target)` descriptor
|
|
6588
|
+
* (only its `target` is used — links manage their own integrity). `onDelete`
|
|
6589
|
+
* governs what happens to link rows when an endpoint record is deleted
|
|
6590
|
+
* (`'cascade'` default, `'strict'`, `'warn'`).
|
|
6591
|
+
*/
|
|
6592
|
+
link(name, spec) {
|
|
6593
|
+
const a = typeof spec.a === "string" ? spec.a : spec.a.target;
|
|
6594
|
+
const b = typeof spec.b === "string" ? spec.b : spec.b.target;
|
|
6595
|
+
for (const [slot, target] of [["a", a], ["b", b]]) {
|
|
6596
|
+
if (!target || target.startsWith("_") || target.includes("/")) {
|
|
6597
|
+
throw new ValidationError(
|
|
6598
|
+
`vault.link("${name}"): endpoint "${slot}" must be a simple collection name, got "${target}".`
|
|
6599
|
+
);
|
|
6600
|
+
}
|
|
6601
|
+
}
|
|
6602
|
+
const resolved = { a, b, ...spec.onDelete ? { onDelete: spec.onDelete } : {} };
|
|
6603
|
+
const existing = this.linkRegistry.get(name);
|
|
6604
|
+
if (existing) {
|
|
6605
|
+
if (existing.a !== resolved.a || existing.b !== resolved.b || (existing.onDelete ?? "cascade") !== (resolved.onDelete ?? "cascade")) {
|
|
6606
|
+
throw new ValidationError(`vault.link("${name}"): conflicting re-declaration.`);
|
|
6607
|
+
}
|
|
6608
|
+
return;
|
|
6609
|
+
}
|
|
6610
|
+
this.linkRegistry.set(name, resolved);
|
|
6611
|
+
}
|
|
6612
|
+
/**
|
|
6613
|
+
* Access a declared link set (#377-B). Throws if `name` was not first
|
|
6614
|
+
* declared via {@link link}. Returns a cached {@link LinkSetHandle}:
|
|
6615
|
+
* `connect(a, b, meta?)`, `disconnect(a, b)`, `has(a, b)`, `of(id)`, `list()`.
|
|
6616
|
+
*/
|
|
6617
|
+
links(name) {
|
|
6618
|
+
let handle = this.linkSetCache.get(name);
|
|
6619
|
+
if (!handle) {
|
|
6620
|
+
const spec = this.linkRegistry.get(name);
|
|
6621
|
+
if (!spec) {
|
|
6622
|
+
throw new ValidationError(`vault.links("${name}"): not declared. Call vault.link("${name}", { a, b }) first.`);
|
|
6623
|
+
}
|
|
6624
|
+
handle = new LinkSet(
|
|
6625
|
+
this.adapter,
|
|
6626
|
+
this.name,
|
|
6627
|
+
name,
|
|
6628
|
+
spec,
|
|
6629
|
+
this.encrypted,
|
|
6630
|
+
this.getDEK,
|
|
6631
|
+
this.keyring.userId,
|
|
6632
|
+
this.emitter,
|
|
6633
|
+
async (collection, id) => await this.collection(collection).get(id) !== null
|
|
6634
|
+
);
|
|
6635
|
+
this.linkSetCache.set(name, handle);
|
|
6636
|
+
}
|
|
6637
|
+
return handle;
|
|
6638
|
+
}
|
|
5864
6639
|
/**
|
|
5865
6640
|
* Build a `JoinableSource` for a dictKey field, for use in dict joins
|
|
5866
6641
|
*. Returns a source whose snapshot contains `{ key, ...labels }`
|
|
@@ -5887,6 +6662,26 @@ var Vault = class {
|
|
|
5887
6662
|
* Returns `null` when `field` is not a dictKey in `leftCollection`.
|
|
5888
6663
|
*/
|
|
5889
6664
|
resolveDictSource(leftCollection, field) {
|
|
6665
|
+
const staticFields = this.staticDescriptorByField.get(leftCollection);
|
|
6666
|
+
if (staticFields && field in staticFields) {
|
|
6667
|
+
const desc = staticFields[field];
|
|
6668
|
+
const rows = Object.entries(desc.table).map(
|
|
6669
|
+
([key, labels]) => ({ key, labels, ...labels })
|
|
6670
|
+
);
|
|
6671
|
+
const source = {
|
|
6672
|
+
snapshot() {
|
|
6673
|
+
return rows;
|
|
6674
|
+
},
|
|
6675
|
+
lookupById(id) {
|
|
6676
|
+
return rows.find((e) => e["key"] === id);
|
|
6677
|
+
}
|
|
6678
|
+
};
|
|
6679
|
+
if (desc.displayLocale !== void 0) {
|
|
6680
|
+
;
|
|
6681
|
+
source.displayLocale = desc.displayLocale;
|
|
6682
|
+
}
|
|
6683
|
+
return source;
|
|
6684
|
+
}
|
|
5890
6685
|
const dictFields = this.dictKeyFieldRegistry.get(leftCollection);
|
|
5891
6686
|
if (!dictFields || !(field in dictFields)) return null;
|
|
5892
6687
|
const dictName = dictFields[field];
|
|
@@ -6000,65 +6795,16 @@ var Vault = class {
|
|
|
6000
6795
|
});
|
|
6001
6796
|
}
|
|
6002
6797
|
}
|
|
6003
|
-
/**
|
|
6004
|
-
* Bulk blob extraction primitive.
|
|
6005
|
-
*
|
|
6006
|
-
* Returns an async-iterable handle over every blob attached to
|
|
6007
|
-
* records in the vault. Single capability check (`plaintext/blob`)
|
|
6008
|
-
* at handle creation; single audit entry to `_export_audit` before
|
|
6009
|
-
* the first yield. Per-blob decryption happens lazily as the
|
|
6010
|
-
* consumer pulls tuples.
|
|
6011
|
-
*
|
|
6012
|
-
* ```ts
|
|
6013
|
-
* const handle = vault.exportBlobs({
|
|
6014
|
-
* collections: ['invoiceScans'],
|
|
6015
|
-
* where: (rec) => (rec as { clientId?: string }).clientId === 'c-123',
|
|
6016
|
-
* })
|
|
6017
|
-
* for await (const { bytes, meta, recordRef } of handle) {
|
|
6018
|
-
* await uploadToColdStorage(bytes, recordRef)
|
|
6019
|
-
* }
|
|
6020
|
-
* ```
|
|
6021
|
-
*
|
|
6022
|
-
* @see `@noy-db/hub/store/export-blobs` for the full option surface.
|
|
6023
|
-
*/
|
|
6024
|
-
/**
|
|
6025
|
-
* Evict blob slots per the per-collection `blobFields` retention
|
|
6026
|
-
* policy.
|
|
6027
|
-
*
|
|
6028
|
-
* Iterates every collection declared with `{ blobFields: {...} }`.
|
|
6029
|
-
* For each record, checks every configured slot against its
|
|
6030
|
-
* policy — `retainDays` (age-based TTL) and/or `evictWhen(record)`
|
|
6031
|
-
* (predicate) — and evicts matching slots. Every eviction writes
|
|
6032
|
-
* one entry to `_blob_eviction_audit` (actor + eTag + reason +
|
|
6033
|
-
* timestamp, no plaintext). Consumer-scheduled; noy-db never runs
|
|
6034
|
-
* this on its own.
|
|
6035
|
-
*
|
|
6036
|
-
* ```ts
|
|
6037
|
-
* await vault.compact() // run full pass
|
|
6038
|
-
* await vault.compact({ dryRun: true }) // preview counts
|
|
6039
|
-
* await vault.compact({ maxEvictions: 1000 }) // cap batch
|
|
6040
|
-
* ```
|
|
6041
|
-
*/
|
|
6042
|
-
/**
|
|
6043
|
-
* Atomic, gap-free numbering. `vault.sequence('invoice-2026').next()`
|
|
6044
|
-
* returns 1, 2, 3, … with no gaps or duplicates under concurrency, via
|
|
6045
|
-
* an optimistic-CAS counter at `_sequences/<name>`. Each name is an
|
|
6046
|
-
* independent sequence.
|
|
6047
|
-
*
|
|
6048
|
-
* **Online-only:** `next()` throws `SequenceOfflineError` unless the
|
|
6049
|
-
* store advertises `capabilities.casAtomic` — gap-free numbering cannot
|
|
6050
|
-
* be serialized by an offline / non-CAS writer.
|
|
6051
|
-
*
|
|
6052
|
-
* ```ts
|
|
6053
|
-
* const n = await vault.sequence('invoice-2026').next() // 1, then 2, …
|
|
6054
|
-
* const cur = await vault.sequence('invoice-2026').peek() // current value, no allocation
|
|
6055
|
-
* ```
|
|
6056
|
-
*/
|
|
6057
6798
|
sequence(series, opts) {
|
|
6058
6799
|
if (series.includes("\0")) {
|
|
6059
6800
|
throw new ValidationError(`sequence("${series}"): series name must not contain a null byte (\\x00).`);
|
|
6060
6801
|
}
|
|
6061
6802
|
if (this.numberingConfigs.has(series)) {
|
|
6803
|
+
if (opts?.format !== void 0) {
|
|
6804
|
+
throw new ValidationError(
|
|
6805
|
+
`sequence("${series}") is a deferred-numbering series; the format option applies to CAS sequences only.`
|
|
6806
|
+
);
|
|
6807
|
+
}
|
|
6062
6808
|
const eng = this.deferred();
|
|
6063
6809
|
return {
|
|
6064
6810
|
next: async (nextOpts) => {
|
|
@@ -6082,7 +6828,17 @@ var Vault = class {
|
|
|
6082
6828
|
actor: this.keyring.userId
|
|
6083
6829
|
});
|
|
6084
6830
|
}
|
|
6085
|
-
|
|
6831
|
+
const handle = this.sequenceStore.handle(resolveSequenceKey(series, opts));
|
|
6832
|
+
if (opts?.format === void 0) return handle;
|
|
6833
|
+
const render = compileSequenceFormat(opts.format, series, opts.partition);
|
|
6834
|
+
return {
|
|
6835
|
+
next: async (nextOpts) => {
|
|
6836
|
+
const serial = await handle.next(nextOpts);
|
|
6837
|
+
return { serial, formatted: render(serial) };
|
|
6838
|
+
},
|
|
6839
|
+
peek: () => handle.peek(),
|
|
6840
|
+
seedTo: (n) => handle.seedTo(n)
|
|
6841
|
+
};
|
|
6086
6842
|
}
|
|
6087
6843
|
/** @internal — lazily build the deferred-numbering engine with a cache-coherent stamp. */
|
|
6088
6844
|
deferred() {
|
|
@@ -6197,12 +6953,12 @@ var Vault = class {
|
|
|
6197
6953
|
if (!fieldSchema) {
|
|
6198
6954
|
throw new AttestationError(`issueAttestation: collection '${collectionName}' has no attestation field-schema. Declare it via vault.collection('${collectionName}', { attestation: { fields: [...] } }).`);
|
|
6199
6955
|
}
|
|
6200
|
-
const { issueAttestationCore } = await import("./issue-
|
|
6956
|
+
const { issueAttestationCore } = await import("./issue-R2MWQO6K.js");
|
|
6201
6957
|
const out = await issueAttestationCore(this.makeIssueContext(), { collection: collectionName, id, fieldSchema });
|
|
6202
6958
|
return { docId: out.docId, qr: out.qr, keyId: out.keyId, publicKeyB64: out.publicKeyB64 };
|
|
6203
6959
|
}
|
|
6204
6960
|
async getDocumentSigningPublicKey() {
|
|
6205
|
-
const { loadSigner, loadOrCreateSigner } = await import("./signer-
|
|
6961
|
+
const { loadSigner, loadOrCreateSigner } = await import("./signer-HAVDLGOK.js");
|
|
6206
6962
|
const existing = await loadSigner(this.adapter, this.name, this.getDEK);
|
|
6207
6963
|
if (existing) return { keyId: existing.keyId, publicKeyB64: existing.publicKeyB64 };
|
|
6208
6964
|
if (this.keyring.role !== "owner") {
|
|
@@ -6228,19 +6984,19 @@ var Vault = class {
|
|
|
6228
6984
|
};
|
|
6229
6985
|
}
|
|
6230
6986
|
async revokeAttestation(docId) {
|
|
6231
|
-
const { revokeDocCore } = await import("./revoke-
|
|
6987
|
+
const { revokeDocCore } = await import("./revoke-7LCWE2AH.js");
|
|
6232
6988
|
await revokeDocCore(this.makeRevokeContext(), docId);
|
|
6233
6989
|
}
|
|
6234
6990
|
async unrevokeAttestation(docId) {
|
|
6235
|
-
const { unrevokeDocCore } = await import("./revoke-
|
|
6991
|
+
const { unrevokeDocCore } = await import("./revoke-7LCWE2AH.js");
|
|
6236
6992
|
await unrevokeDocCore(this.makeRevokeContext(), docId);
|
|
6237
6993
|
}
|
|
6238
6994
|
async getRevokedDocIds() {
|
|
6239
|
-
const { getRevokedDocIdsCore } = await import("./revoke-
|
|
6995
|
+
const { getRevokedDocIdsCore } = await import("./revoke-7LCWE2AH.js");
|
|
6240
6996
|
return getRevokedDocIdsCore(this.makeRevokeContext());
|
|
6241
6997
|
}
|
|
6242
6998
|
async publishRevocationList() {
|
|
6243
|
-
const { publishRevocationListCore } = await import("./revoke-
|
|
6999
|
+
const { publishRevocationListCore } = await import("./revoke-7LCWE2AH.js");
|
|
6244
7000
|
return publishRevocationListCore(this.makeRevokeContext());
|
|
6245
7001
|
}
|
|
6246
7002
|
makeRevokeContext() {
|
|
@@ -6309,6 +7065,43 @@ var Vault = class {
|
|
|
6309
7065
|
if (descriptor.mode !== "strict") continue;
|
|
6310
7066
|
const rawId = obj[field];
|
|
6311
7067
|
if (rawId === null || rawId === void 0) continue;
|
|
7068
|
+
if (isRefArray(descriptor)) {
|
|
7069
|
+
if (!Array.isArray(rawId)) {
|
|
7070
|
+
throw new RefIntegrityError({
|
|
7071
|
+
collection: collectionName,
|
|
7072
|
+
id: obj["id"] ?? "<unknown>",
|
|
7073
|
+
field,
|
|
7074
|
+
refTo: descriptor.target,
|
|
7075
|
+
refId: null,
|
|
7076
|
+
message: `Array ref field "${collectionName}.${field}" must be an array, got ${typeof rawId}.`
|
|
7077
|
+
});
|
|
7078
|
+
}
|
|
7079
|
+
const arrTarget = this.collection(descriptor.target);
|
|
7080
|
+
for (const el of rawId) {
|
|
7081
|
+
if (typeof el !== "string" && typeof el !== "number") {
|
|
7082
|
+
throw new RefIntegrityError({
|
|
7083
|
+
collection: collectionName,
|
|
7084
|
+
id: obj["id"] ?? "<unknown>",
|
|
7085
|
+
field,
|
|
7086
|
+
refTo: descriptor.target,
|
|
7087
|
+
refId: null,
|
|
7088
|
+
message: `Array ref "${collectionName}.${field}" elements must be strings or numbers, got ${typeof el}.`
|
|
7089
|
+
});
|
|
7090
|
+
}
|
|
7091
|
+
const elId = String(el);
|
|
7092
|
+
if (!await arrTarget.get(elId)) {
|
|
7093
|
+
throw new RefIntegrityError({
|
|
7094
|
+
collection: collectionName,
|
|
7095
|
+
id: obj["id"] ?? "<unknown>",
|
|
7096
|
+
field,
|
|
7097
|
+
refTo: descriptor.target,
|
|
7098
|
+
refId: elId,
|
|
7099
|
+
message: `Strict array ref "${collectionName}.${field}" \u2192 "${descriptor.target}" cannot be satisfied: element id "${elId}" not found in "${descriptor.target}".`
|
|
7100
|
+
});
|
|
7101
|
+
}
|
|
7102
|
+
}
|
|
7103
|
+
continue;
|
|
7104
|
+
}
|
|
6312
7105
|
if (typeof rawId !== "string" && typeof rawId !== "number") {
|
|
6313
7106
|
throw new RefIntegrityError({
|
|
6314
7107
|
collection: collectionName,
|
|
@@ -6358,6 +7151,11 @@ var Vault = class {
|
|
|
6358
7151
|
const allRecords = await fromCollection.list();
|
|
6359
7152
|
const matches = allRecords.filter((rec) => {
|
|
6360
7153
|
const raw = rec[rule.field];
|
|
7154
|
+
if (rule.isArray) {
|
|
7155
|
+
return Array.isArray(raw) && raw.some(
|
|
7156
|
+
(el) => (typeof el === "string" || typeof el === "number") && String(el) === id
|
|
7157
|
+
);
|
|
7158
|
+
}
|
|
6361
7159
|
if (typeof raw !== "string" && typeof raw !== "number") return false;
|
|
6362
7160
|
return String(raw) === id;
|
|
6363
7161
|
});
|
|
@@ -6396,10 +7194,45 @@ var Vault = class {
|
|
|
6396
7194
|
}
|
|
6397
7195
|
}
|
|
6398
7196
|
}
|
|
7197
|
+
await this.enforceLinksOnDelete(collectionName, id);
|
|
6399
7198
|
} finally {
|
|
6400
7199
|
this.cascadeInProgress.delete(key);
|
|
6401
7200
|
}
|
|
6402
7201
|
}
|
|
7202
|
+
/**
|
|
7203
|
+
* @internal — apply link `onDelete` policy when an endpoint record is
|
|
7204
|
+
* deleted (#377-B). `'strict'` throws (blocks the delete), `'cascade'`
|
|
7205
|
+
* removes the touching link rows (tx-atomic when a transaction is active),
|
|
7206
|
+
* `'warn'` leaves orphans for `checkIntegrity()`.
|
|
7207
|
+
*/
|
|
7208
|
+
async enforceLinksOnDelete(collectionName, id) {
|
|
7209
|
+
for (const [name, spec] of this.linkRegistry) {
|
|
7210
|
+
if (spec.a !== collectionName && spec.b !== collectionName) continue;
|
|
7211
|
+
const handle = this.links(name);
|
|
7212
|
+
const touching = await handle._rowsTouchingEndpoint(collectionName, id);
|
|
7213
|
+
if (touching.length === 0) continue;
|
|
7214
|
+
const mode = spec.onDelete ?? "cascade";
|
|
7215
|
+
if (mode === "warn") continue;
|
|
7216
|
+
if (mode === "strict") {
|
|
7217
|
+
throw new LinkIntegrityError(name, collectionName, id, touching.length);
|
|
7218
|
+
}
|
|
7219
|
+
const linkColl = handle._collectionName;
|
|
7220
|
+
const txCtx = this.noydb._activeTxContextOrNull;
|
|
7221
|
+
for (const row of touching) {
|
|
7222
|
+
const rowKey = linkRowKey(row.a, row.b);
|
|
7223
|
+
if (txCtx !== null) {
|
|
7224
|
+
const prior = await this.adapter.get(this.name, linkColl, rowKey);
|
|
7225
|
+
if (prior !== null) {
|
|
7226
|
+
txCtx._executed.push({
|
|
7227
|
+
op: { type: "delete", vaultName: this.name, collectionName: linkColl, id: rowKey },
|
|
7228
|
+
priorEnvelope: prior
|
|
7229
|
+
});
|
|
7230
|
+
}
|
|
7231
|
+
}
|
|
7232
|
+
await handle.disconnect(row.a, row.b);
|
|
7233
|
+
}
|
|
7234
|
+
}
|
|
7235
|
+
}
|
|
6403
7236
|
// ─── Join resolver) ────────────────────
|
|
6404
7237
|
/**
|
|
6405
7238
|
* Look up the `RefDescriptor` the left collection declared for a
|
|
@@ -6460,6 +7293,23 @@ var Vault = class {
|
|
|
6460
7293
|
for (const [field, descriptor] of Object.entries(refs)) {
|
|
6461
7294
|
const rawId = record[field];
|
|
6462
7295
|
if (rawId === null || rawId === void 0) continue;
|
|
7296
|
+
const target = this.collection(descriptor.target);
|
|
7297
|
+
if (isRefArray(descriptor)) {
|
|
7298
|
+
if (!Array.isArray(rawId)) {
|
|
7299
|
+
violations.push({ collection: collectionName, id: recId, field, refTo: descriptor.target, refId: rawId, mode: descriptor.mode });
|
|
7300
|
+
continue;
|
|
7301
|
+
}
|
|
7302
|
+
for (const el of rawId) {
|
|
7303
|
+
if (typeof el !== "string" && typeof el !== "number") {
|
|
7304
|
+
violations.push({ collection: collectionName, id: recId, field, refTo: descriptor.target, refId: el, mode: descriptor.mode });
|
|
7305
|
+
continue;
|
|
7306
|
+
}
|
|
7307
|
+
if (!await target.get(String(el))) {
|
|
7308
|
+
violations.push({ collection: collectionName, id: recId, field, refTo: descriptor.target, refId: el, mode: descriptor.mode });
|
|
7309
|
+
}
|
|
7310
|
+
}
|
|
7311
|
+
continue;
|
|
7312
|
+
}
|
|
6463
7313
|
if (typeof rawId !== "string" && typeof rawId !== "number") {
|
|
6464
7314
|
violations.push({
|
|
6465
7315
|
collection: collectionName,
|
|
@@ -6472,7 +7322,6 @@ var Vault = class {
|
|
|
6472
7322
|
continue;
|
|
6473
7323
|
}
|
|
6474
7324
|
const refId = String(rawId);
|
|
6475
|
-
const target = this.collection(descriptor.target);
|
|
6476
7325
|
const exists = await target.get(refId);
|
|
6477
7326
|
if (!exists) {
|
|
6478
7327
|
violations.push({
|
|
@@ -6487,6 +7336,19 @@ var Vault = class {
|
|
|
6487
7336
|
}
|
|
6488
7337
|
}
|
|
6489
7338
|
}
|
|
7339
|
+
for (const [name, spec] of this.linkRegistry) {
|
|
7340
|
+
const linkColl = linkCollectionName(name);
|
|
7341
|
+
const rows = await this.links(name).list();
|
|
7342
|
+
for (const row of rows) {
|
|
7343
|
+
const rowKey = linkRowKey(row.a, row.b);
|
|
7344
|
+
if (await this.collection(spec.a).get(row.a) === null) {
|
|
7345
|
+
violations.push({ collection: linkColl, id: rowKey, field: "a", refTo: spec.a, refId: row.a, mode: spec.onDelete ?? "cascade" });
|
|
7346
|
+
}
|
|
7347
|
+
if (await this.collection(spec.b).get(row.b) === null) {
|
|
7348
|
+
violations.push({ collection: linkColl, id: rowKey, field: "b", refTo: spec.b, refId: row.b, mode: spec.onDelete ?? "cascade" });
|
|
7349
|
+
}
|
|
7350
|
+
}
|
|
7351
|
+
}
|
|
6490
7352
|
return { violations };
|
|
6491
7353
|
}
|
|
6492
7354
|
/**
|
|
@@ -6530,6 +7392,218 @@ var Vault = class {
|
|
|
6530
7392
|
}
|
|
6531
7393
|
return this.ledgerStore;
|
|
6532
7394
|
}
|
|
7395
|
+
// ─── GDPR right-to-erasure (#304) ────────────────────────────────
|
|
7396
|
+
/** @internal — add a subject→record ref to the encrypted subject index. */
|
|
7397
|
+
async _addSubjectRef(subjectId, ref2) {
|
|
7398
|
+
await addSubjectRef(this.adapter, this.name, this.getDEK, this.encrypted, subjectId, ref2);
|
|
7399
|
+
}
|
|
7400
|
+
/** @internal — drop a subject→record ref from the encrypted subject index. */
|
|
7401
|
+
async _removeSubjectRef(subjectId, ref2) {
|
|
7402
|
+
await removeSubjectRef(this.adapter, this.name, this.getDEK, this.encrypted, subjectId, ref2);
|
|
7403
|
+
}
|
|
7404
|
+
/**
|
|
7405
|
+
* Rebuild the encrypted subject index from canonical records. The recovery
|
|
7406
|
+
* path for the documented read-modify-write race (RISK #3). Returns the
|
|
7407
|
+
* number of distinct subjects re-indexed.
|
|
7408
|
+
*/
|
|
7409
|
+
async rebuildSubjectIndex() {
|
|
7410
|
+
if (Object.keys(this.forgetStrategy.subjects).length === 0) {
|
|
7411
|
+
throw new ForgetStrategyNotConfiguredError();
|
|
7412
|
+
}
|
|
7413
|
+
return rebuildSubjectIndex(
|
|
7414
|
+
this.adapter,
|
|
7415
|
+
this.name,
|
|
7416
|
+
this.getDEK,
|
|
7417
|
+
this.encrypted,
|
|
7418
|
+
this.forgetStrategy.subjects,
|
|
7419
|
+
async (collectionName, id, env) => {
|
|
7420
|
+
const coll = this.collection(collectionName);
|
|
7421
|
+
return coll._decodeEnvelope(env, id);
|
|
7422
|
+
}
|
|
7423
|
+
);
|
|
7424
|
+
}
|
|
7425
|
+
/**
|
|
7426
|
+
* GDPR crypto-shred of a data subject (#304). Consults the encrypted subject
|
|
7427
|
+
* index and, per matching record:
|
|
7428
|
+
* - rewrites the LIVE envelope to a tombstone (drops `_iv`/`_data`/`_cek`/`_det`),
|
|
7429
|
+
* - tombstones every `_history` version of the record,
|
|
7430
|
+
* so the body and all prior versions become permanently undecryptable while
|
|
7431
|
+
* the collection DEK and every OTHER record stay intact. Then appends ONE
|
|
7432
|
+
* `op:'forget'` ledger entry whose `payloadHash` is `sha256Hex(subjectId)` —
|
|
7433
|
+
* the chain still `verify()`s, PROVING the subject existed and was erased
|
|
7434
|
+
* without retaining any plaintext.
|
|
7435
|
+
*
|
|
7436
|
+
* Reports — but does not silently swallow — two completeness gaps:
|
|
7437
|
+
* - `unmigratedRecords`: a record whose body was NOT yet migrated to a
|
|
7438
|
+
* per-record CEK (legacy body still under the shared collection DEK). It
|
|
7439
|
+
* is still tombstoned, but its pre-shred ciphertext (if leaked to a
|
|
7440
|
+
* backup before migration) stays decryptable. Migrate, then re-forget.
|
|
7441
|
+
* - `blobResidueCollections`: a shredded record still has blob attachments,
|
|
7442
|
+
* which are keyed off a separate `_blob` DEK and are out of scope here.
|
|
7443
|
+
*
|
|
7444
|
+
* @throws ForgetStrategyNotConfiguredError when no `withForgetCascade` was set.
|
|
7445
|
+
*/
|
|
7446
|
+
async forget(subjectId) {
|
|
7447
|
+
if (Object.keys(this.forgetStrategy.subjects).length === 0) {
|
|
7448
|
+
throw new ForgetStrategyNotConfiguredError();
|
|
7449
|
+
}
|
|
7450
|
+
const refs = await lookupSubject(this.adapter, this.name, this.getDEK, this.encrypted, subjectId);
|
|
7451
|
+
let recordsShredded = 0;
|
|
7452
|
+
let historyVersionsShredded = 0;
|
|
7453
|
+
const collections = /* @__PURE__ */ new Set();
|
|
7454
|
+
const unmigratedRecords = [];
|
|
7455
|
+
const blobResidueCollections = /* @__PURE__ */ new Set();
|
|
7456
|
+
let blobsShredded = 0;
|
|
7457
|
+
let blobsRetainedShared = 0;
|
|
7458
|
+
const blobsEnabled = this.blobStrategy !== void 0;
|
|
7459
|
+
const actor = this.keyring.userId;
|
|
7460
|
+
for (const ref2 of refs) {
|
|
7461
|
+
const coll = this.collection(ref2.collection);
|
|
7462
|
+
const perRecordKeys = this.forgetStrategy.subjects[ref2.collection] !== void 0;
|
|
7463
|
+
const live = await this.adapter.get(this.name, ref2.collection, ref2.id);
|
|
7464
|
+
if (perRecordKeys && live && live._data && live._cek === void 0) {
|
|
7465
|
+
unmigratedRecords.push(`${ref2.collection}:${ref2.id}`);
|
|
7466
|
+
}
|
|
7467
|
+
const shred = await coll._writeTombstone(ref2.id, actor);
|
|
7468
|
+
if (shred !== null) {
|
|
7469
|
+
recordsShredded++;
|
|
7470
|
+
collections.add(ref2.collection);
|
|
7471
|
+
}
|
|
7472
|
+
historyVersionsShredded += await this.historyStrategy.tombstoneHistory(
|
|
7473
|
+
this.adapter,
|
|
7474
|
+
this.name,
|
|
7475
|
+
ref2.collection,
|
|
7476
|
+
ref2.id,
|
|
7477
|
+
actor
|
|
7478
|
+
);
|
|
7479
|
+
if (blobsEnabled) {
|
|
7480
|
+
const r = await this.collection(ref2.collection).blob(ref2.id).shredAllForRecord();
|
|
7481
|
+
blobsShredded += r.shredded.length;
|
|
7482
|
+
blobsRetainedShared += r.retainedShared.length;
|
|
7483
|
+
if (r.residue.length > 0) blobResidueCollections.add(ref2.collection);
|
|
7484
|
+
} else {
|
|
7485
|
+
try {
|
|
7486
|
+
const slotIds = await this.adapter.list(this.name, `_blob_slots_${ref2.collection}`);
|
|
7487
|
+
if (slotIds.includes(ref2.id)) blobResidueCollections.add(ref2.collection);
|
|
7488
|
+
} catch {
|
|
7489
|
+
}
|
|
7490
|
+
}
|
|
7491
|
+
await this._removeSubjectRef(subjectId, ref2);
|
|
7492
|
+
}
|
|
7493
|
+
const subjectHash = await sha256Hex2(subjectId);
|
|
7494
|
+
const ledger = this.getLedgerOrNull();
|
|
7495
|
+
if (!ledger) {
|
|
7496
|
+
throw new Error(
|
|
7497
|
+
'vault.forget() requires the history strategy for the erasure-proof ledger entry. Pass `historyStrategy: withHistory()` from "@noy-db/hub/history" to createNoydb().'
|
|
7498
|
+
);
|
|
7499
|
+
}
|
|
7500
|
+
const ledgerEntry = await ledger.append({
|
|
7501
|
+
op: "forget",
|
|
7502
|
+
collection: "",
|
|
7503
|
+
id: "",
|
|
7504
|
+
version: 0,
|
|
7505
|
+
actor,
|
|
7506
|
+
payloadHash: subjectHash,
|
|
7507
|
+
reason: JSON.stringify({
|
|
7508
|
+
recordsShredded,
|
|
7509
|
+
historyVersionsShredded,
|
|
7510
|
+
collections: [...collections],
|
|
7511
|
+
unmigratedCount: unmigratedRecords.length,
|
|
7512
|
+
blobsShredded,
|
|
7513
|
+
blobsRetainedShared,
|
|
7514
|
+
blobResidueCollections: [...blobResidueCollections]
|
|
7515
|
+
})
|
|
7516
|
+
});
|
|
7517
|
+
return {
|
|
7518
|
+
subject: subjectId,
|
|
7519
|
+
recordsShredded,
|
|
7520
|
+
historyVersionsShredded,
|
|
7521
|
+
collections: [...collections],
|
|
7522
|
+
unmigratedRecords,
|
|
7523
|
+
blobsShredded,
|
|
7524
|
+
blobsRetainedShared,
|
|
7525
|
+
blobResidueCollections: [...blobResidueCollections],
|
|
7526
|
+
ledgerEntry
|
|
7527
|
+
};
|
|
7528
|
+
}
|
|
7529
|
+
// ─── Record-scoped CEK sealing (#306 slices 2-3) ──────────────────────
|
|
7530
|
+
/**
|
|
7531
|
+
* Seal ONE record's content-encryption key (CEK) to an `at-*` host so that
|
|
7532
|
+
* host — and only that host — can decrypt exactly that record, with no
|
|
7533
|
+
* access to the vault DEK and no ability to read any other record.
|
|
7534
|
+
*
|
|
7535
|
+
* The grantor (this caller, who holds the collection DEK) reads the record's
|
|
7536
|
+
* live `_cek`, unwraps it under the collection DEK, exports the raw CEK
|
|
7537
|
+
* bytes, builds a {@link SealedCekBinding} `{collection, id, cek, expiresAt}`,
|
|
7538
|
+
* seals that binding for the recipient host via the host's published hint,
|
|
7539
|
+
* and persists a thin {@link SealedCekDeliveryEnvelope} at
|
|
7540
|
+
* `_sealed_cek/<collection>/<id>/<pid>`. The binding (not the delivery
|
|
7541
|
+
* envelope) is the security boundary: the host re-verifies `{collection, id}`
|
|
7542
|
+
* and `expiresAt` from inside the sealed payload.
|
|
7543
|
+
*
|
|
7544
|
+
* Only works on a `perRecordKeys` record — a legacy record has no `_cek` to
|
|
7545
|
+
* seal (its body is under the shared collection DEK, which is never exposed
|
|
7546
|
+
* by sealing) → {@link RecordCekNotFoundError}.
|
|
7547
|
+
*
|
|
7548
|
+
* @param collection Collection holding the record.
|
|
7549
|
+
* @param id Record id.
|
|
7550
|
+
* @param hostSealer The recipient host's {@link RecipientSealer}.
|
|
7551
|
+
* @param opts.expiresAt REQUIRED authoritative expiry (ISO 8601), sealed into
|
|
7552
|
+
* the binding the host verifies.
|
|
7553
|
+
* @returns `{ pid, envelopeKey }` — the host provider id and the
|
|
7554
|
+
* `<collection>/<id>/<pid>` key the delivery envelope was written under.
|
|
7555
|
+
*/
|
|
7556
|
+
async sealRecordToHost(collection, id, hostSealer, opts) {
|
|
7557
|
+
return sealRecordToHost(this.sealingContext(), collection, id, hostSealer, opts);
|
|
7558
|
+
}
|
|
7559
|
+
/**
|
|
7560
|
+
* Revoke a single sealed-CEK delivery envelope by deleting it from the store.
|
|
7561
|
+
* A soft revocation: it removes the host's copy of the sealed CEK, but a host
|
|
7562
|
+
* that already fetched the envelope keeps whatever it cached. For a hard
|
|
7563
|
+
* revocation that makes the live record undecryptable to every prior grant,
|
|
7564
|
+
* use {@link rotateRecordCek}.
|
|
7565
|
+
*/
|
|
7566
|
+
async revokeSealedRecord(collection, id, pid) {
|
|
7567
|
+
return revokeSealedRecord(this.sealingContext(), collection, id, pid);
|
|
7568
|
+
}
|
|
7569
|
+
/**
|
|
7570
|
+
* HARD-rotate a record's CEK: decrypt the live body under the old CEK,
|
|
7571
|
+
* re-encrypt it under a freshly-minted CEK, write the new live envelope, evict
|
|
7572
|
+
* the in-memory caches, and delete EVERY sealed-CEK delivery envelope for the
|
|
7573
|
+
* record. After this, any host holding a previously-sealed CEK can still
|
|
7574
|
+
* decrypt PRE-rotation history versions (they keep their old `_cek`) but NOT
|
|
7575
|
+
* the rotated live record (its body is under the new CEK → the old CEK fails
|
|
7576
|
+
* the AES-GCM auth tag → `TamperedError`). That asymmetry IS the revocation:
|
|
7577
|
+
* old grants lose the live record.
|
|
7578
|
+
*
|
|
7579
|
+
* Administrative path — bypasses `Collection.put` deliberately (no guards, no
|
|
7580
|
+
* history snapshot, no materialized-view refresh): rotation is a key-rotation
|
|
7581
|
+
* operation, not a business write, and must not version-bump history (which
|
|
7582
|
+
* would re-encrypt the prior version under the NEW CEK and defeat the point).
|
|
7583
|
+
*
|
|
7584
|
+
* @throws {@link RecordCekNotFoundError} if the record is missing or has no `_cek`.
|
|
7585
|
+
*/
|
|
7586
|
+
async rotateRecordCek(collection, id) {
|
|
7587
|
+
return rotateRecordCek(this.sealingContext(), collection, id);
|
|
7588
|
+
}
|
|
7589
|
+
/**
|
|
7590
|
+
* Build the {@link SealingContext} the record-keys grantor functions need:
|
|
7591
|
+
* the vault-bound adapter, DEK resolver, actor, and the dual-cache eviction
|
|
7592
|
+
* `rotateRecordCek` performs (per-record CEK cache + decrypted-record cache).
|
|
7593
|
+
*/
|
|
7594
|
+
sealingContext() {
|
|
7595
|
+
return {
|
|
7596
|
+
adapter: this.adapter,
|
|
7597
|
+
vault: this.name,
|
|
7598
|
+
getDEK: (collection) => this.getDEK(collection),
|
|
7599
|
+
actor: this.keyring.userId,
|
|
7600
|
+
invalidateRecordCaches: async (collection, id) => {
|
|
7601
|
+
const coll = this.collection(collection);
|
|
7602
|
+
coll._invalidateCekCacheEntry(id);
|
|
7603
|
+
await coll._invalidateCacheEntry(id);
|
|
7604
|
+
}
|
|
7605
|
+
};
|
|
7606
|
+
}
|
|
6533
7607
|
/**
|
|
6534
7608
|
* @internal — called by `Noydb.openVault` after construction.
|
|
6535
7609
|
* Dynamic-imports `GuardRegistry` + `ReadOnlyVaultFacade` and seeds
|
|
@@ -6545,12 +7619,12 @@ var Vault = class {
|
|
|
6545
7619
|
if (handles.length === 0) return;
|
|
6546
7620
|
const [{ GuardRegistry }, { ReadOnlyVaultFacade }] = await Promise.all([
|
|
6547
7621
|
import("./registry-DKEXOJVO.js"),
|
|
6548
|
-
import("./read-only-facade-
|
|
7622
|
+
import("./read-only-facade-EX6WZZBP.js")
|
|
6549
7623
|
]);
|
|
6550
7624
|
const registry = new GuardRegistry();
|
|
6551
7625
|
for (const h of handles) registry.register(h.spec);
|
|
6552
7626
|
this.guardRegistry = registry;
|
|
6553
|
-
this.
|
|
7627
|
+
this.guardFacade = new ReadOnlyVaultFacade(this, "guard");
|
|
6554
7628
|
}
|
|
6555
7629
|
/**
|
|
6556
7630
|
* @internal — The gate handler in Noydb.#registerGuardGate calls into
|
|
@@ -6572,8 +7646,8 @@ var Vault = class {
|
|
|
6572
7646
|
async _initDerivations(handles) {
|
|
6573
7647
|
if (handles.length === 0) return;
|
|
6574
7648
|
const [{ DerivationRegistry }, { ReadOnlyVaultFacade }] = await Promise.all([
|
|
6575
|
-
import("./registry-
|
|
6576
|
-
import("./read-only-facade-
|
|
7649
|
+
import("./registry-DMS7OKBM.js"),
|
|
7650
|
+
import("./read-only-facade-EX6WZZBP.js")
|
|
6577
7651
|
]);
|
|
6578
7652
|
const registry = new DerivationRegistry();
|
|
6579
7653
|
for (const h of handles) {
|
|
@@ -6581,8 +7655,8 @@ var Vault = class {
|
|
|
6581
7655
|
}
|
|
6582
7656
|
registry.validate();
|
|
6583
7657
|
this.derivationRegistry = registry;
|
|
6584
|
-
if (this.
|
|
6585
|
-
this.
|
|
7658
|
+
if (this.derivationFacade === null) {
|
|
7659
|
+
this.derivationFacade = new ReadOnlyVaultFacade(this, "derivation");
|
|
6586
7660
|
}
|
|
6587
7661
|
}
|
|
6588
7662
|
/**
|
|
@@ -6603,7 +7677,7 @@ var Vault = class {
|
|
|
6603
7677
|
*/
|
|
6604
7678
|
async _initMaterializedViews(handles) {
|
|
6605
7679
|
if (handles.length === 0) return;
|
|
6606
|
-
const { MaterializedViewRegistry } = await import("./registry-
|
|
7680
|
+
const { MaterializedViewRegistry } = await import("./registry-WVXO6NH5.js");
|
|
6607
7681
|
const registry = new MaterializedViewRegistry();
|
|
6608
7682
|
this.materializedViewRegistry = registry;
|
|
6609
7683
|
const db = this;
|
|
@@ -6627,7 +7701,7 @@ var Vault = class {
|
|
|
6627
7701
|
*/
|
|
6628
7702
|
async _initOverlayedViews(handles) {
|
|
6629
7703
|
if (handles.length === 0) return;
|
|
6630
|
-
const { OverlayedViewRegistry } = await import("./registry-
|
|
7704
|
+
const { OverlayedViewRegistry } = await import("./registry-3T2RZC5A.js");
|
|
6631
7705
|
const registry = new OverlayedViewRegistry();
|
|
6632
7706
|
const mvRegistry = this.materializedViewRegistry;
|
|
6633
7707
|
const overlayNames = /* @__PURE__ */ new Set();
|
|
@@ -6674,13 +7748,13 @@ var Vault = class {
|
|
|
6674
7748
|
if (!reg) {
|
|
6675
7749
|
throw new Error(`refreshView: no MV registered with name "${name}"`);
|
|
6676
7750
|
}
|
|
6677
|
-
const { MaterializedViewExecutor } = await import("./executor-
|
|
7751
|
+
const { MaterializedViewExecutor } = await import("./executor-IZ2NVXCY.js");
|
|
6678
7752
|
const result = await MaterializedViewExecutor.refresh(reg, {
|
|
6679
7753
|
getCollection: (n) => this.collection(n),
|
|
6680
7754
|
getActiveTxContext: () => this.noydb._activeTxContextOrNull,
|
|
6681
7755
|
getQueryContext: () => this
|
|
6682
7756
|
});
|
|
6683
|
-
const { clearMVStale } = await import("./stale-
|
|
7757
|
+
const { clearMVStale } = await import("./stale-PGTEGJDI.js");
|
|
6684
7758
|
clearMVStale(registry, name);
|
|
6685
7759
|
return result;
|
|
6686
7760
|
}
|
|
@@ -6696,10 +7770,10 @@ var Vault = class {
|
|
|
6696
7770
|
if (registry === null) return { derived: 0, failed: 0 };
|
|
6697
7771
|
const strategies = registry.strategiesForSource(sourceCollection);
|
|
6698
7772
|
if (strategies.length === 0) return { derived: 0, failed: 0 };
|
|
6699
|
-
const { DerivationExecutor } = await import("./executor-
|
|
7773
|
+
const { DerivationExecutor } = await import("./executor-WLFDUTOM.js");
|
|
6700
7774
|
const sourceColl = this.collection(sourceCollection);
|
|
6701
7775
|
const records = await sourceColl.list();
|
|
6702
|
-
const ctx = { vault: this.
|
|
7776
|
+
const ctx = { vault: this.derivationFacade ?? new (await import("./read-only-facade-EX6WZZBP.js")).ReadOnlyVaultFacade(this, "derivation") };
|
|
6703
7777
|
let derived = 0;
|
|
6704
7778
|
let failed = 0;
|
|
6705
7779
|
for (const record of records) {
|
|
@@ -6721,7 +7795,7 @@ var Vault = class {
|
|
|
6721
7795
|
if (!outSpec) continue;
|
|
6722
7796
|
const outputColl = this.collection(outSpec.collection);
|
|
6723
7797
|
if (out.kind === "array") {
|
|
6724
|
-
const { loadFanoutSidecar, saveFanoutSidecar } = await import("./fanout-sidecar-
|
|
7798
|
+
const { loadFanoutSidecar, saveFanoutSidecar } = await import("./fanout-sidecar-JGHXAJO5.js");
|
|
6725
7799
|
const prior = await loadFanoutSidecar(this.adapter, this.name, spec.source, id, key);
|
|
6726
7800
|
const prevKeys = new Set(prior?.keys ?? []);
|
|
6727
7801
|
const newKeysList = out.entries.map((e) => e.key);
|
|
@@ -6764,17 +7838,18 @@ var Vault = class {
|
|
|
6764
7838
|
* never see null).
|
|
6765
7839
|
*/
|
|
6766
7840
|
_getReadOnlyFacade() {
|
|
6767
|
-
return this.
|
|
7841
|
+
return this.guardFacade;
|
|
6768
7842
|
}
|
|
6769
7843
|
/**
|
|
6770
|
-
* Internal lazy-allocator for the read-only facade
|
|
6771
|
-
* defensive fallback; in practice
|
|
6772
|
-
* instantiates this, so the lazy path is
|
|
7844
|
+
* Internal lazy-allocator for the derivation read-only facade
|
|
7845
|
+
* (`layer:'derivation'`). Used as a defensive fallback; in practice
|
|
7846
|
+
* `_initDerivations()` eagerly instantiates this, so the lazy path is
|
|
7847
|
+
* a no-op.
|
|
6773
7848
|
*/
|
|
6774
7849
|
_ensureReadOnlyFacade() {
|
|
6775
|
-
if (this.
|
|
7850
|
+
if (this.derivationFacade !== null) return this.derivationFacade;
|
|
6776
7851
|
throw new Error(
|
|
6777
|
-
"Vault:
|
|
7852
|
+
"Vault: derivation hook fired before _initDerivations() completed. This typically means the vault was opened via the sync fallback path (Noydb.vault(name)) without first calling await db.openVault(name). See issue #132."
|
|
6778
7853
|
);
|
|
6779
7854
|
}
|
|
6780
7855
|
/**
|
|
@@ -6942,7 +8017,7 @@ var Vault = class {
|
|
|
6942
8017
|
* collection.
|
|
6943
8018
|
*/
|
|
6944
8019
|
async delegate(opts) {
|
|
6945
|
-
const { issueDelegation, DELEGATIONS_COLLECTION } = await import("./delegation-
|
|
8020
|
+
const { issueDelegation, DELEGATIONS_COLLECTION } = await import("./delegation-FMXNUWE6.js");
|
|
6946
8021
|
if (!this.keyring.kek) {
|
|
6947
8022
|
throw new ValidationError(
|
|
6948
8023
|
"issueDelegation: keyring.kek is null \u2014 issuing a delegation requires a tier-1 unlock. Re-authenticate at tier 1 (passphrase) first."
|
|
@@ -6964,7 +8039,7 @@ var Vault = class {
|
|
|
6964
8039
|
* if the id does not exist.
|
|
6965
8040
|
*/
|
|
6966
8041
|
async revokeDelegation(id) {
|
|
6967
|
-
const { revokeDelegation, DELEGATIONS_COLLECTION } = await import("./delegation-
|
|
8042
|
+
const { revokeDelegation, DELEGATIONS_COLLECTION } = await import("./delegation-FMXNUWE6.js");
|
|
6968
8043
|
await revokeDelegation(this.adapter, this.name, id);
|
|
6969
8044
|
void DELEGATIONS_COLLECTION;
|
|
6970
8045
|
}
|
|
@@ -7433,7 +8508,7 @@ var Vault = class {
|
|
|
7433
8508
|
* @see docs/subsystems/public-envelope.md
|
|
7434
8509
|
*/
|
|
7435
8510
|
async getPublicEnvelope(opts = {}) {
|
|
7436
|
-
const { readPublicEnvelope: readPublicEnvelope2 } = await import("./public-envelope-
|
|
8511
|
+
const { readPublicEnvelope: readPublicEnvelope2 } = await import("./public-envelope-HXOFHY4N.js");
|
|
7437
8512
|
return readPublicEnvelope2(this.adapter, this.name, opts);
|
|
7438
8513
|
}
|
|
7439
8514
|
/**
|
|
@@ -8977,6 +10052,7 @@ var Noydb = class {
|
|
|
8977
10052
|
policyEnforcers = /* @__PURE__ */ new Map();
|
|
8978
10053
|
vaultTemplates = /* @__PURE__ */ new Map();
|
|
8979
10054
|
txStrategy;
|
|
10055
|
+
forgetStrategy;
|
|
8980
10056
|
sessionStrategy;
|
|
8981
10057
|
syncStrategy;
|
|
8982
10058
|
snapshotStrategy;
|
|
@@ -9004,6 +10080,7 @@ var Noydb = class {
|
|
|
9004
10080
|
constructor(options) {
|
|
9005
10081
|
this.options = options;
|
|
9006
10082
|
this.txStrategy = options.txStrategy ?? NO_TX;
|
|
10083
|
+
this.forgetStrategy = options.forgetStrategy ?? NO_FORGET;
|
|
9007
10084
|
this.sessionStrategy = options.sessionStrategy ?? NO_SESSION;
|
|
9008
10085
|
this.syncStrategy = options.syncStrategy ?? NO_SYNC;
|
|
9009
10086
|
this.snapshotStrategy = options.snapshotStrategy ?? NO_SNAPSHOTS;
|
|
@@ -9014,8 +10091,61 @@ var Noydb = class {
|
|
|
9014
10091
|
}
|
|
9015
10092
|
this.#registerGuardGate();
|
|
9016
10093
|
this.#registerPeriodGate();
|
|
10094
|
+
this.#registerForgetHooks();
|
|
9017
10095
|
this.resetSessionTimer();
|
|
9018
10096
|
}
|
|
10097
|
+
/** @internal — resolved forget strategy (NO_FORGET when not configured). */
|
|
10098
|
+
get _forgetStrategy() {
|
|
10099
|
+
return this.forgetStrategy;
|
|
10100
|
+
}
|
|
10101
|
+
// #304 — GDPR subject-index maintenance. When `withForgetCascade` declares
|
|
10102
|
+
// any subject fields, keep the encrypted `_subject_index` in lock-step with
|
|
10103
|
+
// writes so `vault.forget(subjectId)` can find every record for a subject.
|
|
10104
|
+
//
|
|
10105
|
+
// Two consumers are required because they cover disjoint events:
|
|
10106
|
+
// - onAfterWrite fires on create/update (NOT delete) — add the new ref;
|
|
10107
|
+
// on an update that changed the subject value, drop the stale ref.
|
|
10108
|
+
// - the subsystemBus `afterDelete` observer fires on delete (onAfterWrite
|
|
10109
|
+
// does NOT) — drop the ref so a deleted record never lingers in the
|
|
10110
|
+
// index (RISK #2). Without it, forget() would try to shred a ghost.
|
|
10111
|
+
#registerForgetHooks() {
|
|
10112
|
+
const subjects = this.forgetStrategy.subjects;
|
|
10113
|
+
if (Object.keys(subjects).length === 0) return;
|
|
10114
|
+
const subjectFieldFor = (collection) => subjects[collection];
|
|
10115
|
+
this.writeHooks.onAfterWrite(async (event) => {
|
|
10116
|
+
const field = subjectFieldFor(event.collection);
|
|
10117
|
+
if (field === void 0) return;
|
|
10118
|
+
const vault = this.vaultCache.get(event.vault);
|
|
10119
|
+
if (!vault) return;
|
|
10120
|
+
if (event.after !== null && typeof event.after === "object") {
|
|
10121
|
+
const subjectValue = readDottedPath(event.after, field);
|
|
10122
|
+
if (subjectValue !== void 0 && subjectValue !== null) {
|
|
10123
|
+
await vault._addSubjectRef(coerceSubjectId(subjectValue), { collection: event.collection, id: event.docId });
|
|
10124
|
+
}
|
|
10125
|
+
}
|
|
10126
|
+
if (event.op === "update" && event.before !== null && typeof event.before === "object") {
|
|
10127
|
+
const beforeValue = readDottedPath(event.before, field);
|
|
10128
|
+
const afterValue = event.after !== null && typeof event.after === "object" ? readDottedPath(event.after, field) : void 0;
|
|
10129
|
+
const beforeId = beforeValue === void 0 || beforeValue === null ? void 0 : coerceSubjectId(beforeValue);
|
|
10130
|
+
const afterId = afterValue === void 0 || afterValue === null ? void 0 : coerceSubjectId(afterValue);
|
|
10131
|
+
if (beforeId !== void 0 && beforeId !== afterId) {
|
|
10132
|
+
await vault._removeSubjectRef(beforeId, { collection: event.collection, id: event.docId });
|
|
10133
|
+
}
|
|
10134
|
+
}
|
|
10135
|
+
});
|
|
10136
|
+
this.subsystemBus.register("afterDelete", async (event) => {
|
|
10137
|
+
const field = subjectFieldFor(event.collection);
|
|
10138
|
+
if (field === void 0) return;
|
|
10139
|
+
const vault = this.vaultCache.get(event.vault);
|
|
10140
|
+
if (!vault) return;
|
|
10141
|
+
if (event.before !== null && typeof event.before === "object") {
|
|
10142
|
+
const subjectValue = readDottedPath(event.before, field);
|
|
10143
|
+
if (subjectValue !== void 0 && subjectValue !== null) {
|
|
10144
|
+
await vault._removeSubjectRef(coerceSubjectId(subjectValue), { collection: event.collection, id: event.docId });
|
|
10145
|
+
}
|
|
10146
|
+
}
|
|
10147
|
+
});
|
|
10148
|
+
}
|
|
9019
10149
|
// Track A — guards migration. Registers record-lock / field-freeze / onDelete
|
|
9020
10150
|
// / amendment-collect as gate-bus handlers (only when guards are opted in, so
|
|
9021
10151
|
// the write path is zero-cost otherwise). Resolves the live vault's
|
|
@@ -9043,7 +10173,7 @@ var Noydb = class {
|
|
|
9043
10173
|
if (!facade) return;
|
|
9044
10174
|
const ctx = { existing, vault: facade, userId: e.userId, role: e.role };
|
|
9045
10175
|
await registry.runChecks(e.collection, incoming, ctx);
|
|
9046
|
-
const { GuardExecutor } = await import("./executor-
|
|
10176
|
+
const { GuardExecutor } = await import("./executor-THSEYEJG.js");
|
|
9047
10177
|
for (const g of guards) {
|
|
9048
10178
|
await GuardExecutor.checkFrozenFields(g, e.docId, existing, incoming, e.computedFieldNames);
|
|
9049
10179
|
}
|
|
@@ -9236,6 +10366,7 @@ var Noydb = class {
|
|
|
9236
10366
|
...this.options.syncStrategy !== void 0 ? { syncStrategy: this.options.syncStrategy } : {},
|
|
9237
10367
|
...this.options.guardStrategies !== void 0 ? { guardStrategies: this.options.guardStrategies } : {},
|
|
9238
10368
|
...this.options.numbering !== void 0 ? { numberingConfigs: this.options.numbering } : {},
|
|
10369
|
+
forgetStrategy: this.forgetStrategy,
|
|
9239
10370
|
locale: opts?.locale,
|
|
9240
10371
|
// Thread the translator hook so Collection.put() can invoke it
|
|
9241
10372
|
plaintextTranslator: this.options.plaintextTranslator ? (text, from, to, field, collection) => this.invokeTranslator(text, from, to, field, collection) : void 0,
|
|
@@ -9290,7 +10421,8 @@ var Noydb = class {
|
|
|
9290
10421
|
...this.options.i18nStrategy !== void 0 ? { i18nStrategy: this.options.i18nStrategy } : {},
|
|
9291
10422
|
...this.options.syncStrategy !== void 0 ? { syncStrategy: this.options.syncStrategy } : {},
|
|
9292
10423
|
...this.options.guardStrategies !== void 0 ? { guardStrategies: this.options.guardStrategies } : {},
|
|
9293
|
-
...this.options.numbering !== void 0 ? { numberingConfigs: this.options.numbering } : {}
|
|
10424
|
+
...this.options.numbering !== void 0 ? { numberingConfigs: this.options.numbering } : {},
|
|
10425
|
+
forgetStrategy: this.forgetStrategy
|
|
9294
10426
|
});
|
|
9295
10427
|
this.vaultCache.set(name, comp2);
|
|
9296
10428
|
return comp2;
|
|
@@ -9642,11 +10774,11 @@ var Noydb = class {
|
|
|
9642
10774
|
if (name === STATE_VAULT_NAME) throw new ReservedVaultNameError(name);
|
|
9643
10775
|
const template = this.vaultTemplates.get(opts.sharding.vaultTemplate);
|
|
9644
10776
|
if (!template) throw new VaultTemplateNotFoundError(opts.sharding.vaultTemplate);
|
|
9645
|
-
const { VaultGroup } = await import("./vault-group-
|
|
9646
|
-
const { StateManagementVault } = await import("./state-vault-
|
|
10777
|
+
const { VaultGroup } = await import("./vault-group-DPZVFRI5.js");
|
|
10778
|
+
const { StateManagementVault } = await import("./state-vault-QKQKN3H3.js");
|
|
9647
10779
|
const stateVault = opts.registry ? void 0 : await StateManagementVault.open(this);
|
|
9648
10780
|
const registry = opts.registry ?? stateVault.registry;
|
|
9649
|
-
const group = new VaultGroup(this, name, registry, opts.sharding, template);
|
|
10781
|
+
const group = new VaultGroup(this, name, registry, opts.sharding, template, opts.migrateOnOpen ?? false);
|
|
9650
10782
|
if (stateVault) {
|
|
9651
10783
|
group._attachStateVault(stateVault);
|
|
9652
10784
|
await stateVault.recordManifest(opts.sharding.vaultTemplate, template);
|
|
@@ -9670,7 +10802,7 @@ var Noydb = class {
|
|
|
9670
10802
|
*/
|
|
9671
10803
|
async openStateManagementVault() {
|
|
9672
10804
|
if (this.closed) throw new ValidationError("Instance is closed");
|
|
9673
|
-
const { StateManagementVault } = await import("./state-vault-
|
|
10805
|
+
const { StateManagementVault } = await import("./state-vault-QKQKN3H3.js");
|
|
9674
10806
|
return StateManagementVault.open(this);
|
|
9675
10807
|
}
|
|
9676
10808
|
/**
|
|
@@ -11172,6 +12304,7 @@ function normalizeSyncTargets(sync) {
|
|
|
11172
12304
|
|
|
11173
12305
|
export {
|
|
11174
12306
|
withArchive,
|
|
12307
|
+
compileSequenceFormat,
|
|
11175
12308
|
resolveSequenceKey,
|
|
11176
12309
|
SequenceStore,
|
|
11177
12310
|
validateSchemaInput,
|
|
@@ -11179,10 +12312,15 @@ export {
|
|
|
11179
12312
|
isZodSchema,
|
|
11180
12313
|
derivePersistedSchema,
|
|
11181
12314
|
persistSchemaIfNeeded,
|
|
12315
|
+
isRefArray,
|
|
11182
12316
|
RefIntegrityError,
|
|
11183
12317
|
RefScopeError,
|
|
11184
12318
|
ref,
|
|
12319
|
+
refArray,
|
|
11185
12320
|
RefRegistry,
|
|
12321
|
+
isLinkCollectionName,
|
|
12322
|
+
LinkEndpointError,
|
|
12323
|
+
LinkIntegrityError,
|
|
11186
12324
|
QuickUnlockStore,
|
|
11187
12325
|
UserApi,
|
|
11188
12326
|
META_COLLECTION,
|
|
@@ -11211,4 +12349,4 @@ export {
|
|
|
11211
12349
|
Noydb,
|
|
11212
12350
|
createNoydb
|
|
11213
12351
|
};
|
|
11214
|
-
//# sourceMappingURL=chunk-
|
|
12352
|
+
//# sourceMappingURL=chunk-F2IJ2HGD.js.map
|