@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
package/dist/bundle/index.cjs
CHANGED
|
@@ -31,7 +31,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
31
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
32
|
|
|
33
33
|
// src/errors.ts
|
|
34
|
-
var NoydbError, DecryptionError, TamperedError, InvalidKeyError, KeyringCorruptError, NoAccessError, ReadOnlyError, PermissionDeniedError, ExportCapabilityError, KeyringExpiredError, ImportCapabilityError, StoreCapabilityError, PrivilegeEscalationError, ReservedVaultNameError, FieldFrozenError, InvariantError, AmendmentForbiddenError, TierNotGrantedError, ElevationExpiredError, AlreadyElevatedError, TierDemoteDeniedError, DelegationTargetMissingError, ConflictError, LedgerContentionError, SequenceContentionError, SequenceOfflineError, NumberingUncertaintyError, BundleVersionConflictError, ValidationError, SchemaValidationError, SchemaUpdateError, SchemaFenceError, MigrationRequiredError, QuiesceTimeoutError, GroupCardinalityError, IndexRequiredError, UniqueConstraintError, UnsupportedIndexOptionError, IndexWriteFailureError, BundleIntegrityError, BundleSealMismatchError, ReservedCollectionNameError, LocaleNotSpecifiedError, TranslatorNotConfiguredError, BackupLedgerError, BackupCorruptedError, PartitionExtractionError, TransferSealError, AdoptionStateError, AttestationError, JoinTooLargeError, CrossJoinTooLargeError, CrossJoinSourceUnknownError, DanglingReferenceError, DerivationCycleError, DerivationOutputShapeError, DerivationCapExceededError, MaterializedViewCycleError, MaterializedViewSourceUnknownError, MaterializedViewTooLargeError, OverlayBaseIsVirtualError, OverlayCollectionUnavailableError, OverlayNameCollisionError, OverlayIdMismatchError, UnknownShardError, ShardProvisioningError, CrossShardJoinError, VaultTemplateNotFoundError;
|
|
34
|
+
var NoydbError, DecryptionError, TamperedError, InvalidKeyError, KeyringCorruptError, NoAccessError, ReadOnlyError, PermissionDeniedError, ExportCapabilityError, KeyringExpiredError, ImportCapabilityError, StoreCapabilityError, PrivilegeEscalationError, ReservedVaultNameError, FieldFrozenError, InvariantError, AmendmentForbiddenError, TierNotGrantedError, ElevationExpiredError, AlreadyElevatedError, TierDemoteDeniedError, DelegationTargetMissingError, ConflictError, LedgerContentionError, SequenceContentionError, SequenceOfflineError, NumberingUncertaintyError, BundleVersionConflictError, ValidationError, SchemaValidationError, SchemaUpdateError, SchemaFenceError, MigrationRequiredError, QuiesceTimeoutError, GroupCardinalityError, IndexRequiredError, UniqueConstraintError, UnsupportedIndexOptionError, IndexWriteFailureError, BundleIntegrityError, BundleSealMismatchError, ReservedCollectionNameError, LocaleNotSpecifiedError, StaticDictReadonlyError, UnknownDictCodeError, TranslatorNotConfiguredError, BackupLedgerError, BackupCorruptedError, PartitionExtractionError, TransferSealError, AdoptionStateError, AttestationError, JoinTooLargeError, CrossJoinTooLargeError, CrossJoinSourceUnknownError, DanglingReferenceError, DerivationCycleError, DerivationOutputShapeError, DerivationCapExceededError, MaterializedViewCycleError, MaterializedViewSourceUnknownError, MaterializedViewTooLargeError, OverlayBaseIsVirtualError, OverlayCollectionUnavailableError, OverlayNameCollisionError, OverlayIdMismatchError, UnknownShardError, ShardProvisioningError, CrossShardJoinError, VaultTemplateNotFoundError, ForgetStrategyNotConfiguredError, RecordCekNotFoundError;
|
|
35
35
|
var init_errors = __esm({
|
|
36
36
|
"src/errors.ts"() {
|
|
37
37
|
"use strict";
|
|
@@ -485,6 +485,36 @@ Resolutions:
|
|
|
485
485
|
this.field = field;
|
|
486
486
|
}
|
|
487
487
|
};
|
|
488
|
+
StaticDictReadonlyError = class extends NoydbError {
|
|
489
|
+
/** The static dictionary name that was the target of the mutation. */
|
|
490
|
+
dictionaryName;
|
|
491
|
+
constructor(dictionaryName) {
|
|
492
|
+
super(
|
|
493
|
+
"STATIC_DICT_READONLY",
|
|
494
|
+
`Dictionary "${dictionaryName}" is a staticDict \u2014 its labels are code constants with no mutation surface. put/putAll/rename/delete are not supported; change the label in the staticDict() table and redeploy.`
|
|
495
|
+
);
|
|
496
|
+
this.name = "StaticDictReadonlyError";
|
|
497
|
+
this.dictionaryName = dictionaryName;
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
UnknownDictCodeError = class extends NoydbError {
|
|
501
|
+
/** The static dictionary name. */
|
|
502
|
+
dictionaryName;
|
|
503
|
+
/** The field that carried the unknown code. */
|
|
504
|
+
field;
|
|
505
|
+
/** The offending code value. */
|
|
506
|
+
code;
|
|
507
|
+
constructor(dictionaryName, field, code) {
|
|
508
|
+
super(
|
|
509
|
+
"UNKNOWN_DICT_CODE",
|
|
510
|
+
`Field "${field}": code "${code}" is not a known key of staticDict "${dictionaryName}". Use a declared code, or pass { validateCodes: false } on the descriptor to allow open codes.`
|
|
511
|
+
);
|
|
512
|
+
this.name = "UnknownDictCodeError";
|
|
513
|
+
this.dictionaryName = dictionaryName;
|
|
514
|
+
this.field = field;
|
|
515
|
+
this.code = code;
|
|
516
|
+
}
|
|
517
|
+
};
|
|
488
518
|
TranslatorNotConfiguredError = class extends NoydbError {
|
|
489
519
|
/** The field that requested auto-translation. */
|
|
490
520
|
field;
|
|
@@ -763,6 +793,25 @@ Resolutions:
|
|
|
763
793
|
this.templateName = templateName;
|
|
764
794
|
}
|
|
765
795
|
};
|
|
796
|
+
ForgetStrategyNotConfiguredError = class extends NoydbError {
|
|
797
|
+
constructor(message = 'vault.forget() requires a forget strategy. Pass `forgetStrategy: withForgetCascade({ subjects: { <collection>: <subjectField> } })` from "@noy-db/hub/forget" to createNoydb().') {
|
|
798
|
+
super("FORGET_NOT_CONFIGURED", message);
|
|
799
|
+
this.name = "ForgetStrategyNotConfiguredError";
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
RecordCekNotFoundError = class extends NoydbError {
|
|
803
|
+
collection;
|
|
804
|
+
id;
|
|
805
|
+
constructor(collection, id) {
|
|
806
|
+
super(
|
|
807
|
+
"RECORD_CEK_NOT_FOUND",
|
|
808
|
+
`No per-record CEK for ${collection}/${id}. The record is missing, or its collection was not opened with { perRecordKeys: true } \u2014 only per-record-key records carry a sealable CEK.`
|
|
809
|
+
);
|
|
810
|
+
this.name = "RecordCekNotFoundError";
|
|
811
|
+
this.collection = collection;
|
|
812
|
+
this.id = id;
|
|
813
|
+
}
|
|
814
|
+
};
|
|
766
815
|
}
|
|
767
816
|
});
|
|
768
817
|
|
|
@@ -1016,6 +1065,39 @@ async function unwrapKey(wrappedBase64, kek) {
|
|
|
1016
1065
|
throw new InvalidKeyError();
|
|
1017
1066
|
}
|
|
1018
1067
|
}
|
|
1068
|
+
async function asKwKey(dek) {
|
|
1069
|
+
const rawDek = await subtle.exportKey("raw", dek);
|
|
1070
|
+
const hkdfKey = await subtle.importKey("raw", rawDek, "HKDF", false, ["deriveBits"]);
|
|
1071
|
+
const salt = new TextEncoder().encode("noydb-cek-wrap");
|
|
1072
|
+
const info = new TextEncoder().encode("v1");
|
|
1073
|
+
const bits = await subtle.deriveBits(
|
|
1074
|
+
{ name: "HKDF", hash: "SHA-256", salt, info },
|
|
1075
|
+
hkdfKey,
|
|
1076
|
+
KEY_BITS
|
|
1077
|
+
);
|
|
1078
|
+
return subtle.importKey("raw", bits, "AES-KW", false, ["wrapKey", "unwrapKey"]);
|
|
1079
|
+
}
|
|
1080
|
+
async function wrapCek(cek, dek) {
|
|
1081
|
+
const kw = await asKwKey(dek);
|
|
1082
|
+
const wrapped = await subtle.wrapKey("raw", cek, kw, "AES-KW");
|
|
1083
|
+
return bufferToBase64(wrapped);
|
|
1084
|
+
}
|
|
1085
|
+
async function unwrapCek(wrappedBase64, dek) {
|
|
1086
|
+
const kw = await asKwKey(dek);
|
|
1087
|
+
try {
|
|
1088
|
+
return await subtle.unwrapKey(
|
|
1089
|
+
"raw",
|
|
1090
|
+
base64ToBuffer(wrappedBase64),
|
|
1091
|
+
kw,
|
|
1092
|
+
"AES-KW",
|
|
1093
|
+
{ name: "AES-GCM", length: KEY_BITS },
|
|
1094
|
+
true,
|
|
1095
|
+
["encrypt", "decrypt"]
|
|
1096
|
+
);
|
|
1097
|
+
} catch {
|
|
1098
|
+
throw new InvalidKeyError();
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1019
1101
|
async function encrypt(plaintext, dek) {
|
|
1020
1102
|
const iv = generateIV();
|
|
1021
1103
|
const encoded = new TextEncoder().encode(plaintext);
|
|
@@ -1112,6 +1194,163 @@ var init_crypto = __esm({
|
|
|
1112
1194
|
}
|
|
1113
1195
|
});
|
|
1114
1196
|
|
|
1197
|
+
// src/record-keys/tombstone.ts
|
|
1198
|
+
function isTombstone(envelope, encrypted) {
|
|
1199
|
+
if (!encrypted) return false;
|
|
1200
|
+
return !envelope._data && envelope._cek === void 0;
|
|
1201
|
+
}
|
|
1202
|
+
function buildTombstone(version, actor) {
|
|
1203
|
+
return {
|
|
1204
|
+
_noydb: NOYDB_FORMAT_VERSION,
|
|
1205
|
+
_v: version,
|
|
1206
|
+
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1207
|
+
_iv: "",
|
|
1208
|
+
_data: "",
|
|
1209
|
+
...actor ? { _by: actor } : {}
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
var init_tombstone = __esm({
|
|
1213
|
+
"src/record-keys/tombstone.ts"() {
|
|
1214
|
+
"use strict";
|
|
1215
|
+
init_types();
|
|
1216
|
+
}
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
// src/record-keys/lifecycle.ts
|
|
1220
|
+
async function resolveStableCek(deps, id) {
|
|
1221
|
+
const cached = deps.cache?.get(id);
|
|
1222
|
+
if (cached) return cached;
|
|
1223
|
+
const live = await deps.getLive(id);
|
|
1224
|
+
if (live?._cek !== void 0) {
|
|
1225
|
+
const cek = await unwrapCek(live._cek, await deps.getDEK());
|
|
1226
|
+
deps.cache?.set(id, cek, 1);
|
|
1227
|
+
return cek;
|
|
1228
|
+
}
|
|
1229
|
+
const fresh = await generateDEK();
|
|
1230
|
+
deps.cache?.set(id, fresh, 1);
|
|
1231
|
+
return fresh;
|
|
1232
|
+
}
|
|
1233
|
+
async function rewrapBodyToDek(envelope, fromDek, toDek) {
|
|
1234
|
+
if (envelope._cek !== void 0) {
|
|
1235
|
+
const cek = await unwrapCek(envelope._cek, fromDek);
|
|
1236
|
+
const plaintext2 = await decrypt(envelope._iv, envelope._data, cek);
|
|
1237
|
+
const { iv: iv2, data: data2 } = await encrypt(plaintext2, cek);
|
|
1238
|
+
return { _iv: iv2, _data: data2, _cek: await wrapCek(cek, toDek), cek };
|
|
1239
|
+
}
|
|
1240
|
+
const plaintext = await decrypt(envelope._iv, envelope._data, fromDek);
|
|
1241
|
+
const { iv, data } = await encrypt(plaintext, toDek);
|
|
1242
|
+
return { _iv: iv, _data: data, cek: null };
|
|
1243
|
+
}
|
|
1244
|
+
var init_lifecycle = __esm({
|
|
1245
|
+
"src/record-keys/lifecycle.ts"() {
|
|
1246
|
+
"use strict";
|
|
1247
|
+
init_crypto();
|
|
1248
|
+
}
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
// src/record-keys/sealing.ts
|
|
1252
|
+
async function sealRecordToHost(ctx, collection, id, hostSealer, opts) {
|
|
1253
|
+
if (collection.includes("/")) throw new ValidationError(`sealRecordToHost: collection "${collection}" must not contain "/"`);
|
|
1254
|
+
if (id.includes("/")) throw new ValidationError(`sealRecordToHost: id "${id}" must not contain "/"`);
|
|
1255
|
+
const live = await ctx.adapter.get(ctx.vault, collection, id);
|
|
1256
|
+
if (!live || live._cek === void 0) {
|
|
1257
|
+
throw new RecordCekNotFoundError(collection, id);
|
|
1258
|
+
}
|
|
1259
|
+
const dek = await ctx.getDEK(collection);
|
|
1260
|
+
const cek = await unwrapCek(live._cek, dek);
|
|
1261
|
+
const rawCek = await subtle2.exportKey("raw", cek);
|
|
1262
|
+
const cekB64 = bufferToBase64(rawCek);
|
|
1263
|
+
const hint = await hostSealer.publishRecipientHint();
|
|
1264
|
+
if (hint.pid.includes("/")) throw new ValidationError(`sealRecordToHost: recipient pid "${hint.pid}" must not contain "/"`);
|
|
1265
|
+
const binding = {
|
|
1266
|
+
collection,
|
|
1267
|
+
id,
|
|
1268
|
+
cek: cekB64,
|
|
1269
|
+
expiresAt: opts.expiresAt
|
|
1270
|
+
};
|
|
1271
|
+
const sealed = await hostSealer.sealForRecipient(
|
|
1272
|
+
new TextEncoder().encode(JSON.stringify(binding)),
|
|
1273
|
+
hint
|
|
1274
|
+
);
|
|
1275
|
+
const delivery = {
|
|
1276
|
+
v: 1,
|
|
1277
|
+
_noydb_sealed_cek: 1,
|
|
1278
|
+
pid: hint.pid,
|
|
1279
|
+
payload: bufferToBase64(sealed),
|
|
1280
|
+
expiresAt: opts.expiresAt
|
|
1281
|
+
};
|
|
1282
|
+
const envelopeKey = `${collection}/${id}/${hint.pid}`;
|
|
1283
|
+
const prior = await ctx.adapter.get(ctx.vault, SEALED_CEK_NS, envelopeKey);
|
|
1284
|
+
const env = {
|
|
1285
|
+
_noydb: NOYDB_FORMAT_VERSION,
|
|
1286
|
+
_v: (prior?._v ?? 0) + 1,
|
|
1287
|
+
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1288
|
+
// AES-GCM bypassed — the sealing layer is the security boundary, exactly
|
|
1289
|
+
// like the managed-passphrase `_meta/sealed-passphrase` envelope.
|
|
1290
|
+
_iv: "",
|
|
1291
|
+
_data: JSON.stringify(delivery),
|
|
1292
|
+
...ctx.actor ? { _by: ctx.actor } : {}
|
|
1293
|
+
};
|
|
1294
|
+
await ctx.adapter.put(ctx.vault, SEALED_CEK_NS, envelopeKey, env);
|
|
1295
|
+
return { pid: hint.pid, envelopeKey };
|
|
1296
|
+
}
|
|
1297
|
+
async function revokeSealedRecord(ctx, collection, id, pid) {
|
|
1298
|
+
await ctx.adapter.delete(ctx.vault, SEALED_CEK_NS, `${collection}/${id}/${pid}`);
|
|
1299
|
+
}
|
|
1300
|
+
async function rotateRecordCek(ctx, collection, id) {
|
|
1301
|
+
const live = await ctx.adapter.get(ctx.vault, collection, id);
|
|
1302
|
+
if (!live || live._cek === void 0) {
|
|
1303
|
+
throw new RecordCekNotFoundError(collection, id);
|
|
1304
|
+
}
|
|
1305
|
+
const dek = await ctx.getDEK(collection);
|
|
1306
|
+
const oldCek = await unwrapCek(live._cek, dek);
|
|
1307
|
+
const json = await decrypt(live._iv, live._data, oldCek);
|
|
1308
|
+
const newCek = await generateDEK();
|
|
1309
|
+
const { iv, data } = await encrypt(json, newCek);
|
|
1310
|
+
const env = {
|
|
1311
|
+
_noydb: NOYDB_FORMAT_VERSION,
|
|
1312
|
+
_v: live._v + 1,
|
|
1313
|
+
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1314
|
+
_iv: iv,
|
|
1315
|
+
_data: data,
|
|
1316
|
+
_cek: await wrapCek(newCek, dek),
|
|
1317
|
+
...ctx.actor ? { _by: ctx.actor } : {},
|
|
1318
|
+
...live._tier !== void 0 ? { _tier: live._tier } : {},
|
|
1319
|
+
...live._det !== void 0 ? { _det: live._det } : {}
|
|
1320
|
+
};
|
|
1321
|
+
await ctx.adapter.put(ctx.vault, collection, id, env);
|
|
1322
|
+
await ctx.invalidateRecordCaches(collection, id);
|
|
1323
|
+
const prefix = `${collection}/${id}/`;
|
|
1324
|
+
const keys = await ctx.adapter.list(ctx.vault, SEALED_CEK_NS);
|
|
1325
|
+
for (const key of keys) {
|
|
1326
|
+
if (key.startsWith(prefix)) {
|
|
1327
|
+
await ctx.adapter.delete(ctx.vault, SEALED_CEK_NS, key);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
var subtle2, SEALED_CEK_NS;
|
|
1332
|
+
var init_sealing = __esm({
|
|
1333
|
+
"src/record-keys/sealing.ts"() {
|
|
1334
|
+
"use strict";
|
|
1335
|
+
init_crypto();
|
|
1336
|
+
init_types();
|
|
1337
|
+
init_errors();
|
|
1338
|
+
subtle2 = globalThis.crypto.subtle;
|
|
1339
|
+
SEALED_CEK_NS = "_sealed_cek";
|
|
1340
|
+
}
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
// src/record-keys/index.ts
|
|
1344
|
+
var init_record_keys = __esm({
|
|
1345
|
+
"src/record-keys/index.ts"() {
|
|
1346
|
+
"use strict";
|
|
1347
|
+
init_crypto();
|
|
1348
|
+
init_tombstone();
|
|
1349
|
+
init_lifecycle();
|
|
1350
|
+
init_sealing();
|
|
1351
|
+
}
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1115
1354
|
// src/persisted-schemas/storage.ts
|
|
1116
1355
|
async function loadPersistedSchema(store, vault, collection, dek) {
|
|
1117
1356
|
const envelope = await store.get(vault, SCHEMAS_COLLECTION, collection);
|
|
@@ -2815,11 +3054,11 @@ async function mintWrappedDeksBlob(deks, credential) {
|
|
|
2815
3054
|
const wrappingKey = await deriveWrappingKey(credential, salt);
|
|
2816
3055
|
const exported = {};
|
|
2817
3056
|
for (const [coll, dek] of deks) {
|
|
2818
|
-
const raw = await
|
|
3057
|
+
const raw = await subtle3.exportKey("raw", dek);
|
|
2819
3058
|
exported[coll] = bytesToBase643(new Uint8Array(raw));
|
|
2820
3059
|
}
|
|
2821
3060
|
const plaintext = new TextEncoder().encode(JSON.stringify({ deks: exported }));
|
|
2822
|
-
const ciphertext = await
|
|
3061
|
+
const ciphertext = await subtle3.encrypt(
|
|
2823
3062
|
{ name: "AES-GCM", iv },
|
|
2824
3063
|
wrappingKey,
|
|
2825
3064
|
plaintext
|
|
@@ -2832,7 +3071,7 @@ async function mintWrappedDeksBlob(deks, credential) {
|
|
|
2832
3071
|
}
|
|
2833
3072
|
async function unwrapDeksFromBlob(blob, credential) {
|
|
2834
3073
|
const wrappingKey = await deriveWrappingKey(credential, base64ToBytes3(blob.salt));
|
|
2835
|
-
const plaintext = await
|
|
3074
|
+
const plaintext = await subtle3.decrypt(
|
|
2836
3075
|
{ name: "AES-GCM", iv: base64ToBytes3(blob.iv) },
|
|
2837
3076
|
wrappingKey,
|
|
2838
3077
|
base64ToBytes3(blob.wrappedDeks)
|
|
@@ -2841,7 +3080,7 @@ async function unwrapDeksFromBlob(blob, credential) {
|
|
|
2841
3080
|
const deks = /* @__PURE__ */ new Map();
|
|
2842
3081
|
for (const [coll, b64] of Object.entries(parsed.deks)) {
|
|
2843
3082
|
const raw = base64ToBytes3(b64);
|
|
2844
|
-
const key = await
|
|
3083
|
+
const key = await subtle3.importKey(
|
|
2845
3084
|
"raw",
|
|
2846
3085
|
raw,
|
|
2847
3086
|
{ name: "AES-GCM", length: 256 },
|
|
@@ -2853,14 +3092,14 @@ async function unwrapDeksFromBlob(blob, credential) {
|
|
|
2853
3092
|
return deks;
|
|
2854
3093
|
}
|
|
2855
3094
|
async function deriveWrappingKey(credential, salt) {
|
|
2856
|
-
const ikm = await
|
|
3095
|
+
const ikm = await subtle3.importKey(
|
|
2857
3096
|
"raw",
|
|
2858
3097
|
new TextEncoder().encode(credential),
|
|
2859
3098
|
"PBKDF2",
|
|
2860
3099
|
false,
|
|
2861
3100
|
["deriveKey"]
|
|
2862
3101
|
);
|
|
2863
|
-
return
|
|
3102
|
+
return subtle3.deriveKey(
|
|
2864
3103
|
{
|
|
2865
3104
|
name: "PBKDF2",
|
|
2866
3105
|
salt,
|
|
@@ -2884,14 +3123,14 @@ function base64ToBytes3(b64) {
|
|
|
2884
3123
|
for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
|
|
2885
3124
|
return out;
|
|
2886
3125
|
}
|
|
2887
|
-
var PBKDF2_ITERATIONS2, SALT_BYTES2, IV_BYTES2,
|
|
3126
|
+
var PBKDF2_ITERATIONS2, SALT_BYTES2, IV_BYTES2, subtle3;
|
|
2888
3127
|
var init_wrapped_deks = __esm({
|
|
2889
3128
|
"src/team/wrapped-deks.ts"() {
|
|
2890
3129
|
"use strict";
|
|
2891
3130
|
PBKDF2_ITERATIONS2 = 6e5;
|
|
2892
3131
|
SALT_BYTES2 = 32;
|
|
2893
3132
|
IV_BYTES2 = 12;
|
|
2894
|
-
|
|
3133
|
+
subtle3 = globalThis.crypto.subtle;
|
|
2895
3134
|
}
|
|
2896
3135
|
});
|
|
2897
3136
|
|
|
@@ -3696,6 +3935,21 @@ var init_core = __esm({
|
|
|
3696
3935
|
}
|
|
3697
3936
|
});
|
|
3698
3937
|
|
|
3938
|
+
// src/i18n/dictionary.ts
|
|
3939
|
+
function isDictCollectionName(name) {
|
|
3940
|
+
return name.startsWith(DICT_COLLECTION_PREFIX);
|
|
3941
|
+
}
|
|
3942
|
+
function isStaticDictDescriptor(x) {
|
|
3943
|
+
return typeof x === "object" && x !== null && x._noydbStaticDict === true;
|
|
3944
|
+
}
|
|
3945
|
+
var DICT_COLLECTION_PREFIX;
|
|
3946
|
+
var init_dictionary = __esm({
|
|
3947
|
+
"src/i18n/dictionary.ts"() {
|
|
3948
|
+
"use strict";
|
|
3949
|
+
DICT_COLLECTION_PREFIX = "_dict_";
|
|
3950
|
+
}
|
|
3951
|
+
});
|
|
3952
|
+
|
|
3699
3953
|
// src/money/fixed-point.ts
|
|
3700
3954
|
function expandExponent(s) {
|
|
3701
3955
|
const m = /^([+-]?)(\d+)(?:\.(\d+))?[eE]([+-]?\d+)$/.exec(s);
|
|
@@ -4089,6 +4343,21 @@ function formatCurrency(decimal, currency, scale, locale) {
|
|
|
4089
4343
|
});
|
|
4090
4344
|
return fmt.format(decimal);
|
|
4091
4345
|
}
|
|
4346
|
+
function moneyScaledValue(stored, desc) {
|
|
4347
|
+
let raw;
|
|
4348
|
+
if (desc.mode === "fixed") {
|
|
4349
|
+
raw = stored;
|
|
4350
|
+
} else {
|
|
4351
|
+
if (!isMoneyValueObject(stored)) return null;
|
|
4352
|
+
raw = stored.amount;
|
|
4353
|
+
}
|
|
4354
|
+
if (typeof raw !== "string" && typeof raw !== "number") return null;
|
|
4355
|
+
try {
|
|
4356
|
+
return BigInt(String(raw));
|
|
4357
|
+
} catch {
|
|
4358
|
+
return null;
|
|
4359
|
+
}
|
|
4360
|
+
}
|
|
4092
4361
|
function decodeValue(stored, desc) {
|
|
4093
4362
|
let currency;
|
|
4094
4363
|
let scaledIntString;
|
|
@@ -4315,6 +4584,9 @@ var init_strategy3 = __esm({
|
|
|
4315
4584
|
async clearHistory() {
|
|
4316
4585
|
return 0;
|
|
4317
4586
|
},
|
|
4587
|
+
async tombstoneHistory() {
|
|
4588
|
+
return 0;
|
|
4589
|
+
},
|
|
4318
4590
|
async envelopePayloadHash() {
|
|
4319
4591
|
return "";
|
|
4320
4592
|
},
|
|
@@ -5043,7 +5315,7 @@ function executePlanWithSource(source, plan, joinContext) {
|
|
|
5043
5315
|
result = remainingClauses.length === 0 ? [...candidates] : filterRecords(candidates, remainingClauses, fnViewDecoder(source));
|
|
5044
5316
|
}
|
|
5045
5317
|
if (plan.orderBy.length > 0) {
|
|
5046
|
-
result = sortRecords(result, plan.orderBy);
|
|
5318
|
+
result = sortRecords(result, plan.orderBy, source.moneyFields);
|
|
5047
5319
|
}
|
|
5048
5320
|
if (plan.offset > 0) {
|
|
5049
5321
|
result = result.slice(plan.offset);
|
|
@@ -5182,17 +5454,25 @@ function applyCrossJoin(leftRel, clause, rightSource) {
|
|
|
5182
5454
|
}
|
|
5183
5455
|
return expanded;
|
|
5184
5456
|
}
|
|
5185
|
-
function sortRecords(records, orderBy) {
|
|
5457
|
+
function sortRecords(records, orderBy, moneyFields) {
|
|
5186
5458
|
return [...records].sort((a, b) => {
|
|
5187
5459
|
for (const { field, direction } of orderBy) {
|
|
5188
5460
|
const av = readField(a, field);
|
|
5189
5461
|
const bv = readField(b, field);
|
|
5190
|
-
const
|
|
5462
|
+
const desc = moneyFields?.[field];
|
|
5463
|
+
const cmp = desc ? compareMoney(av, bv, desc) : compareValues(av, bv);
|
|
5191
5464
|
if (cmp !== 0) return direction === "asc" ? cmp : -cmp;
|
|
5192
5465
|
}
|
|
5193
5466
|
return 0;
|
|
5194
5467
|
});
|
|
5195
5468
|
}
|
|
5469
|
+
function compareMoney(a, b, desc) {
|
|
5470
|
+
const av = moneyScaledValue(a, desc);
|
|
5471
|
+
const bv = moneyScaledValue(b, desc);
|
|
5472
|
+
if (av === null) return bv === null ? 0 : 1;
|
|
5473
|
+
if (bv === null) return -1;
|
|
5474
|
+
return av < bv ? -1 : av > bv ? 1 : 0;
|
|
5475
|
+
}
|
|
5196
5476
|
function readField(record, field) {
|
|
5197
5477
|
if (record === null || record === void 0) return void 0;
|
|
5198
5478
|
if (!field.includes(".")) {
|
|
@@ -5279,6 +5559,7 @@ function buildDictLabelResolver(joinCtx, field) {
|
|
|
5279
5559
|
const dictSource = joinCtx.resolveDictSource(field);
|
|
5280
5560
|
if (!dictSource) return void 0;
|
|
5281
5561
|
const snapshot = dictSource.snapshot();
|
|
5562
|
+
const displayLocale = dictSource.displayLocale;
|
|
5282
5563
|
const dictMap = /* @__PURE__ */ new Map();
|
|
5283
5564
|
for (const entry of snapshot) {
|
|
5284
5565
|
const k = entry["key"];
|
|
@@ -5288,9 +5569,11 @@ function buildDictLabelResolver(joinCtx, field) {
|
|
|
5288
5569
|
}
|
|
5289
5570
|
}
|
|
5290
5571
|
return async (key, locale, fallback) => {
|
|
5572
|
+
const effLocale = locale || displayLocale;
|
|
5573
|
+
if (!effLocale) return void 0;
|
|
5291
5574
|
const labels = dictMap.get(key);
|
|
5292
5575
|
if (!labels) return void 0;
|
|
5293
|
-
if (labels[
|
|
5576
|
+
if (labels[effLocale] !== void 0) return labels[effLocale];
|
|
5294
5577
|
const chain = Array.isArray(fallback) ? fallback : fallback ? [fallback] : [];
|
|
5295
5578
|
for (const fb of chain) {
|
|
5296
5579
|
if (fb === "any") {
|
|
@@ -8012,6 +8295,15 @@ var init_fanout_sidecar = __esm({
|
|
|
8012
8295
|
});
|
|
8013
8296
|
|
|
8014
8297
|
// src/collection.ts
|
|
8298
|
+
function selfWriteFieldEqual(a, b) {
|
|
8299
|
+
if (a === b) return true;
|
|
8300
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
8301
|
+
try {
|
|
8302
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
8303
|
+
} catch {
|
|
8304
|
+
return false;
|
|
8305
|
+
}
|
|
8306
|
+
}
|
|
8015
8307
|
function warnOnceFallback(adapterName) {
|
|
8016
8308
|
if (fallbackWarned.has(adapterName)) return;
|
|
8017
8309
|
fallbackWarned.add(adapterName);
|
|
@@ -8064,12 +8356,14 @@ var init_collection = __esm({
|
|
|
8064
8356
|
init_types();
|
|
8065
8357
|
init_strategy();
|
|
8066
8358
|
init_core();
|
|
8359
|
+
init_dictionary();
|
|
8067
8360
|
init_normalize();
|
|
8068
8361
|
init_paths();
|
|
8069
8362
|
init_computed();
|
|
8070
8363
|
init_strategy2();
|
|
8071
8364
|
init_policy();
|
|
8072
8365
|
init_crypto();
|
|
8366
|
+
init_record_keys();
|
|
8073
8367
|
init_errors();
|
|
8074
8368
|
init_tiers();
|
|
8075
8369
|
init_keyring();
|
|
@@ -8267,6 +8561,25 @@ var init_collection = __esm({
|
|
|
8267
8561
|
* is inactive for this collection; a frozen `Set` otherwise.
|
|
8268
8562
|
*/
|
|
8269
8563
|
deterministicFields;
|
|
8564
|
+
/**
|
|
8565
|
+
* Per-record CEK opt-in (`perRecordKeys: true`). When set, writes mint /
|
|
8566
|
+
* reuse a per-record content-encryption key and stamp `_cek` on the
|
|
8567
|
+
* envelope (see {@link EncryptedEnvelope._cek}). OFF by default — a
|
|
8568
|
+
* non-adopting collection takes the byte-identical legacy path. The READ
|
|
8569
|
+
* path does not consult this flag: `_cek` presence on the envelope is the
|
|
8570
|
+
* format discriminant, so a mixed vault (and a recipient that never set the
|
|
8571
|
+
* flag) still decrypts CEK records.
|
|
8572
|
+
*/
|
|
8573
|
+
perRecordCek;
|
|
8574
|
+
/**
|
|
8575
|
+
* Session-scoped `(id) → CEK` cache for this collection. Lets updates
|
|
8576
|
+
* reuse a record's stable CEK and lets repeated reads skip the AES-KW
|
|
8577
|
+
* unwrap. Bounded by LRU; never persisted. Dropped when the owning
|
|
8578
|
+
* collection instance is discarded — `vault.load()` clears the
|
|
8579
|
+
* collectionCache, so a keyring refresh drops every CEK alongside the
|
|
8580
|
+
* DEK cache. `null` unless `perRecordCek` is set.
|
|
8581
|
+
*/
|
|
8582
|
+
cekCache;
|
|
8270
8583
|
/**
|
|
8271
8584
|
* declared tiers for this collection. `null` when
|
|
8272
8585
|
* tier-aware methods are disabled. Tier 0 is implicit and never
|
|
@@ -8413,19 +8726,24 @@ var init_collection = __esm({
|
|
|
8413
8726
|
} else {
|
|
8414
8727
|
this.deterministicFields = null;
|
|
8415
8728
|
}
|
|
8729
|
+
this.perRecordCek = opts.perRecordKeys === true;
|
|
8730
|
+
this.cekCache = this.perRecordCek ? new Lru({ maxRecords: 4096 }) : null;
|
|
8416
8731
|
if (opts.crdt && opts.onRegisterConflictResolver) {
|
|
8417
8732
|
const crdtMode = opts.crdt;
|
|
8418
|
-
const crdtResolver = async (
|
|
8733
|
+
const crdtResolver = async (id, local, remote) => {
|
|
8419
8734
|
if (crdtMode === "yjs") {
|
|
8420
8735
|
return local._v >= remote._v ? local : remote;
|
|
8421
8736
|
}
|
|
8422
|
-
const localJson = await this.decryptJsonString(local);
|
|
8423
|
-
const remoteJson = await this.decryptJsonString(remote);
|
|
8737
|
+
const localJson = await this.decryptJsonString(local, id);
|
|
8738
|
+
const remoteJson = await this.decryptJsonString(remote, id);
|
|
8739
|
+
if (localJson === null) return local;
|
|
8740
|
+
if (remoteJson === null) return remote;
|
|
8424
8741
|
const localState = JSON.parse(localJson);
|
|
8425
8742
|
const remoteState = JSON.parse(remoteJson);
|
|
8426
8743
|
const merged = this.crdtStrategy.mergeCrdtStates(localState, remoteState);
|
|
8427
8744
|
const mergedVersion = Math.max(local._v, remote._v) + 1;
|
|
8428
|
-
|
|
8745
|
+
const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
|
|
8746
|
+
return this.encryptJsonString(JSON.stringify(merged), mergedVersion, cek);
|
|
8429
8747
|
};
|
|
8430
8748
|
opts.onRegisterConflictResolver(this.name, crdtResolver);
|
|
8431
8749
|
}
|
|
@@ -8465,12 +8783,15 @@ var init_collection = __esm({
|
|
|
8465
8783
|
});
|
|
8466
8784
|
} else {
|
|
8467
8785
|
const mergeFn = policy;
|
|
8468
|
-
resolver = async (
|
|
8469
|
-
const localRecord = await this.decryptRecord(local, { skipValidation: true });
|
|
8470
|
-
const remoteRecord = await this.decryptRecord(remote, { skipValidation: true });
|
|
8786
|
+
resolver = async (id, local, remote) => {
|
|
8787
|
+
const localRecord = await this.decryptRecord(local, { skipValidation: true, id });
|
|
8788
|
+
const remoteRecord = await this.decryptRecord(remote, { skipValidation: true, id });
|
|
8789
|
+
if (localRecord === null) return local;
|
|
8790
|
+
if (remoteRecord === null) return remote;
|
|
8471
8791
|
const merged = mergeFn(localRecord, remoteRecord);
|
|
8472
8792
|
const mergedVersion = Math.max(local._v, remote._v) + 1;
|
|
8473
|
-
|
|
8793
|
+
const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
|
|
8794
|
+
return this.encryptRecord(merged, mergedVersion, cek);
|
|
8474
8795
|
};
|
|
8475
8796
|
}
|
|
8476
8797
|
opts.onRegisterConflictResolver(collectionName, resolver);
|
|
@@ -8562,7 +8883,9 @@ var init_collection = __esm({
|
|
|
8562
8883
|
} else {
|
|
8563
8884
|
const envelope = await this.adapter.get(this.vault, this.name, id);
|
|
8564
8885
|
if (!envelope) return null;
|
|
8565
|
-
|
|
8886
|
+
if (isTombstone(envelope, this.encrypted)) return null;
|
|
8887
|
+
record = await this.decryptRecord(envelope, { id });
|
|
8888
|
+
if (record === null) return null;
|
|
8566
8889
|
this.lru.set(id, { record, version: envelope._v }, estimateRecordBytes(record));
|
|
8567
8890
|
}
|
|
8568
8891
|
} else {
|
|
@@ -8589,6 +8912,7 @@ var init_collection = __esm({
|
|
|
8589
8912
|
const envelope = await this.adapter.get(this.vault, this.name, id);
|
|
8590
8913
|
if (!envelope) return null;
|
|
8591
8914
|
const json = await this.decryptJsonString(envelope);
|
|
8915
|
+
if (json === null) return null;
|
|
8592
8916
|
return JSON.parse(json);
|
|
8593
8917
|
}
|
|
8594
8918
|
/**
|
|
@@ -8677,7 +9001,7 @@ var init_collection = __esm({
|
|
|
8677
9001
|
if (cached2) return { record: cached2.record, version: cached2.version };
|
|
8678
9002
|
const env = await this.adapter.get(this.vault, this.name, id);
|
|
8679
9003
|
if (!env) return { record: null, version: 0 };
|
|
8680
|
-
return { record: await this.decryptRecord(env, { skipValidation: true }), version: env._v };
|
|
9004
|
+
return { record: await this.decryptRecord(env, { skipValidation: true }) ?? null, version: env._v };
|
|
8681
9005
|
}
|
|
8682
9006
|
await this.ensureHydrated();
|
|
8683
9007
|
const cached = this.cache.get(id);
|
|
@@ -8790,9 +9114,11 @@ var init_collection = __esm({
|
|
|
8790
9114
|
let existingState;
|
|
8791
9115
|
if (existingEnvelope) {
|
|
8792
9116
|
const prevJson = await this.decryptJsonString(existingEnvelope);
|
|
8793
|
-
|
|
8794
|
-
|
|
8795
|
-
|
|
9117
|
+
if (prevJson !== null) {
|
|
9118
|
+
const prevParsed = JSON.parse(prevJson);
|
|
9119
|
+
if (prevParsed !== null && typeof prevParsed === "object" && "_crdt" in prevParsed) {
|
|
9120
|
+
existingState = prevParsed;
|
|
9121
|
+
}
|
|
8796
9122
|
}
|
|
8797
9123
|
}
|
|
8798
9124
|
crdtState = this.crdtStrategy.buildLwwMapState(record, existingState, now);
|
|
@@ -8800,9 +9126,11 @@ var init_collection = __esm({
|
|
|
8800
9126
|
let existingState;
|
|
8801
9127
|
if (existingEnvelope) {
|
|
8802
9128
|
const prevJson = await this.decryptJsonString(existingEnvelope);
|
|
8803
|
-
|
|
8804
|
-
|
|
8805
|
-
|
|
9129
|
+
if (prevJson !== null) {
|
|
9130
|
+
const prevParsed = JSON.parse(prevJson);
|
|
9131
|
+
if (prevParsed !== null && typeof prevParsed === "object" && "_crdt" in prevParsed) {
|
|
9132
|
+
existingState = prevParsed;
|
|
9133
|
+
}
|
|
8806
9134
|
}
|
|
8807
9135
|
}
|
|
8808
9136
|
const arr = Array.isArray(record) ? record : [record];
|
|
@@ -8811,12 +9139,14 @@ var init_collection = __esm({
|
|
|
8811
9139
|
crdtState = { _crdt: "yjs", update: record };
|
|
8812
9140
|
}
|
|
8813
9141
|
const version2 = existingVersion + 1;
|
|
8814
|
-
const
|
|
9142
|
+
const cek2 = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
|
|
9143
|
+
const envelope2 = await this.encryptJsonString(JSON.stringify(crdtState), version2, cek2);
|
|
8815
9144
|
await this.adapter.put(this.vault, this.name, id, envelope2);
|
|
8816
9145
|
const resolvedRecord = this.crdtStrategy.resolveCrdtSnapshot(crdtState);
|
|
8817
|
-
const
|
|
9146
|
+
const existingResolvedRecord = existingEnvelope ? await this.decryptRecord(existingEnvelope, { skipValidation: true }) : null;
|
|
9147
|
+
const existingResolved = existingResolvedRecord !== null ? { record: existingResolvedRecord, version: existingVersion } : void 0;
|
|
8818
9148
|
if (existingResolved && this.historyConfig.enabled !== false) {
|
|
8819
|
-
const histEnvelope = await this.encryptRecord(existingResolved.record, existingResolved.version);
|
|
9149
|
+
const histEnvelope = await this.encryptRecord(existingResolved.record, existingResolved.version, cek2);
|
|
8820
9150
|
await this.historyStrategy.saveHistory(this.adapter, this.vault, this.name, id, histEnvelope);
|
|
8821
9151
|
this.emitter.emit("history:save", { vault: this.vault, collection: this.name, id, version: existingResolved.version });
|
|
8822
9152
|
if (this.historyConfig.maxVersions) {
|
|
@@ -8862,7 +9192,9 @@ var init_collection = __esm({
|
|
|
8862
9192
|
const previousEnvelope = await this.adapter.get(this.vault, this.name, id);
|
|
8863
9193
|
if (previousEnvelope) {
|
|
8864
9194
|
const previousRecord = await this.decryptRecord(previousEnvelope);
|
|
8865
|
-
|
|
9195
|
+
if (previousRecord !== null) {
|
|
9196
|
+
existing = { record: previousRecord, version: previousEnvelope._v };
|
|
9197
|
+
}
|
|
8866
9198
|
}
|
|
8867
9199
|
}
|
|
8868
9200
|
} else {
|
|
@@ -8871,8 +9203,9 @@ var init_collection = __esm({
|
|
|
8871
9203
|
}
|
|
8872
9204
|
const version = existing ? existing.version + 1 : 1;
|
|
8873
9205
|
this.uniqueConstraints?.check(id, record);
|
|
9206
|
+
const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
|
|
8874
9207
|
if (existing && this.historyConfig.enabled !== false) {
|
|
8875
|
-
const historyEnvelope = await this.encryptRecord(existing.record, existing.version);
|
|
9208
|
+
const historyEnvelope = await this.encryptRecord(existing.record, existing.version, cek);
|
|
8876
9209
|
await this.historyStrategy.saveHistory(this.adapter, this.vault, this.name, id, historyEnvelope);
|
|
8877
9210
|
this.emitter.emit("history:save", {
|
|
8878
9211
|
vault: this.vault,
|
|
@@ -8886,7 +9219,7 @@ var init_collection = __esm({
|
|
|
8886
9219
|
});
|
|
8887
9220
|
}
|
|
8888
9221
|
}
|
|
8889
|
-
const envelope = await this.encryptRecord(record, version);
|
|
9222
|
+
const envelope = await this.encryptRecord(record, version, cek);
|
|
8890
9223
|
await this.adapter.put(this.vault, this.name, id, envelope);
|
|
8891
9224
|
if (this.ledger) {
|
|
8892
9225
|
const appendInput = {
|
|
@@ -8975,6 +9308,111 @@ var init_collection = __esm({
|
|
|
8975
9308
|
* output (carries `_derivedFrom`) — defensive guard against missed
|
|
8976
9309
|
* cycle detection.
|
|
8977
9310
|
*/
|
|
9311
|
+
/**
|
|
9312
|
+
* @internal #376 — the RAW stored record (canonical-money form, i18n maps
|
|
9313
|
+
* intact), WITHOUT the locale resolution `get()` applies. Used as the
|
|
9314
|
+
* patch base for self-write reverse-denorm so writing back never clobbers
|
|
9315
|
+
* an i18n map or re-quantizes money incorrectly. Returns null for
|
|
9316
|
+
* missing / tombstoned records.
|
|
9317
|
+
*/
|
|
9318
|
+
async _getStoredRecord(id) {
|
|
9319
|
+
let raw;
|
|
9320
|
+
if (this.lazy && this.lru) {
|
|
9321
|
+
const cached = this.lru.get(id);
|
|
9322
|
+
if (cached) raw = cached.record;
|
|
9323
|
+
else {
|
|
9324
|
+
const env = await this.adapter.get(this.vault, this.name, id);
|
|
9325
|
+
if (!env || isTombstone(env, this.encrypted)) return null;
|
|
9326
|
+
raw = await this.decryptRecord(env, { id });
|
|
9327
|
+
if (raw === null) return null;
|
|
9328
|
+
this.lru.set(id, { record: raw, version: env._v }, estimateRecordBytes(raw));
|
|
9329
|
+
}
|
|
9330
|
+
} else {
|
|
9331
|
+
await this.ensureHydrated();
|
|
9332
|
+
raw = this.cache.get(id)?.record ?? null;
|
|
9333
|
+
}
|
|
9334
|
+
if (raw === null) return null;
|
|
9335
|
+
return canonicalizeStoredMoney(raw, this.moneyFields);
|
|
9336
|
+
}
|
|
9337
|
+
/**
|
|
9338
|
+
* @internal #376 — ids of records whose top-level `field` equals `value`.
|
|
9339
|
+
* Uses the FK index when the field is indexed (O(matches)); otherwise a
|
|
9340
|
+
* linear scan (O(N) — fine for small child sets; index the FK to scale).
|
|
9341
|
+
*/
|
|
9342
|
+
async _findMatchingIds(field, value) {
|
|
9343
|
+
const hit = this.getIndexes()?.lookupEqual(field, value);
|
|
9344
|
+
if (hit) return [...hit];
|
|
9345
|
+
const target = String(value);
|
|
9346
|
+
const matches = (rec) => {
|
|
9347
|
+
const fv = rec[field];
|
|
9348
|
+
return (typeof fv === "string" || typeof fv === "number") && String(fv) === target;
|
|
9349
|
+
};
|
|
9350
|
+
if (!this.lazy) {
|
|
9351
|
+
await this.ensureHydrated();
|
|
9352
|
+
const out2 = [];
|
|
9353
|
+
for (const [rid, e] of this.cache) {
|
|
9354
|
+
if (matches(e.record)) out2.push(rid);
|
|
9355
|
+
}
|
|
9356
|
+
return out2;
|
|
9357
|
+
}
|
|
9358
|
+
const ids = await this.adapter.list(this.vault, this.name);
|
|
9359
|
+
const out = [];
|
|
9360
|
+
for (const rid of ids) {
|
|
9361
|
+
const raw = await this._getStoredRecord(rid);
|
|
9362
|
+
if (raw !== null && matches(raw)) out.push(rid);
|
|
9363
|
+
}
|
|
9364
|
+
return out;
|
|
9365
|
+
}
|
|
9366
|
+
/**
|
|
9367
|
+
* @internal #376 slice 2 — recompute a rollup aggregate onto the parent.
|
|
9368
|
+
* Gathers every child of `parentId`, runs `compute`, and patches only the
|
|
9369
|
+
* rollup `field` onto the parent's raw stored record (value-equality
|
|
9370
|
+
* guarded). No-op when the parent record does not exist.
|
|
9371
|
+
*/
|
|
9372
|
+
async recomputeRollup(spec, parentId) {
|
|
9373
|
+
if (this.derivationSource === void 0 || spec.rollup === void 0) return;
|
|
9374
|
+
const { from, key, field, compute } = spec.rollup;
|
|
9375
|
+
const into = spec.source;
|
|
9376
|
+
const intoColl = this.derivationSource.getCollection(into);
|
|
9377
|
+
const base = await intoColl._getStoredRecord(parentId);
|
|
9378
|
+
if (base === null) return;
|
|
9379
|
+
const fromColl = this.derivationSource.getCollection(from);
|
|
9380
|
+
const childIds = await fromColl._findMatchingIds(key, parentId);
|
|
9381
|
+
const children = [];
|
|
9382
|
+
for (const cid of childIds) {
|
|
9383
|
+
const c = await fromColl.get(cid);
|
|
9384
|
+
if (c !== null && c !== void 0) children.push(c);
|
|
9385
|
+
}
|
|
9386
|
+
const newValue = compute(children);
|
|
9387
|
+
if (selfWriteFieldEqual(base[field], newValue)) return;
|
|
9388
|
+
const patched = { ...base, [field]: newValue };
|
|
9389
|
+
const txCtx = this.derivationSource.getActiveTxContext();
|
|
9390
|
+
if (txCtx !== null) {
|
|
9391
|
+
const prior = await this.adapter.get(this.vault, into, parentId);
|
|
9392
|
+
txCtx._executed.push({
|
|
9393
|
+
op: { type: "put", vaultName: this.vault, collectionName: into, id: parentId },
|
|
9394
|
+
priorEnvelope: prior
|
|
9395
|
+
});
|
|
9396
|
+
}
|
|
9397
|
+
await intoColl.put(parentId, patched);
|
|
9398
|
+
}
|
|
9399
|
+
/**
|
|
9400
|
+
* @internal #376 slice 2 — fire any rollups for which THIS collection is the
|
|
9401
|
+
* child `from`, recomputing the affected parent after a child delete. Called
|
|
9402
|
+
* from the delete path with the just-removed record's key value. Other
|
|
9403
|
+
* derivation kinds do not react to deletes (unchanged).
|
|
9404
|
+
*/
|
|
9405
|
+
async dispatchRollupsOnDelete(deleted) {
|
|
9406
|
+
if (this.derivationSource === void 0) return;
|
|
9407
|
+
const registry = this.derivationSource.registry();
|
|
9408
|
+
const rec = deleted;
|
|
9409
|
+
for (const { spec } of registry.strategiesForSource(this.name)) {
|
|
9410
|
+
if (!spec.rollup || spec.rollup.from !== this.name) continue;
|
|
9411
|
+
const kv = rec[spec.rollup.key];
|
|
9412
|
+
if (typeof kv !== "string" && typeof kv !== "number") continue;
|
|
9413
|
+
await this.recomputeRollup(spec, String(kv));
|
|
9414
|
+
}
|
|
9415
|
+
}
|
|
8978
9416
|
async dispatchDerivations(id, record, version) {
|
|
8979
9417
|
if (this.derivationSource === void 0) return;
|
|
8980
9418
|
const incoming = canonicalizeStoredMoney(record, this.moneyFields);
|
|
@@ -8985,29 +9423,60 @@ var init_collection = __esm({
|
|
|
8985
9423
|
let DerivationExecutor2 = null;
|
|
8986
9424
|
for (const { spec, strategyHash } of strategies) {
|
|
8987
9425
|
const mode = typeof spec.lifecycle === "string" ? spec.lifecycle : spec.lifecycle.mode;
|
|
8988
|
-
if (
|
|
8989
|
-
if (
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
if (spec.source === this.name) {
|
|
8995
|
-
sourceWithId = { ...incoming, id };
|
|
9426
|
+
if (spec.rollup) {
|
|
9427
|
+
if (mode !== "eager") continue;
|
|
9428
|
+
let parentId;
|
|
9429
|
+
if (this.name === spec.rollup.from) {
|
|
9430
|
+
const kv = incoming[spec.rollup.key];
|
|
9431
|
+
parentId = typeof kv === "string" || typeof kv === "number" ? String(kv) : null;
|
|
8996
9432
|
} else {
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
|
|
9433
|
+
parentId = id;
|
|
9434
|
+
}
|
|
9435
|
+
if (parentId !== null) await this.recomputeRollup(spec, parentId);
|
|
9436
|
+
continue;
|
|
9437
|
+
}
|
|
9438
|
+
const isSource = spec.source === this.name;
|
|
9439
|
+
const isSibling = !isSource && (spec.sources?.includes(this.name) ?? false);
|
|
9440
|
+
const trigger = !isSource && !isSibling ? spec.triggerBy?.find((t) => t.collection === this.name) : void 0;
|
|
9441
|
+
const runs = [];
|
|
9442
|
+
if (isSource) {
|
|
9443
|
+
runs.push({ input: { ...incoming, id }, base: incoming, runId: id, version });
|
|
9444
|
+
} else if (isSibling) {
|
|
9445
|
+
const p = await this.derivationSource.getCollection(spec.source).get(id);
|
|
9446
|
+
if (p !== null && p !== void 0) {
|
|
9447
|
+
const raw = await this.derivationSource.getCollection(spec.source)._getStoredRecord(id);
|
|
9448
|
+
runs.push({ input: { ...p, id }, base: raw ?? p, runId: id, version: 0 });
|
|
9449
|
+
}
|
|
9450
|
+
} else if (trigger) {
|
|
9451
|
+
const srcColl = this.derivationSource.getCollection(spec.source);
|
|
9452
|
+
const ids = await srcColl._findMatchingIds(trigger.on, id);
|
|
9453
|
+
if (trigger.maxFanout !== void 0 && ids.length > trigger.maxFanout) {
|
|
9454
|
+
throw new DerivationCapExceededError(`triggerBy ${this.name}\u2192${spec.source}`, ids.length, trigger.maxFanout);
|
|
9001
9455
|
}
|
|
9456
|
+
for (const sid of ids) {
|
|
9457
|
+
const raw = await srcColl._getStoredRecord(sid);
|
|
9458
|
+
if (raw === null) continue;
|
|
9459
|
+
runs.push({ input: { ...raw, id: sid }, base: raw, runId: sid, version: 0 });
|
|
9460
|
+
}
|
|
9461
|
+
}
|
|
9462
|
+
if (runs.length === 0) continue;
|
|
9463
|
+
if (mode !== "eager") {
|
|
9464
|
+
for (const run of runs) await markStale(registry, spec, run.runId);
|
|
9465
|
+
continue;
|
|
9466
|
+
}
|
|
9467
|
+
if (DerivationExecutor2 === null) {
|
|
9468
|
+
({ DerivationExecutor: DerivationExecutor2 } = await Promise.resolve().then(() => (init_executor(), executor_exports)));
|
|
9469
|
+
}
|
|
9470
|
+
for (const run of runs) {
|
|
9002
9471
|
const ctx = { vault: this.derivationSource.getReadOnlyFacade() };
|
|
9003
|
-
const result = await DerivationExecutor2.run(spec,
|
|
9472
|
+
const result = await DerivationExecutor2.run(spec, run.input, run.version, strategyHash, ctx);
|
|
9004
9473
|
for (const key of Object.keys(spec.outputs)) {
|
|
9005
9474
|
const out = result.outputs[key];
|
|
9006
9475
|
if (!out) continue;
|
|
9007
9476
|
if (out.kind === "failed") {
|
|
9008
9477
|
const err = out.error;
|
|
9009
9478
|
if (spec.strict) throw err;
|
|
9010
|
-
console.warn(`[derivation] output "${key}" for source "${spec.source}" id="${
|
|
9479
|
+
console.warn(`[derivation] output "${key}" for source "${spec.source}" id="${run.runId}" failed:`, err);
|
|
9011
9480
|
continue;
|
|
9012
9481
|
}
|
|
9013
9482
|
const outSpec = spec.outputs[key];
|
|
@@ -9020,7 +9489,7 @@ var init_collection = __esm({
|
|
|
9020
9489
|
this.adapter,
|
|
9021
9490
|
this.vault,
|
|
9022
9491
|
spec.source,
|
|
9023
|
-
|
|
9492
|
+
run.runId,
|
|
9024
9493
|
key
|
|
9025
9494
|
);
|
|
9026
9495
|
const prevKeys = new Set(prior?.keys ?? []);
|
|
@@ -9047,7 +9516,7 @@ var init_collection = __esm({
|
|
|
9047
9516
|
}
|
|
9048
9517
|
await saveFanoutSidecar2(this.adapter, this.vault, {
|
|
9049
9518
|
source: spec.source,
|
|
9050
|
-
sourceId:
|
|
9519
|
+
sourceId: run.runId,
|
|
9051
9520
|
outputKey: key,
|
|
9052
9521
|
outputCollection: outSpec.collection,
|
|
9053
9522
|
keys: newKeysList
|
|
@@ -9055,25 +9524,44 @@ var init_collection = __esm({
|
|
|
9055
9524
|
continue;
|
|
9056
9525
|
}
|
|
9057
9526
|
if (out.skipped === true) {
|
|
9058
|
-
await outputCollection._internalDelete(
|
|
9527
|
+
await outputCollection._internalDelete(run.runId, txCtx);
|
|
9528
|
+
continue;
|
|
9529
|
+
}
|
|
9530
|
+
if (outSpec.shape === "record" && outSpec.denorm !== void 0 && outSpec.collection === spec.source) {
|
|
9531
|
+
const value = out.value;
|
|
9532
|
+
const patched = { ...run.base };
|
|
9533
|
+
let changed = false;
|
|
9534
|
+
for (const f of outSpec.denorm) {
|
|
9535
|
+
if (!selfWriteFieldEqual(run.base[f], value[f])) {
|
|
9536
|
+
patched[f] = value[f];
|
|
9537
|
+
changed = true;
|
|
9538
|
+
}
|
|
9539
|
+
}
|
|
9540
|
+
if (!changed) continue;
|
|
9541
|
+
if (txCtx !== null) {
|
|
9542
|
+
const prior = await this.adapter.get(this.vault, outSpec.collection, run.runId);
|
|
9543
|
+
txCtx._executed.push({
|
|
9544
|
+
op: { type: "put", vaultName: this.vault, collectionName: outSpec.collection, id: run.runId },
|
|
9545
|
+
priorEnvelope: prior
|
|
9546
|
+
});
|
|
9547
|
+
}
|
|
9548
|
+
await outputCollection.put(run.runId, patched);
|
|
9059
9549
|
continue;
|
|
9060
9550
|
}
|
|
9061
9551
|
if (txCtx !== null) {
|
|
9062
|
-
const prior = await this.adapter.get(this.vault, outSpec.collection,
|
|
9552
|
+
const prior = await this.adapter.get(this.vault, outSpec.collection, run.runId);
|
|
9063
9553
|
txCtx._executed.push({
|
|
9064
9554
|
op: {
|
|
9065
9555
|
type: "put",
|
|
9066
9556
|
vaultName: this.vault,
|
|
9067
9557
|
collectionName: outSpec.collection,
|
|
9068
|
-
id
|
|
9558
|
+
id: run.runId
|
|
9069
9559
|
},
|
|
9070
9560
|
priorEnvelope: prior
|
|
9071
9561
|
});
|
|
9072
9562
|
}
|
|
9073
|
-
await outputCollection.put(
|
|
9563
|
+
await outputCollection.put(run.runId, out.value);
|
|
9074
9564
|
}
|
|
9075
|
-
} else {
|
|
9076
|
-
await markStale(registry, spec, id);
|
|
9077
9565
|
}
|
|
9078
9566
|
}
|
|
9079
9567
|
}
|
|
@@ -9122,11 +9610,14 @@ var init_collection = __esm({
|
|
|
9122
9610
|
let count = 0;
|
|
9123
9611
|
for (const id of ids) {
|
|
9124
9612
|
const env = await this.adapter.get(this.vault, this.name, id);
|
|
9125
|
-
if (!env) continue;
|
|
9126
|
-
const
|
|
9613
|
+
if (!env || isTombstone(env, this.encrypted)) continue;
|
|
9614
|
+
const decoded = await this.decryptRecord(env, { skipValidation: true, id });
|
|
9615
|
+
if (decoded === null) continue;
|
|
9616
|
+
const record = decoded;
|
|
9127
9617
|
const next = transform(record);
|
|
9128
9618
|
const nextVersion = (env._v ?? 0) + 1;
|
|
9129
|
-
const
|
|
9619
|
+
const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
|
|
9620
|
+
const newEnv = await this.encryptRecord(next, nextVersion, cek);
|
|
9130
9621
|
await this.adapter.put(this.vault, this.name, id, newEnv);
|
|
9131
9622
|
await this._invalidateCacheEntry(id);
|
|
9132
9623
|
if (this.ledger) {
|
|
@@ -9238,14 +9729,17 @@ var init_collection = __esm({
|
|
|
9238
9729
|
const previousEnvelope2 = await this.adapter.get(this.vault, this.name, id);
|
|
9239
9730
|
if (previousEnvelope2) {
|
|
9240
9731
|
const previousRecord = await this.decryptRecord(previousEnvelope2);
|
|
9241
|
-
|
|
9732
|
+
if (previousRecord !== null) {
|
|
9733
|
+
existing = { record: previousRecord, version: previousEnvelope2._v };
|
|
9734
|
+
}
|
|
9242
9735
|
}
|
|
9243
9736
|
}
|
|
9244
9737
|
} else {
|
|
9245
9738
|
existing = this.cache.get(id);
|
|
9246
9739
|
}
|
|
9247
9740
|
if (existing && this.historyConfig.enabled !== false) {
|
|
9248
|
-
const
|
|
9741
|
+
const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
|
|
9742
|
+
const historyEnvelope = await this.encryptRecord(existing.record, existing.version, cek);
|
|
9249
9743
|
await this.historyStrategy.saveHistory(this.adapter, this.vault, this.name, id, historyEnvelope);
|
|
9250
9744
|
}
|
|
9251
9745
|
const previousEnvelope = await this.adapter.get(this.vault, this.name, id);
|
|
@@ -9284,8 +9778,53 @@ var init_collection = __esm({
|
|
|
9284
9778
|
if (!internal) {
|
|
9285
9779
|
await this.dispatchMaterializedViewsOnDelete(id);
|
|
9286
9780
|
await this.dispatchArrayDerivationsOnDelete(id);
|
|
9781
|
+
if (existing) await this.dispatchRollupsOnDelete(existing.record);
|
|
9287
9782
|
}
|
|
9288
9783
|
}
|
|
9784
|
+
/**
|
|
9785
|
+
* @internal — GDPR crypto-shred a LIVE record to a tombstone (#304).
|
|
9786
|
+
*
|
|
9787
|
+
* Rewrites the on-disk envelope to `{ _noydb, _v, _ts, _by, _iv:'', _data:'' }`,
|
|
9788
|
+
* dropping `_iv`/`_data`/`_cek`/`_det`. The wrapped per-record CEK is gone, so
|
|
9789
|
+
* the body — and (via {@link tombstoneHistory}) every history version under
|
|
9790
|
+
* the same CEK — is permanently undecryptable; the collection DEK and every
|
|
9791
|
+
* other record are untouched. `_det` is stripped too, so `findByDet` no
|
|
9792
|
+
* longer matches the shredded record (avoiding a post-shred TamperedError).
|
|
9793
|
+
*
|
|
9794
|
+
* Unlike `delete()`/`_internalDelete`, this:
|
|
9795
|
+
* - does NOT fire onDelete guards / MV / derivation dispatch (a shred is an
|
|
9796
|
+
* erasure, not a domain delete — re-running those would be wrong),
|
|
9797
|
+
* - does NOT append a per-record ledger entry (`vault.forget()` appends a
|
|
9798
|
+
* single `op:'forget'` summary for the whole subject),
|
|
9799
|
+
* - keeps the record KEY present (it's an overwrite, not an adapter delete)
|
|
9800
|
+
* so the version counter + "record existed" survive for audit.
|
|
9801
|
+
*
|
|
9802
|
+
* Idempotent: returns `null` when the record is absent or already a tombstone.
|
|
9803
|
+
* Otherwise returns `{ previousVersion }`. Invalidates the eager cache, the
|
|
9804
|
+
* lazy LRU, and the per-record CEK cache for this id.
|
|
9805
|
+
*/
|
|
9806
|
+
/**
|
|
9807
|
+
* @internal — decrypt an envelope to a plain record for subject-index
|
|
9808
|
+
* rebuild (#304). Returns `null` for a tombstone or unreadable envelope.
|
|
9809
|
+
* Skips schema validation — the rebuild only reads the subject field.
|
|
9810
|
+
*/
|
|
9811
|
+
async _decodeEnvelope(envelope, id) {
|
|
9812
|
+
try {
|
|
9813
|
+
const rec = await this.decryptRecord(envelope, { skipValidation: true, id });
|
|
9814
|
+
return rec === null ? null : rec;
|
|
9815
|
+
} catch {
|
|
9816
|
+
return null;
|
|
9817
|
+
}
|
|
9818
|
+
}
|
|
9819
|
+
async _writeTombstone(id, actor) {
|
|
9820
|
+
const live = await this.adapter.get(this.vault, this.name, id);
|
|
9821
|
+
if (!live || isTombstone(live, this.encrypted)) return null;
|
|
9822
|
+
await this.adapter.put(this.vault, this.name, id, buildTombstone(live._v, actor));
|
|
9823
|
+
this.cache.delete(id);
|
|
9824
|
+
this.lru?.remove(id);
|
|
9825
|
+
this.cekCache?.remove(id);
|
|
9826
|
+
return { previousVersion: live._v };
|
|
9827
|
+
}
|
|
9289
9828
|
/**
|
|
9290
9829
|
* Cascade deletes of array-shape derived rows when a source row is
|
|
9291
9830
|
* deleted. Reads each registered strategy's fanout sidecar
|
|
@@ -9698,6 +10237,7 @@ var init_collection = __esm({
|
|
|
9698
10237
|
const entries = [];
|
|
9699
10238
|
for (const env of envelopes) {
|
|
9700
10239
|
const record = await this.decryptRecord(env, { skipValidation: true });
|
|
10240
|
+
if (record === null) continue;
|
|
9701
10241
|
entries.push({
|
|
9702
10242
|
version: env._v,
|
|
9703
10243
|
timestamp: env._ts,
|
|
@@ -9834,6 +10374,7 @@ var init_collection = __esm({
|
|
|
9834
10374
|
const envelope = await this.adapter.get(this.vault, this.name, id);
|
|
9835
10375
|
if (envelope) {
|
|
9836
10376
|
const record = await this.decryptRecord(envelope);
|
|
10377
|
+
if (record === null) continue;
|
|
9837
10378
|
items.push(record);
|
|
9838
10379
|
if (!this.lazy && !this.cache.has(id)) {
|
|
9839
10380
|
this.cache.set(id, { record, version: envelope._v });
|
|
@@ -9910,6 +10451,7 @@ var init_collection = __esm({
|
|
|
9910
10451
|
const out = [];
|
|
9911
10452
|
for (const { id, envelope } of items) {
|
|
9912
10453
|
const record = await this.decryptRecord(envelope);
|
|
10454
|
+
if (record === null) continue;
|
|
9913
10455
|
out.push({ id, record, version: envelope._v });
|
|
9914
10456
|
}
|
|
9915
10457
|
return out;
|
|
@@ -9931,6 +10473,18 @@ var init_collection = __esm({
|
|
|
9931
10473
|
* the cache entry (record still present) or deletes it (record was
|
|
9932
10474
|
* gone before the tx and the revert deleted it again).
|
|
9933
10475
|
*/
|
|
10476
|
+
/**
|
|
10477
|
+
* @internal — evict ONLY the per-record CEK cache entry for `id`. Used by
|
|
10478
|
+
* `vault.rotateRecordCek()`: after a hard CEK rotation the cached unwrapped
|
|
10479
|
+
* CEK is stale (it would decrypt the pre-rotation body and fail GCM auth on
|
|
10480
|
+
* the post-rotation body). Eviction must be synchronous with the live-envelope
|
|
10481
|
+
* rewrite so no concurrent read observes the old CEK. Paired with
|
|
10482
|
+
* {@link _invalidateCacheEntry} (which refreshes the decrypted-record cache).
|
|
10483
|
+
* No-op when the collection is not `perRecordKeys`.
|
|
10484
|
+
*/
|
|
10485
|
+
_invalidateCekCacheEntry(id) {
|
|
10486
|
+
this.cekCache?.remove(id);
|
|
10487
|
+
}
|
|
9934
10488
|
async _invalidateCacheEntry(id) {
|
|
9935
10489
|
if (this.lazy && this.lru) {
|
|
9936
10490
|
this.lru.remove(id);
|
|
@@ -9948,6 +10502,14 @@ var init_collection = __esm({
|
|
|
9948
10502
|
return;
|
|
9949
10503
|
}
|
|
9950
10504
|
const record = await this.decryptRecord(envelope);
|
|
10505
|
+
if (record === null) {
|
|
10506
|
+
this.cache.delete(id);
|
|
10507
|
+
if (previous) {
|
|
10508
|
+
this.indexes?.remove(id, previous.record);
|
|
10509
|
+
this.uniqueConstraints?.remove(id, previous.record);
|
|
10510
|
+
}
|
|
10511
|
+
return;
|
|
10512
|
+
}
|
|
9951
10513
|
this.cache.set(id, { record, version: envelope._v });
|
|
9952
10514
|
this.indexes?.upsert(id, record, previous ? previous.record : null);
|
|
9953
10515
|
this.uniqueConstraints?.upsert(id, record, previous?.record);
|
|
@@ -9972,8 +10534,9 @@ var init_collection = __esm({
|
|
|
9972
10534
|
const ids = await this.adapter.list(this.vault, this.name);
|
|
9973
10535
|
for (const id of ids) {
|
|
9974
10536
|
const envelope = await this.adapter.get(this.vault, this.name, id);
|
|
9975
|
-
if (envelope) {
|
|
9976
|
-
const record = await this.decryptRecord(envelope);
|
|
10537
|
+
if (envelope && !isTombstone(envelope, this.encrypted)) {
|
|
10538
|
+
const record = await this.decryptRecord(envelope, { id });
|
|
10539
|
+
if (record === null) continue;
|
|
9977
10540
|
this.cache.set(id, { record, version: envelope._v });
|
|
9978
10541
|
}
|
|
9979
10542
|
}
|
|
@@ -9984,7 +10547,9 @@ var init_collection = __esm({
|
|
|
9984
10547
|
/** Hydrate from a pre-loaded snapshot (used by Vault). */
|
|
9985
10548
|
async hydrateFromSnapshot(records) {
|
|
9986
10549
|
for (const [id, envelope] of Object.entries(records)) {
|
|
9987
|
-
|
|
10550
|
+
if (isTombstone(envelope, this.encrypted)) continue;
|
|
10551
|
+
const record = await this.decryptRecord(envelope, { id });
|
|
10552
|
+
if (record === null) continue;
|
|
9988
10553
|
this.cache.set(id, { record, version: envelope._v });
|
|
9989
10554
|
}
|
|
9990
10555
|
this.hydrated = true;
|
|
@@ -10072,6 +10637,7 @@ var init_collection = __esm({
|
|
|
10072
10637
|
const envelope = await this.adapter.get(this.vault, this.name, recordId2);
|
|
10073
10638
|
if (!envelope) continue;
|
|
10074
10639
|
const record = await this.decryptRecord(envelope, { skipValidation: true });
|
|
10640
|
+
if (record === null) continue;
|
|
10075
10641
|
await this.maintainPersistedIndexesOnPut(recordId2, record, null, envelope._v);
|
|
10076
10642
|
}
|
|
10077
10643
|
this.persistedIndexesLoaded = true;
|
|
@@ -10122,8 +10688,13 @@ var init_collection = __esm({
|
|
|
10122
10688
|
const env = await this.adapter.get(this.vault, this.name, id);
|
|
10123
10689
|
if (!env) continue;
|
|
10124
10690
|
try {
|
|
10125
|
-
const
|
|
10126
|
-
|
|
10691
|
+
const sidecarJson = await this.decryptJsonString(env);
|
|
10692
|
+
if (sidecarJson === null) {
|
|
10693
|
+
sidecar.set(decoded.recordId, void 0);
|
|
10694
|
+
} else {
|
|
10695
|
+
const body = JSON.parse(sidecarJson);
|
|
10696
|
+
sidecar.set(decoded.recordId, body.value);
|
|
10697
|
+
}
|
|
10127
10698
|
} catch {
|
|
10128
10699
|
sidecar.set(decoded.recordId, void 0);
|
|
10129
10700
|
}
|
|
@@ -10137,6 +10708,7 @@ var init_collection = __esm({
|
|
|
10137
10708
|
const env = await this.adapter.get(this.vault, this.name, id);
|
|
10138
10709
|
if (!env) continue;
|
|
10139
10710
|
const record = await this.decryptRecord(env, { skipValidation: true });
|
|
10711
|
+
if (record === null) continue;
|
|
10140
10712
|
const live = readPersistedValue(record, field);
|
|
10141
10713
|
const stored = sidecar.get(id);
|
|
10142
10714
|
const hasSidecar = sidecarIds.has(id);
|
|
@@ -10219,7 +10791,8 @@ var init_collection = __esm({
|
|
|
10219
10791
|
recordId: id,
|
|
10220
10792
|
getDEK: this.getDEK,
|
|
10221
10793
|
encrypted: this.encrypted,
|
|
10222
|
-
userId: this.keyring.userId
|
|
10794
|
+
userId: this.keyring.userId,
|
|
10795
|
+
erasableBlobs: this.perRecordCek
|
|
10223
10796
|
});
|
|
10224
10797
|
}
|
|
10225
10798
|
/** Get all records as encrypted envelopes (for dump). */
|
|
@@ -10227,7 +10800,8 @@ var init_collection = __esm({
|
|
|
10227
10800
|
await this.ensureHydrated();
|
|
10228
10801
|
const result = {};
|
|
10229
10802
|
for (const [id, entry] of this.cache) {
|
|
10230
|
-
|
|
10803
|
+
const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
|
|
10804
|
+
result[id] = await this.encryptRecord(entry.record, entry.version, cek);
|
|
10231
10805
|
}
|
|
10232
10806
|
return result;
|
|
10233
10807
|
}
|
|
@@ -10255,23 +10829,37 @@ var init_collection = __esm({
|
|
|
10255
10829
|
if (hasMoney && this.moneyFields) {
|
|
10256
10830
|
result = decodeMoneyFields(result, this.moneyFields, typeof locale === "string" ? locale : void 0);
|
|
10257
10831
|
}
|
|
10258
|
-
|
|
10259
|
-
|
|
10260
|
-
|
|
10832
|
+
const hasStaticDisplay = hasDict && this.dictKeyFields !== void 0 && Object.values(this.dictKeyFields).some(
|
|
10833
|
+
(d) => isStaticDictDescriptor(d) && d.displayLocale !== void 0
|
|
10834
|
+
);
|
|
10835
|
+
if (!locale && !hasStaticDisplay) return result;
|
|
10836
|
+
const layer = localeOpts?._layer ?? "read";
|
|
10837
|
+
if (locale && hasI18n && this.i18nFields) {
|
|
10838
|
+
result = this.i18nStrategy.applyI18nLocale(result, this.i18nFields, locale, localeOpts?.fallback, layer);
|
|
10261
10839
|
}
|
|
10262
10840
|
if (hasDict && this.dictKeyFields && this.dictLabelResolver && locale !== "raw") {
|
|
10263
10841
|
const withLabels = { ...result };
|
|
10264
10842
|
const resolver = this.dictLabelResolver;
|
|
10265
10843
|
for (const [field, desc] of Object.entries(this.dictKeyFields)) {
|
|
10266
|
-
const policy = desc.onMissing ? resolvePolicy(desc.onMissing,
|
|
10844
|
+
const policy = desc.onMissing ? resolvePolicy(desc.onMissing, layer) : "null";
|
|
10267
10845
|
const fallback = policy === "substitute" ? localeOpts?.fallback ?? desc.substitute : localeOpts?.fallback;
|
|
10846
|
+
const effLocale = locale ?? (isStaticDictDescriptor(desc) ? desc.displayLocale : void 0);
|
|
10268
10847
|
const resolveKey = async (key) => {
|
|
10269
|
-
|
|
10848
|
+
if (!effLocale) {
|
|
10849
|
+
if (policy === "throw") {
|
|
10850
|
+
throw new LocaleNotSpecifiedError(
|
|
10851
|
+
field,
|
|
10852
|
+
`dictKey "${field}": no locale active to resolve key "${key}".`
|
|
10853
|
+
);
|
|
10854
|
+
}
|
|
10855
|
+
return null;
|
|
10856
|
+
}
|
|
10857
|
+
const label = await resolver(desc.name, key, effLocale, fallback);
|
|
10270
10858
|
if (label === void 0) {
|
|
10271
10859
|
if (policy === "throw") {
|
|
10272
10860
|
throw new LocaleNotSpecifiedError(
|
|
10273
10861
|
field,
|
|
10274
|
-
`dictKey "${field}": no label for key "${key}" in locale "${
|
|
10862
|
+
`dictKey "${field}": no label for key "${key}" in locale "${effLocale}".`
|
|
10275
10863
|
);
|
|
10276
10864
|
}
|
|
10277
10865
|
return null;
|
|
@@ -10428,6 +11016,7 @@ var init_collection = __esm({
|
|
|
10428
11016
|
if (!envelope) continue;
|
|
10429
11017
|
try {
|
|
10430
11018
|
const json = await this.decryptJsonString(envelope);
|
|
11019
|
+
if (json === null) continue;
|
|
10431
11020
|
const body = JSON.parse(json);
|
|
10432
11021
|
if (typeof body.recordId !== "string") continue;
|
|
10433
11022
|
const rows = byField.get(decoded.field) ?? [];
|
|
@@ -10537,7 +11126,31 @@ var init_collection = __esm({
|
|
|
10537
11126
|
};
|
|
10538
11127
|
return new LazyQuery(source);
|
|
10539
11128
|
}
|
|
10540
|
-
|
|
11129
|
+
/**
|
|
11130
|
+
* Resolve the stable CEK for a record on the WRITE path — see
|
|
11131
|
+
* {@link resolveStableCek}. Thin delegate that supplies the collection's
|
|
11132
|
+
* CEK cache, live-envelope reader, and DEK resolver.
|
|
11133
|
+
*/
|
|
11134
|
+
resolveRecordCek(id) {
|
|
11135
|
+
return resolveStableCek(
|
|
11136
|
+
{
|
|
11137
|
+
cache: this.cekCache,
|
|
11138
|
+
getLive: (rid) => this.adapter.get(this.vault, this.name, rid),
|
|
11139
|
+
getDEK: () => this.getDEK(this.name)
|
|
11140
|
+
},
|
|
11141
|
+
id
|
|
11142
|
+
);
|
|
11143
|
+
}
|
|
11144
|
+
/**
|
|
11145
|
+
* Encrypt a JSON body into an envelope.
|
|
11146
|
+
*
|
|
11147
|
+
* When `cek` is supplied (per-record CEK collections), the body is
|
|
11148
|
+
* encrypted under the CEK and the CEK is AES-KW-wrapped under the
|
|
11149
|
+
* collection DEK and stamped on `_cek`. When `cek` is omitted, the legacy
|
|
11150
|
+
* path encrypts the body directly under the collection DEK — byte-identical
|
|
11151
|
+
* to pre-CEK behaviour, so non-adopting collections pay nothing.
|
|
11152
|
+
*/
|
|
11153
|
+
async encryptJsonString(json, version, cek) {
|
|
10541
11154
|
const by = this.keyring.userId;
|
|
10542
11155
|
if (!this.encrypted) {
|
|
10543
11156
|
return {
|
|
@@ -10550,6 +11163,19 @@ var init_collection = __esm({
|
|
|
10550
11163
|
};
|
|
10551
11164
|
}
|
|
10552
11165
|
const dek = await this.getDEK(this.name);
|
|
11166
|
+
if (cek !== void 0) {
|
|
11167
|
+
const { iv: iv2, data: data2 } = await encrypt(json, cek);
|
|
11168
|
+
const wrapped = await wrapCek(cek, dek);
|
|
11169
|
+
return {
|
|
11170
|
+
_noydb: NOYDB_FORMAT_VERSION,
|
|
11171
|
+
_v: version,
|
|
11172
|
+
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11173
|
+
_iv: iv2,
|
|
11174
|
+
_data: data2,
|
|
11175
|
+
_by: by,
|
|
11176
|
+
_cek: wrapped
|
|
11177
|
+
};
|
|
11178
|
+
}
|
|
10553
11179
|
const { iv, data } = await encrypt(json, dek);
|
|
10554
11180
|
return {
|
|
10555
11181
|
_noydb: NOYDB_FORMAT_VERSION,
|
|
@@ -10560,8 +11186,8 @@ var init_collection = __esm({
|
|
|
10560
11186
|
_by: by
|
|
10561
11187
|
};
|
|
10562
11188
|
}
|
|
10563
|
-
async encryptRecord(record, version) {
|
|
10564
|
-
const base = await this.encryptJsonString(JSON.stringify(record), version);
|
|
11189
|
+
async encryptRecord(record, version, cek) {
|
|
11190
|
+
const base = await this.encryptJsonString(JSON.stringify(record), version, cek);
|
|
10565
11191
|
if (!this.deterministicFields || !this.encrypted) return base;
|
|
10566
11192
|
const dek = await this.getDEK(this.name);
|
|
10567
11193
|
const rec = record;
|
|
@@ -10639,7 +11265,8 @@ var init_collection = __esm({
|
|
|
10639
11265
|
const env = await this.adapter.get(this.vault, this.name, id);
|
|
10640
11266
|
if (!env || !env._det) continue;
|
|
10641
11267
|
if (env._det[field] === target) {
|
|
10642
|
-
|
|
11268
|
+
const rec = await this.decryptRecord(env);
|
|
11269
|
+
if (rec !== null) matches.push(rec);
|
|
10643
11270
|
}
|
|
10644
11271
|
}
|
|
10645
11272
|
return matches;
|
|
@@ -10740,7 +11367,14 @@ var init_collection = __esm({
|
|
|
10740
11367
|
return null;
|
|
10741
11368
|
}
|
|
10742
11369
|
const dek = await this.getDEK(key);
|
|
10743
|
-
|
|
11370
|
+
let plaintext;
|
|
11371
|
+
if (envelope._cek !== void 0) {
|
|
11372
|
+
const cek = await unwrapCek(envelope._cek, dek);
|
|
11373
|
+
this.cekCache?.set(id, cek, 1);
|
|
11374
|
+
plaintext = await decrypt(envelope._iv, envelope._data, cek);
|
|
11375
|
+
} else {
|
|
11376
|
+
plaintext = await decrypt(envelope._iv, envelope._data, dek);
|
|
11377
|
+
}
|
|
10744
11378
|
const record = JSON.parse(plaintext);
|
|
10745
11379
|
this.emitCrossTierEvent({
|
|
10746
11380
|
actor: this.keyring.userId,
|
|
@@ -10796,18 +11430,19 @@ var init_collection = __esm({
|
|
|
10796
11430
|
const toKey = dekKey(this.name, toTier);
|
|
10797
11431
|
const fromDek = await this.getDEK(fromKey);
|
|
10798
11432
|
const toDek = await this.getDEK(toKey);
|
|
10799
|
-
const plaintext = await decrypt(envelope._iv, envelope._data, fromDek);
|
|
10800
|
-
const { iv, data } = await encrypt(plaintext, toDek);
|
|
10801
11433
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11434
|
+
const body = await rewrapBodyToDek(envelope, fromDek, toDek);
|
|
11435
|
+
if (body.cek) this.cekCache?.set(id, body.cek, 1);
|
|
10802
11436
|
const next = {
|
|
10803
11437
|
_noydb: NOYDB_FORMAT_VERSION,
|
|
10804
11438
|
_v: envelope._v + 1,
|
|
10805
11439
|
_ts: now,
|
|
10806
|
-
_iv:
|
|
10807
|
-
_data:
|
|
11440
|
+
_iv: body._iv,
|
|
11441
|
+
_data: body._data,
|
|
10808
11442
|
_by: this.keyring.userId,
|
|
10809
11443
|
_tier: toTier,
|
|
10810
|
-
_elevatedBy: this.keyring.userId
|
|
11444
|
+
_elevatedBy: this.keyring.userId,
|
|
11445
|
+
...body._cek !== void 0 ? { _cek: body._cek } : {}
|
|
10811
11446
|
};
|
|
10812
11447
|
await this.adapter.put(this.vault, this.name, id, next);
|
|
10813
11448
|
this.emitCrossTierEvent({
|
|
@@ -10843,17 +11478,18 @@ var init_collection = __esm({
|
|
|
10843
11478
|
if (toTier > 0) this.assertDeclaredTier(toTier);
|
|
10844
11479
|
const fromDek = await this.getDEK(dekKey(this.name, fromTier));
|
|
10845
11480
|
const toDek = await this.getDEK(dekKey(this.name, toTier));
|
|
10846
|
-
const plaintext = await decrypt(envelope._iv, envelope._data, fromDek);
|
|
10847
|
-
const { iv, data } = await encrypt(plaintext, toDek);
|
|
10848
11481
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11482
|
+
const body = await rewrapBodyToDek(envelope, fromDek, toDek);
|
|
11483
|
+
if (body.cek) this.cekCache?.set(id, body.cek, 1);
|
|
10849
11484
|
const next = {
|
|
10850
11485
|
_noydb: NOYDB_FORMAT_VERSION,
|
|
10851
11486
|
_v: envelope._v + 1,
|
|
10852
11487
|
_ts: now,
|
|
10853
|
-
_iv:
|
|
10854
|
-
_data:
|
|
11488
|
+
_iv: body._iv,
|
|
11489
|
+
_data: body._data,
|
|
10855
11490
|
_by: this.keyring.userId,
|
|
10856
|
-
...toTier > 0 && { _tier: toTier }
|
|
11491
|
+
...toTier > 0 && { _tier: toTier },
|
|
11492
|
+
...body._cek !== void 0 ? { _cek: body._cek } : {}
|
|
10857
11493
|
};
|
|
10858
11494
|
await this.adapter.put(this.vault, this.name, id, next);
|
|
10859
11495
|
this.emitCrossTierEvent({
|
|
@@ -10875,10 +11511,30 @@ var init_collection = __esm({
|
|
|
10875
11511
|
} catch {
|
|
10876
11512
|
}
|
|
10877
11513
|
}
|
|
10878
|
-
/**
|
|
10879
|
-
|
|
11514
|
+
/**
|
|
11515
|
+
* Low-level: decrypt an envelope and return the raw JSON string.
|
|
11516
|
+
*
|
|
11517
|
+
* `_cek` presence is the format discriminant (NOT `this.perRecordCek`),
|
|
11518
|
+
* so a mixed vault — and a recipient that never opted into
|
|
11519
|
+
* `perRecordKeys` — decrypts both legacy and CEK records:
|
|
11520
|
+
* - `_cek` present → unwrap the CEK under the collection DEK, decrypt the
|
|
11521
|
+
* body under the CEK (cache the unwrapped CEK so repeated reads skip it).
|
|
11522
|
+
* - `_cek` absent → legacy path, body decrypts directly under the
|
|
11523
|
+
* collection DEK.
|
|
11524
|
+
*
|
|
11525
|
+
* The optional `id` lets reads populate the CEK cache; it is omitted by
|
|
11526
|
+
* callers (history, conflict merge) that have only the envelope.
|
|
11527
|
+
*/
|
|
11528
|
+
async decryptJsonString(envelope, id) {
|
|
11529
|
+
if (isTombstone(envelope, this.encrypted)) return null;
|
|
10880
11530
|
if (!this.encrypted) return envelope._data;
|
|
10881
11531
|
const dek = await this.getDEK(this.name);
|
|
11532
|
+
if (envelope._cek !== void 0) {
|
|
11533
|
+
const cached = id !== void 0 ? this.cekCache?.get(id) : void 0;
|
|
11534
|
+
const cek = cached ?? await unwrapCek(envelope._cek, dek);
|
|
11535
|
+
if (cached === void 0 && id !== void 0) this.cekCache?.set(id, cek, 1);
|
|
11536
|
+
return decrypt(envelope._iv, envelope._data, cek);
|
|
11537
|
+
}
|
|
10882
11538
|
return decrypt(envelope._iv, envelope._data, dek);
|
|
10883
11539
|
}
|
|
10884
11540
|
/**
|
|
@@ -10897,7 +11553,8 @@ var init_collection = __esm({
|
|
|
10897
11553
|
* false positive. Every non-history read leaves this flag `false`.
|
|
10898
11554
|
*/
|
|
10899
11555
|
async decryptRecord(envelope, opts = {}) {
|
|
10900
|
-
const json = await this.decryptJsonString(envelope);
|
|
11556
|
+
const json = await this.decryptJsonString(envelope, opts.id);
|
|
11557
|
+
if (json === null) return null;
|
|
10901
11558
|
let parsed = JSON.parse(json);
|
|
10902
11559
|
if (this.crdtMode && parsed !== null && typeof parsed === "object" && "_crdt" in parsed) {
|
|
10903
11560
|
parsed = this.crdtStrategy.resolveCrdtSnapshot(parsed);
|
|
@@ -11204,6 +11861,35 @@ var init_archive = __esm({
|
|
|
11204
11861
|
});
|
|
11205
11862
|
|
|
11206
11863
|
// src/sequence/index.ts
|
|
11864
|
+
function compileSequenceFormat(format, series, partition) {
|
|
11865
|
+
const parts = partition ?? [];
|
|
11866
|
+
for (const m of format.matchAll(SEQ_FORMAT_TOKEN)) {
|
|
11867
|
+
const token = m[1] ?? "";
|
|
11868
|
+
if (token === "seq") continue;
|
|
11869
|
+
if (SEQ_PAD_TOKEN.test(token)) continue;
|
|
11870
|
+
const partMatch = SEQ_PARTITION_TOKEN.exec(token);
|
|
11871
|
+
if (partMatch) {
|
|
11872
|
+
const idx = Number(partMatch[1]);
|
|
11873
|
+
if (idx >= parts.length) {
|
|
11874
|
+
throw new ValidationError(
|
|
11875
|
+
`sequence("${series}"): format token "{${token}}" references partition index ${idx}, but only ${parts.length} partition component(s) were supplied.`
|
|
11876
|
+
);
|
|
11877
|
+
}
|
|
11878
|
+
continue;
|
|
11879
|
+
}
|
|
11880
|
+
throw new ValidationError(
|
|
11881
|
+
`sequence("${series}"): format contains unknown token "{${token}}". Accepted tokens: {seq}, {seq:0N}, {partition.i}.`
|
|
11882
|
+
);
|
|
11883
|
+
}
|
|
11884
|
+
return (serial) => format.replace(SEQ_FORMAT_TOKEN, (full, token) => {
|
|
11885
|
+
if (token === "seq") return String(serial);
|
|
11886
|
+
const padMatch = SEQ_PAD_TOKEN.exec(token);
|
|
11887
|
+
if (padMatch) return String(serial).padStart(Number(padMatch[1]), "0");
|
|
11888
|
+
const partMatch = SEQ_PARTITION_TOKEN.exec(token);
|
|
11889
|
+
if (partMatch) return String(parts[Number(partMatch[1])]);
|
|
11890
|
+
return full;
|
|
11891
|
+
});
|
|
11892
|
+
}
|
|
11207
11893
|
function resolveSequenceKey(series, opts) {
|
|
11208
11894
|
const partition = opts?.partition;
|
|
11209
11895
|
if (!partition || partition.length === 0) return series;
|
|
@@ -11224,7 +11910,7 @@ async function sleepBackoff2(attempt) {
|
|
|
11224
11910
|
const ms = Math.floor(Math.random() * ceil);
|
|
11225
11911
|
await new Promise((r) => setTimeout(r, ms));
|
|
11226
11912
|
}
|
|
11227
|
-
var SEQUENCE_COLLECTION, MAX_NEXT_ATTEMPTS, SequenceStore;
|
|
11913
|
+
var SEQUENCE_COLLECTION, MAX_NEXT_ATTEMPTS, SEQ_FORMAT_TOKEN, SEQ_PAD_TOKEN, SEQ_PARTITION_TOKEN, SequenceStore;
|
|
11228
11914
|
var init_sequence = __esm({
|
|
11229
11915
|
"src/sequence/index.ts"() {
|
|
11230
11916
|
"use strict";
|
|
@@ -11233,6 +11919,9 @@ var init_sequence = __esm({
|
|
|
11233
11919
|
init_errors();
|
|
11234
11920
|
SEQUENCE_COLLECTION = "_sequences";
|
|
11235
11921
|
MAX_NEXT_ATTEMPTS = 16;
|
|
11922
|
+
SEQ_FORMAT_TOKEN = /\{([^{}]*)\}/g;
|
|
11923
|
+
SEQ_PAD_TOKEN = /^seq:0(\d+)$/;
|
|
11924
|
+
SEQ_PARTITION_TOKEN = /^partition\.(\d+)$/;
|
|
11236
11925
|
SequenceStore = class {
|
|
11237
11926
|
adapter;
|
|
11238
11927
|
vault;
|
|
@@ -11502,9 +12191,125 @@ var init_numbering = __esm({
|
|
|
11502
12191
|
}
|
|
11503
12192
|
});
|
|
11504
12193
|
|
|
12194
|
+
// src/forget/strategy.ts
|
|
12195
|
+
var NO_FORGET;
|
|
12196
|
+
var init_strategy7 = __esm({
|
|
12197
|
+
"src/forget/strategy.ts"() {
|
|
12198
|
+
"use strict";
|
|
12199
|
+
NO_FORGET = { subjects: {} };
|
|
12200
|
+
}
|
|
12201
|
+
});
|
|
12202
|
+
|
|
12203
|
+
// src/forget/subject-index.ts
|
|
12204
|
+
async function sha256HexString(input) {
|
|
12205
|
+
const bytes = new TextEncoder().encode(input);
|
|
12206
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
|
|
12207
|
+
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
12208
|
+
}
|
|
12209
|
+
async function subjectKey(subjectId) {
|
|
12210
|
+
return sha256HexString(subjectId);
|
|
12211
|
+
}
|
|
12212
|
+
async function readRefs(adapter, vault, getDEK, encrypted, key) {
|
|
12213
|
+
const env = await adapter.get(vault, SUBJECT_INDEX_COLLECTION, key);
|
|
12214
|
+
if (!env || !env._data) return [];
|
|
12215
|
+
if (!encrypted) return JSON.parse(env._data);
|
|
12216
|
+
const dek = await getDEK(SUBJECT_INDEX_COLLECTION);
|
|
12217
|
+
const json = await decrypt(env._iv, env._data, dek);
|
|
12218
|
+
return JSON.parse(json);
|
|
12219
|
+
}
|
|
12220
|
+
async function writeRefs(adapter, vault, getDEK, encrypted, key, refs) {
|
|
12221
|
+
const json = JSON.stringify(refs);
|
|
12222
|
+
let env;
|
|
12223
|
+
if (!encrypted) {
|
|
12224
|
+
env = { _noydb: NOYDB_FORMAT_VERSION, _v: 1, _ts: (/* @__PURE__ */ new Date()).toISOString(), _iv: "", _data: json };
|
|
12225
|
+
} else {
|
|
12226
|
+
const dek = await getDEK(SUBJECT_INDEX_COLLECTION);
|
|
12227
|
+
const { iv, data } = await encrypt(json, dek);
|
|
12228
|
+
env = { _noydb: NOYDB_FORMAT_VERSION, _v: 1, _ts: (/* @__PURE__ */ new Date()).toISOString(), _iv: iv, _data: data };
|
|
12229
|
+
}
|
|
12230
|
+
await adapter.put(vault, SUBJECT_INDEX_COLLECTION, key, env);
|
|
12231
|
+
}
|
|
12232
|
+
async function addSubjectRef(adapter, vault, getDEK, encrypted, subjectId, ref) {
|
|
12233
|
+
const key = await subjectKey(subjectId);
|
|
12234
|
+
const refs = await readRefs(adapter, vault, getDEK, encrypted, key);
|
|
12235
|
+
if (refs.some((r) => r.collection === ref.collection && r.id === ref.id)) return;
|
|
12236
|
+
refs.push(ref);
|
|
12237
|
+
await writeRefs(adapter, vault, getDEK, encrypted, key, refs);
|
|
12238
|
+
}
|
|
12239
|
+
async function removeSubjectRef(adapter, vault, getDEK, encrypted, subjectId, ref) {
|
|
12240
|
+
const key = await subjectKey(subjectId);
|
|
12241
|
+
const refs = await readRefs(adapter, vault, getDEK, encrypted, key);
|
|
12242
|
+
const next = refs.filter((r) => !(r.collection === ref.collection && r.id === ref.id));
|
|
12243
|
+
if (next.length === refs.length) return;
|
|
12244
|
+
if (next.length === 0) {
|
|
12245
|
+
await adapter.delete(vault, SUBJECT_INDEX_COLLECTION, key);
|
|
12246
|
+
return;
|
|
12247
|
+
}
|
|
12248
|
+
await writeRefs(adapter, vault, getDEK, encrypted, key, next);
|
|
12249
|
+
}
|
|
12250
|
+
async function lookupSubject(adapter, vault, getDEK, encrypted, subjectId) {
|
|
12251
|
+
const key = await subjectKey(subjectId);
|
|
12252
|
+
return readRefs(adapter, vault, getDEK, encrypted, key);
|
|
12253
|
+
}
|
|
12254
|
+
async function rebuildSubjectIndex(adapter, vault, getDEK, encrypted, subjects, decodeRecord) {
|
|
12255
|
+
const existing = await adapter.list(vault, SUBJECT_INDEX_COLLECTION);
|
|
12256
|
+
for (const k of existing) {
|
|
12257
|
+
await adapter.delete(vault, SUBJECT_INDEX_COLLECTION, k);
|
|
12258
|
+
}
|
|
12259
|
+
const bySubject = /* @__PURE__ */ new Map();
|
|
12260
|
+
for (const [collection, field] of Object.entries(subjects)) {
|
|
12261
|
+
const ids = await adapter.list(vault, collection);
|
|
12262
|
+
for (const id of ids) {
|
|
12263
|
+
if (id.startsWith("_")) continue;
|
|
12264
|
+
const env = await adapter.get(vault, collection, id);
|
|
12265
|
+
if (!env || !env._data) continue;
|
|
12266
|
+
const record = await decodeRecord(collection, id, env);
|
|
12267
|
+
if (record === null) continue;
|
|
12268
|
+
const subjectValue = readDottedPath(record, field);
|
|
12269
|
+
if (subjectValue === void 0 || subjectValue === null) continue;
|
|
12270
|
+
const subjectId = coerceSubjectId(subjectValue);
|
|
12271
|
+
const list = bySubject.get(subjectId) ?? [];
|
|
12272
|
+
list.push({ collection, id });
|
|
12273
|
+
bySubject.set(subjectId, list);
|
|
12274
|
+
}
|
|
12275
|
+
}
|
|
12276
|
+
let entries = 0;
|
|
12277
|
+
for (const [subjectId, refs] of bySubject) {
|
|
12278
|
+
const key = await subjectKey(subjectId);
|
|
12279
|
+
await writeRefs(adapter, vault, getDEK, encrypted, key, refs);
|
|
12280
|
+
entries++;
|
|
12281
|
+
}
|
|
12282
|
+
return entries;
|
|
12283
|
+
}
|
|
12284
|
+
function coerceSubjectId(value) {
|
|
12285
|
+
if (typeof value === "string") return value;
|
|
12286
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
12287
|
+
return String(value);
|
|
12288
|
+
}
|
|
12289
|
+
return JSON.stringify(value);
|
|
12290
|
+
}
|
|
12291
|
+
function readDottedPath(record, field) {
|
|
12292
|
+
if (!field.includes(".")) return record[field];
|
|
12293
|
+
let cursor = record;
|
|
12294
|
+
for (const segment of field.split(".")) {
|
|
12295
|
+
if (cursor === null || cursor === void 0) return void 0;
|
|
12296
|
+
cursor = cursor[segment];
|
|
12297
|
+
}
|
|
12298
|
+
return cursor;
|
|
12299
|
+
}
|
|
12300
|
+
var SUBJECT_INDEX_COLLECTION;
|
|
12301
|
+
var init_subject_index = __esm({
|
|
12302
|
+
"src/forget/subject-index.ts"() {
|
|
12303
|
+
"use strict";
|
|
12304
|
+
init_crypto();
|
|
12305
|
+
init_types();
|
|
12306
|
+
SUBJECT_INDEX_COLLECTION = "_subject_index";
|
|
12307
|
+
}
|
|
12308
|
+
});
|
|
12309
|
+
|
|
11505
12310
|
// src/shadow/strategy.ts
|
|
11506
12311
|
var NOT_ENABLED3, NO_SHADOW;
|
|
11507
|
-
var
|
|
12312
|
+
var init_strategy8 = __esm({
|
|
11508
12313
|
"src/shadow/strategy.ts"() {
|
|
11509
12314
|
"use strict";
|
|
11510
12315
|
NOT_ENABLED3 = new Error(
|
|
@@ -11520,7 +12325,7 @@ var init_strategy7 = __esm({
|
|
|
11520
12325
|
|
|
11521
12326
|
// src/consent/strategy.ts
|
|
11522
12327
|
var NO_CONSENT;
|
|
11523
|
-
var
|
|
12328
|
+
var init_strategy9 = __esm({
|
|
11524
12329
|
"src/consent/strategy.ts"() {
|
|
11525
12330
|
"use strict";
|
|
11526
12331
|
NO_CONSENT = {
|
|
@@ -11535,7 +12340,7 @@ var init_strategy8 = __esm({
|
|
|
11535
12340
|
|
|
11536
12341
|
// src/periods/strategy.ts
|
|
11537
12342
|
var NOT_ENABLED4, NO_PERIODS;
|
|
11538
|
-
var
|
|
12343
|
+
var init_strategy10 = __esm({
|
|
11539
12344
|
"src/periods/strategy.ts"() {
|
|
11540
12345
|
"use strict";
|
|
11541
12346
|
NOT_ENABLED4 = new Error(
|
|
@@ -11561,6 +12366,9 @@ var init_strategy9 = __esm({
|
|
|
11561
12366
|
});
|
|
11562
12367
|
|
|
11563
12368
|
// src/refs.ts
|
|
12369
|
+
function isRefArray(desc) {
|
|
12370
|
+
return desc.isArray === true;
|
|
12371
|
+
}
|
|
11564
12372
|
var RefIntegrityError, RefRegistry;
|
|
11565
12373
|
var init_refs = __esm({
|
|
11566
12374
|
"src/refs.ts"() {
|
|
@@ -11606,7 +12414,7 @@ var init_refs = __esm({
|
|
|
11606
12414
|
for (const k of existingKeys) {
|
|
11607
12415
|
const a = existing[k];
|
|
11608
12416
|
const b = refs[k];
|
|
11609
|
-
if (!a || !b || a.target !== b.target || a.mode !== b.mode) {
|
|
12417
|
+
if (!a || !b || a.target !== b.target || a.mode !== b.mode || a.isArray !== b.isArray) {
|
|
11610
12418
|
throw new Error(
|
|
11611
12419
|
`RefRegistry: conflicting ref declarations for collection "${collection}" field "${k}"`
|
|
11612
12420
|
);
|
|
@@ -11617,7 +12425,7 @@ var init_refs = __esm({
|
|
|
11617
12425
|
this.outbound.set(collection, { ...refs });
|
|
11618
12426
|
for (const [field, desc] of Object.entries(refs)) {
|
|
11619
12427
|
const list = this.inbound.get(desc.target) ?? [];
|
|
11620
|
-
list.push({ collection, field, mode: desc.mode });
|
|
12428
|
+
list.push({ collection, field, mode: desc.mode, ...desc.isArray ? { isArray: true } : {} });
|
|
11621
12429
|
this.inbound.set(desc.target, list);
|
|
11622
12430
|
}
|
|
11623
12431
|
}
|
|
@@ -11647,15 +12455,147 @@ var init_refs = __esm({
|
|
|
11647
12455
|
}
|
|
11648
12456
|
});
|
|
11649
12457
|
|
|
11650
|
-
// src/
|
|
11651
|
-
function
|
|
11652
|
-
return name
|
|
12458
|
+
// src/links/link-set.ts
|
|
12459
|
+
function linkCollectionName(name) {
|
|
12460
|
+
return `${LINK_COLLECTION_PREFIX}${name}`;
|
|
11653
12461
|
}
|
|
11654
|
-
|
|
11655
|
-
|
|
11656
|
-
|
|
12462
|
+
function isLinkCollectionName(name) {
|
|
12463
|
+
return name.startsWith(LINK_COLLECTION_PREFIX);
|
|
12464
|
+
}
|
|
12465
|
+
function linkRowKey(aId, bId) {
|
|
12466
|
+
return `${encodeURIComponent(aId)}|${encodeURIComponent(bId)}`;
|
|
12467
|
+
}
|
|
12468
|
+
var LINK_COLLECTION_PREFIX, LinkSet, LinkEndpointError, LinkIntegrityError;
|
|
12469
|
+
var init_link_set = __esm({
|
|
12470
|
+
"src/links/link-set.ts"() {
|
|
11657
12471
|
"use strict";
|
|
11658
|
-
|
|
12472
|
+
init_types();
|
|
12473
|
+
init_crypto();
|
|
12474
|
+
init_errors();
|
|
12475
|
+
LINK_COLLECTION_PREFIX = "_links_";
|
|
12476
|
+
LinkSet = class {
|
|
12477
|
+
constructor(adapter, vault, name, spec, encrypted, getDEK, actor, emitter, endpointExists) {
|
|
12478
|
+
this.adapter = adapter;
|
|
12479
|
+
this.vault = vault;
|
|
12480
|
+
this.name = name;
|
|
12481
|
+
this.spec = spec;
|
|
12482
|
+
this.encrypted = encrypted;
|
|
12483
|
+
this.getDEK = getDEK;
|
|
12484
|
+
this.actor = actor;
|
|
12485
|
+
this.emitter = emitter;
|
|
12486
|
+
this.endpointExists = endpointExists;
|
|
12487
|
+
this.collName = linkCollectionName(name);
|
|
12488
|
+
}
|
|
12489
|
+
adapter;
|
|
12490
|
+
vault;
|
|
12491
|
+
name;
|
|
12492
|
+
spec;
|
|
12493
|
+
encrypted;
|
|
12494
|
+
getDEK;
|
|
12495
|
+
actor;
|
|
12496
|
+
emitter;
|
|
12497
|
+
endpointExists;
|
|
12498
|
+
collName;
|
|
12499
|
+
dekPromise = null;
|
|
12500
|
+
dek() {
|
|
12501
|
+
if (!this.dekPromise) this.dekPromise = this.getDEK(this.collName);
|
|
12502
|
+
return this.dekPromise;
|
|
12503
|
+
}
|
|
12504
|
+
async encryptEntry(entry, version) {
|
|
12505
|
+
const json = JSON.stringify(entry);
|
|
12506
|
+
const base = { _noydb: NOYDB_FORMAT_VERSION, _v: version, _ts: (/* @__PURE__ */ new Date()).toISOString(), _by: this.actor };
|
|
12507
|
+
if (!this.encrypted) return { ...base, _iv: "", _data: json };
|
|
12508
|
+
const { iv, data } = await encrypt(json, await this.dek());
|
|
12509
|
+
return { ...base, _iv: iv, _data: data };
|
|
12510
|
+
}
|
|
12511
|
+
async decryptEntry(env) {
|
|
12512
|
+
const json = this.encrypted ? await decrypt(env._iv, env._data, await this.dek()) : env._data;
|
|
12513
|
+
return JSON.parse(json);
|
|
12514
|
+
}
|
|
12515
|
+
async connect(aId, bId, meta) {
|
|
12516
|
+
if (!await this.endpointExists(this.spec.a, aId)) {
|
|
12517
|
+
throw new LinkEndpointError(this.name, this.spec.a, aId);
|
|
12518
|
+
}
|
|
12519
|
+
if (!await this.endpointExists(this.spec.b, bId)) {
|
|
12520
|
+
throw new LinkEndpointError(this.name, this.spec.b, bId);
|
|
12521
|
+
}
|
|
12522
|
+
const key = linkRowKey(aId, bId);
|
|
12523
|
+
const entry = meta !== void 0 ? { a: aId, b: bId, meta } : { a: aId, b: bId };
|
|
12524
|
+
const existing = await this.adapter.get(this.vault, this.collName, key);
|
|
12525
|
+
const env = await this.encryptEntry(entry, (existing?._v ?? 0) + 1);
|
|
12526
|
+
await this.adapter.put(this.vault, this.collName, key, env, existing?._v);
|
|
12527
|
+
this.emitter.emit("change", { vault: this.vault, collection: this.collName, id: key, action: "put" });
|
|
12528
|
+
}
|
|
12529
|
+
async disconnect(aId, bId) {
|
|
12530
|
+
const key = linkRowKey(aId, bId);
|
|
12531
|
+
const existing = await this.adapter.get(this.vault, this.collName, key);
|
|
12532
|
+
if (!existing) return;
|
|
12533
|
+
await this.adapter.delete(this.vault, this.collName, key);
|
|
12534
|
+
this.emitter.emit("change", { vault: this.vault, collection: this.collName, id: key, action: "delete" });
|
|
12535
|
+
}
|
|
12536
|
+
async has(aId, bId) {
|
|
12537
|
+
return await this.adapter.get(this.vault, this.collName, linkRowKey(aId, bId)) !== null;
|
|
12538
|
+
}
|
|
12539
|
+
async of(id) {
|
|
12540
|
+
const rows = await this.list();
|
|
12541
|
+
return rows.filter((r) => r.a === id || r.b === id);
|
|
12542
|
+
}
|
|
12543
|
+
async list() {
|
|
12544
|
+
const keys = await this.adapter.list(this.vault, this.collName);
|
|
12545
|
+
const out = [];
|
|
12546
|
+
for (const key of keys) {
|
|
12547
|
+
const env = await this.adapter.get(this.vault, this.collName, key);
|
|
12548
|
+
if (!env) continue;
|
|
12549
|
+
const e = await this.decryptEntry(env);
|
|
12550
|
+
out.push(e.meta !== void 0 ? { a: e.a, b: e.b, meta: e.meta } : { a: e.a, b: e.b });
|
|
12551
|
+
}
|
|
12552
|
+
return out;
|
|
12553
|
+
}
|
|
12554
|
+
// ── Vault-internal cascade helpers ──────────────────────────────────
|
|
12555
|
+
/** @internal — rows where the deleted endpoint id matches the relevant slot. */
|
|
12556
|
+
async _rowsTouchingEndpoint(collection, id) {
|
|
12557
|
+
const rows = await this.list();
|
|
12558
|
+
return rows.filter(
|
|
12559
|
+
(r) => this.spec.a === collection && r.a === id || this.spec.b === collection && r.b === id
|
|
12560
|
+
);
|
|
12561
|
+
}
|
|
12562
|
+
/** @internal — the storage collection name (for tx pre-image capture). */
|
|
12563
|
+
get _collectionName() {
|
|
12564
|
+
return this.collName;
|
|
12565
|
+
}
|
|
12566
|
+
};
|
|
12567
|
+
LinkEndpointError = class extends NoydbError {
|
|
12568
|
+
link;
|
|
12569
|
+
endpoint;
|
|
12570
|
+
missingId;
|
|
12571
|
+
constructor(link, endpoint, missingId) {
|
|
12572
|
+
super(
|
|
12573
|
+
"LINK_ENDPOINT",
|
|
12574
|
+
`link("${link}").connect: endpoint "${endpoint}" has no record "${missingId}".`
|
|
12575
|
+
);
|
|
12576
|
+
this.name = "LinkEndpointError";
|
|
12577
|
+
this.link = link;
|
|
12578
|
+
this.endpoint = endpoint;
|
|
12579
|
+
this.missingId = missingId;
|
|
12580
|
+
}
|
|
12581
|
+
};
|
|
12582
|
+
LinkIntegrityError = class extends NoydbError {
|
|
12583
|
+
link;
|
|
12584
|
+
endpoint;
|
|
12585
|
+
id;
|
|
12586
|
+
count;
|
|
12587
|
+
constructor(link, endpoint, id, count) {
|
|
12588
|
+
super(
|
|
12589
|
+
"LINK_INTEGRITY",
|
|
12590
|
+
`Cannot delete "${endpoint}"/"${id}": ${count} link(s) in "${link}" still reference it (onDelete: 'strict').`
|
|
12591
|
+
);
|
|
12592
|
+
this.name = "LinkIntegrityError";
|
|
12593
|
+
this.link = link;
|
|
12594
|
+
this.endpoint = endpoint;
|
|
12595
|
+
this.id = id;
|
|
12596
|
+
this.count = count;
|
|
12597
|
+
}
|
|
12598
|
+
};
|
|
11659
12599
|
}
|
|
11660
12600
|
});
|
|
11661
12601
|
|
|
@@ -13334,14 +14274,17 @@ var init_read_only_facade = __esm({
|
|
|
13334
14274
|
"use strict";
|
|
13335
14275
|
ReadOnlyVaultFacade = class {
|
|
13336
14276
|
_vault;
|
|
13337
|
-
|
|
14277
|
+
_layer;
|
|
14278
|
+
constructor(vault, layer = "read") {
|
|
13338
14279
|
this._vault = vault;
|
|
14280
|
+
this._layer = layer;
|
|
13339
14281
|
}
|
|
13340
14282
|
collection(name) {
|
|
13341
14283
|
const c = this._vault.collection(name);
|
|
14284
|
+
const layer = this._layer;
|
|
13342
14285
|
return {
|
|
13343
|
-
get: (id) => c.get(id),
|
|
13344
|
-
list: () => c.list(),
|
|
14286
|
+
get: (id) => c.get(id, { _layer: layer }),
|
|
14287
|
+
list: () => c.list({ _layer: layer }),
|
|
13345
14288
|
query: () => c.query()
|
|
13346
14289
|
};
|
|
13347
14290
|
}
|
|
@@ -13397,6 +14340,16 @@ var init_registry3 = __esm({
|
|
|
13397
14340
|
if (fromExtra) fromExtra.push(reg);
|
|
13398
14341
|
else this._bySource.set(extra, [reg]);
|
|
13399
14342
|
}
|
|
14343
|
+
for (const t of spec.triggerBy ?? []) {
|
|
14344
|
+
const fromTrigger = this._bySource.get(t.collection);
|
|
14345
|
+
if (fromTrigger) fromTrigger.push(reg);
|
|
14346
|
+
else this._bySource.set(t.collection, [reg]);
|
|
14347
|
+
}
|
|
14348
|
+
if (spec.rollup) {
|
|
14349
|
+
const fromRollup = this._bySource.get(spec.rollup.from);
|
|
14350
|
+
if (fromRollup) fromRollup.push(reg);
|
|
14351
|
+
else this._bySource.set(spec.rollup.from, [reg]);
|
|
14352
|
+
}
|
|
13400
14353
|
for (const key of outputKeys) {
|
|
13401
14354
|
const output = spec.outputs[key];
|
|
13402
14355
|
if (!output) continue;
|
|
@@ -13450,6 +14403,9 @@ var init_registry3 = __esm({
|
|
|
13450
14403
|
for (const key of Object.keys(s.spec.outputs)) {
|
|
13451
14404
|
const output = s.spec.outputs[key];
|
|
13452
14405
|
if (!output) continue;
|
|
14406
|
+
if (output.shape === "record" && output.collection === s.spec.source && output.denorm !== void 0) {
|
|
14407
|
+
continue;
|
|
14408
|
+
}
|
|
13453
14409
|
visit(output.collection);
|
|
13454
14410
|
}
|
|
13455
14411
|
}
|
|
@@ -13627,6 +14583,19 @@ var init_delegation = __esm({
|
|
|
13627
14583
|
});
|
|
13628
14584
|
|
|
13629
14585
|
// src/vault.ts
|
|
14586
|
+
function resolveLabelFromMap(labels, locale, fallback) {
|
|
14587
|
+
if (labels[locale] !== void 0) return labels[locale];
|
|
14588
|
+
const chain = Array.isArray(fallback) ? fallback : fallback ? [fallback] : [];
|
|
14589
|
+
for (const fb of chain) {
|
|
14590
|
+
if (fb === "any") {
|
|
14591
|
+
const any = Object.values(labels)[0];
|
|
14592
|
+
if (any !== void 0) return any;
|
|
14593
|
+
} else if (labels[fb] !== void 0) {
|
|
14594
|
+
return labels[fb];
|
|
14595
|
+
}
|
|
14596
|
+
}
|
|
14597
|
+
return void 0;
|
|
14598
|
+
}
|
|
13630
14599
|
var Vault, ELEVATION_AUDIT_COLLECTION, ElevatedHandle;
|
|
13631
14600
|
var init_vault = __esm({
|
|
13632
14601
|
"src/vault.ts"() {
|
|
@@ -13645,16 +14614,21 @@ var init_vault = __esm({
|
|
|
13645
14614
|
init_entry();
|
|
13646
14615
|
init_strategy3();
|
|
13647
14616
|
init_strategy7();
|
|
14617
|
+
init_subject_index();
|
|
14618
|
+
init_errors();
|
|
13648
14619
|
init_strategy8();
|
|
13649
14620
|
init_strategy9();
|
|
14621
|
+
init_strategy10();
|
|
13650
14622
|
init_refs();
|
|
13651
14623
|
init_dictionary();
|
|
14624
|
+
init_link_set();
|
|
13652
14625
|
init_core();
|
|
13653
14626
|
init_strategy2();
|
|
13654
14627
|
init_sync_strategy();
|
|
13655
14628
|
init_errors();
|
|
13656
14629
|
init_periods2();
|
|
13657
14630
|
init_crypto();
|
|
14631
|
+
init_record_keys();
|
|
13658
14632
|
init_export_blobs();
|
|
13659
14633
|
init_blob_compaction();
|
|
13660
14634
|
init_magic_link_grant();
|
|
@@ -13710,6 +14684,7 @@ var init_vault = __esm({
|
|
|
13710
14684
|
periodsStrategy;
|
|
13711
14685
|
shadowStrategy;
|
|
13712
14686
|
historyStrategy;
|
|
14687
|
+
forgetStrategy;
|
|
13713
14688
|
i18nStrategy;
|
|
13714
14689
|
syncStrategy;
|
|
13715
14690
|
/**
|
|
@@ -13739,13 +14714,17 @@ var init_vault = __esm({
|
|
|
13739
14714
|
*/
|
|
13740
14715
|
overlayedViewRegistry = null;
|
|
13741
14716
|
/**
|
|
13742
|
-
* Cached read-only
|
|
13743
|
-
* and to derivation callbacks via `derive(source, ctx)`.
|
|
13744
|
-
*
|
|
14717
|
+
* Cached read-only facades handed to guard callbacks via `ctx.vault`
|
|
14718
|
+
* and to derivation callbacks via `derive(source, ctx)`. Split by
|
|
14719
|
+
* resolution layer (#285): the guard facade reads at `layer:'guard'`,
|
|
14720
|
+
* the derivation facade at `layer:'derivation'`, so i18nText / dictKey
|
|
14721
|
+
* fields resolve under that layer's `onMissing` policy. Allocated
|
|
14722
|
+
* eagerly inside `_initGuards()` / `_initDerivations()` so read
|
|
13745
14723
|
* accessors stay synchronous (callers in `tx/transaction.ts` rely on
|
|
13746
|
-
* that).
|
|
14724
|
+
* that). Each stays `null` for vaults without that subsystem.
|
|
13747
14725
|
*/
|
|
13748
|
-
|
|
14726
|
+
guardFacade = null;
|
|
14727
|
+
derivationFacade = null;
|
|
13749
14728
|
getDEK;
|
|
13750
14729
|
/**
|
|
13751
14730
|
* Per-principal user envelope API.
|
|
@@ -13876,6 +14855,27 @@ var init_vault = __esm({
|
|
|
13876
14855
|
* Populated by `collection()` when the `dictKeyFields` option is passed.
|
|
13877
14856
|
*/
|
|
13878
14857
|
dictKeyFieldRegistry = /* @__PURE__ */ new Map();
|
|
14858
|
+
/**
|
|
14859
|
+
* Names of dictionaries backed by a `staticDict()` descriptor (#291).
|
|
14860
|
+
* A static dict skips the `dictKeyFieldRegistry` rename machinery, but the
|
|
14861
|
+
* vault must still *know* a name is static so `vault.dictionary(name)` can
|
|
14862
|
+
* refuse mutation (`StaticDictReadonlyError`). Populated at `collection()`
|
|
14863
|
+
* config time whenever a `StaticDictDescriptor` is seen.
|
|
14864
|
+
*/
|
|
14865
|
+
staticDictNames = /* @__PURE__ */ new Set();
|
|
14866
|
+
/**
|
|
14867
|
+
* Static-dict descriptors keyed by dictionary name (#291). Backs the
|
|
14868
|
+
* read-path label resolver (resolve from the in-memory table) and the
|
|
14869
|
+
* query-seam `resolveDictSource` snapshot. Last writer wins when the same
|
|
14870
|
+
* name is registered by multiple collections (identical-across-vaults by
|
|
14871
|
+
* construction, so the tables match).
|
|
14872
|
+
*/
|
|
14873
|
+
staticByName = /* @__PURE__ */ new Map();
|
|
14874
|
+
/**
|
|
14875
|
+
* Per-collection map of field name → StaticDictDescriptor (#291). Used by
|
|
14876
|
+
* `enforceStaticDictOnPut` to validate stored codes against `desc.keys`.
|
|
14877
|
+
*/
|
|
14878
|
+
staticDescriptorByField = /* @__PURE__ */ new Map();
|
|
13879
14879
|
/**
|
|
13880
14880
|
* Registry of i18nText fields declared across all collections. Keyed
|
|
13881
14881
|
* by collection name → field name → I18nTextDescriptor. Used by
|
|
@@ -13886,6 +14886,10 @@ var init_vault = __esm({
|
|
|
13886
14886
|
i18nFieldRegistry = /* @__PURE__ */ new Map();
|
|
13887
14887
|
/** Cache of DictionaryHandle instances, one per dictionary name. */
|
|
13888
14888
|
dictionaryCache = /* @__PURE__ */ new Map();
|
|
14889
|
+
/** Registered link specs (#377-B), keyed by link name; set by `vault.link()`. */
|
|
14890
|
+
linkRegistry = /* @__PURE__ */ new Map();
|
|
14891
|
+
/** Cache of LinkSet handles, one per link name. */
|
|
14892
|
+
linkSetCache = /* @__PURE__ */ new Map();
|
|
13889
14893
|
/** — subscribers for cross-tier access events. */
|
|
13890
14894
|
crossTierSubs = /* @__PURE__ */ new Set();
|
|
13891
14895
|
/** — currently-active elevation, or null. One per vault. */
|
|
@@ -13922,6 +14926,7 @@ var init_vault = __esm({
|
|
|
13922
14926
|
this.periodsStrategy = opts.periodsStrategy ?? NO_PERIODS;
|
|
13923
14927
|
this.shadowStrategy = opts.shadowStrategy ?? NO_SHADOW;
|
|
13924
14928
|
this.historyStrategy = opts.historyStrategy ?? NO_HISTORY;
|
|
14929
|
+
this.forgetStrategy = opts.forgetStrategy ?? NO_FORGET;
|
|
13925
14930
|
this.i18nStrategy = opts.i18nStrategy ?? NO_I18N;
|
|
13926
14931
|
this.syncStrategy = opts.syncStrategy ?? NO_SYNC;
|
|
13927
14932
|
void opts.guardStrategies;
|
|
@@ -14001,6 +15006,9 @@ var init_vault = __esm({
|
|
|
14001
15006
|
if (collectionName === SEQUENCE_COLLECTION) {
|
|
14002
15007
|
throw new ReservedCollectionNameError(collectionName);
|
|
14003
15008
|
}
|
|
15009
|
+
if (isLinkCollectionName(collectionName)) {
|
|
15010
|
+
throw new ReservedCollectionNameError(collectionName);
|
|
15011
|
+
}
|
|
14004
15012
|
let coll = this.collectionCache.get(collectionName);
|
|
14005
15013
|
if (coll && options?.moneyFields) {
|
|
14006
15014
|
coll._applyMoneyFields(options.moneyFields);
|
|
@@ -14026,10 +15034,22 @@ var init_vault = __esm({
|
|
|
14026
15034
|
}
|
|
14027
15035
|
if (options?.dictKeyFields) {
|
|
14028
15036
|
const dictFieldMap = {};
|
|
15037
|
+
const staticFieldMap = {};
|
|
14029
15038
|
for (const [field, desc] of Object.entries(options.dictKeyFields)) {
|
|
14030
|
-
|
|
15039
|
+
if (isStaticDictDescriptor(desc)) {
|
|
15040
|
+
staticFieldMap[field] = desc;
|
|
15041
|
+
this.staticDictNames.add(desc.name);
|
|
15042
|
+
this.staticByName.set(desc.name, desc);
|
|
15043
|
+
} else {
|
|
15044
|
+
dictFieldMap[field] = desc.name;
|
|
15045
|
+
}
|
|
15046
|
+
}
|
|
15047
|
+
if (Object.keys(dictFieldMap).length > 0) {
|
|
15048
|
+
this.dictKeyFieldRegistry.set(collectionName, dictFieldMap);
|
|
15049
|
+
}
|
|
15050
|
+
if (Object.keys(staticFieldMap).length > 0) {
|
|
15051
|
+
this.staticDescriptorByField.set(collectionName, staticFieldMap);
|
|
14031
15052
|
}
|
|
14032
|
-
this.dictKeyFieldRegistry.set(collectionName, dictFieldMap);
|
|
14033
15053
|
}
|
|
14034
15054
|
if ((options?.schemaUpdate?.length ?? 0) > 0) {
|
|
14035
15055
|
this.#schemaUpdateNames.set(collectionName, (options.schemaUpdate ?? []).map((s) => s.name));
|
|
@@ -14060,6 +15080,7 @@ var init_vault = __esm({
|
|
|
14060
15080
|
}));
|
|
14061
15081
|
schemaUpdateGate = new SchemaUpdateGate(work);
|
|
14062
15082
|
}
|
|
15083
|
+
const effectiveHistoryConfig = options?.historyConfig ?? this.historyConfig;
|
|
14063
15084
|
const collOpts = {
|
|
14064
15085
|
adapter: this.adapter,
|
|
14065
15086
|
vault: this.name,
|
|
@@ -14075,7 +15096,7 @@ var init_vault = __esm({
|
|
|
14075
15096
|
schemaFence: this.schemaFence,
|
|
14076
15097
|
getDEK: this.getDEK,
|
|
14077
15098
|
onDirty: this.onDirty,
|
|
14078
|
-
historyConfig:
|
|
15099
|
+
historyConfig: effectiveHistoryConfig,
|
|
14079
15100
|
// thread the vault-wide blob strategy into every
|
|
14080
15101
|
// collection. `undefined` is intentionally preserved so the
|
|
14081
15102
|
// Collection constructor uses its NO_BLOBS default.
|
|
@@ -14086,7 +15107,11 @@ var init_vault = __esm({
|
|
|
14086
15107
|
historyStrategy: this.historyStrategy,
|
|
14087
15108
|
i18nStrategy: this.i18nStrategy,
|
|
14088
15109
|
syncStrategy: this.syncStrategy,
|
|
14089
|
-
ledger
|
|
15110
|
+
// Per-collection ledger opt-out (#361): when this collection sets
|
|
15111
|
+
// `historyConfig.ledger: false`, withhold the ledger reference so all
|
|
15112
|
+
// four `if (this.ledger)` append sites in Collection no-op. The chain
|
|
15113
|
+
// stays valid — it simply never receives this collection's entries.
|
|
15114
|
+
ledger: effectiveHistoryConfig.ledger === false ? void 0 : this.getLedgerOrNull() ?? void 0,
|
|
14090
15115
|
refEnforcer: this,
|
|
14091
15116
|
joinResolver: this,
|
|
14092
15117
|
defaultLocale: this.locale,
|
|
@@ -14131,6 +15156,17 @@ var init_vault = __esm({
|
|
|
14131
15156
|
if (options?.acknowledgeDeterministicRisk !== void 0) {
|
|
14132
15157
|
collOpts.acknowledgeDeterministicRisk = options.acknowledgeDeterministicRisk;
|
|
14133
15158
|
}
|
|
15159
|
+
if (options?.perRecordKeys !== void 0) {
|
|
15160
|
+
collOpts.perRecordKeys = options.perRecordKeys;
|
|
15161
|
+
}
|
|
15162
|
+
if (this.forgetStrategy.subjects[collectionName] !== void 0) {
|
|
15163
|
+
if (options?.perRecordKeys === false) {
|
|
15164
|
+
console.warn(
|
|
15165
|
+
`[noy-db] Collection "${collectionName}" is declared in withForgetCascade but opened with perRecordKeys: false. Forcing perRecordKeys: true \u2014 GDPR crypto-shred requires per-record CEKs.`
|
|
15166
|
+
);
|
|
15167
|
+
}
|
|
15168
|
+
collOpts.perRecordKeys = true;
|
|
15169
|
+
}
|
|
14134
15170
|
if (options?.tiers !== void 0) collOpts.tiers = options.tiers;
|
|
14135
15171
|
if (options?.tierMode !== void 0) collOpts.tierMode = options.tierMode;
|
|
14136
15172
|
collOpts.onCrossTierAccess = (event) => this.emitCrossTier(event);
|
|
@@ -14140,6 +15176,11 @@ var init_vault = __esm({
|
|
|
14140
15176
|
if (options?.computed !== void 0) collOpts.computed = options.computed;
|
|
14141
15177
|
if (options?.dictKeyFields !== void 0) {
|
|
14142
15178
|
collOpts.dictLabelResolver = async (dictName, key, locale, fallback) => {
|
|
15179
|
+
const stat = this.staticByName.get(dictName);
|
|
15180
|
+
if (stat) {
|
|
15181
|
+
const labels = stat.table[key];
|
|
15182
|
+
return labels ? resolveLabelFromMap(labels, locale, fallback) : void 0;
|
|
15183
|
+
}
|
|
14143
15184
|
const handle = this.dictionary(dictName);
|
|
14144
15185
|
return handle.resolveLabel(key, locale, fallback);
|
|
14145
15186
|
};
|
|
@@ -14148,6 +15189,7 @@ var init_vault = __esm({
|
|
|
14148
15189
|
if (options?.i18nFields !== void 0 || options?.dictKeyFields !== void 0) {
|
|
14149
15190
|
collOpts.i18nPutValidator = (record) => {
|
|
14150
15191
|
this.enforceI18nOnPut(collectionName, record);
|
|
15192
|
+
this.enforceStaticDictOnPut(collectionName, record);
|
|
14151
15193
|
};
|
|
14152
15194
|
}
|
|
14153
15195
|
if (options?.i18nFields !== void 0 && this.translateText) {
|
|
@@ -14287,6 +15329,34 @@ var init_vault = __esm({
|
|
|
14287
15329
|
}
|
|
14288
15330
|
}
|
|
14289
15331
|
}
|
|
15332
|
+
/**
|
|
15333
|
+
* Validate staticDict codes on a `put()` (#291). For each `staticDict()`
|
|
15334
|
+
* field, every stored code must be a declared key of the descriptor's
|
|
15335
|
+
* table, else `UnknownDictCodeError`. Opt out per descriptor with
|
|
15336
|
+
* `{ validateCodes: false }`. Supports scalar, dotted, and `[].`-wildcard
|
|
15337
|
+
* field paths via `getAtPath` (same path support as i18n validation).
|
|
15338
|
+
*/
|
|
15339
|
+
enforceStaticDictOnPut(collectionName, record) {
|
|
15340
|
+
const staticFields = this.staticDescriptorByField.get(collectionName);
|
|
15341
|
+
if (!staticFields || Object.keys(staticFields).length === 0) return;
|
|
15342
|
+
if (!record || typeof record !== "object") return;
|
|
15343
|
+
const obj = record;
|
|
15344
|
+
for (const [field, desc] of Object.entries(staticFields)) {
|
|
15345
|
+
if (desc.validateCodes === false) continue;
|
|
15346
|
+
const known = new Set(desc.keys);
|
|
15347
|
+
const values = getAtPath(obj, field);
|
|
15348
|
+
for (const value of values) {
|
|
15349
|
+
if (value === void 0 || value === null) continue;
|
|
15350
|
+
const codes = Array.isArray(value) ? value : [value];
|
|
15351
|
+
for (const code of codes) {
|
|
15352
|
+
if (typeof code !== "string") continue;
|
|
15353
|
+
if (!known.has(code)) {
|
|
15354
|
+
throw new UnknownDictCodeError(desc.name, field, code);
|
|
15355
|
+
}
|
|
15356
|
+
}
|
|
15357
|
+
}
|
|
15358
|
+
}
|
|
15359
|
+
}
|
|
14290
15360
|
/**
|
|
14291
15361
|
* Apply locale resolution to a record for the given collection.
|
|
14292
15362
|
*
|
|
@@ -14295,14 +15365,18 @@ var init_vault = __esm({
|
|
|
14295
15365
|
*/
|
|
14296
15366
|
async applyLocale(collectionName, record, localeOpts) {
|
|
14297
15367
|
const locale = localeOpts.locale ?? this.locale;
|
|
14298
|
-
|
|
15368
|
+
const staticFields = this.staticDescriptorByField.get(collectionName);
|
|
15369
|
+
const hasStaticDisplay = staticFields !== void 0 && Object.values(staticFields).some((d) => d.displayLocale !== void 0);
|
|
15370
|
+
if (!locale && !hasStaticDisplay) return record;
|
|
14299
15371
|
let result = record;
|
|
14300
|
-
|
|
14301
|
-
|
|
14302
|
-
|
|
15372
|
+
if (locale) {
|
|
15373
|
+
const i18nFields = this.i18nFieldRegistry.get(collectionName);
|
|
15374
|
+
if (i18nFields && Object.keys(i18nFields).length > 0) {
|
|
15375
|
+
result = this.i18nStrategy.applyI18nLocale(result, i18nFields, locale, localeOpts.fallback);
|
|
15376
|
+
}
|
|
14303
15377
|
}
|
|
14304
15378
|
const dictFields = this.dictKeyFieldRegistry.get(collectionName);
|
|
14305
|
-
if (dictFields && Object.keys(dictFields).length > 0 && locale !== "raw") {
|
|
15379
|
+
if (locale && dictFields && Object.keys(dictFields).length > 0 && locale !== "raw") {
|
|
14306
15380
|
const withLabels = { ...result };
|
|
14307
15381
|
for (const [field, dictName] of Object.entries(dictFields)) {
|
|
14308
15382
|
const key = result[field];
|
|
@@ -14315,6 +15389,22 @@ var init_vault = __esm({
|
|
|
14315
15389
|
}
|
|
14316
15390
|
result = withLabels;
|
|
14317
15391
|
}
|
|
15392
|
+
if (staticFields && Object.keys(staticFields).length > 0 && locale !== "raw") {
|
|
15393
|
+
const withLabels = { ...result };
|
|
15394
|
+
for (const [field, desc] of Object.entries(staticFields)) {
|
|
15395
|
+
const effLocale = locale ?? desc.displayLocale;
|
|
15396
|
+
if (!effLocale) continue;
|
|
15397
|
+
const key = result[field];
|
|
15398
|
+
if (typeof key !== "string") continue;
|
|
15399
|
+
const labels = desc.table[key];
|
|
15400
|
+
if (!labels) continue;
|
|
15401
|
+
const label = resolveLabelFromMap(labels, effLocale, localeOpts.fallback ?? desc.substitute);
|
|
15402
|
+
if (label !== void 0) {
|
|
15403
|
+
withLabels[`${field}Label`] = label;
|
|
15404
|
+
}
|
|
15405
|
+
}
|
|
15406
|
+
result = withLabels;
|
|
15407
|
+
}
|
|
14318
15408
|
return result;
|
|
14319
15409
|
}
|
|
14320
15410
|
/**
|
|
@@ -14336,6 +15426,9 @@ var init_vault = __esm({
|
|
|
14336
15426
|
* ```
|
|
14337
15427
|
*/
|
|
14338
15428
|
dictionary(name, options = {}) {
|
|
15429
|
+
if (this.staticDictNames.has(name)) {
|
|
15430
|
+
throw new StaticDictReadonlyError(name);
|
|
15431
|
+
}
|
|
14339
15432
|
let handle = this.dictionaryCache.get(name);
|
|
14340
15433
|
if (!handle) {
|
|
14341
15434
|
handle = this.i18nStrategy.buildDictionaryHandle({
|
|
@@ -14379,6 +15472,68 @@ var init_vault = __esm({
|
|
|
14379
15472
|
}
|
|
14380
15473
|
return handle;
|
|
14381
15474
|
}
|
|
15475
|
+
/**
|
|
15476
|
+
* Declare a managed many-to-many link set (#377-B). Registers a
|
|
15477
|
+
* `_links_<name>` junction between two endpoint collections; access its
|
|
15478
|
+
* rows via `vault.links(name)`. Idempotent for an identical re-declaration;
|
|
15479
|
+
* a conflicting one throws. See {@link links}.
|
|
15480
|
+
*
|
|
15481
|
+
* ```ts
|
|
15482
|
+
* vault.link('saleLineLinks', { a: ref('saleLines'), b: ref('purchaseLines'), onDelete: 'cascade' })
|
|
15483
|
+
* ```
|
|
15484
|
+
*
|
|
15485
|
+
* `a` / `b` accept either a collection name or a `ref(target)` descriptor
|
|
15486
|
+
* (only its `target` is used — links manage their own integrity). `onDelete`
|
|
15487
|
+
* governs what happens to link rows when an endpoint record is deleted
|
|
15488
|
+
* (`'cascade'` default, `'strict'`, `'warn'`).
|
|
15489
|
+
*/
|
|
15490
|
+
link(name, spec) {
|
|
15491
|
+
const a = typeof spec.a === "string" ? spec.a : spec.a.target;
|
|
15492
|
+
const b = typeof spec.b === "string" ? spec.b : spec.b.target;
|
|
15493
|
+
for (const [slot, target] of [["a", a], ["b", b]]) {
|
|
15494
|
+
if (!target || target.startsWith("_") || target.includes("/")) {
|
|
15495
|
+
throw new ValidationError(
|
|
15496
|
+
`vault.link("${name}"): endpoint "${slot}" must be a simple collection name, got "${target}".`
|
|
15497
|
+
);
|
|
15498
|
+
}
|
|
15499
|
+
}
|
|
15500
|
+
const resolved = { a, b, ...spec.onDelete ? { onDelete: spec.onDelete } : {} };
|
|
15501
|
+
const existing = this.linkRegistry.get(name);
|
|
15502
|
+
if (existing) {
|
|
15503
|
+
if (existing.a !== resolved.a || existing.b !== resolved.b || (existing.onDelete ?? "cascade") !== (resolved.onDelete ?? "cascade")) {
|
|
15504
|
+
throw new ValidationError(`vault.link("${name}"): conflicting re-declaration.`);
|
|
15505
|
+
}
|
|
15506
|
+
return;
|
|
15507
|
+
}
|
|
15508
|
+
this.linkRegistry.set(name, resolved);
|
|
15509
|
+
}
|
|
15510
|
+
/**
|
|
15511
|
+
* Access a declared link set (#377-B). Throws if `name` was not first
|
|
15512
|
+
* declared via {@link link}. Returns a cached {@link LinkSetHandle}:
|
|
15513
|
+
* `connect(a, b, meta?)`, `disconnect(a, b)`, `has(a, b)`, `of(id)`, `list()`.
|
|
15514
|
+
*/
|
|
15515
|
+
links(name) {
|
|
15516
|
+
let handle = this.linkSetCache.get(name);
|
|
15517
|
+
if (!handle) {
|
|
15518
|
+
const spec = this.linkRegistry.get(name);
|
|
15519
|
+
if (!spec) {
|
|
15520
|
+
throw new ValidationError(`vault.links("${name}"): not declared. Call vault.link("${name}", { a, b }) first.`);
|
|
15521
|
+
}
|
|
15522
|
+
handle = new LinkSet(
|
|
15523
|
+
this.adapter,
|
|
15524
|
+
this.name,
|
|
15525
|
+
name,
|
|
15526
|
+
spec,
|
|
15527
|
+
this.encrypted,
|
|
15528
|
+
this.getDEK,
|
|
15529
|
+
this.keyring.userId,
|
|
15530
|
+
this.emitter,
|
|
15531
|
+
async (collection, id) => await this.collection(collection).get(id) !== null
|
|
15532
|
+
);
|
|
15533
|
+
this.linkSetCache.set(name, handle);
|
|
15534
|
+
}
|
|
15535
|
+
return handle;
|
|
15536
|
+
}
|
|
14382
15537
|
/**
|
|
14383
15538
|
* Build a `JoinableSource` for a dictKey field, for use in dict joins
|
|
14384
15539
|
*. Returns a source whose snapshot contains `{ key, ...labels }`
|
|
@@ -14405,6 +15560,26 @@ var init_vault = __esm({
|
|
|
14405
15560
|
* Returns `null` when `field` is not a dictKey in `leftCollection`.
|
|
14406
15561
|
*/
|
|
14407
15562
|
resolveDictSource(leftCollection, field) {
|
|
15563
|
+
const staticFields = this.staticDescriptorByField.get(leftCollection);
|
|
15564
|
+
if (staticFields && field in staticFields) {
|
|
15565
|
+
const desc = staticFields[field];
|
|
15566
|
+
const rows = Object.entries(desc.table).map(
|
|
15567
|
+
([key, labels]) => ({ key, labels, ...labels })
|
|
15568
|
+
);
|
|
15569
|
+
const source = {
|
|
15570
|
+
snapshot() {
|
|
15571
|
+
return rows;
|
|
15572
|
+
},
|
|
15573
|
+
lookupById(id) {
|
|
15574
|
+
return rows.find((e) => e["key"] === id);
|
|
15575
|
+
}
|
|
15576
|
+
};
|
|
15577
|
+
if (desc.displayLocale !== void 0) {
|
|
15578
|
+
;
|
|
15579
|
+
source.displayLocale = desc.displayLocale;
|
|
15580
|
+
}
|
|
15581
|
+
return source;
|
|
15582
|
+
}
|
|
14408
15583
|
const dictFields = this.dictKeyFieldRegistry.get(leftCollection);
|
|
14409
15584
|
if (!dictFields || !(field in dictFields)) return null;
|
|
14410
15585
|
const dictName = dictFields[field];
|
|
@@ -14518,65 +15693,16 @@ var init_vault = __esm({
|
|
|
14518
15693
|
});
|
|
14519
15694
|
}
|
|
14520
15695
|
}
|
|
14521
|
-
/**
|
|
14522
|
-
* Bulk blob extraction primitive.
|
|
14523
|
-
*
|
|
14524
|
-
* Returns an async-iterable handle over every blob attached to
|
|
14525
|
-
* records in the vault. Single capability check (`plaintext/blob`)
|
|
14526
|
-
* at handle creation; single audit entry to `_export_audit` before
|
|
14527
|
-
* the first yield. Per-blob decryption happens lazily as the
|
|
14528
|
-
* consumer pulls tuples.
|
|
14529
|
-
*
|
|
14530
|
-
* ```ts
|
|
14531
|
-
* const handle = vault.exportBlobs({
|
|
14532
|
-
* collections: ['invoiceScans'],
|
|
14533
|
-
* where: (rec) => (rec as { clientId?: string }).clientId === 'c-123',
|
|
14534
|
-
* })
|
|
14535
|
-
* for await (const { bytes, meta, recordRef } of handle) {
|
|
14536
|
-
* await uploadToColdStorage(bytes, recordRef)
|
|
14537
|
-
* }
|
|
14538
|
-
* ```
|
|
14539
|
-
*
|
|
14540
|
-
* @see `@noy-db/hub/store/export-blobs` for the full option surface.
|
|
14541
|
-
*/
|
|
14542
|
-
/**
|
|
14543
|
-
* Evict blob slots per the per-collection `blobFields` retention
|
|
14544
|
-
* policy.
|
|
14545
|
-
*
|
|
14546
|
-
* Iterates every collection declared with `{ blobFields: {...} }`.
|
|
14547
|
-
* For each record, checks every configured slot against its
|
|
14548
|
-
* policy — `retainDays` (age-based TTL) and/or `evictWhen(record)`
|
|
14549
|
-
* (predicate) — and evicts matching slots. Every eviction writes
|
|
14550
|
-
* one entry to `_blob_eviction_audit` (actor + eTag + reason +
|
|
14551
|
-
* timestamp, no plaintext). Consumer-scheduled; noy-db never runs
|
|
14552
|
-
* this on its own.
|
|
14553
|
-
*
|
|
14554
|
-
* ```ts
|
|
14555
|
-
* await vault.compact() // run full pass
|
|
14556
|
-
* await vault.compact({ dryRun: true }) // preview counts
|
|
14557
|
-
* await vault.compact({ maxEvictions: 1000 }) // cap batch
|
|
14558
|
-
* ```
|
|
14559
|
-
*/
|
|
14560
|
-
/**
|
|
14561
|
-
* Atomic, gap-free numbering. `vault.sequence('invoice-2026').next()`
|
|
14562
|
-
* returns 1, 2, 3, … with no gaps or duplicates under concurrency, via
|
|
14563
|
-
* an optimistic-CAS counter at `_sequences/<name>`. Each name is an
|
|
14564
|
-
* independent sequence.
|
|
14565
|
-
*
|
|
14566
|
-
* **Online-only:** `next()` throws `SequenceOfflineError` unless the
|
|
14567
|
-
* store advertises `capabilities.casAtomic` — gap-free numbering cannot
|
|
14568
|
-
* be serialized by an offline / non-CAS writer.
|
|
14569
|
-
*
|
|
14570
|
-
* ```ts
|
|
14571
|
-
* const n = await vault.sequence('invoice-2026').next() // 1, then 2, …
|
|
14572
|
-
* const cur = await vault.sequence('invoice-2026').peek() // current value, no allocation
|
|
14573
|
-
* ```
|
|
14574
|
-
*/
|
|
14575
15696
|
sequence(series, opts) {
|
|
14576
15697
|
if (series.includes("\0")) {
|
|
14577
15698
|
throw new ValidationError(`sequence("${series}"): series name must not contain a null byte (\\x00).`);
|
|
14578
15699
|
}
|
|
14579
15700
|
if (this.numberingConfigs.has(series)) {
|
|
15701
|
+
if (opts?.format !== void 0) {
|
|
15702
|
+
throw new ValidationError(
|
|
15703
|
+
`sequence("${series}") is a deferred-numbering series; the format option applies to CAS sequences only.`
|
|
15704
|
+
);
|
|
15705
|
+
}
|
|
14580
15706
|
const eng = this.deferred();
|
|
14581
15707
|
return {
|
|
14582
15708
|
next: async (nextOpts) => {
|
|
@@ -14600,7 +15726,17 @@ var init_vault = __esm({
|
|
|
14600
15726
|
actor: this.keyring.userId
|
|
14601
15727
|
});
|
|
14602
15728
|
}
|
|
14603
|
-
|
|
15729
|
+
const handle = this.sequenceStore.handle(resolveSequenceKey(series, opts));
|
|
15730
|
+
if (opts?.format === void 0) return handle;
|
|
15731
|
+
const render = compileSequenceFormat(opts.format, series, opts.partition);
|
|
15732
|
+
return {
|
|
15733
|
+
next: async (nextOpts) => {
|
|
15734
|
+
const serial = await handle.next(nextOpts);
|
|
15735
|
+
return { serial, formatted: render(serial) };
|
|
15736
|
+
},
|
|
15737
|
+
peek: () => handle.peek(),
|
|
15738
|
+
seedTo: (n) => handle.seedTo(n)
|
|
15739
|
+
};
|
|
14604
15740
|
}
|
|
14605
15741
|
/** @internal — lazily build the deferred-numbering engine with a cache-coherent stamp. */
|
|
14606
15742
|
deferred() {
|
|
@@ -14827,6 +15963,43 @@ var init_vault = __esm({
|
|
|
14827
15963
|
if (descriptor.mode !== "strict") continue;
|
|
14828
15964
|
const rawId = obj[field];
|
|
14829
15965
|
if (rawId === null || rawId === void 0) continue;
|
|
15966
|
+
if (isRefArray(descriptor)) {
|
|
15967
|
+
if (!Array.isArray(rawId)) {
|
|
15968
|
+
throw new RefIntegrityError({
|
|
15969
|
+
collection: collectionName,
|
|
15970
|
+
id: obj["id"] ?? "<unknown>",
|
|
15971
|
+
field,
|
|
15972
|
+
refTo: descriptor.target,
|
|
15973
|
+
refId: null,
|
|
15974
|
+
message: `Array ref field "${collectionName}.${field}" must be an array, got ${typeof rawId}.`
|
|
15975
|
+
});
|
|
15976
|
+
}
|
|
15977
|
+
const arrTarget = this.collection(descriptor.target);
|
|
15978
|
+
for (const el of rawId) {
|
|
15979
|
+
if (typeof el !== "string" && typeof el !== "number") {
|
|
15980
|
+
throw new RefIntegrityError({
|
|
15981
|
+
collection: collectionName,
|
|
15982
|
+
id: obj["id"] ?? "<unknown>",
|
|
15983
|
+
field,
|
|
15984
|
+
refTo: descriptor.target,
|
|
15985
|
+
refId: null,
|
|
15986
|
+
message: `Array ref "${collectionName}.${field}" elements must be strings or numbers, got ${typeof el}.`
|
|
15987
|
+
});
|
|
15988
|
+
}
|
|
15989
|
+
const elId = String(el);
|
|
15990
|
+
if (!await arrTarget.get(elId)) {
|
|
15991
|
+
throw new RefIntegrityError({
|
|
15992
|
+
collection: collectionName,
|
|
15993
|
+
id: obj["id"] ?? "<unknown>",
|
|
15994
|
+
field,
|
|
15995
|
+
refTo: descriptor.target,
|
|
15996
|
+
refId: elId,
|
|
15997
|
+
message: `Strict array ref "${collectionName}.${field}" \u2192 "${descriptor.target}" cannot be satisfied: element id "${elId}" not found in "${descriptor.target}".`
|
|
15998
|
+
});
|
|
15999
|
+
}
|
|
16000
|
+
}
|
|
16001
|
+
continue;
|
|
16002
|
+
}
|
|
14830
16003
|
if (typeof rawId !== "string" && typeof rawId !== "number") {
|
|
14831
16004
|
throw new RefIntegrityError({
|
|
14832
16005
|
collection: collectionName,
|
|
@@ -14876,6 +16049,11 @@ var init_vault = __esm({
|
|
|
14876
16049
|
const allRecords = await fromCollection.list();
|
|
14877
16050
|
const matches = allRecords.filter((rec) => {
|
|
14878
16051
|
const raw = rec[rule.field];
|
|
16052
|
+
if (rule.isArray) {
|
|
16053
|
+
return Array.isArray(raw) && raw.some(
|
|
16054
|
+
(el) => (typeof el === "string" || typeof el === "number") && String(el) === id
|
|
16055
|
+
);
|
|
16056
|
+
}
|
|
14879
16057
|
if (typeof raw !== "string" && typeof raw !== "number") return false;
|
|
14880
16058
|
return String(raw) === id;
|
|
14881
16059
|
});
|
|
@@ -14914,10 +16092,45 @@ var init_vault = __esm({
|
|
|
14914
16092
|
}
|
|
14915
16093
|
}
|
|
14916
16094
|
}
|
|
16095
|
+
await this.enforceLinksOnDelete(collectionName, id);
|
|
14917
16096
|
} finally {
|
|
14918
16097
|
this.cascadeInProgress.delete(key);
|
|
14919
16098
|
}
|
|
14920
16099
|
}
|
|
16100
|
+
/**
|
|
16101
|
+
* @internal — apply link `onDelete` policy when an endpoint record is
|
|
16102
|
+
* deleted (#377-B). `'strict'` throws (blocks the delete), `'cascade'`
|
|
16103
|
+
* removes the touching link rows (tx-atomic when a transaction is active),
|
|
16104
|
+
* `'warn'` leaves orphans for `checkIntegrity()`.
|
|
16105
|
+
*/
|
|
16106
|
+
async enforceLinksOnDelete(collectionName, id) {
|
|
16107
|
+
for (const [name, spec] of this.linkRegistry) {
|
|
16108
|
+
if (spec.a !== collectionName && spec.b !== collectionName) continue;
|
|
16109
|
+
const handle = this.links(name);
|
|
16110
|
+
const touching = await handle._rowsTouchingEndpoint(collectionName, id);
|
|
16111
|
+
if (touching.length === 0) continue;
|
|
16112
|
+
const mode = spec.onDelete ?? "cascade";
|
|
16113
|
+
if (mode === "warn") continue;
|
|
16114
|
+
if (mode === "strict") {
|
|
16115
|
+
throw new LinkIntegrityError(name, collectionName, id, touching.length);
|
|
16116
|
+
}
|
|
16117
|
+
const linkColl = handle._collectionName;
|
|
16118
|
+
const txCtx = this.noydb._activeTxContextOrNull;
|
|
16119
|
+
for (const row of touching) {
|
|
16120
|
+
const rowKey = linkRowKey(row.a, row.b);
|
|
16121
|
+
if (txCtx !== null) {
|
|
16122
|
+
const prior = await this.adapter.get(this.name, linkColl, rowKey);
|
|
16123
|
+
if (prior !== null) {
|
|
16124
|
+
txCtx._executed.push({
|
|
16125
|
+
op: { type: "delete", vaultName: this.name, collectionName: linkColl, id: rowKey },
|
|
16126
|
+
priorEnvelope: prior
|
|
16127
|
+
});
|
|
16128
|
+
}
|
|
16129
|
+
}
|
|
16130
|
+
await handle.disconnect(row.a, row.b);
|
|
16131
|
+
}
|
|
16132
|
+
}
|
|
16133
|
+
}
|
|
14921
16134
|
// ─── Join resolver) ────────────────────
|
|
14922
16135
|
/**
|
|
14923
16136
|
* Look up the `RefDescriptor` the left collection declared for a
|
|
@@ -14978,6 +16191,23 @@ var init_vault = __esm({
|
|
|
14978
16191
|
for (const [field, descriptor] of Object.entries(refs)) {
|
|
14979
16192
|
const rawId = record[field];
|
|
14980
16193
|
if (rawId === null || rawId === void 0) continue;
|
|
16194
|
+
const target = this.collection(descriptor.target);
|
|
16195
|
+
if (isRefArray(descriptor)) {
|
|
16196
|
+
if (!Array.isArray(rawId)) {
|
|
16197
|
+
violations.push({ collection: collectionName, id: recId, field, refTo: descriptor.target, refId: rawId, mode: descriptor.mode });
|
|
16198
|
+
continue;
|
|
16199
|
+
}
|
|
16200
|
+
for (const el of rawId) {
|
|
16201
|
+
if (typeof el !== "string" && typeof el !== "number") {
|
|
16202
|
+
violations.push({ collection: collectionName, id: recId, field, refTo: descriptor.target, refId: el, mode: descriptor.mode });
|
|
16203
|
+
continue;
|
|
16204
|
+
}
|
|
16205
|
+
if (!await target.get(String(el))) {
|
|
16206
|
+
violations.push({ collection: collectionName, id: recId, field, refTo: descriptor.target, refId: el, mode: descriptor.mode });
|
|
16207
|
+
}
|
|
16208
|
+
}
|
|
16209
|
+
continue;
|
|
16210
|
+
}
|
|
14981
16211
|
if (typeof rawId !== "string" && typeof rawId !== "number") {
|
|
14982
16212
|
violations.push({
|
|
14983
16213
|
collection: collectionName,
|
|
@@ -14990,7 +16220,6 @@ var init_vault = __esm({
|
|
|
14990
16220
|
continue;
|
|
14991
16221
|
}
|
|
14992
16222
|
const refId = String(rawId);
|
|
14993
|
-
const target = this.collection(descriptor.target);
|
|
14994
16223
|
const exists = await target.get(refId);
|
|
14995
16224
|
if (!exists) {
|
|
14996
16225
|
violations.push({
|
|
@@ -15005,6 +16234,19 @@ var init_vault = __esm({
|
|
|
15005
16234
|
}
|
|
15006
16235
|
}
|
|
15007
16236
|
}
|
|
16237
|
+
for (const [name, spec] of this.linkRegistry) {
|
|
16238
|
+
const linkColl = linkCollectionName(name);
|
|
16239
|
+
const rows = await this.links(name).list();
|
|
16240
|
+
for (const row of rows) {
|
|
16241
|
+
const rowKey = linkRowKey(row.a, row.b);
|
|
16242
|
+
if (await this.collection(spec.a).get(row.a) === null) {
|
|
16243
|
+
violations.push({ collection: linkColl, id: rowKey, field: "a", refTo: spec.a, refId: row.a, mode: spec.onDelete ?? "cascade" });
|
|
16244
|
+
}
|
|
16245
|
+
if (await this.collection(spec.b).get(row.b) === null) {
|
|
16246
|
+
violations.push({ collection: linkColl, id: rowKey, field: "b", refTo: spec.b, refId: row.b, mode: spec.onDelete ?? "cascade" });
|
|
16247
|
+
}
|
|
16248
|
+
}
|
|
16249
|
+
}
|
|
15008
16250
|
return { violations };
|
|
15009
16251
|
}
|
|
15010
16252
|
/**
|
|
@@ -15048,6 +16290,218 @@ var init_vault = __esm({
|
|
|
15048
16290
|
}
|
|
15049
16291
|
return this.ledgerStore;
|
|
15050
16292
|
}
|
|
16293
|
+
// ─── GDPR right-to-erasure (#304) ────────────────────────────────
|
|
16294
|
+
/** @internal — add a subject→record ref to the encrypted subject index. */
|
|
16295
|
+
async _addSubjectRef(subjectId, ref) {
|
|
16296
|
+
await addSubjectRef(this.adapter, this.name, this.getDEK, this.encrypted, subjectId, ref);
|
|
16297
|
+
}
|
|
16298
|
+
/** @internal — drop a subject→record ref from the encrypted subject index. */
|
|
16299
|
+
async _removeSubjectRef(subjectId, ref) {
|
|
16300
|
+
await removeSubjectRef(this.adapter, this.name, this.getDEK, this.encrypted, subjectId, ref);
|
|
16301
|
+
}
|
|
16302
|
+
/**
|
|
16303
|
+
* Rebuild the encrypted subject index from canonical records. The recovery
|
|
16304
|
+
* path for the documented read-modify-write race (RISK #3). Returns the
|
|
16305
|
+
* number of distinct subjects re-indexed.
|
|
16306
|
+
*/
|
|
16307
|
+
async rebuildSubjectIndex() {
|
|
16308
|
+
if (Object.keys(this.forgetStrategy.subjects).length === 0) {
|
|
16309
|
+
throw new ForgetStrategyNotConfiguredError();
|
|
16310
|
+
}
|
|
16311
|
+
return rebuildSubjectIndex(
|
|
16312
|
+
this.adapter,
|
|
16313
|
+
this.name,
|
|
16314
|
+
this.getDEK,
|
|
16315
|
+
this.encrypted,
|
|
16316
|
+
this.forgetStrategy.subjects,
|
|
16317
|
+
async (collectionName, id, env) => {
|
|
16318
|
+
const coll = this.collection(collectionName);
|
|
16319
|
+
return coll._decodeEnvelope(env, id);
|
|
16320
|
+
}
|
|
16321
|
+
);
|
|
16322
|
+
}
|
|
16323
|
+
/**
|
|
16324
|
+
* GDPR crypto-shred of a data subject (#304). Consults the encrypted subject
|
|
16325
|
+
* index and, per matching record:
|
|
16326
|
+
* - rewrites the LIVE envelope to a tombstone (drops `_iv`/`_data`/`_cek`/`_det`),
|
|
16327
|
+
* - tombstones every `_history` version of the record,
|
|
16328
|
+
* so the body and all prior versions become permanently undecryptable while
|
|
16329
|
+
* the collection DEK and every OTHER record stay intact. Then appends ONE
|
|
16330
|
+
* `op:'forget'` ledger entry whose `payloadHash` is `sha256Hex(subjectId)` —
|
|
16331
|
+
* the chain still `verify()`s, PROVING the subject existed and was erased
|
|
16332
|
+
* without retaining any plaintext.
|
|
16333
|
+
*
|
|
16334
|
+
* Reports — but does not silently swallow — two completeness gaps:
|
|
16335
|
+
* - `unmigratedRecords`: a record whose body was NOT yet migrated to a
|
|
16336
|
+
* per-record CEK (legacy body still under the shared collection DEK). It
|
|
16337
|
+
* is still tombstoned, but its pre-shred ciphertext (if leaked to a
|
|
16338
|
+
* backup before migration) stays decryptable. Migrate, then re-forget.
|
|
16339
|
+
* - `blobResidueCollections`: a shredded record still has blob attachments,
|
|
16340
|
+
* which are keyed off a separate `_blob` DEK and are out of scope here.
|
|
16341
|
+
*
|
|
16342
|
+
* @throws ForgetStrategyNotConfiguredError when no `withForgetCascade` was set.
|
|
16343
|
+
*/
|
|
16344
|
+
async forget(subjectId) {
|
|
16345
|
+
if (Object.keys(this.forgetStrategy.subjects).length === 0) {
|
|
16346
|
+
throw new ForgetStrategyNotConfiguredError();
|
|
16347
|
+
}
|
|
16348
|
+
const refs = await lookupSubject(this.adapter, this.name, this.getDEK, this.encrypted, subjectId);
|
|
16349
|
+
let recordsShredded = 0;
|
|
16350
|
+
let historyVersionsShredded = 0;
|
|
16351
|
+
const collections = /* @__PURE__ */ new Set();
|
|
16352
|
+
const unmigratedRecords = [];
|
|
16353
|
+
const blobResidueCollections = /* @__PURE__ */ new Set();
|
|
16354
|
+
let blobsShredded = 0;
|
|
16355
|
+
let blobsRetainedShared = 0;
|
|
16356
|
+
const blobsEnabled = this.blobStrategy !== void 0;
|
|
16357
|
+
const actor = this.keyring.userId;
|
|
16358
|
+
for (const ref of refs) {
|
|
16359
|
+
const coll = this.collection(ref.collection);
|
|
16360
|
+
const perRecordKeys = this.forgetStrategy.subjects[ref.collection] !== void 0;
|
|
16361
|
+
const live = await this.adapter.get(this.name, ref.collection, ref.id);
|
|
16362
|
+
if (perRecordKeys && live && live._data && live._cek === void 0) {
|
|
16363
|
+
unmigratedRecords.push(`${ref.collection}:${ref.id}`);
|
|
16364
|
+
}
|
|
16365
|
+
const shred = await coll._writeTombstone(ref.id, actor);
|
|
16366
|
+
if (shred !== null) {
|
|
16367
|
+
recordsShredded++;
|
|
16368
|
+
collections.add(ref.collection);
|
|
16369
|
+
}
|
|
16370
|
+
historyVersionsShredded += await this.historyStrategy.tombstoneHistory(
|
|
16371
|
+
this.adapter,
|
|
16372
|
+
this.name,
|
|
16373
|
+
ref.collection,
|
|
16374
|
+
ref.id,
|
|
16375
|
+
actor
|
|
16376
|
+
);
|
|
16377
|
+
if (blobsEnabled) {
|
|
16378
|
+
const r = await this.collection(ref.collection).blob(ref.id).shredAllForRecord();
|
|
16379
|
+
blobsShredded += r.shredded.length;
|
|
16380
|
+
blobsRetainedShared += r.retainedShared.length;
|
|
16381
|
+
if (r.residue.length > 0) blobResidueCollections.add(ref.collection);
|
|
16382
|
+
} else {
|
|
16383
|
+
try {
|
|
16384
|
+
const slotIds = await this.adapter.list(this.name, `_blob_slots_${ref.collection}`);
|
|
16385
|
+
if (slotIds.includes(ref.id)) blobResidueCollections.add(ref.collection);
|
|
16386
|
+
} catch {
|
|
16387
|
+
}
|
|
16388
|
+
}
|
|
16389
|
+
await this._removeSubjectRef(subjectId, ref);
|
|
16390
|
+
}
|
|
16391
|
+
const subjectHash = await sha256Hex3(subjectId);
|
|
16392
|
+
const ledger = this.getLedgerOrNull();
|
|
16393
|
+
if (!ledger) {
|
|
16394
|
+
throw new Error(
|
|
16395
|
+
'vault.forget() requires the history strategy for the erasure-proof ledger entry. Pass `historyStrategy: withHistory()` from "@noy-db/hub/history" to createNoydb().'
|
|
16396
|
+
);
|
|
16397
|
+
}
|
|
16398
|
+
const ledgerEntry = await ledger.append({
|
|
16399
|
+
op: "forget",
|
|
16400
|
+
collection: "",
|
|
16401
|
+
id: "",
|
|
16402
|
+
version: 0,
|
|
16403
|
+
actor,
|
|
16404
|
+
payloadHash: subjectHash,
|
|
16405
|
+
reason: JSON.stringify({
|
|
16406
|
+
recordsShredded,
|
|
16407
|
+
historyVersionsShredded,
|
|
16408
|
+
collections: [...collections],
|
|
16409
|
+
unmigratedCount: unmigratedRecords.length,
|
|
16410
|
+
blobsShredded,
|
|
16411
|
+
blobsRetainedShared,
|
|
16412
|
+
blobResidueCollections: [...blobResidueCollections]
|
|
16413
|
+
})
|
|
16414
|
+
});
|
|
16415
|
+
return {
|
|
16416
|
+
subject: subjectId,
|
|
16417
|
+
recordsShredded,
|
|
16418
|
+
historyVersionsShredded,
|
|
16419
|
+
collections: [...collections],
|
|
16420
|
+
unmigratedRecords,
|
|
16421
|
+
blobsShredded,
|
|
16422
|
+
blobsRetainedShared,
|
|
16423
|
+
blobResidueCollections: [...blobResidueCollections],
|
|
16424
|
+
ledgerEntry
|
|
16425
|
+
};
|
|
16426
|
+
}
|
|
16427
|
+
// ─── Record-scoped CEK sealing (#306 slices 2-3) ──────────────────────
|
|
16428
|
+
/**
|
|
16429
|
+
* Seal ONE record's content-encryption key (CEK) to an `at-*` host so that
|
|
16430
|
+
* host — and only that host — can decrypt exactly that record, with no
|
|
16431
|
+
* access to the vault DEK and no ability to read any other record.
|
|
16432
|
+
*
|
|
16433
|
+
* The grantor (this caller, who holds the collection DEK) reads the record's
|
|
16434
|
+
* live `_cek`, unwraps it under the collection DEK, exports the raw CEK
|
|
16435
|
+
* bytes, builds a {@link SealedCekBinding} `{collection, id, cek, expiresAt}`,
|
|
16436
|
+
* seals that binding for the recipient host via the host's published hint,
|
|
16437
|
+
* and persists a thin {@link SealedCekDeliveryEnvelope} at
|
|
16438
|
+
* `_sealed_cek/<collection>/<id>/<pid>`. The binding (not the delivery
|
|
16439
|
+
* envelope) is the security boundary: the host re-verifies `{collection, id}`
|
|
16440
|
+
* and `expiresAt` from inside the sealed payload.
|
|
16441
|
+
*
|
|
16442
|
+
* Only works on a `perRecordKeys` record — a legacy record has no `_cek` to
|
|
16443
|
+
* seal (its body is under the shared collection DEK, which is never exposed
|
|
16444
|
+
* by sealing) → {@link RecordCekNotFoundError}.
|
|
16445
|
+
*
|
|
16446
|
+
* @param collection Collection holding the record.
|
|
16447
|
+
* @param id Record id.
|
|
16448
|
+
* @param hostSealer The recipient host's {@link RecipientSealer}.
|
|
16449
|
+
* @param opts.expiresAt REQUIRED authoritative expiry (ISO 8601), sealed into
|
|
16450
|
+
* the binding the host verifies.
|
|
16451
|
+
* @returns `{ pid, envelopeKey }` — the host provider id and the
|
|
16452
|
+
* `<collection>/<id>/<pid>` key the delivery envelope was written under.
|
|
16453
|
+
*/
|
|
16454
|
+
async sealRecordToHost(collection, id, hostSealer, opts) {
|
|
16455
|
+
return sealRecordToHost(this.sealingContext(), collection, id, hostSealer, opts);
|
|
16456
|
+
}
|
|
16457
|
+
/**
|
|
16458
|
+
* Revoke a single sealed-CEK delivery envelope by deleting it from the store.
|
|
16459
|
+
* A soft revocation: it removes the host's copy of the sealed CEK, but a host
|
|
16460
|
+
* that already fetched the envelope keeps whatever it cached. For a hard
|
|
16461
|
+
* revocation that makes the live record undecryptable to every prior grant,
|
|
16462
|
+
* use {@link rotateRecordCek}.
|
|
16463
|
+
*/
|
|
16464
|
+
async revokeSealedRecord(collection, id, pid) {
|
|
16465
|
+
return revokeSealedRecord(this.sealingContext(), collection, id, pid);
|
|
16466
|
+
}
|
|
16467
|
+
/**
|
|
16468
|
+
* HARD-rotate a record's CEK: decrypt the live body under the old CEK,
|
|
16469
|
+
* re-encrypt it under a freshly-minted CEK, write the new live envelope, evict
|
|
16470
|
+
* the in-memory caches, and delete EVERY sealed-CEK delivery envelope for the
|
|
16471
|
+
* record. After this, any host holding a previously-sealed CEK can still
|
|
16472
|
+
* decrypt PRE-rotation history versions (they keep their old `_cek`) but NOT
|
|
16473
|
+
* the rotated live record (its body is under the new CEK → the old CEK fails
|
|
16474
|
+
* the AES-GCM auth tag → `TamperedError`). That asymmetry IS the revocation:
|
|
16475
|
+
* old grants lose the live record.
|
|
16476
|
+
*
|
|
16477
|
+
* Administrative path — bypasses `Collection.put` deliberately (no guards, no
|
|
16478
|
+
* history snapshot, no materialized-view refresh): rotation is a key-rotation
|
|
16479
|
+
* operation, not a business write, and must not version-bump history (which
|
|
16480
|
+
* would re-encrypt the prior version under the NEW CEK and defeat the point).
|
|
16481
|
+
*
|
|
16482
|
+
* @throws {@link RecordCekNotFoundError} if the record is missing or has no `_cek`.
|
|
16483
|
+
*/
|
|
16484
|
+
async rotateRecordCek(collection, id) {
|
|
16485
|
+
return rotateRecordCek(this.sealingContext(), collection, id);
|
|
16486
|
+
}
|
|
16487
|
+
/**
|
|
16488
|
+
* Build the {@link SealingContext} the record-keys grantor functions need:
|
|
16489
|
+
* the vault-bound adapter, DEK resolver, actor, and the dual-cache eviction
|
|
16490
|
+
* `rotateRecordCek` performs (per-record CEK cache + decrypted-record cache).
|
|
16491
|
+
*/
|
|
16492
|
+
sealingContext() {
|
|
16493
|
+
return {
|
|
16494
|
+
adapter: this.adapter,
|
|
16495
|
+
vault: this.name,
|
|
16496
|
+
getDEK: (collection) => this.getDEK(collection),
|
|
16497
|
+
actor: this.keyring.userId,
|
|
16498
|
+
invalidateRecordCaches: async (collection, id) => {
|
|
16499
|
+
const coll = this.collection(collection);
|
|
16500
|
+
coll._invalidateCekCacheEntry(id);
|
|
16501
|
+
await coll._invalidateCacheEntry(id);
|
|
16502
|
+
}
|
|
16503
|
+
};
|
|
16504
|
+
}
|
|
15051
16505
|
/**
|
|
15052
16506
|
* @internal — called by `Noydb.openVault` after construction.
|
|
15053
16507
|
* Dynamic-imports `GuardRegistry` + `ReadOnlyVaultFacade` and seeds
|
|
@@ -15068,7 +16522,7 @@ var init_vault = __esm({
|
|
|
15068
16522
|
const registry = new GuardRegistry2();
|
|
15069
16523
|
for (const h of handles) registry.register(h.spec);
|
|
15070
16524
|
this.guardRegistry = registry;
|
|
15071
|
-
this.
|
|
16525
|
+
this.guardFacade = new ReadOnlyVaultFacade2(this, "guard");
|
|
15072
16526
|
}
|
|
15073
16527
|
/**
|
|
15074
16528
|
* @internal — The gate handler in Noydb.#registerGuardGate calls into
|
|
@@ -15099,8 +16553,8 @@ var init_vault = __esm({
|
|
|
15099
16553
|
}
|
|
15100
16554
|
registry.validate();
|
|
15101
16555
|
this.derivationRegistry = registry;
|
|
15102
|
-
if (this.
|
|
15103
|
-
this.
|
|
16556
|
+
if (this.derivationFacade === null) {
|
|
16557
|
+
this.derivationFacade = new ReadOnlyVaultFacade2(this, "derivation");
|
|
15104
16558
|
}
|
|
15105
16559
|
}
|
|
15106
16560
|
/**
|
|
@@ -15217,7 +16671,7 @@ var init_vault = __esm({
|
|
|
15217
16671
|
const { DerivationExecutor: DerivationExecutor2 } = await Promise.resolve().then(() => (init_executor(), executor_exports));
|
|
15218
16672
|
const sourceColl = this.collection(sourceCollection);
|
|
15219
16673
|
const records = await sourceColl.list();
|
|
15220
|
-
const ctx = { vault: this.
|
|
16674
|
+
const ctx = { vault: this.derivationFacade ?? new (await Promise.resolve().then(() => (init_read_only_facade(), read_only_facade_exports))).ReadOnlyVaultFacade(this, "derivation") };
|
|
15221
16675
|
let derived = 0;
|
|
15222
16676
|
let failed = 0;
|
|
15223
16677
|
for (const record of records) {
|
|
@@ -15282,17 +16736,18 @@ var init_vault = __esm({
|
|
|
15282
16736
|
* never see null).
|
|
15283
16737
|
*/
|
|
15284
16738
|
_getReadOnlyFacade() {
|
|
15285
|
-
return this.
|
|
16739
|
+
return this.guardFacade;
|
|
15286
16740
|
}
|
|
15287
16741
|
/**
|
|
15288
|
-
* Internal lazy-allocator for the read-only facade
|
|
15289
|
-
* defensive fallback; in practice
|
|
15290
|
-
* instantiates this, so the lazy path is
|
|
16742
|
+
* Internal lazy-allocator for the derivation read-only facade
|
|
16743
|
+
* (`layer:'derivation'`). Used as a defensive fallback; in practice
|
|
16744
|
+
* `_initDerivations()` eagerly instantiates this, so the lazy path is
|
|
16745
|
+
* a no-op.
|
|
15291
16746
|
*/
|
|
15292
16747
|
_ensureReadOnlyFacade() {
|
|
15293
|
-
if (this.
|
|
16748
|
+
if (this.derivationFacade !== null) return this.derivationFacade;
|
|
15294
16749
|
throw new Error(
|
|
15295
|
-
"Vault:
|
|
16750
|
+
"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."
|
|
15296
16751
|
);
|
|
15297
16752
|
}
|
|
15298
16753
|
/**
|
|
@@ -17150,7 +18605,7 @@ var init_unlock_state = __esm({
|
|
|
17150
18605
|
|
|
17151
18606
|
// src/snapshots/strategy.ts
|
|
17152
18607
|
var NOT_ENABLED5, NO_SNAPSHOTS;
|
|
17153
|
-
var
|
|
18608
|
+
var init_strategy11 = __esm({
|
|
17154
18609
|
"src/snapshots/strategy.ts"() {
|
|
17155
18610
|
"use strict";
|
|
17156
18611
|
NOT_ENABLED5 = new Error(
|
|
@@ -17291,7 +18746,7 @@ var init_scheduler = __esm({
|
|
|
17291
18746
|
|
|
17292
18747
|
// src/tx/strategy.ts
|
|
17293
18748
|
var NOT_ENABLED6, NO_TX;
|
|
17294
|
-
var
|
|
18749
|
+
var init_strategy12 = __esm({
|
|
17295
18750
|
"src/tx/strategy.ts"() {
|
|
17296
18751
|
"use strict";
|
|
17297
18752
|
NOT_ENABLED6 = new Error(
|
|
@@ -17327,7 +18782,7 @@ function notEnabled4(op) {
|
|
|
17327
18782
|
);
|
|
17328
18783
|
}
|
|
17329
18784
|
var NO_SESSION;
|
|
17330
|
-
var
|
|
18785
|
+
var init_strategy13 = __esm({
|
|
17331
18786
|
"src/session/strategy.ts"() {
|
|
17332
18787
|
"use strict";
|
|
17333
18788
|
NO_SESSION = {
|
|
@@ -17691,6 +19146,177 @@ var init_executor3 = __esm({
|
|
|
17691
19146
|
}
|
|
17692
19147
|
});
|
|
17693
19148
|
|
|
19149
|
+
// src/federation/schema-manifest.ts
|
|
19150
|
+
function captureBlueprint(configure) {
|
|
19151
|
+
const recorded = [];
|
|
19152
|
+
const collectionStub = new Proxy(
|
|
19153
|
+
{},
|
|
19154
|
+
{
|
|
19155
|
+
get: () => () => collectionStub
|
|
19156
|
+
}
|
|
19157
|
+
);
|
|
19158
|
+
const proxy = new Proxy(
|
|
19159
|
+
{},
|
|
19160
|
+
{
|
|
19161
|
+
get: (_t, prop) => {
|
|
19162
|
+
if (prop === "collection") {
|
|
19163
|
+
return (name, opts) => {
|
|
19164
|
+
recorded.push({
|
|
19165
|
+
name,
|
|
19166
|
+
indexes: opts?.indexes ?? [],
|
|
19167
|
+
persistJsonSchema: !!opts?.persistJsonSchema
|
|
19168
|
+
});
|
|
19169
|
+
return collectionStub;
|
|
19170
|
+
};
|
|
19171
|
+
}
|
|
19172
|
+
return () => proxy;
|
|
19173
|
+
}
|
|
19174
|
+
}
|
|
19175
|
+
);
|
|
19176
|
+
configure(proxy);
|
|
19177
|
+
const sorted = [...recorded].sort((a, b) => a.name.localeCompare(b.name));
|
|
19178
|
+
const indexes = {};
|
|
19179
|
+
const persistJsonSchema = [];
|
|
19180
|
+
for (const c of sorted) {
|
|
19181
|
+
indexes[c.name] = c.indexes;
|
|
19182
|
+
if (c.persistJsonSchema) persistJsonSchema.push(c.name);
|
|
19183
|
+
}
|
|
19184
|
+
return {
|
|
19185
|
+
// `persistJsonSchema` is already name-sorted: it is populated while
|
|
19186
|
+
// iterating `sorted` (collections in name order).
|
|
19187
|
+
collections: sorted.map((c) => c.name),
|
|
19188
|
+
indexes,
|
|
19189
|
+
persistJsonSchema
|
|
19190
|
+
};
|
|
19191
|
+
}
|
|
19192
|
+
function canonical(value) {
|
|
19193
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
19194
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
19195
|
+
const obj = value;
|
|
19196
|
+
const keys = Object.keys(obj).sort();
|
|
19197
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonical(obj[k])}`).join(",")}}`;
|
|
19198
|
+
}
|
|
19199
|
+
async function fingerprintBlueprint(bp) {
|
|
19200
|
+
return sha256Hex2(new TextEncoder().encode(canonical(bp)));
|
|
19201
|
+
}
|
|
19202
|
+
var init_schema_manifest = __esm({
|
|
19203
|
+
"src/federation/schema-manifest.ts"() {
|
|
19204
|
+
"use strict";
|
|
19205
|
+
init_crypto();
|
|
19206
|
+
}
|
|
19207
|
+
});
|
|
19208
|
+
|
|
19209
|
+
// src/federation/state-vault.ts
|
|
19210
|
+
var state_vault_exports = {};
|
|
19211
|
+
__export(state_vault_exports, {
|
|
19212
|
+
STATE_VAULT_NAME: () => STATE_VAULT_NAME,
|
|
19213
|
+
StateManagementVault: () => StateManagementVault
|
|
19214
|
+
});
|
|
19215
|
+
var REGISTRY, MANIFEST, EVENTS, MIGRATION_STATUS, StateManagementVault;
|
|
19216
|
+
var init_state_vault = __esm({
|
|
19217
|
+
"src/federation/state-vault.ts"() {
|
|
19218
|
+
"use strict";
|
|
19219
|
+
init_schema_manifest();
|
|
19220
|
+
init_constants2();
|
|
19221
|
+
init_ulid();
|
|
19222
|
+
init_constants2();
|
|
19223
|
+
REGISTRY = "vaultRegistry";
|
|
19224
|
+
MANIFEST = "schemaManifest";
|
|
19225
|
+
EVENTS = "deploymentEvents";
|
|
19226
|
+
MIGRATION_STATUS = "migrationStatus";
|
|
19227
|
+
StateManagementVault = class _StateManagementVault {
|
|
19228
|
+
constructor(registry, schemaManifest, events, migrationStatus) {
|
|
19229
|
+
this.registry = registry;
|
|
19230
|
+
this.schemaManifest = schemaManifest;
|
|
19231
|
+
this.#events = events;
|
|
19232
|
+
this.#migrationStatus = migrationStatus;
|
|
19233
|
+
}
|
|
19234
|
+
registry;
|
|
19235
|
+
schemaManifest;
|
|
19236
|
+
/**
|
|
19237
|
+
* The append-only deployment-events log is kept truly private so the raw
|
|
19238
|
+
* mutable Collection is never surfaced — events may only be written via
|
|
19239
|
+
* `appendEvent` and read via `queryEvents`. (`registry` and
|
|
19240
|
+
* `schemaManifest` are deliberately public: consumers read and write them.)
|
|
19241
|
+
*/
|
|
19242
|
+
#events;
|
|
19243
|
+
/** Per-shard fleet-migration progress (#271). Surfaced via typed methods only. */
|
|
19244
|
+
#migrationStatus;
|
|
19245
|
+
/** Idempotently open the reserved state vault and bind the control-plane collections. */
|
|
19246
|
+
static async open(db) {
|
|
19247
|
+
const vault = await db.openVault(STATE_VAULT_NAME);
|
|
19248
|
+
return new _StateManagementVault(
|
|
19249
|
+
vault.collection(REGISTRY),
|
|
19250
|
+
vault.collection(MANIFEST),
|
|
19251
|
+
vault.collection(EVENTS),
|
|
19252
|
+
vault.collection(MIGRATION_STATUS)
|
|
19253
|
+
);
|
|
19254
|
+
}
|
|
19255
|
+
/** Read one shard's migration status (or null). */
|
|
19256
|
+
async getMigrationStatus(vaultId) {
|
|
19257
|
+
return this.#migrationStatus.get(vaultId);
|
|
19258
|
+
}
|
|
19259
|
+
/** All migration-status rows (hydrates first). */
|
|
19260
|
+
async listMigrationStatus() {
|
|
19261
|
+
await this.#migrationStatus.list();
|
|
19262
|
+
return this.#migrationStatus.query().toArray();
|
|
19263
|
+
}
|
|
19264
|
+
/** Upsert one shard's migration status (keyed by vaultId). */
|
|
19265
|
+
async upsertMigrationStatus(row) {
|
|
19266
|
+
await this.#migrationStatus.put(row.vaultId, row);
|
|
19267
|
+
}
|
|
19268
|
+
/** Read-only query over the append-only deployment-events log. */
|
|
19269
|
+
queryEvents() {
|
|
19270
|
+
return this.#events.query();
|
|
19271
|
+
}
|
|
19272
|
+
/**
|
|
19273
|
+
* Append a deployment event with a fresh unique (ULID) id. This is the
|
|
19274
|
+
* only write path to the events log; no update/delete is exposed.
|
|
19275
|
+
* Callers should treat failures as non-fatal — this method does not
|
|
19276
|
+
* swallow errors, so wrap the call site in try/catch where appropriate.
|
|
19277
|
+
*/
|
|
19278
|
+
async appendEvent(event) {
|
|
19279
|
+
const ts = event.ts ?? Date.now();
|
|
19280
|
+
const id = generateULID();
|
|
19281
|
+
await this.#events.put(id, { ...event, id, ts });
|
|
19282
|
+
}
|
|
19283
|
+
/**
|
|
19284
|
+
* Ensure a manifest row exists for `(templateName, template.version)`.
|
|
19285
|
+
* Safe to call repeatedly: the `fingerprint` is a deterministic hash of
|
|
19286
|
+
* the template's declared shape (stable across calls), though each call
|
|
19287
|
+
* refreshes `recordedAt`.
|
|
19288
|
+
*/
|
|
19289
|
+
async recordManifest(templateName, template) {
|
|
19290
|
+
const bp = captureBlueprint(template.configure);
|
|
19291
|
+
const fingerprint = await fingerprintBlueprint(bp);
|
|
19292
|
+
await this.schemaManifest.put(`${templateName}:${template.version}`, {
|
|
19293
|
+
templateName,
|
|
19294
|
+
version: template.version,
|
|
19295
|
+
collections: bp.collections,
|
|
19296
|
+
indexes: bp.indexes,
|
|
19297
|
+
persistJsonSchema: bp.persistJsonSchema,
|
|
19298
|
+
fingerprint,
|
|
19299
|
+
recordedAt: Date.now()
|
|
19300
|
+
});
|
|
19301
|
+
return fingerprint;
|
|
19302
|
+
}
|
|
19303
|
+
/**
|
|
19304
|
+
* True when `template`'s current declared shape does not match the recorded
|
|
19305
|
+
* manifest for `(templateName, template.version)`. Because shards carry no
|
|
19306
|
+
* schema state independent of their template, this catches "a template's
|
|
19307
|
+
* shape changed without bumping `version`" — not independent per-shard drift.
|
|
19308
|
+
* A missing manifest is treated as drift (nothing to verify against).
|
|
19309
|
+
*/
|
|
19310
|
+
async detectDrift(templateName, template) {
|
|
19311
|
+
const row = await this.schemaManifest.get(`${templateName}:${template.version}`);
|
|
19312
|
+
if (!row) return true;
|
|
19313
|
+
const current = await fingerprintBlueprint(captureBlueprint(template.configure));
|
|
19314
|
+
return current !== row.fingerprint;
|
|
19315
|
+
}
|
|
19316
|
+
};
|
|
19317
|
+
}
|
|
19318
|
+
});
|
|
19319
|
+
|
|
17694
19320
|
// src/federation/classify-skip.ts
|
|
17695
19321
|
function classifyShardSkip(err) {
|
|
17696
19322
|
return err instanceof NoAccessError ? "no-grant" : "error";
|
|
@@ -17975,6 +19601,7 @@ var SHARD_SEPARATOR, SAFE_PARTITION_KEY, VaultGroup, ShardedCollection, ShardedQ
|
|
|
17975
19601
|
var init_vault_group = __esm({
|
|
17976
19602
|
"src/federation/vault-group.ts"() {
|
|
17977
19603
|
"use strict";
|
|
19604
|
+
init_state_vault();
|
|
17978
19605
|
init_errors();
|
|
17979
19606
|
init_constants2();
|
|
17980
19607
|
init_classify_skip();
|
|
@@ -17984,12 +19611,13 @@ var init_vault_group = __esm({
|
|
|
17984
19611
|
SHARD_SEPARATOR = "--";
|
|
17985
19612
|
SAFE_PARTITION_KEY = /^[A-Za-z0-9._-]+$/;
|
|
17986
19613
|
VaultGroup = class {
|
|
17987
|
-
constructor(db, name, registry, sharding, template) {
|
|
19614
|
+
constructor(db, name, registry, sharding, template, migrateOnOpen = false) {
|
|
17988
19615
|
this.db = db;
|
|
17989
19616
|
this.name = name;
|
|
17990
19617
|
this.registry = registry;
|
|
17991
19618
|
this.sharding = sharding;
|
|
17992
19619
|
this.template = template;
|
|
19620
|
+
this.migrateOnOpen = migrateOnOpen;
|
|
17993
19621
|
if (name.includes(SHARD_SEPARATOR)) {
|
|
17994
19622
|
throw new ValidationError(
|
|
17995
19623
|
`VaultGroup name "${name}" must not contain "--" (reserved shard vault-id separator).`
|
|
@@ -18001,6 +19629,7 @@ var init_vault_group = __esm({
|
|
|
18001
19629
|
registry;
|
|
18002
19630
|
sharding;
|
|
18003
19631
|
template;
|
|
19632
|
+
migrateOnOpen;
|
|
18004
19633
|
/** @internal — set when the group is managed (no explicit registry). */
|
|
18005
19634
|
stateVault;
|
|
18006
19635
|
/** @internal */
|
|
@@ -18034,8 +19663,22 @@ var init_vault_group = __esm({
|
|
|
18034
19663
|
const rows = this.registry.query().toArray();
|
|
18035
19664
|
return rows.filter((r) => r.group === this.name);
|
|
18036
19665
|
}
|
|
18037
|
-
/**
|
|
19666
|
+
/**
|
|
19667
|
+
* Open an existing shard and apply the template. When `migrateOnOpen` is set
|
|
19668
|
+
* (#271) and the shard's registry version is behind the template, its cutover
|
|
19669
|
+
* runs inline first — so a behind shard never surfaces a stale handle.
|
|
19670
|
+
*/
|
|
18038
19671
|
async openShard(partitionKey) {
|
|
19672
|
+
if (this.migrateOnOpen) {
|
|
19673
|
+
const row = await this.registry.get(this.registryId(partitionKey));
|
|
19674
|
+
if (row && row.schemaVersion < this.template.version) {
|
|
19675
|
+
await this.migrateShard(partitionKey);
|
|
19676
|
+
}
|
|
19677
|
+
}
|
|
19678
|
+
return this._openShardRaw(partitionKey);
|
|
19679
|
+
}
|
|
19680
|
+
/** @internal — open + configure with no migrate-on-open hook (used by the migration path itself to avoid recursion). */
|
|
19681
|
+
async _openShardRaw(partitionKey) {
|
|
18039
19682
|
const vault = await this.db.openVault(this.shardVaultId(partitionKey), { create: false });
|
|
18040
19683
|
this.template.configure(vault);
|
|
18041
19684
|
return vault;
|
|
@@ -18113,6 +19756,161 @@ var init_vault_group = __esm({
|
|
|
18113
19756
|
});
|
|
18114
19757
|
return { eligible, skipped };
|
|
18115
19758
|
}
|
|
19759
|
+
/** @internal — registered push-model cross-vault derivations (#271 Insight Vault). */
|
|
19760
|
+
crossVaultDerivations = [];
|
|
19761
|
+
/**
|
|
19762
|
+
* Register a push-model cross-vault derivation — the Insight Vault pattern
|
|
19763
|
+
* (#271, Layer 4). Drive it with {@link refreshInsights}.
|
|
19764
|
+
*
|
|
19765
|
+
* For each shard, `derive(records, ctx)` runs on that shard's `source`
|
|
19766
|
+
* records and its return value is written into the analytics
|
|
19767
|
+
* (`target.vault` / `target.collection`) vault, keyed by partition key —
|
|
19768
|
+
* one summary row per shard. The derivation runs in-process under THIS
|
|
19769
|
+
* group's `Noydb` (which already holds both the shard and Insight Vault
|
|
19770
|
+
* keyrings); the shard's decrypted records are reduced to a summary that is
|
|
19771
|
+
* re-encrypted under the Insight Vault's own DEK, so no shard ciphertext
|
|
19772
|
+
* crosses a DEK boundary.
|
|
19773
|
+
*
|
|
19774
|
+
* **Zero-knowledge note:** the Insight Vault backend sees aggregated
|
|
19775
|
+
* structure (totals, counts, timestamps) drawn from many shards — a weaker
|
|
19776
|
+
* ZK profile than the per-shard vaults. Opt-in; keep summaries to aggregate
|
|
19777
|
+
* scalars (no embeddings / no raw records).
|
|
19778
|
+
*
|
|
19779
|
+
* v1 is explicit-refresh (no write-path push); call `refreshInsights()`
|
|
19780
|
+
* after a batch of writes, or on a schedule.
|
|
19781
|
+
*/
|
|
19782
|
+
withCrossVaultDerivation(spec) {
|
|
19783
|
+
this.crossVaultDerivations.push(spec);
|
|
19784
|
+
}
|
|
19785
|
+
/**
|
|
19786
|
+
* Run every registered {@link withCrossVaultDerivation}: read each eligible
|
|
19787
|
+
* shard's source records, derive a per-shard summary, and write it into the
|
|
19788
|
+
* Insight Vault keyed by partition key. Shards behind `minVersion`,
|
|
19789
|
+
* unprovisioned, or whose read errors are reported in `skippedVaults` and
|
|
19790
|
+
* are not written (a stale summary is never left behind for a failed shard).
|
|
19791
|
+
*/
|
|
19792
|
+
async refreshInsights(options = {}) {
|
|
19793
|
+
if (this.crossVaultDerivations.length === 0) return { written: 0, skippedVaults: [] };
|
|
19794
|
+
const { eligible, skipped } = await this.resolveEligible(
|
|
19795
|
+
options.minVersion !== void 0 ? { minVersion: options.minVersion } : {}
|
|
19796
|
+
);
|
|
19797
|
+
let written = 0;
|
|
19798
|
+
for (const spec of this.crossVaultDerivations) {
|
|
19799
|
+
const results = await this.db.queryAcross(
|
|
19800
|
+
eligible.map((r) => r.vaultId),
|
|
19801
|
+
async (vault) => {
|
|
19802
|
+
this.template.configure(vault);
|
|
19803
|
+
return vault.collection(spec.source).list();
|
|
19804
|
+
},
|
|
19805
|
+
{ create: false, ...options.concurrency !== void 0 ? { concurrency: options.concurrency } : {} }
|
|
19806
|
+
);
|
|
19807
|
+
const insight = await this.db.openVault(spec.target.vault);
|
|
19808
|
+
const out = insight.collection(spec.target.collection);
|
|
19809
|
+
for (let i = 0; i < eligible.length; i++) {
|
|
19810
|
+
const row = eligible[i];
|
|
19811
|
+
const res = results[i];
|
|
19812
|
+
if (!res || res.result === void 0) {
|
|
19813
|
+
skipped.push({ vaultId: row.vaultId, reason: "error", ...res?.error ? { error: res.error } : {} });
|
|
19814
|
+
continue;
|
|
19815
|
+
}
|
|
19816
|
+
const ctx = {
|
|
19817
|
+
vaultId: row.vaultId,
|
|
19818
|
+
partitionKey: row.partitionKey,
|
|
19819
|
+
schemaVersion: row.schemaVersion
|
|
19820
|
+
};
|
|
19821
|
+
const summary = spec.derive(res.result, ctx);
|
|
19822
|
+
await out.put(row.partitionKey, summary);
|
|
19823
|
+
written++;
|
|
19824
|
+
}
|
|
19825
|
+
}
|
|
19826
|
+
return { written, skippedVaults: skipped };
|
|
19827
|
+
}
|
|
19828
|
+
/** @internal — the control-plane vault for migration status; lazily opened. */
|
|
19829
|
+
async ensureStateVault() {
|
|
19830
|
+
if (!this.stateVault) this.stateVault = await StateManagementVault.open(this.db);
|
|
19831
|
+
return this.stateVault;
|
|
19832
|
+
}
|
|
19833
|
+
/**
|
|
19834
|
+
* Migrate ONE shard to the template's current version (#271 fleet runner,
|
|
19835
|
+
* per-shard step). Opens the shard (applying the template, which arms the
|
|
19836
|
+
* M12 cutover), drains schema-write detection, runs `vault.runSchemaCutover()`
|
|
19837
|
+
* (the per-vault drain-barrier-transform protocol), then advances the
|
|
19838
|
+
* registry row's `schemaVersion` and records `migration-status`. A shard
|
|
19839
|
+
* already at the template version is a no-op (`status: 'done'`, migrated 0).
|
|
19840
|
+
* Never throws on a cutover failure — it records `status: 'failed'` and
|
|
19841
|
+
* returns the row, so a fleet run continues past a bad shard.
|
|
19842
|
+
*/
|
|
19843
|
+
async migrateShard(partitionKey) {
|
|
19844
|
+
const vaultId = this.shardVaultId(partitionKey);
|
|
19845
|
+
const row = await this.registry.get(this.registryId(partitionKey));
|
|
19846
|
+
if (!row) throw new UnknownShardError(partitionKey, this.name);
|
|
19847
|
+
const target = this.template.version;
|
|
19848
|
+
const sv = await this.ensureStateVault();
|
|
19849
|
+
const base = { vaultId, group: this.name, currentVersion: row.schemaVersion, targetVersion: target };
|
|
19850
|
+
if (row.schemaVersion >= target) {
|
|
19851
|
+
const done = { ...base, status: "done", migrated: 0, finishedAt: Date.now() };
|
|
19852
|
+
await sv.upsertMigrationStatus(done);
|
|
19853
|
+
return done;
|
|
19854
|
+
}
|
|
19855
|
+
await sv.upsertMigrationStatus({ ...base, status: "running", startedAt: Date.now() });
|
|
19856
|
+
try {
|
|
19857
|
+
await sv.appendEvent({ type: "migration-started", group: this.name, vaultId, version: target });
|
|
19858
|
+
} catch {
|
|
19859
|
+
}
|
|
19860
|
+
try {
|
|
19861
|
+
const vault = await this._openShardRaw(partitionKey);
|
|
19862
|
+
await vault._drainPendingSchemaWrites();
|
|
19863
|
+
const { migrated } = await vault.runSchemaCutover();
|
|
19864
|
+
await this.registry.put(this.registryId(partitionKey), { ...row, schemaVersion: target });
|
|
19865
|
+
const done = { ...base, currentVersion: target, status: "done", migrated, finishedAt: Date.now() };
|
|
19866
|
+
await sv.upsertMigrationStatus(done);
|
|
19867
|
+
try {
|
|
19868
|
+
await sv.appendEvent({ type: "migration-completed", group: this.name, vaultId, version: target });
|
|
19869
|
+
} catch {
|
|
19870
|
+
}
|
|
19871
|
+
return done;
|
|
19872
|
+
} catch (err) {
|
|
19873
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
19874
|
+
const failed = { ...base, status: "failed", error, finishedAt: Date.now() };
|
|
19875
|
+
await sv.upsertMigrationStatus(failed);
|
|
19876
|
+
try {
|
|
19877
|
+
await sv.appendEvent({ type: "migration-failed", group: this.name, vaultId, version: target, detail: error });
|
|
19878
|
+
} catch {
|
|
19879
|
+
}
|
|
19880
|
+
return failed;
|
|
19881
|
+
}
|
|
19882
|
+
}
|
|
19883
|
+
/**
|
|
19884
|
+
* Active batch runner (#271): migrate every shard behind the template version
|
|
19885
|
+
* to it, in controlled batches. **Resumable + crash-safe** — shards already at
|
|
19886
|
+
* the target are skipped (the registry version is the source of truth), so a
|
|
19887
|
+
* re-run after a crash only picks up the unfinished + previously-failed shards.
|
|
19888
|
+
*
|
|
19889
|
+
* - `cohort` — restrict to these partition keys (the staged / canary rollout:
|
|
19890
|
+
* migrate a small cohort, verify the Insight Vault, then run the rest).
|
|
19891
|
+
* - `batchSize` — max shards migrated concurrently per batch (back-pressure).
|
|
19892
|
+
* Default 4. Batches run sequentially; shards within a batch run in parallel.
|
|
19893
|
+
*/
|
|
19894
|
+
async migrateFleet(options = {}) {
|
|
19895
|
+
const target = this.template.version;
|
|
19896
|
+
const rows = await this.allRows();
|
|
19897
|
+
const cohort = options.cohort;
|
|
19898
|
+
const todo = rows.filter(
|
|
19899
|
+
(r) => r.schemaVersion < target && (cohort === void 0 || cohort.includes(r.partitionKey))
|
|
19900
|
+
);
|
|
19901
|
+
const batchSize = Math.max(1, options.batchSize ?? 4);
|
|
19902
|
+
const migrated = [];
|
|
19903
|
+
const failed = [];
|
|
19904
|
+
for (let i = 0; i < todo.length; i += batchSize) {
|
|
19905
|
+
const batch = todo.slice(i, i + batchSize);
|
|
19906
|
+
const settled = await Promise.all(batch.map((r) => this.migrateShard(r.partitionKey)));
|
|
19907
|
+
for (const res of settled) {
|
|
19908
|
+
if (res.status === "done") migrated.push(res.vaultId);
|
|
19909
|
+
else failed.push({ vaultId: res.vaultId, error: res.error ?? "unknown" });
|
|
19910
|
+
}
|
|
19911
|
+
}
|
|
19912
|
+
return { target, migrated, failed };
|
|
19913
|
+
}
|
|
18116
19914
|
};
|
|
18117
19915
|
ShardedCollection = class {
|
|
18118
19916
|
constructor(group, collectionName) {
|
|
@@ -18320,159 +20118,6 @@ var init_vault_group = __esm({
|
|
|
18320
20118
|
}
|
|
18321
20119
|
});
|
|
18322
20120
|
|
|
18323
|
-
// src/federation/schema-manifest.ts
|
|
18324
|
-
function captureBlueprint(configure) {
|
|
18325
|
-
const recorded = [];
|
|
18326
|
-
const collectionStub = new Proxy(
|
|
18327
|
-
{},
|
|
18328
|
-
{
|
|
18329
|
-
get: () => () => collectionStub
|
|
18330
|
-
}
|
|
18331
|
-
);
|
|
18332
|
-
const proxy = new Proxy(
|
|
18333
|
-
{},
|
|
18334
|
-
{
|
|
18335
|
-
get: (_t, prop) => {
|
|
18336
|
-
if (prop === "collection") {
|
|
18337
|
-
return (name, opts) => {
|
|
18338
|
-
recorded.push({
|
|
18339
|
-
name,
|
|
18340
|
-
indexes: opts?.indexes ?? [],
|
|
18341
|
-
persistJsonSchema: !!opts?.persistJsonSchema
|
|
18342
|
-
});
|
|
18343
|
-
return collectionStub;
|
|
18344
|
-
};
|
|
18345
|
-
}
|
|
18346
|
-
return () => proxy;
|
|
18347
|
-
}
|
|
18348
|
-
}
|
|
18349
|
-
);
|
|
18350
|
-
configure(proxy);
|
|
18351
|
-
const sorted = [...recorded].sort((a, b) => a.name.localeCompare(b.name));
|
|
18352
|
-
const indexes = {};
|
|
18353
|
-
const persistJsonSchema = [];
|
|
18354
|
-
for (const c of sorted) {
|
|
18355
|
-
indexes[c.name] = c.indexes;
|
|
18356
|
-
if (c.persistJsonSchema) persistJsonSchema.push(c.name);
|
|
18357
|
-
}
|
|
18358
|
-
return {
|
|
18359
|
-
// `persistJsonSchema` is already name-sorted: it is populated while
|
|
18360
|
-
// iterating `sorted` (collections in name order).
|
|
18361
|
-
collections: sorted.map((c) => c.name),
|
|
18362
|
-
indexes,
|
|
18363
|
-
persistJsonSchema
|
|
18364
|
-
};
|
|
18365
|
-
}
|
|
18366
|
-
function canonical(value) {
|
|
18367
|
-
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
18368
|
-
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
18369
|
-
const obj = value;
|
|
18370
|
-
const keys = Object.keys(obj).sort();
|
|
18371
|
-
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonical(obj[k])}`).join(",")}}`;
|
|
18372
|
-
}
|
|
18373
|
-
async function fingerprintBlueprint(bp) {
|
|
18374
|
-
return sha256Hex2(new TextEncoder().encode(canonical(bp)));
|
|
18375
|
-
}
|
|
18376
|
-
var init_schema_manifest = __esm({
|
|
18377
|
-
"src/federation/schema-manifest.ts"() {
|
|
18378
|
-
"use strict";
|
|
18379
|
-
init_crypto();
|
|
18380
|
-
}
|
|
18381
|
-
});
|
|
18382
|
-
|
|
18383
|
-
// src/federation/state-vault.ts
|
|
18384
|
-
var state_vault_exports = {};
|
|
18385
|
-
__export(state_vault_exports, {
|
|
18386
|
-
STATE_VAULT_NAME: () => STATE_VAULT_NAME,
|
|
18387
|
-
StateManagementVault: () => StateManagementVault
|
|
18388
|
-
});
|
|
18389
|
-
var REGISTRY, MANIFEST, EVENTS, StateManagementVault;
|
|
18390
|
-
var init_state_vault = __esm({
|
|
18391
|
-
"src/federation/state-vault.ts"() {
|
|
18392
|
-
"use strict";
|
|
18393
|
-
init_schema_manifest();
|
|
18394
|
-
init_constants2();
|
|
18395
|
-
init_ulid();
|
|
18396
|
-
init_constants2();
|
|
18397
|
-
REGISTRY = "vaultRegistry";
|
|
18398
|
-
MANIFEST = "schemaManifest";
|
|
18399
|
-
EVENTS = "deploymentEvents";
|
|
18400
|
-
StateManagementVault = class _StateManagementVault {
|
|
18401
|
-
constructor(registry, schemaManifest, events) {
|
|
18402
|
-
this.registry = registry;
|
|
18403
|
-
this.schemaManifest = schemaManifest;
|
|
18404
|
-
this.#events = events;
|
|
18405
|
-
}
|
|
18406
|
-
registry;
|
|
18407
|
-
schemaManifest;
|
|
18408
|
-
/**
|
|
18409
|
-
* The append-only deployment-events log is kept truly private so the raw
|
|
18410
|
-
* mutable Collection is never surfaced — events may only be written via
|
|
18411
|
-
* `appendEvent` and read via `queryEvents`. (`registry` and
|
|
18412
|
-
* `schemaManifest` are deliberately public: consumers read and write them.)
|
|
18413
|
-
*/
|
|
18414
|
-
#events;
|
|
18415
|
-
/** Idempotently open the reserved state vault and bind the three control-plane collections. */
|
|
18416
|
-
static async open(db) {
|
|
18417
|
-
const vault = await db.openVault(STATE_VAULT_NAME);
|
|
18418
|
-
return new _StateManagementVault(
|
|
18419
|
-
vault.collection(REGISTRY),
|
|
18420
|
-
vault.collection(MANIFEST),
|
|
18421
|
-
vault.collection(EVENTS)
|
|
18422
|
-
);
|
|
18423
|
-
}
|
|
18424
|
-
/** Read-only query over the append-only deployment-events log. */
|
|
18425
|
-
queryEvents() {
|
|
18426
|
-
return this.#events.query();
|
|
18427
|
-
}
|
|
18428
|
-
/**
|
|
18429
|
-
* Append a deployment event with a fresh unique (ULID) id. This is the
|
|
18430
|
-
* only write path to the events log; no update/delete is exposed.
|
|
18431
|
-
* Callers should treat failures as non-fatal — this method does not
|
|
18432
|
-
* swallow errors, so wrap the call site in try/catch where appropriate.
|
|
18433
|
-
*/
|
|
18434
|
-
async appendEvent(event) {
|
|
18435
|
-
const ts = event.ts ?? Date.now();
|
|
18436
|
-
const id = generateULID();
|
|
18437
|
-
await this.#events.put(id, { ...event, id, ts });
|
|
18438
|
-
}
|
|
18439
|
-
/**
|
|
18440
|
-
* Ensure a manifest row exists for `(templateName, template.version)`.
|
|
18441
|
-
* Safe to call repeatedly: the `fingerprint` is a deterministic hash of
|
|
18442
|
-
* the template's declared shape (stable across calls), though each call
|
|
18443
|
-
* refreshes `recordedAt`.
|
|
18444
|
-
*/
|
|
18445
|
-
async recordManifest(templateName, template) {
|
|
18446
|
-
const bp = captureBlueprint(template.configure);
|
|
18447
|
-
const fingerprint = await fingerprintBlueprint(bp);
|
|
18448
|
-
await this.schemaManifest.put(`${templateName}:${template.version}`, {
|
|
18449
|
-
templateName,
|
|
18450
|
-
version: template.version,
|
|
18451
|
-
collections: bp.collections,
|
|
18452
|
-
indexes: bp.indexes,
|
|
18453
|
-
persistJsonSchema: bp.persistJsonSchema,
|
|
18454
|
-
fingerprint,
|
|
18455
|
-
recordedAt: Date.now()
|
|
18456
|
-
});
|
|
18457
|
-
return fingerprint;
|
|
18458
|
-
}
|
|
18459
|
-
/**
|
|
18460
|
-
* True when `template`'s current declared shape does not match the recorded
|
|
18461
|
-
* manifest for `(templateName, template.version)`. Because shards carry no
|
|
18462
|
-
* schema state independent of their template, this catches "a template's
|
|
18463
|
-
* shape changed without bumping `version`" — not independent per-shard drift.
|
|
18464
|
-
* A missing manifest is treated as drift (nothing to verify against).
|
|
18465
|
-
*/
|
|
18466
|
-
async detectDrift(templateName, template) {
|
|
18467
|
-
const row = await this.schemaManifest.get(`${templateName}:${template.version}`);
|
|
18468
|
-
if (!row) return true;
|
|
18469
|
-
const current = await fingerprintBlueprint(captureBlueprint(template.configure));
|
|
18470
|
-
return current !== row.fingerprint;
|
|
18471
|
-
}
|
|
18472
|
-
};
|
|
18473
|
-
}
|
|
18474
|
-
});
|
|
18475
|
-
|
|
18476
20121
|
// src/noydb.ts
|
|
18477
20122
|
var noydb_exports = {};
|
|
18478
20123
|
__export(noydb_exports, {
|
|
@@ -18553,12 +20198,14 @@ var init_noydb = __esm({
|
|
|
18553
20198
|
init_authenticators();
|
|
18554
20199
|
init_unlock_state();
|
|
18555
20200
|
init_sync_strategy();
|
|
18556
|
-
|
|
20201
|
+
init_strategy11();
|
|
18557
20202
|
init_scheduler();
|
|
18558
20203
|
init_transaction();
|
|
18559
|
-
init_strategy11();
|
|
18560
|
-
init_sync_policy();
|
|
18561
20204
|
init_strategy12();
|
|
20205
|
+
init_strategy7();
|
|
20206
|
+
init_subject_index();
|
|
20207
|
+
init_sync_policy();
|
|
20208
|
+
init_strategy13();
|
|
18562
20209
|
init_policy3();
|
|
18563
20210
|
ROLE_RANK = {
|
|
18564
20211
|
client: 1,
|
|
@@ -18614,6 +20261,7 @@ var init_noydb = __esm({
|
|
|
18614
20261
|
policyEnforcers = /* @__PURE__ */ new Map();
|
|
18615
20262
|
vaultTemplates = /* @__PURE__ */ new Map();
|
|
18616
20263
|
txStrategy;
|
|
20264
|
+
forgetStrategy;
|
|
18617
20265
|
sessionStrategy;
|
|
18618
20266
|
syncStrategy;
|
|
18619
20267
|
snapshotStrategy;
|
|
@@ -18641,6 +20289,7 @@ var init_noydb = __esm({
|
|
|
18641
20289
|
constructor(options) {
|
|
18642
20290
|
this.options = options;
|
|
18643
20291
|
this.txStrategy = options.txStrategy ?? NO_TX;
|
|
20292
|
+
this.forgetStrategy = options.forgetStrategy ?? NO_FORGET;
|
|
18644
20293
|
this.sessionStrategy = options.sessionStrategy ?? NO_SESSION;
|
|
18645
20294
|
this.syncStrategy = options.syncStrategy ?? NO_SYNC;
|
|
18646
20295
|
this.snapshotStrategy = options.snapshotStrategy ?? NO_SNAPSHOTS;
|
|
@@ -18651,8 +20300,61 @@ var init_noydb = __esm({
|
|
|
18651
20300
|
}
|
|
18652
20301
|
this.#registerGuardGate();
|
|
18653
20302
|
this.#registerPeriodGate();
|
|
20303
|
+
this.#registerForgetHooks();
|
|
18654
20304
|
this.resetSessionTimer();
|
|
18655
20305
|
}
|
|
20306
|
+
/** @internal — resolved forget strategy (NO_FORGET when not configured). */
|
|
20307
|
+
get _forgetStrategy() {
|
|
20308
|
+
return this.forgetStrategy;
|
|
20309
|
+
}
|
|
20310
|
+
// #304 — GDPR subject-index maintenance. When `withForgetCascade` declares
|
|
20311
|
+
// any subject fields, keep the encrypted `_subject_index` in lock-step with
|
|
20312
|
+
// writes so `vault.forget(subjectId)` can find every record for a subject.
|
|
20313
|
+
//
|
|
20314
|
+
// Two consumers are required because they cover disjoint events:
|
|
20315
|
+
// - onAfterWrite fires on create/update (NOT delete) — add the new ref;
|
|
20316
|
+
// on an update that changed the subject value, drop the stale ref.
|
|
20317
|
+
// - the subsystemBus `afterDelete` observer fires on delete (onAfterWrite
|
|
20318
|
+
// does NOT) — drop the ref so a deleted record never lingers in the
|
|
20319
|
+
// index (RISK #2). Without it, forget() would try to shred a ghost.
|
|
20320
|
+
#registerForgetHooks() {
|
|
20321
|
+
const subjects = this.forgetStrategy.subjects;
|
|
20322
|
+
if (Object.keys(subjects).length === 0) return;
|
|
20323
|
+
const subjectFieldFor = (collection) => subjects[collection];
|
|
20324
|
+
this.writeHooks.onAfterWrite(async (event) => {
|
|
20325
|
+
const field = subjectFieldFor(event.collection);
|
|
20326
|
+
if (field === void 0) return;
|
|
20327
|
+
const vault = this.vaultCache.get(event.vault);
|
|
20328
|
+
if (!vault) return;
|
|
20329
|
+
if (event.after !== null && typeof event.after === "object") {
|
|
20330
|
+
const subjectValue = readDottedPath(event.after, field);
|
|
20331
|
+
if (subjectValue !== void 0 && subjectValue !== null) {
|
|
20332
|
+
await vault._addSubjectRef(coerceSubjectId(subjectValue), { collection: event.collection, id: event.docId });
|
|
20333
|
+
}
|
|
20334
|
+
}
|
|
20335
|
+
if (event.op === "update" && event.before !== null && typeof event.before === "object") {
|
|
20336
|
+
const beforeValue = readDottedPath(event.before, field);
|
|
20337
|
+
const afterValue = event.after !== null && typeof event.after === "object" ? readDottedPath(event.after, field) : void 0;
|
|
20338
|
+
const beforeId = beforeValue === void 0 || beforeValue === null ? void 0 : coerceSubjectId(beforeValue);
|
|
20339
|
+
const afterId = afterValue === void 0 || afterValue === null ? void 0 : coerceSubjectId(afterValue);
|
|
20340
|
+
if (beforeId !== void 0 && beforeId !== afterId) {
|
|
20341
|
+
await vault._removeSubjectRef(beforeId, { collection: event.collection, id: event.docId });
|
|
20342
|
+
}
|
|
20343
|
+
}
|
|
20344
|
+
});
|
|
20345
|
+
this.subsystemBus.register("afterDelete", async (event) => {
|
|
20346
|
+
const field = subjectFieldFor(event.collection);
|
|
20347
|
+
if (field === void 0) return;
|
|
20348
|
+
const vault = this.vaultCache.get(event.vault);
|
|
20349
|
+
if (!vault) return;
|
|
20350
|
+
if (event.before !== null && typeof event.before === "object") {
|
|
20351
|
+
const subjectValue = readDottedPath(event.before, field);
|
|
20352
|
+
if (subjectValue !== void 0 && subjectValue !== null) {
|
|
20353
|
+
await vault._removeSubjectRef(coerceSubjectId(subjectValue), { collection: event.collection, id: event.docId });
|
|
20354
|
+
}
|
|
20355
|
+
}
|
|
20356
|
+
});
|
|
20357
|
+
}
|
|
18656
20358
|
// Track A — guards migration. Registers record-lock / field-freeze / onDelete
|
|
18657
20359
|
// / amendment-collect as gate-bus handlers (only when guards are opted in, so
|
|
18658
20360
|
// the write path is zero-cost otherwise). Resolves the live vault's
|
|
@@ -18873,6 +20575,7 @@ var init_noydb = __esm({
|
|
|
18873
20575
|
...this.options.syncStrategy !== void 0 ? { syncStrategy: this.options.syncStrategy } : {},
|
|
18874
20576
|
...this.options.guardStrategies !== void 0 ? { guardStrategies: this.options.guardStrategies } : {},
|
|
18875
20577
|
...this.options.numbering !== void 0 ? { numberingConfigs: this.options.numbering } : {},
|
|
20578
|
+
forgetStrategy: this.forgetStrategy,
|
|
18876
20579
|
locale: opts?.locale,
|
|
18877
20580
|
// Thread the translator hook so Collection.put() can invoke it
|
|
18878
20581
|
plaintextTranslator: this.options.plaintextTranslator ? (text, from, to, field, collection) => this.invokeTranslator(text, from, to, field, collection) : void 0,
|
|
@@ -18927,7 +20630,8 @@ var init_noydb = __esm({
|
|
|
18927
20630
|
...this.options.i18nStrategy !== void 0 ? { i18nStrategy: this.options.i18nStrategy } : {},
|
|
18928
20631
|
...this.options.syncStrategy !== void 0 ? { syncStrategy: this.options.syncStrategy } : {},
|
|
18929
20632
|
...this.options.guardStrategies !== void 0 ? { guardStrategies: this.options.guardStrategies } : {},
|
|
18930
|
-
...this.options.numbering !== void 0 ? { numberingConfigs: this.options.numbering } : {}
|
|
20633
|
+
...this.options.numbering !== void 0 ? { numberingConfigs: this.options.numbering } : {},
|
|
20634
|
+
forgetStrategy: this.forgetStrategy
|
|
18931
20635
|
});
|
|
18932
20636
|
this.vaultCache.set(name, comp2);
|
|
18933
20637
|
return comp2;
|
|
@@ -19283,7 +20987,7 @@ var init_noydb = __esm({
|
|
|
19283
20987
|
const { StateManagementVault: StateManagementVault2 } = await Promise.resolve().then(() => (init_state_vault(), state_vault_exports));
|
|
19284
20988
|
const stateVault = opts.registry ? void 0 : await StateManagementVault2.open(this);
|
|
19285
20989
|
const registry = opts.registry ?? stateVault.registry;
|
|
19286
|
-
const group = new VaultGroup2(this, name, registry, opts.sharding, template);
|
|
20990
|
+
const group = new VaultGroup2(this, name, registry, opts.sharding, template, opts.migrateOnOpen ?? false);
|
|
19287
20991
|
if (stateVault) {
|
|
19288
20992
|
group._attachStateVault(stateVault);
|
|
19289
20993
|
await stateVault.recordManifest(opts.sharding.vaultTemplate, template);
|
|
@@ -21712,6 +23416,7 @@ async function describeExtraction(vault, opts) {
|
|
|
21712
23416
|
// src/bundle/extract-partition.ts
|
|
21713
23417
|
init_types();
|
|
21714
23418
|
init_crypto();
|
|
23419
|
+
init_record_keys();
|
|
21715
23420
|
init_errors();
|
|
21716
23421
|
init_ulid();
|
|
21717
23422
|
init_storage2();
|
|
@@ -21731,6 +23436,14 @@ async function reKeyClosure(vault, closure) {
|
|
|
21731
23436
|
for (const id of ids) {
|
|
21732
23437
|
const env = await adapter.get(vaultName, collectionName, id);
|
|
21733
23438
|
if (!env) continue;
|
|
23439
|
+
if (env._cek !== void 0) {
|
|
23440
|
+
const cek = await unwrapCek(env._cek, srcDek);
|
|
23441
|
+
const plaintext2 = await decrypt(env._iv, env._data, cek);
|
|
23442
|
+
const { iv: iv2, data: data2 } = await encrypt(plaintext2, cek);
|
|
23443
|
+
const wrapped = await wrapCek(cek, destDek);
|
|
23444
|
+
out[id] = { ...env, _iv: iv2, _data: data2, _cek: wrapped };
|
|
23445
|
+
continue;
|
|
23446
|
+
}
|
|
21734
23447
|
const plaintext = await decrypt(env._iv, env._data, srcDek);
|
|
21735
23448
|
const { iv, data } = await encrypt(plaintext, destDek);
|
|
21736
23449
|
out[id] = { ...env, _iv: iv, _data: data };
|