@noy-db/hub 0.3.0-pre.7 → 0.3.0-pre.9
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/adapter/index.d.ts +2 -3
- package/dist/adapter/index.js +1 -1
- package/dist/aggregate/index.d.ts +3 -4
- package/dist/aggregate/index.js +9 -8
- package/dist/aggregate/index.js.map +1 -1
- package/dist/api-HV4PDUWF.js +20 -0
- package/dist/as/index.d.ts +4 -5
- package/dist/as/index.js +11 -11
- package/dist/at/index.d.ts +2 -3
- package/dist/attestation/index.d.ts +3 -4
- package/dist/attestation/index.js +18 -18
- package/dist/{backup-7PX6NDKT.js → backup-ODXO7OGA.js} +12 -12
- package/dist/blobs/index.d.ts +4 -5
- package/dist/blobs/index.js +11 -11
- package/dist/blobs/index.js.map +1 -1
- package/dist/bundle/index.d.ts +5 -6
- package/dist/bundle/index.js +20 -20
- package/dist/{bundle-BN_VSq5L.d.ts → bundle-DlSxo31R.d.ts} +1 -1
- package/dist/by/index.js +2 -2
- package/dist/cargo/index.d.ts +4 -5
- package/dist/cargo/index.js +23 -22
- package/dist/{chunk-QYNBPDGO.js → chunk-2644KEGW.js} +18 -10
- package/dist/chunk-2644KEGW.js.map +1 -0
- package/dist/{chunk-XW5LZYV4.js → chunk-2HVUKRNE.js} +2 -2
- package/dist/{chunk-VFQOE76I.js → chunk-2KOSJTMU.js} +2 -2
- package/dist/{chunk-7AERMJP6.js → chunk-2UQ4RMHV.js} +4 -4
- package/dist/{chunk-5GQE5IDB.js → chunk-2XE6LIXT.js} +13 -11
- package/dist/chunk-2XE6LIXT.js.map +1 -0
- package/dist/{chunk-VIWOGJ2O.js → chunk-34RRRIC3.js} +2 -2
- package/dist/{chunk-BASCPHNM.js → chunk-3C5CKXFI.js} +289 -48
- package/dist/chunk-3C5CKXFI.js.map +1 -0
- package/dist/{chunk-LFGJG4XZ.js → chunk-3I4KYWNP.js} +2 -2
- package/dist/{chunk-RUWEHFRB.js → chunk-42KVY3OC.js} +4 -4
- package/dist/{chunk-5K7KZMXG.js → chunk-4C4WBS4V.js} +2048 -948
- package/dist/chunk-4C4WBS4V.js.map +1 -0
- package/dist/{chunk-J54DC5IC.js → chunk-4G3IT7J7.js} +7 -7
- package/dist/{chunk-WJEMUO32.js → chunk-4RQ62VSR.js} +2 -2
- package/dist/{chunk-QAVFI23O.js → chunk-4ZSGSRBI.js} +8 -8
- package/dist/{chunk-C4NOLA5X.js → chunk-5GAX2QOH.js} +2 -2
- package/dist/{chunk-IEKK2XI4.js → chunk-5SAZMBW7.js} +7 -7
- package/dist/{chunk-II6DLDZM.js → chunk-5TZZUL5X.js} +2 -2
- package/dist/{chunk-GMZFHDSG.js → chunk-6CU3FN74.js} +8 -8
- package/dist/chunk-6G2SCQYS.js +29 -0
- package/dist/chunk-6G2SCQYS.js.map +1 -0
- package/dist/{chunk-UWZ7O4NR.js → chunk-6MWUANRF.js} +2 -2
- package/dist/{chunk-U7LV2DUN.js → chunk-6ULCYCMU.js} +2 -2
- package/dist/{chunk-6Z435B6N.js → chunk-7HAPHVRJ.js} +131 -92
- package/dist/chunk-7HAPHVRJ.js.map +1 -0
- package/dist/{chunk-VFG5YXWM.js → chunk-7IZQTCTR.js} +3 -3
- package/dist/chunk-7ZANJ6JZ.js +265 -0
- package/dist/chunk-7ZANJ6JZ.js.map +1 -0
- package/dist/{chunk-VMRXI3IF.js → chunk-A2APWS32.js} +6 -6
- package/dist/chunk-A2APWS32.js.map +1 -0
- package/dist/{chunk-U5IJHUHZ.js → chunk-A4MAD6PO.js} +5 -5
- package/dist/chunk-ANDBBA2P.js +36 -0
- package/dist/chunk-ANDBBA2P.js.map +1 -0
- package/dist/{chunk-5HZULLHE.js → chunk-AUR7MRHA.js} +2 -2
- package/dist/{chunk-YSJG4L4A.js → chunk-AYGZMQP5.js} +3 -3
- package/dist/chunk-AYGZMQP5.js.map +1 -0
- package/dist/{chunk-H7WQMGXH.js → chunk-CJEMA2Z6.js} +2 -2
- package/dist/{chunk-WRLPV6SI.js → chunk-CTSFKDGA.js} +2 -2
- package/dist/{chunk-OJ64XMPJ.js → chunk-CU62BBRB.js} +32 -1
- package/dist/chunk-CU62BBRB.js.map +1 -0
- package/dist/{chunk-OFYY5Q4R.js → chunk-CWCCYH34.js} +2 -2
- package/dist/{chunk-WGH3YV6Y.js → chunk-DEYAO3N6.js} +10 -10
- package/dist/{chunk-7L6EH6S7.js → chunk-DFIBOSOQ.js} +8 -8
- package/dist/{chunk-3CHNKEFI.js → chunk-E5AHO65E.js} +7 -7
- package/dist/{chunk-AG2PLGQ2.js → chunk-FC3XK5JT.js} +8 -8
- package/dist/{chunk-CP3GUIAN.js → chunk-FGOGS7CE.js} +10 -12
- package/dist/chunk-FGOGS7CE.js.map +1 -0
- package/dist/{chunk-DJ4TNF4N.js → chunk-GX2BYNCT.js} +7 -7
- package/dist/{chunk-YTCCC6NG.js → chunk-GZ2MOPVX.js} +34 -63
- package/dist/chunk-GZ2MOPVX.js.map +1 -0
- package/dist/chunk-H57P2SJB.js +126 -0
- package/dist/chunk-H57P2SJB.js.map +1 -0
- package/dist/{chunk-55LSDABW.js → chunk-HBTT7YQX.js} +2 -2
- package/dist/{chunk-PYIUDUGS.js → chunk-HOCEGVGQ.js} +2 -2
- package/dist/{chunk-5JQMX2EH.js → chunk-ICV32FPK.js} +2 -2
- package/dist/{chunk-WFBZRXS5.js → chunk-IIP5WFVT.js} +138 -92
- package/dist/chunk-IIP5WFVT.js.map +1 -0
- package/dist/{chunk-YSU2E4GZ.js → chunk-IUKGIB2Q.js} +19 -6
- package/dist/chunk-IUKGIB2Q.js.map +1 -0
- package/dist/{chunk-OWGAXPRE.js → chunk-IWTYFS3D.js} +2 -2
- package/dist/{chunk-FCOZ7DEC.js → chunk-JC5R7P44.js} +3 -3
- package/dist/{chunk-K6WHZBCO.js → chunk-KBBWY46Y.js} +2 -2
- package/dist/{chunk-RNYDHIQL.js → chunk-KBF5GSGX.js} +3 -3
- package/dist/{chunk-6P5AJ2ZS.js → chunk-KCG3BHFG.js} +2 -23
- package/dist/chunk-KCG3BHFG.js.map +1 -0
- package/dist/{chunk-T4NQMPBN.js → chunk-KNXN47MO.js} +3 -3
- package/dist/{chunk-VK6OC22R.js → chunk-KWLD2D75.js} +4 -3
- package/dist/chunk-KWLD2D75.js.map +1 -0
- package/dist/{chunk-7F2P2NHX.js → chunk-KXKLKJW4.js} +6 -6
- package/dist/{chunk-PAJCRV7O.js → chunk-KYC4LIEU.js} +2 -2
- package/dist/{chunk-AZPGB4OD.js → chunk-LEOXZY36.js} +8 -8
- package/dist/{chunk-2L7PPF2C.js → chunk-M5I354EE.js} +9 -9
- package/dist/{chunk-26FSVNBL.js → chunk-MK2BSS44.js} +2 -2
- package/dist/{chunk-G5CFMITA.js → chunk-MSVY7ULZ.js} +3 -3
- package/dist/{chunk-PE25C4QL.js → chunk-NNWK3PSY.js} +9 -9
- package/dist/{chunk-JT4A6OAJ.js → chunk-NSZ6J5DF.js} +7 -7
- package/dist/{chunk-RHB7BOEO.js → chunk-NUEFXLZU.js} +7 -7
- package/dist/{chunk-I6Q7YECW.js → chunk-NYVQCZ54.js} +5 -5
- package/dist/chunk-NYVQCZ54.js.map +1 -0
- package/dist/{chunk-DKGZIX6S.js → chunk-OQAODTYR.js} +2 -2
- package/dist/{chunk-HV3VNAYI.js → chunk-OV4CMJGR.js} +5 -5
- package/dist/{chunk-V76PHWLE.js → chunk-PEDZFFKW.js} +2 -2
- package/dist/chunk-PF3FQCCF.js +164 -0
- package/dist/chunk-PF3FQCCF.js.map +1 -0
- package/dist/{chunk-CND5WQLO.js → chunk-Q2KVKTC2.js} +5 -5
- package/dist/{chunk-RUIMKQTA.js → chunk-QJR3W3MF.js} +1 -1
- package/dist/chunk-QJR3W3MF.js.map +1 -0
- package/dist/{chunk-HBVEGUON.js → chunk-QTXQ6RNZ.js} +3 -3
- package/dist/{chunk-J5YDMJHG.js → chunk-RJ5522XX.js} +12 -12
- package/dist/{chunk-H3MPSYCH.js → chunk-RMUXPHIH.js} +56 -7
- package/dist/chunk-RMUXPHIH.js.map +1 -0
- package/dist/{chunk-CHK5VVMI.js → chunk-S6AQXDN4.js} +2 -2
- package/dist/{chunk-E3LURVGQ.js → chunk-SSFC3IYA.js} +9 -9
- package/dist/chunk-SSFC3IYA.js.map +1 -0
- package/dist/{chunk-MOQ5M46U.js → chunk-SZCHEDML.js} +2 -2
- package/dist/{chunk-UB3XHGFG.js → chunk-TDPEJINP.js} +14 -7
- package/dist/chunk-TDPEJINP.js.map +1 -0
- package/dist/{chunk-HCK2DFEK.js → chunk-UNJU345Q.js} +1 -1
- package/dist/chunk-UNJU345Q.js.map +1 -0
- package/dist/chunk-V7RNIND6.js +265 -0
- package/dist/chunk-V7RNIND6.js.map +1 -0
- package/dist/{chunk-VUHXLL32.js → chunk-VJ3NQK4K.js} +5 -5
- package/dist/{chunk-62TTFKU4.js → chunk-WNA6T3MA.js} +8 -8
- package/dist/{chunk-7CZ6YXMO.js → chunk-WVCQ75OK.js} +2 -2
- package/dist/{chunk-NO53W6O5.js → chunk-X66MXMR5.js} +2 -2
- package/dist/{chunk-JIW3Q5JN.js → chunk-XHD3L3JO.js} +199 -35
- package/dist/chunk-XHD3L3JO.js.map +1 -0
- package/dist/{chunk-6PALJTC2.js → chunk-XJK2WIYN.js} +3 -3
- package/dist/{chunk-5UOPIPZC.js → chunk-XQ7UYPVN.js} +3 -3
- package/dist/{chunk-HU55NPHZ.js → chunk-Y2OCKFZ3.js} +2 -2
- package/dist/{chunk-UJJJPKJO.js → chunk-YBFOBRTO.js} +3 -3
- package/dist/{chunk-I6JJ6VJS.js → chunk-YLN33ADH.js} +2 -2
- package/dist/{chunk-HGQ6UNHE.js → chunk-Z5YFRMG5.js} +2 -2
- package/dist/{chunk-EU7HDMT3.js → chunk-ZWQFOXBR.js} +2 -2
- package/dist/classified/index.d.ts +4 -5
- package/dist/classified/index.js +5 -3
- package/dist/{config-drift-LRKCC3VZ.js → classified-marker-IO5EBW7Q.js} +4 -4
- package/dist/classified-marker-IO5EBW7Q.js.map +1 -0
- package/dist/collection-facade-3RAXG2OU.js +38 -0
- package/dist/{computed-6FOFXQPE.js → computed-R62HGCO4.js} +3 -3
- package/dist/consent/index.d.ts +3 -4
- package/dist/consent/index.js +9 -9
- package/dist/crdt/index.d.ts +3 -4
- package/dist/{dead-filter-NZORV45S.js → dead-filter-VNA4M3GP.js} +2 -2
- package/dist/delegation-ZKII5UBI.js +25 -0
- package/dist/derivations/index.d.ts +4 -5
- package/dist/derivations/index.js +12 -11
- package/dist/derive-5D2C3WG4.js +21 -0
- package/dist/describe/index.d.ts +4 -1
- package/dist/{dev-unlock-CsOqX7O2.d.ts → dev-unlock-DDTafVO6.d.ts} +1 -1
- package/dist/{enclave-7X5BUAMS.js → enclave-EUBEJGPG.js} +11 -9
- package/dist/executor-IL7SRNUZ.js +23 -0
- package/dist/executor-OUA2Y6YC.js +9 -0
- package/dist/executor-WAEI6PDV.js +9 -0
- package/dist/export-accessible-O55RKAGC.js +23 -0
- package/dist/extract-partition-VBGCBA6Q.js +36 -0
- package/dist/{fanout-sidecar-BDKLP5GG.js → fanout-sidecar-CI5BCMQM.js} +11 -11
- package/dist/find-FISGUWSK.js +11 -0
- package/dist/forget/index.d.ts +2 -2
- package/dist/forget/index.js +9 -9
- package/dist/guards/index.d.ts +4 -5
- package/dist/guards/index.js +4 -4
- package/dist/{hash-BPzm02Za.d.ts → hash-DBkZfIBW.d.ts} +1 -1
- package/dist/history/index.d.ts +4 -5
- package/dist/history/index.js +11 -11
- package/dist/i18n/index.d.ts +24 -5
- package/dist/i18n/index.js +45 -34
- package/dist/i18n/index.js.map +1 -1
- package/dist/in/index.d.ts +2 -3
- package/dist/{index-CLucFLeN.d.ts → index-CyclmyDr.d.ts} +2 -3
- package/dist/{index-BJ3L-XcV.d.ts → index-DzhNI2vE.d.ts} +6378 -5022
- package/dist/index.d.ts +64 -23
- package/dist/index.js +411 -132
- package/dist/index.js.map +1 -1
- package/dist/indexing/index.d.ts +3 -4
- package/dist/indexing/index.js +4 -4
- package/dist/issue-EQAWIYFQ.js +19 -0
- package/dist/kernel/index.d.ts +3 -4
- package/dist/kernel/index.js +13 -12
- package/dist/lazy/index.d.ts +3 -4
- package/dist/{ledger-EMDHRJJN.js → ledger-AJA2SR3C.js} +11 -11
- package/dist/liberate-SEGR4JLY.js +24 -0
- package/dist/{link-set-62Y36TOG.js → link-set-YBAC2DQT.js} +11 -11
- package/dist/materialized-views/index.d.ts +14 -5
- package/dist/materialized-views/index.js +17 -15
- package/dist/{mime-magic-BlE9fzn2.d.ts → mime-magic-CbV1quyp.d.ts} +1 -1
- package/dist/noydb-AMEOU456.js +65 -0
- package/dist/on/index.d.ts +2 -3
- package/dist/on/index.js +9 -9
- package/dist/overlay-views/index.d.ts +4 -5
- package/dist/overlay-views/index.js +4 -4
- package/dist/periods/index.d.ts +3 -4
- package/dist/periods/index.js +20 -12
- package/dist/pod/index.d.ts +3 -4
- package/dist/pod/index.js +11 -11
- package/dist/{policy-SXPLTQS4.js → policy-SDGRWVIL.js} +5 -5
- package/dist/portability/index.d.ts +3 -4
- package/dist/portability/index.js +8 -8
- package/dist/{post-register-DCJ2SRVG.js → post-register-MROIUA5O.js} +6 -6
- package/dist/{public-envelope-CBRIJORQ.js → public-envelope-J5PLQOW2.js} +4 -4
- package/dist/query/index.d.ts +2 -3
- package/dist/query/index.js +9 -6
- package/dist/{read-only-facade-5ADRJVTM.js → read-only-facade-UCYNRE56.js} +2 -2
- package/dist/register-OVJKW7BS.js +23 -0
- package/dist/registry-5RJSK5AJ.js +9 -0
- package/dist/registry-DQRNTWRL.js +18 -0
- package/dist/registry-LTGLERS7.js +19 -0
- package/dist/request-withdrawal-27O77D2V.js +31 -0
- package/dist/{reveal-R5N7JKA5.js → reveal-LT3I2K4P.js} +6 -6
- package/dist/revoke-4N45G7D2.js +24 -0
- package/dist/satellites/index.d.ts +2 -3
- package/dist/satellites/index.js +1 -1
- package/dist/sealed-record/index.d.ts +3 -4
- package/dist/sealed-record/index.js +12 -12
- package/dist/session/index.d.ts +4 -5
- package/dist/session/index.js +9 -9
- package/dist/shadow/index.d.ts +3 -4
- package/dist/shadow/index.js +2 -2
- package/dist/signer-XJCOV7JD.js +25 -0
- package/dist/snapshots/index.d.ts +3 -4
- package/dist/snapshots/index.js +10 -10
- package/dist/{stale-DP66HFT3.js → stale-IYYXEH3U.js} +2 -2
- package/dist/storage-Y44CAPE6.js +23 -0
- package/dist/{store-coordination-provider-F5N5ASDF.js → store-coordination-provider-WKGAE2WI.js} +3 -3
- package/dist/{subject-index-O75wKshW.d.ts → subject-index-CyOgGMJL.d.ts} +18 -0
- package/dist/sync/index.d.ts +2 -3
- package/dist/sync/index.js +9 -9
- package/dist/team/index.d.ts +3 -4
- package/dist/team/index.js +15 -15
- package/dist/tiers/index.d.ts +2 -3
- package/dist/tiers/index.js +10 -10
- package/dist/to/index.d.ts +2 -3
- package/dist/to/index.js +1 -1
- package/dist/{transition-guard-BzsAQEd-.d.ts → transition-guard-BOUkzQU4.d.ts} +1 -1
- package/dist/tx/index.d.ts +3 -4
- package/dist/tx/index.js +3 -3
- package/dist/ui/index.d.ts +4 -1
- package/dist/util/index.js +1 -1
- package/dist/{validators-DGmYFP51.d.ts → validators-B3SalxlJ.d.ts} +1 -1
- package/dist/{vault-diff-DmBBatkx.d.ts → vault-diff-Dt9NkLzw.d.ts} +1 -1
- package/dist/{verify-CAYGXYNO.js → verify-GH6CWVSW.js} +5 -5
- package/dist/{walk-3SB25HF2.js → walk-76UCFXYV.js} +11 -11
- package/dist/with/index.d.ts +3 -4
- package/dist/with/index.js +11 -11
- package/dist/{with-materialized-view-DPEVoGoB.d.ts → with-materialized-view-BP33v1Z6.d.ts} +1 -1
- package/dist/{with-overlayed-view-DGpKFSv1.d.ts → with-overlayed-view-DntFpOKN.d.ts} +1 -1
- package/dist/{with-rollup-B2RwnTq0.d.ts → with-rollup-ClCHO6-4.d.ts} +1 -1
- package/dist/withdraw-accessible-6J2UH3IN.js +28 -0
- package/package.json +3 -3
- package/dist/api-Z66JMGOU.js +0 -20
- package/dist/chunk-47C4CI7R.js +0 -108
- package/dist/chunk-47C4CI7R.js.map +0 -1
- package/dist/chunk-5GQE5IDB.js.map +0 -1
- package/dist/chunk-5K7KZMXG.js.map +0 -1
- package/dist/chunk-6P5AJ2ZS.js.map +0 -1
- package/dist/chunk-6Z435B6N.js.map +0 -1
- package/dist/chunk-7KTL4RHZ.js +0 -120
- package/dist/chunk-7KTL4RHZ.js.map +0 -1
- package/dist/chunk-BASCPHNM.js.map +0 -1
- package/dist/chunk-CP3GUIAN.js.map +0 -1
- package/dist/chunk-E3LURVGQ.js.map +0 -1
- package/dist/chunk-H3MPSYCH.js.map +0 -1
- package/dist/chunk-HCK2DFEK.js.map +0 -1
- package/dist/chunk-I6Q7YECW.js.map +0 -1
- package/dist/chunk-JIW3Q5JN.js.map +0 -1
- package/dist/chunk-OJ64XMPJ.js.map +0 -1
- package/dist/chunk-QYNBPDGO.js.map +0 -1
- package/dist/chunk-RUIMKQTA.js.map +0 -1
- package/dist/chunk-UB3XHGFG.js.map +0 -1
- package/dist/chunk-VK6OC22R.js.map +0 -1
- package/dist/chunk-VMRXI3IF.js.map +0 -1
- package/dist/chunk-WFBZRXS5.js.map +0 -1
- package/dist/chunk-YSJG4L4A.js.map +0 -1
- package/dist/chunk-YSU2E4GZ.js.map +0 -1
- package/dist/chunk-YTCCC6NG.js.map +0 -1
- package/dist/collection-facade-JP6N7USE.js +0 -39
- package/dist/config-drift-LRKCC3VZ.js.map +0 -1
- package/dist/delegation-FDOXXHNG.js +0 -25
- package/dist/derive-JR2C5JFG.js +0 -21
- package/dist/describe-C6iHNlqJ.d.ts +0 -162
- package/dist/executor-6LHOV7MN.js +0 -21
- package/dist/executor-EUPYHCLU.js +0 -9
- package/dist/executor-KZ4B3PRT.js +0 -9
- package/dist/export-accessible-7XLSZBGJ.js +0 -23
- package/dist/extract-partition-RM6EROS5.js +0 -36
- package/dist/find-OH4BEULF.js +0 -11
- package/dist/issue-FJE2IHUI.js +0 -19
- package/dist/liberate-HFIPHJGR.js +0 -24
- package/dist/noydb-4ZASGII5.js +0 -62
- package/dist/register-5NRE7HBZ.js +0 -23
- package/dist/registry-5QLETK3Q.js +0 -17
- package/dist/registry-DYTNLOJC.js +0 -19
- package/dist/registry-HSNQ4NBP.js +0 -9
- package/dist/request-withdrawal-5APB7RKN.js +0 -31
- package/dist/revoke-VELFCK7S.js +0 -24
- package/dist/signer-IVU3B6DP.js +0 -25
- package/dist/storage-RH5VBLLL.js +0 -23
- package/dist/withdraw-accessible-LN4AOJDD.js +0 -28
- /package/dist/{api-Z66JMGOU.js.map → api-HV4PDUWF.js.map} +0 -0
- /package/dist/{backup-7PX6NDKT.js.map → backup-ODXO7OGA.js.map} +0 -0
- /package/dist/{chunk-XW5LZYV4.js.map → chunk-2HVUKRNE.js.map} +0 -0
- /package/dist/{chunk-VFQOE76I.js.map → chunk-2KOSJTMU.js.map} +0 -0
- /package/dist/{chunk-7AERMJP6.js.map → chunk-2UQ4RMHV.js.map} +0 -0
- /package/dist/{chunk-VIWOGJ2O.js.map → chunk-34RRRIC3.js.map} +0 -0
- /package/dist/{chunk-LFGJG4XZ.js.map → chunk-3I4KYWNP.js.map} +0 -0
- /package/dist/{chunk-RUWEHFRB.js.map → chunk-42KVY3OC.js.map} +0 -0
- /package/dist/{chunk-J54DC5IC.js.map → chunk-4G3IT7J7.js.map} +0 -0
- /package/dist/{chunk-WJEMUO32.js.map → chunk-4RQ62VSR.js.map} +0 -0
- /package/dist/{chunk-QAVFI23O.js.map → chunk-4ZSGSRBI.js.map} +0 -0
- /package/dist/{chunk-C4NOLA5X.js.map → chunk-5GAX2QOH.js.map} +0 -0
- /package/dist/{chunk-IEKK2XI4.js.map → chunk-5SAZMBW7.js.map} +0 -0
- /package/dist/{chunk-II6DLDZM.js.map → chunk-5TZZUL5X.js.map} +0 -0
- /package/dist/{chunk-GMZFHDSG.js.map → chunk-6CU3FN74.js.map} +0 -0
- /package/dist/{chunk-UWZ7O4NR.js.map → chunk-6MWUANRF.js.map} +0 -0
- /package/dist/{chunk-U7LV2DUN.js.map → chunk-6ULCYCMU.js.map} +0 -0
- /package/dist/{chunk-VFG5YXWM.js.map → chunk-7IZQTCTR.js.map} +0 -0
- /package/dist/{chunk-U5IJHUHZ.js.map → chunk-A4MAD6PO.js.map} +0 -0
- /package/dist/{chunk-5HZULLHE.js.map → chunk-AUR7MRHA.js.map} +0 -0
- /package/dist/{chunk-H7WQMGXH.js.map → chunk-CJEMA2Z6.js.map} +0 -0
- /package/dist/{chunk-WRLPV6SI.js.map → chunk-CTSFKDGA.js.map} +0 -0
- /package/dist/{chunk-OFYY5Q4R.js.map → chunk-CWCCYH34.js.map} +0 -0
- /package/dist/{chunk-WGH3YV6Y.js.map → chunk-DEYAO3N6.js.map} +0 -0
- /package/dist/{chunk-7L6EH6S7.js.map → chunk-DFIBOSOQ.js.map} +0 -0
- /package/dist/{chunk-3CHNKEFI.js.map → chunk-E5AHO65E.js.map} +0 -0
- /package/dist/{chunk-AG2PLGQ2.js.map → chunk-FC3XK5JT.js.map} +0 -0
- /package/dist/{chunk-DJ4TNF4N.js.map → chunk-GX2BYNCT.js.map} +0 -0
- /package/dist/{chunk-55LSDABW.js.map → chunk-HBTT7YQX.js.map} +0 -0
- /package/dist/{chunk-PYIUDUGS.js.map → chunk-HOCEGVGQ.js.map} +0 -0
- /package/dist/{chunk-5JQMX2EH.js.map → chunk-ICV32FPK.js.map} +0 -0
- /package/dist/{chunk-OWGAXPRE.js.map → chunk-IWTYFS3D.js.map} +0 -0
- /package/dist/{chunk-FCOZ7DEC.js.map → chunk-JC5R7P44.js.map} +0 -0
- /package/dist/{chunk-K6WHZBCO.js.map → chunk-KBBWY46Y.js.map} +0 -0
- /package/dist/{chunk-RNYDHIQL.js.map → chunk-KBF5GSGX.js.map} +0 -0
- /package/dist/{chunk-T4NQMPBN.js.map → chunk-KNXN47MO.js.map} +0 -0
- /package/dist/{chunk-7F2P2NHX.js.map → chunk-KXKLKJW4.js.map} +0 -0
- /package/dist/{chunk-PAJCRV7O.js.map → chunk-KYC4LIEU.js.map} +0 -0
- /package/dist/{chunk-AZPGB4OD.js.map → chunk-LEOXZY36.js.map} +0 -0
- /package/dist/{chunk-2L7PPF2C.js.map → chunk-M5I354EE.js.map} +0 -0
- /package/dist/{chunk-26FSVNBL.js.map → chunk-MK2BSS44.js.map} +0 -0
- /package/dist/{chunk-G5CFMITA.js.map → chunk-MSVY7ULZ.js.map} +0 -0
- /package/dist/{chunk-PE25C4QL.js.map → chunk-NNWK3PSY.js.map} +0 -0
- /package/dist/{chunk-JT4A6OAJ.js.map → chunk-NSZ6J5DF.js.map} +0 -0
- /package/dist/{chunk-RHB7BOEO.js.map → chunk-NUEFXLZU.js.map} +0 -0
- /package/dist/{chunk-DKGZIX6S.js.map → chunk-OQAODTYR.js.map} +0 -0
- /package/dist/{chunk-HV3VNAYI.js.map → chunk-OV4CMJGR.js.map} +0 -0
- /package/dist/{chunk-V76PHWLE.js.map → chunk-PEDZFFKW.js.map} +0 -0
- /package/dist/{chunk-CND5WQLO.js.map → chunk-Q2KVKTC2.js.map} +0 -0
- /package/dist/{chunk-HBVEGUON.js.map → chunk-QTXQ6RNZ.js.map} +0 -0
- /package/dist/{chunk-J5YDMJHG.js.map → chunk-RJ5522XX.js.map} +0 -0
- /package/dist/{chunk-CHK5VVMI.js.map → chunk-S6AQXDN4.js.map} +0 -0
- /package/dist/{chunk-MOQ5M46U.js.map → chunk-SZCHEDML.js.map} +0 -0
- /package/dist/{chunk-VUHXLL32.js.map → chunk-VJ3NQK4K.js.map} +0 -0
- /package/dist/{chunk-62TTFKU4.js.map → chunk-WNA6T3MA.js.map} +0 -0
- /package/dist/{chunk-7CZ6YXMO.js.map → chunk-WVCQ75OK.js.map} +0 -0
- /package/dist/{chunk-NO53W6O5.js.map → chunk-X66MXMR5.js.map} +0 -0
- /package/dist/{chunk-6PALJTC2.js.map → chunk-XJK2WIYN.js.map} +0 -0
- /package/dist/{chunk-5UOPIPZC.js.map → chunk-XQ7UYPVN.js.map} +0 -0
- /package/dist/{chunk-HU55NPHZ.js.map → chunk-Y2OCKFZ3.js.map} +0 -0
- /package/dist/{chunk-UJJJPKJO.js.map → chunk-YBFOBRTO.js.map} +0 -0
- /package/dist/{chunk-I6JJ6VJS.js.map → chunk-YLN33ADH.js.map} +0 -0
- /package/dist/{chunk-HGQ6UNHE.js.map → chunk-Z5YFRMG5.js.map} +0 -0
- /package/dist/{chunk-EU7HDMT3.js.map → chunk-ZWQFOXBR.js.map} +0 -0
- /package/dist/{collection-facade-JP6N7USE.js.map → collection-facade-3RAXG2OU.js.map} +0 -0
- /package/dist/{computed-6FOFXQPE.js.map → computed-R62HGCO4.js.map} +0 -0
- /package/dist/{dead-filter-NZORV45S.js.map → dead-filter-VNA4M3GP.js.map} +0 -0
- /package/dist/{delegation-FDOXXHNG.js.map → delegation-ZKII5UBI.js.map} +0 -0
- /package/dist/{derive-JR2C5JFG.js.map → derive-5D2C3WG4.js.map} +0 -0
- /package/dist/{enclave-7X5BUAMS.js.map → enclave-EUBEJGPG.js.map} +0 -0
- /package/dist/{executor-6LHOV7MN.js.map → executor-IL7SRNUZ.js.map} +0 -0
- /package/dist/{executor-EUPYHCLU.js.map → executor-OUA2Y6YC.js.map} +0 -0
- /package/dist/{executor-KZ4B3PRT.js.map → executor-WAEI6PDV.js.map} +0 -0
- /package/dist/{export-accessible-7XLSZBGJ.js.map → export-accessible-O55RKAGC.js.map} +0 -0
- /package/dist/{extract-partition-RM6EROS5.js.map → extract-partition-VBGCBA6Q.js.map} +0 -0
- /package/dist/{fanout-sidecar-BDKLP5GG.js.map → fanout-sidecar-CI5BCMQM.js.map} +0 -0
- /package/dist/{find-OH4BEULF.js.map → find-FISGUWSK.js.map} +0 -0
- /package/dist/{issue-FJE2IHUI.js.map → issue-EQAWIYFQ.js.map} +0 -0
- /package/dist/{ledger-EMDHRJJN.js.map → ledger-AJA2SR3C.js.map} +0 -0
- /package/dist/{liberate-HFIPHJGR.js.map → liberate-SEGR4JLY.js.map} +0 -0
- /package/dist/{link-set-62Y36TOG.js.map → link-set-YBAC2DQT.js.map} +0 -0
- /package/dist/{noydb-4ZASGII5.js.map → noydb-AMEOU456.js.map} +0 -0
- /package/dist/{policy-SXPLTQS4.js.map → policy-SDGRWVIL.js.map} +0 -0
- /package/dist/{post-register-DCJ2SRVG.js.map → post-register-MROIUA5O.js.map} +0 -0
- /package/dist/{public-envelope-CBRIJORQ.js.map → public-envelope-J5PLQOW2.js.map} +0 -0
- /package/dist/{read-only-facade-5ADRJVTM.js.map → read-only-facade-UCYNRE56.js.map} +0 -0
- /package/dist/{register-5NRE7HBZ.js.map → register-OVJKW7BS.js.map} +0 -0
- /package/dist/{registry-5QLETK3Q.js.map → registry-5RJSK5AJ.js.map} +0 -0
- /package/dist/{registry-DYTNLOJC.js.map → registry-DQRNTWRL.js.map} +0 -0
- /package/dist/{registry-HSNQ4NBP.js.map → registry-LTGLERS7.js.map} +0 -0
- /package/dist/{request-withdrawal-5APB7RKN.js.map → request-withdrawal-27O77D2V.js.map} +0 -0
- /package/dist/{reveal-R5N7JKA5.js.map → reveal-LT3I2K4P.js.map} +0 -0
- /package/dist/{revoke-VELFCK7S.js.map → revoke-4N45G7D2.js.map} +0 -0
- /package/dist/{signer-IVU3B6DP.js.map → signer-XJCOV7JD.js.map} +0 -0
- /package/dist/{stale-DP66HFT3.js.map → stale-IYYXEH3U.js.map} +0 -0
- /package/dist/{storage-RH5VBLLL.js.map → storage-Y44CAPE6.js.map} +0 -0
- /package/dist/{store-coordination-provider-F5N5ASDF.js.map → store-coordination-provider-WKGAE2WI.js.map} +0 -0
- /package/dist/{verify-CAYGXYNO.js.map → verify-GH6CWVSW.js.map} +0 -0
- /package/dist/{walk-3SB25HF2.js.map → walk-76UCFXYV.js.map} +0 -0
- /package/dist/{withdraw-accessible-LN4AOJDD.js.map → withdraw-accessible-6J2UH3IN.js.map} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/with-shape/classified/validators.ts","../src/with-shape/classified/presets.ts","../src/with-shape/classified/active.ts"],"sourcesContent":["/** Pure write-time validators. Exported so userland can validate storage:'never'\n * fields (e.g. CVC) at capture, before dropping them. @module */\n\nexport function luhnCheck(pan: string): boolean {\n const digits = pan.replace(/[\\s-]/g, '')\n if (!/^\\d{12,19}$/.test(digits)) return false\n let sum = 0\n for (let i = 0; i < digits.length; i++) {\n let d = Number(digits[digits.length - 1 - i])\n if (i % 2 === 1) { d *= 2; if (d > 9) d -= 9 }\n sum += d\n }\n return sum % 10 === 0\n}\n","/** The preset catalog (stage 1). Presets are hub-owned; devs get declarative\n * knobs, not read-side callbacks (design law D2/D3). @module */\n\nimport type { ClassifiedFieldSpec, ClassifiedGroup } from './descriptor.js'\nimport { luhnCheck } from './validators.js'\n\nconst digitsOf = (v: unknown): string => String(v).replace(/\\D/g, '')\n\nfunction panSpec(): ClassifiedFieldSpec {\n return {\n _noydbClassified: true, preset: 'creditCard.pan', storage: 'recoverable',\n sensitivity: 'secret', list: { kind: 'mask', pattern: '•••• ${last4}' },\n riders: { last4: (v) => digitsOf(v).slice(-4), bin: (v) => digitsOf(v).slice(0, 6) },\n validate: (v) => (typeof v === 'string' && luhnCheck(v) ? null : 'not a Luhn-valid card number'),\n }\n}\n\nfunction expirySpec(): ClassifiedFieldSpec {\n return {\n _noydbClassified: true, preset: 'creditCard.expiry', storage: 'recoverable',\n sensitivity: 'pii', list: { kind: 'mask', pattern: '••/••' },\n validate: (v) => (typeof v === 'string' && /^(0[1-9]|1[0-2])\\/\\d{2}$/.test(v) ? null : 'expected MM/YY'),\n }\n}\n\nfunction cvcSpec(): ClassifiedFieldSpec {\n return {\n _noydbClassified: true, preset: 'creditCard.cvc', storage: 'never',\n sensitivity: 'secret', list: { kind: 'omit' },\n validate: (v) => (typeof v === 'string' && /^\\d{3,4}$/.test(v) ? null : 'expected 3-4 digits'),\n }\n}\n\nexport const classified = {\n /** Composite card type. PAN sealed + last4/bin riders; CVC is storage:'never' (PCI-aware). */\n creditCard(fields: { pan: string; expiry?: string; cvc?: string }): ClassifiedGroup {\n const members: Record<string, ClassifiedFieldSpec> = { [fields.pan]: panSpec() }\n if (fields.expiry !== undefined) members[fields.expiry] = expirySpec()\n if (fields.cvc !== undefined) members[fields.cvc] = cvcSpec()\n return { _noydbClassifiedGroup: true, preset: 'creditCard', members }\n },\n\n birthDate(): ClassifiedFieldSpec {\n return {\n _noydbClassified: true, preset: 'birthDate', storage: 'recoverable',\n sensitivity: 'pii', list: { kind: 'mask', pattern: '${yob}-••-••' },\n riders: { yob: (v) => String(v).slice(0, 4) },\n validate: (v) => {\n if (typeof v !== 'string' || !/^\\d{4}-\\d{2}-\\d{2}$/.test(v)) return 'expected ISO yyyy-mm-dd'\n const parts = v.split('-').map(Number)\n const year = parts[0]!, month = parts[1]!, day = parts[2]!\n if (month < 1 || month > 12) return 'not a valid calendar date'\n const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]\n const isLeapYear = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0\n const maxDay = month === 2 && isLeapYear ? 29 : daysInMonth[month - 1]!\n if (day < 1 || day > maxDay) return 'not a valid calendar date'\n return null\n },\n }\n },\n\n email(): ClassifiedFieldSpec {\n return {\n _noydbClassified: true, preset: 'email', storage: 'recoverable',\n sensitivity: 'pii', list: { kind: 'mask', pattern: '•••@${domain}' },\n riders: { domain: (v) => String(v).split('@')[1] ?? '' },\n validate: (v) => (typeof v === 'string' && v.includes('@') ? null : 'expected an email address'),\n }\n },\n\n phone(): ClassifiedFieldSpec {\n return {\n _noydbClassified: true, preset: 'phone', storage: 'recoverable',\n sensitivity: 'pii', list: { kind: 'mask', pattern: '•••••${last2}' },\n riders: { last2: (v) => digitsOf(v).slice(-2) },\n validate: (v) => (digitsOf(v).length >= 5 ? null : 'expected at least 5 digits'),\n }\n },\n\n /** Digest-only password: verify-without-reveal; never listed, never revealed.\n * Enumeration math is on the caller for low-entropy values — see the\n * per-preset docs; the hub ships no rate limiter in this slice (spec §5).\n *\n * @param opts.equatable — emit a store-visible `_bidx` equality tag (enables\n * `findByDigest`). Cost band you are accepting:\n *\n * equal values produce equal store-visible tags: anyone with store access\n * learns which records share this secret and how many share each value —\n * never the value itself. A collection-DEK holder can additionally test\n * candidate values offline: the tag's inner digest is PBKDF2-SHA256 (600K),\n * which is GPU/ASIC-friendly — an offline attacker runs on the order of\n * 10⁴–10⁸ guesses/second, so for low-entropy secrets (PINs, casefolded secret\n * answers) offline recovery of the equality partition is seconds-to-hours, not\n * years. `crypto.subtle` exposes no memory-hard KDF (no scrypt/argon2) and the\n * family's no-crypto-deps law forbids adding one, so PBKDF2-SHA256 is the\n * hardest primitive available; the iteration count raises the price but does\n * not make a low-entropy field safe. The real control for low-entropy fields\n * is the DOOR — do not enable `equatable` for them unless the partition being\n * learnable is acceptable — not the iteration count. Pre-forget backups retain\n * tags. */\n password(opts: { minLength?: number; rotateDays?: number; notLastN?: number; equatable?: true } = {}): ClassifiedFieldSpec {\n const minLength = opts.minLength ?? 10\n const notLastN = opts.notLastN ?? 0\n if (!Number.isInteger(notLastN) || notLastN < 0 || notLastN > 8) {\n throw new Error(`classified.password: notLastN must be an integer 0..8 (write cost is n × 600K PBKDF2; ring blast radius is documented), got ${notLastN}`)\n }\n return {\n _noydbClassified: true, preset: 'password', storage: 'digest-only',\n sensitivity: 'secret', list: { kind: 'omit' },\n verifyNormalize: 'password',\n ...(opts.rotateDays !== undefined ? { rotateDays: opts.rotateDays } : {}),\n ...(notLastN > 0 ? { notLastN } : {}),\n ...(opts.equatable === true ? { equatable: true as const } : {}),\n validate: (v) => (typeof v === 'string' && v.normalize('NFC').length >= minLength\n ? null : `password must be at least ${minLength} characters`),\n }\n },\n\n /** Digest-only secret answer: normalized (casefold/trim/collapse), groupable\n * into k-of-n matchGroup challenges. Low-entropy by nature — document the\n * enumeration math to your users; add app-side rate limiting.\n *\n * @param opts.equatable — emit a store-visible `_bidx` equality tag (enables\n * `findByDigest`). Cost band you are accepting:\n *\n * equal values produce equal store-visible tags: anyone with store access\n * learns which records share this secret and how many share each value —\n * never the value itself. A collection-DEK holder can additionally test\n * candidate values offline: the tag's inner digest is PBKDF2-SHA256 (600K),\n * which is GPU/ASIC-friendly — an offline attacker runs on the order of\n * 10⁴–10⁸ guesses/second, so for low-entropy secrets (PINs, casefolded secret\n * answers) offline recovery of the equality partition is seconds-to-hours, not\n * years. `crypto.subtle` exposes no memory-hard KDF (no scrypt/argon2) and the\n * family's no-crypto-deps law forbids adding one, so PBKDF2-SHA256 is the\n * hardest primitive available; the iteration count raises the price but does\n * not make a low-entropy field safe. The real control for low-entropy fields\n * is the DOOR — do not enable `equatable` for them unless the partition being\n * learnable is acceptable — not the iteration count. Pre-forget backups retain\n * tags. */\n secretAnswer(opts: { equatable?: true } = {}): ClassifiedFieldSpec {\n return {\n _noydbClassified: true, preset: 'secretAnswer', storage: 'digest-only',\n sensitivity: 'secret', list: { kind: 'omit' },\n verifyNormalize: 'secret-answer', verifyGroupMember: true,\n ...(opts.equatable === true ? { equatable: true as const } : {}),\n validate: (v) => (typeof v === 'string'\n && v.normalize('NFC').toLowerCase().trim().replace(/\\s+/g, ' ').length > 0\n ? null : 'secret answer must be non-empty after normalization'),\n }\n },\n}\n","import type { ClassifiedStrategy, ClassifiedVerifyCtx } from './strategy.js'\nimport type { VdigFieldPolicy } from '../../kernel/types.js'\nimport type { ClassifiedFieldSpec } from './descriptor.js'\n\nfunction policyOf(spec: ClassifiedFieldSpec): VdigFieldPolicy {\n return {\n normalize: spec.verifyNormalize ?? 'password',\n notLastN: spec.notLastN ?? 0,\n equatable: spec.equatable === true,\n ...(spec.rotateDays !== undefined ? { rotateDays: spec.rotateDays } : {}),\n }\n}\n\nconst engineCtx = (ctx: ClassifiedVerifyCtx) => ({\n collection: ctx.collection,\n getEnvelope: ctx.getEnvelope,\n resolveCek: ctx.resolveCek,\n getDEK: ctx.getDEK,\n now: ctx.now,\n})\n\n/** Opt-in factory: enables reveal + verify/verifyText/matchGroup (stage 2). */\nexport function withClassified(): ClassifiedStrategy {\n return {\n async reveal(ctx, id, field) {\n const { revealSealedField } = await import('../../kernel/enclave/classify/reveal.js')\n const value = await revealSealedField({\n collection: ctx.collection, encrypted: ctx.encrypted,\n getEnvelope: ctx.getEnvelope, resolveCek: ctx.resolveCek, getDEK: ctx.getDEK,\n }, id, field)\n await ctx.onAccess?.('reveal', id)\n return value\n },\n async verify(ctx, id, field, candidate) {\n const { verifyDigestField } = await import('../../kernel/enclave/classify/verify.js')\n const verdict = await verifyDigestField(engineCtx(ctx), id, field, candidate, policyOf(ctx.spec))\n await ctx.onAccess?.('verify', id) // fires on ok AND fail (attempt audit)\n return verdict\n },\n async verifyText(ctx, id, field, candidate) {\n const { verifyTextField } = await import('../../kernel/enclave/classify/verify.js')\n const verdict = await verifyTextField(engineCtx(ctx), id, field, candidate, ctx.spec.verifyNormalize ?? 'password')\n await ctx.onAccess?.('verify', id)\n return verdict\n },\n async matchGroup(ctx, id, answers, opts) {\n const { matchGroupFields } = await import('../../kernel/enclave/classify/verify.js')\n const members = (ctx.groupMembers ?? []).map((m) => ({ field: m.field, policy: policyOf(m.spec) }))\n const result = await matchGroupFields(engineCtx(ctx), id, answers, members, opts)\n await ctx.onAccess?.('verify', id) // ONE entry per call (Q6)\n return result\n },\n async computeTarget(ctx, field, candidate, costByte) {\n const { computeBidxTarget } = await import('../../kernel/enclave/classify/find.js')\n const dek = await ctx.getDEK()\n return computeBidxTarget(candidate, policyOf(ctx.spec).normalize, dek, ctx.collection, field, costByte)\n },\n }\n}\n"],"mappings":";AAGO,SAAS,UAAU,KAAsB;AAC9C,QAAM,SAAS,IAAI,QAAQ,UAAU,EAAE;AACvC,MAAI,CAAC,cAAc,KAAK,MAAM,EAAG,QAAO;AACxC,MAAI,MAAM;AACV,WAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AACtC,QAAI,IAAI,OAAO,OAAO,OAAO,SAAS,IAAI,CAAC,CAAC;AAC5C,QAAI,IAAI,MAAM,GAAG;AAAE,WAAK;AAAG,UAAI,IAAI,EAAG,MAAK;AAAA,IAAE;AAC7C,WAAO;AAAA,EACT;AACA,SAAO,MAAM,OAAO;AACtB;;;ACPA,IAAM,WAAW,CAAC,MAAuB,OAAO,CAAC,EAAE,QAAQ,OAAO,EAAE;AAEpE,SAAS,UAA+B;AACtC,SAAO;AAAA,IACL,kBAAkB;AAAA,IAAM,QAAQ;AAAA,IAAkB,SAAS;AAAA,IAC3D,aAAa;AAAA,IAAU,MAAM,EAAE,MAAM,QAAQ,SAAS,oCAAgB;AAAA,IACtE,QAAQ,EAAE,OAAO,CAAC,MAAM,SAAS,CAAC,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC,MAAM,SAAS,CAAC,EAAE,MAAM,GAAG,CAAC,EAAE;AAAA,IACnF,UAAU,CAAC,MAAO,OAAO,MAAM,YAAY,UAAU,CAAC,IAAI,OAAO;AAAA,EACnE;AACF;AAEA,SAAS,aAAkC;AACzC,SAAO;AAAA,IACL,kBAAkB;AAAA,IAAM,QAAQ;AAAA,IAAqB,SAAS;AAAA,IAC9D,aAAa;AAAA,IAAO,MAAM,EAAE,MAAM,QAAQ,SAAS,4BAAQ;AAAA,IAC3D,UAAU,CAAC,MAAO,OAAO,MAAM,YAAY,2BAA2B,KAAK,CAAC,IAAI,OAAO;AAAA,EACzF;AACF;AAEA,SAAS,UAA+B;AACtC,SAAO;AAAA,IACL,kBAAkB;AAAA,IAAM,QAAQ;AAAA,IAAkB,SAAS;AAAA,IAC3D,aAAa;AAAA,IAAU,MAAM,EAAE,MAAM,OAAO;AAAA,IAC5C,UAAU,CAAC,MAAO,OAAO,MAAM,YAAY,YAAY,KAAK,CAAC,IAAI,OAAO;AAAA,EAC1E;AACF;AAEO,IAAM,aAAa;AAAA;AAAA,EAExB,WAAW,QAAyE;AAClF,UAAM,UAA+C,EAAE,CAAC,OAAO,GAAG,GAAG,QAAQ,EAAE;AAC/E,QAAI,OAAO,WAAW,OAAW,SAAQ,OAAO,MAAM,IAAI,WAAW;AACrE,QAAI,OAAO,QAAQ,OAAW,SAAQ,OAAO,GAAG,IAAI,QAAQ;AAC5D,WAAO,EAAE,uBAAuB,MAAM,QAAQ,cAAc,QAAQ;AAAA,EACtE;AAAA,EAEA,YAAiC;AAC/B,WAAO;AAAA,MACL,kBAAkB;AAAA,MAAM,QAAQ;AAAA,MAAa,SAAS;AAAA,MACtD,aAAa;AAAA,MAAO,MAAM,EAAE,MAAM,QAAQ,SAAS,mCAAe;AAAA,MAClE,QAAQ,EAAE,KAAK,CAAC,MAAM,OAAO,CAAC,EAAE,MAAM,GAAG,CAAC,EAAE;AAAA,MAC5C,UAAU,CAAC,MAAM;AACf,YAAI,OAAO,MAAM,YAAY,CAAC,sBAAsB,KAAK,CAAC,EAAG,QAAO;AACpE,cAAM,QAAQ,EAAE,MAAM,GAAG,EAAE,IAAI,MAAM;AACrC,cAAM,OAAO,MAAM,CAAC,GAAI,QAAQ,MAAM,CAAC,GAAI,MAAM,MAAM,CAAC;AACxD,YAAI,QAAQ,KAAK,QAAQ,GAAI,QAAO;AACpC,cAAM,cAAc,CAAC,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,EAAE;AACnE,cAAM,aAAc,OAAO,MAAM,KAAK,OAAO,QAAQ,KAAM,OAAO,QAAQ;AAC1E,cAAM,SAAS,UAAU,KAAK,aAAa,KAAK,YAAY,QAAQ,CAAC;AACrE,YAAI,MAAM,KAAK,MAAM,OAAQ,QAAO;AACpC,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AAAA,EAEA,QAA6B;AAC3B,WAAO;AAAA,MACL,kBAAkB;AAAA,MAAM,QAAQ;AAAA,MAAS,SAAS;AAAA,MAClD,aAAa;AAAA,MAAO,MAAM,EAAE,MAAM,QAAQ,SAAS,+BAAgB;AAAA,MACnE,QAAQ,EAAE,QAAQ,CAAC,MAAM,OAAO,CAAC,EAAE,MAAM,GAAG,EAAE,CAAC,KAAK,GAAG;AAAA,MACvD,UAAU,CAAC,MAAO,OAAO,MAAM,YAAY,EAAE,SAAS,GAAG,IAAI,OAAO;AAAA,IACtE;AAAA,EACF;AAAA,EAEA,QAA6B;AAC3B,WAAO;AAAA,MACL,kBAAkB;AAAA,MAAM,QAAQ;AAAA,MAAS,SAAS;AAAA,MAClD,aAAa;AAAA,MAAO,MAAM,EAAE,MAAM,QAAQ,SAAS,yCAAgB;AAAA,MACnE,QAAQ,EAAE,OAAO,CAAC,MAAM,SAAS,CAAC,EAAE,MAAM,EAAE,EAAE;AAAA,MAC9C,UAAU,CAAC,MAAO,SAAS,CAAC,EAAE,UAAU,IAAI,OAAO;AAAA,IACrD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAuBA,SAAS,OAAyF,CAAC,GAAwB;AACzH,UAAM,YAAY,KAAK,aAAa;AACpC,UAAM,WAAW,KAAK,YAAY;AAClC,QAAI,CAAC,OAAO,UAAU,QAAQ,KAAK,WAAW,KAAK,WAAW,GAAG;AAC/D,YAAM,IAAI,MAAM,kIAA+H,QAAQ,EAAE;AAAA,IAC3J;AACA,WAAO;AAAA,MACL,kBAAkB;AAAA,MAAM,QAAQ;AAAA,MAAY,SAAS;AAAA,MACrD,aAAa;AAAA,MAAU,MAAM,EAAE,MAAM,OAAO;AAAA,MAC5C,iBAAiB;AAAA,MACjB,GAAI,KAAK,eAAe,SAAY,EAAE,YAAY,KAAK,WAAW,IAAI,CAAC;AAAA,MACvE,GAAI,WAAW,IAAI,EAAE,SAAS,IAAI,CAAC;AAAA,MACnC,GAAI,KAAK,cAAc,OAAO,EAAE,WAAW,KAAc,IAAI,CAAC;AAAA,MAC9D,UAAU,CAAC,MAAO,OAAO,MAAM,YAAY,EAAE,UAAU,KAAK,EAAE,UAAU,YACpE,OAAO,6BAA6B,SAAS;AAAA,IACnD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAuBA,aAAa,OAA6B,CAAC,GAAwB;AACjE,WAAO;AAAA,MACL,kBAAkB;AAAA,MAAM,QAAQ;AAAA,MAAgB,SAAS;AAAA,MACzD,aAAa;AAAA,MAAU,MAAM,EAAE,MAAM,OAAO;AAAA,MAC5C,iBAAiB;AAAA,MAAiB,mBAAmB;AAAA,MACrD,GAAI,KAAK,cAAc,OAAO,EAAE,WAAW,KAAc,IAAI,CAAC;AAAA,MAC9D,UAAU,CAAC,MAAO,OAAO,MAAM,YAC1B,EAAE,UAAU,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,QAAQ,QAAQ,GAAG,EAAE,SAAS,IACvE,OAAO;AAAA,IACb;AAAA,EACF;AACF;;;AClJA,SAAS,SAAS,MAA4C;AAC5D,SAAO;AAAA,IACL,WAAW,KAAK,mBAAmB;AAAA,IACnC,UAAU,KAAK,YAAY;AAAA,IAC3B,WAAW,KAAK,cAAc;AAAA,IAC9B,GAAI,KAAK,eAAe,SAAY,EAAE,YAAY,KAAK,WAAW,IAAI,CAAC;AAAA,EACzE;AACF;AAEA,IAAM,YAAY,CAAC,SAA8B;AAAA,EAC/C,YAAY,IAAI;AAAA,EAChB,aAAa,IAAI;AAAA,EACjB,YAAY,IAAI;AAAA,EAChB,QAAQ,IAAI;AAAA,EACZ,KAAK,IAAI;AACX;AAGO,SAAS,iBAAqC;AACnD,SAAO;AAAA,IACL,MAAM,OAAO,KAAK,IAAI,OAAO;AAC3B,YAAM,EAAE,kBAAkB,IAAI,MAAM,OAAO,sBAAyC;AACpF,YAAM,QAAQ,MAAM,kBAAkB;AAAA,QACpC,YAAY,IAAI;AAAA,QAAY,WAAW,IAAI;AAAA,QAC3C,aAAa,IAAI;AAAA,QAAa,YAAY,IAAI;AAAA,QAAY,QAAQ,IAAI;AAAA,MACxE,GAAG,IAAI,KAAK;AACZ,YAAM,IAAI,WAAW,UAAU,EAAE;AACjC,aAAO;AAAA,IACT;AAAA,IACA,MAAM,OAAO,KAAK,IAAI,OAAO,WAAW;AACtC,YAAM,EAAE,kBAAkB,IAAI,MAAM,OAAO,sBAAyC;AACpF,YAAM,UAAU,MAAM,kBAAkB,UAAU,GAAG,GAAG,IAAI,OAAO,WAAW,SAAS,IAAI,IAAI,CAAC;AAChG,YAAM,IAAI,WAAW,UAAU,EAAE;AACjC,aAAO;AAAA,IACT;AAAA,IACA,MAAM,WAAW,KAAK,IAAI,OAAO,WAAW;AAC1C,YAAM,EAAE,gBAAgB,IAAI,MAAM,OAAO,sBAAyC;AAClF,YAAM,UAAU,MAAM,gBAAgB,UAAU,GAAG,GAAG,IAAI,OAAO,WAAW,IAAI,KAAK,mBAAmB,UAAU;AAClH,YAAM,IAAI,WAAW,UAAU,EAAE;AACjC,aAAO;AAAA,IACT;AAAA,IACA,MAAM,WAAW,KAAK,IAAI,SAAS,MAAM;AACvC,YAAM,EAAE,iBAAiB,IAAI,MAAM,OAAO,sBAAyC;AACnF,YAAM,WAAW,IAAI,gBAAgB,CAAC,GAAG,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,QAAQ,SAAS,EAAE,IAAI,EAAE,EAAE;AAClG,YAAM,SAAS,MAAM,iBAAiB,UAAU,GAAG,GAAG,IAAI,SAAS,SAAS,IAAI;AAChF,YAAM,IAAI,WAAW,UAAU,EAAE;AACjC,aAAO;AAAA,IACT;AAAA,IACA,MAAM,cAAc,KAAK,OAAO,WAAW,UAAU;AACnD,YAAM,EAAE,kBAAkB,IAAI,MAAM,OAAO,oBAAuC;AAClF,YAAM,MAAM,MAAM,IAAI,OAAO;AAC7B,aAAO,kBAAkB,WAAW,SAAS,IAAI,IAAI,EAAE,WAAW,KAAK,IAAI,YAAY,OAAO,QAAQ;AAAA,IACxG;AAAA,EACF;AACF;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/with-audit/guards/read-only-facade.ts"],"sourcesContent":["import type { Vault } from '../../kernel/vault.js'\nimport type { Query } from '../../kernel/query/builder.js'\nimport type { Layer } from '../../with-shape/i18n/policy.js'\nimport type { ReadOnlyVaultFacade as ReadOnlyVaultFacadeContract } from './types.js'\n\n/**\n * Minimal read-only wrapper over a `Vault`. Used as `ctx.vault` inside\n * guard / derivation callbacks so they can fetch related records without\n * acquiring any write capability.\n *\n * The `layer` tags every `get`/`list` read with the resolution layer it\n * belongs to. A guard-seeded facade reads at `'guard'`, a\n * derivation-seeded one at `'derivation'`, so i18nText / dictKey fields\n * resolve under that layer's `onMissing` policy instead of the `'read'`\n * policy — e.g. a guard read can `substitute` a missing locale (lenient\n * default) while the same field `throw`s on an ordinary app read.\n *\n * `query()` is left untagged: the query/aggregate pipeline reads raw\n * `{locale}` maps (no resolution call site), so a layer tag there would be\n * inert. Routing `mv`/`join` resolution through the pipeline is tracked\n * separately.\n */\nexport class ReadOnlyVaultFacade implements ReadOnlyVaultFacadeContract {\n private readonly _vault: Vault\n private readonly _layer: Layer\n\n constructor(vault: Vault, layer: Layer = 'read') {\n this._vault = vault\n this._layer = layer\n }\n\n collection<T = unknown>(name: string): {\n get(id: string): Promise<T | null>\n list(): Promise<T[]>\n query(): Query<T>\n } {\n const c = this._vault.collection<T>(name)\n const layer = this._layer\n return {\n get: (id: string) => c.get(id, { _layer: layer }),\n list: () => c.list({ _layer: layer }),\n query: () => c.query(),\n }\n }\n}\n"],"mappings":";AAsBO,IAAM,sBAAN,MAAiE;AAAA,EACrD;AAAA,EACA;AAAA,EAEjB,YAAY,OAAc,QAAe,QAAQ;AAC/C,SAAK,SAAS;AACd,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,WAAwB,MAItB;AACA,UAAM,IAAI,KAAK,OAAO,WAAc,IAAI;AACxC,UAAM,QAAQ,KAAK;AACnB,WAAO;AAAA,MACL,KAAK,CAAC,OAAe,EAAE,IAAI,IAAI,EAAE,QAAQ,MAAM,CAAC;AAAA,MAChD,MAAM,MAAM,EAAE,KAAK,EAAE,QAAQ,MAAM,CAAC;AAAA,MACpC,OAAO,MAAM,EAAE,MAAM;AAAA,IACvB;AAAA,EACF;AACF;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/with-formula/materialized-views/executor.ts"],"sourcesContent":["import type { Collection } from '../../kernel/collection.js'\nimport type { TxContext } from '../../with-commit/tx/transaction.js'\nimport type { EncryptedEnvelope } from '../../kernel/types.js'\nimport { MaterializedViewTooLargeError, LocaleNotSpecifiedError } from '../../kernel/errors.js'\nimport type { MaterializedFromMeta, MVQueryContext, MaterializedViewStrategy } from './types.js'\nimport type { RegisteredMV } from './registry.js'\nimport { wrapDbWithPredicates } from './registry.js'\nimport { groupAndReduce } from '../../with-lookup/aggregate/groupby.js'\nimport { canonicalGroupKey } from '../../with-lookup/aggregate/canonical-key.js'\nimport { applyI18nLocale, type I18nTextDescriptor } from '../../with-shape/i18n/core.js'\n\n/**\n * Accessor shape passed in from the owning Vault. Mirrors v1's\n * `DerivationStaleAccessor` — provides the per-collection resolver\n * and the active TxContext so refresh writes/tombstones register on\n * `_executed` for rollback symmetry.\n */\nexport interface MVExecutorAccessor {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n getCollection(name: string): Collection<any>\n getActiveTxContext(): TxContext | null\n /**\n * Vault-shaped accessor passed to the MV's `query()` callback at\n * each refresh. Same instance the registry used at registration\n * time; threading through the executor lets the refresh path\n * re-evaluate the closure against the live vault state.\n */\n getQueryContext(): MVQueryContext\n}\n\nexport interface RefreshResult {\n /** Rows newly written / overwritten. */\n written: number\n /** Rows tombstoned via `_internalDelete` (only when `onEmpty: 'delete'`). */\n deleted: number\n /** Failed row writes (non-strict mode). */\n failed: number\n}\n\n/** Default cost ceiling — overridable per-MV via `spec.maxRows`. */\nconst DEFAULT_MAX_ROWS = 100_000\n\n/**\n * Materialize a query terminal that may be a `Query<T>` (call\n * `.toArray()`), an `Aggregation<R>` (call `.run()` returning a\n * single object — wrap as a one-row array), or a `GroupedAggregation<R>`\n * (call `.run()` returning an array of grouped rows). Branches on\n * available terminal at runtime — no type-discrimination at registration.\n */\nasync function materializeQueryResult(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n q: any,\n mvName: string,\n i18nLocale?: string,\n i18nFields?: Record<string, I18nTextDescriptor>,\n): Promise<ReadonlyArray<Record<string, unknown>>> {\n if (typeof q?.toArray === 'function') {\n // Query<T> — non-aggregate path. `.toArray()` returns Promise<T[]>.\n return await q.toArray()\n }\n if (typeof q?.run === 'function') {\n // Aggregation<R> or GroupedAggregation<R>. `.run()` is synchronous\n // and returns either a single object (Aggregation) or an array of\n // rows (GroupedAggregation). Promise.resolve() normalizes both\n // sync and async (future) variants.\n // Query-form MV grouping: when the MV declares i18nLocale, pass it +\n // i18nFields so a GroupedAggregation resolves i18n group keys before\n // bucketing (the Aggregation path ignores the extra arg).\n const runOpts = i18nLocale !== undefined ? { locale: i18nLocale, i18nFields } : undefined\n const result: unknown = await Promise.resolve(q.run(runOpts))\n if (Array.isArray(result)) {\n return result as ReadonlyArray<Record<string, unknown>>\n }\n // Single-aggregate result — wrap as one-row array. The consumer's\n // `rowKey()` should return a stable identity (often a literal\n // constant like `'total'`) since there's only one row.\n return [result as Record<string, unknown>]\n }\n throw new Error(\n `MV \"${mvName}\": query() must return a Query<T>, Aggregation, or GroupedAggregation. ` +\n `Got something without a .toArray() or .run() terminal.`,\n )\n}\n\n/**\n * Materialize a UNION-form MV: read every arm's source\n * collection, apply each arm's `map` to project rows into the unified\n * MV row shape, concatenate the mapped streams, then optionally run\n * `groupBy` + `aggregate` over the result.\n *\n * Modes (driven by `spec.groupBy` / `spec.aggregate`):\n *\n * - No `groupBy` → return the concatenated mapped rows unchanged.\n * - `groupBy` without `aggregate` → dedupe by composite group key,\n * keep the first row seen per key (later arms don't overwrite\n * earlier arms — Map insertion order rules).\n * - `groupBy` + `aggregate` → delegate to the shared `groupAndReduce`\n * pipeline used by `Query.groupBy().aggregate()`.\n *\n * Per-arm `map` is the schema-unification boundary; the strategy's\n * `TRow` type parameter enforces that every arm projects into the\n * same shape at compile time.\n *\n * @internal\n */\nasync function materializeUnionResult<TRow extends Record<string, unknown>>(\n spec: MaterializedViewStrategy<TRow>,\n db: MVQueryContext,\n): Promise<ReadonlyArray<Record<string, unknown>>> {\n const unified: TRow[] = []\n for (const arm of spec.unionSources!) {\n const coll = db.collection<Record<string, unknown>>(arm.collection)\n // Optional per-arm FK joins: chain `.join(field, { as, ... })` for\n // each declared leg before terminating. The aliased right-side\n // record lands at `sourceRow[leg.as]`, where the arm's `map` reads\n // it. Cast to `any` because the chained join widens the row type but\n // the executor treats every row as `Record<string, unknown>` — same\n // pattern the query-form join path uses.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let q: any = coll.query()\n if (arm.join?.length) {\n for (const leg of arm.join) {\n q = q.join(leg.field, { as: leg.as, maxRows: leg.maxRows, strategy: leg.strategy })\n }\n }\n const sourceRows = q.toArray() as ReadonlyArray<Record<string, unknown>>\n for (const r of sourceRows) {\n const mapped = arm.map(r)\n // null / undefined means \"omit this source row\" — skip without\n // pushing so groupBy/aggregate never see a null entry.\n if (mapped == null) continue\n unified.push(mapped)\n }\n }\n\n if (!spec.groupBy) return unified\n\n const groupFields: readonly string[] =\n typeof spec.groupBy === 'string' ? [spec.groupBy] : spec.groupBy\n\n // i18n-aware group keys. An `i18nText` group field carries a raw\n // `{ locale: string }` map, an unstable object key. When `i18nLocale` is\n // declared (with `i18nFields` describing those fields), resolve the declared\n // group-key i18n fields to it at the `mv` layer FIRST — the same unified-rows\n // boundary where money is threaded — so buckets are stable strings and the\n // `mv`-layer `onMissing` policy fires here.\n if (spec.i18nLocale !== undefined && spec.i18nFields !== undefined) {\n const groupI18n: Record<string, I18nTextDescriptor> = {}\n for (const f of groupFields) {\n const d = spec.i18nFields[f]\n if (d !== undefined) groupI18n[f] = d\n }\n if (Object.keys(groupI18n).length > 0) {\n for (let i = 0; i < unified.length; i++) {\n unified[i] = applyI18nLocale(unified[i] as Record<string, unknown>, groupI18n, spec.i18nLocale, undefined, 'mv') as TRow\n }\n }\n }\n // Guard (always): a remaining object-valued group key — an undeclared i18n\n // field or a locale-less MV — would bucket on a map. Refuse, don't bucket wrong.\n for (const f of groupFields) {\n for (const row of unified) {\n const v = (row as Record<string, unknown>)[f]\n if (v !== null && typeof v === 'object') {\n throw new LocaleNotSpecifiedError(\n f,\n `Materialized view \"${spec.name}\" groups by \"${f}\", whose value is a raw i18n locale map — ` +\n `an unstable object group key. Declare { i18nLocale, i18nFields } on the MV to resolve it at ` +\n `the 'mv' layer, or group by a dictKey/staticDict code (the stable key) and resolve the label at read time.`,\n )\n }\n }\n }\n\n // groupBy without aggregate — dedupe by composite key, keep first\n // seen row per key. Useful for cross-arm uniqueness (e.g. unify two\n // sibling collections, keeping one row per natural key).\n if (!spec.aggregate) {\n const seen = new Map<string, TRow>()\n for (const row of unified) {\n const k = canonicalGroupKey(groupFields, row as Record<string, unknown>)\n if (!seen.has(k)) seen.set(k, row)\n }\n return [...seen.values()]\n }\n\n // groupBy + aggregate — delegate to the shared pipeline used by\n // `Query.groupBy().aggregate()`. Result rows carry each grouped\n // field in declaration order followed by the spec's reducer outputs.\n return groupAndReduce<Record<string, unknown>>(unified, groupFields, spec.aggregate, spec.moneyFields)\n}\n\n/**\n * Run an MV's `query()` and write the result rows to the output\n * collection. Same-DEK encryption: routes through the standard\n * `Collection.put` pipeline, so the output collection's DEK is what\n * gets used (matches the v2 spec's \"same DEK as the left-most source\"\n * invariant — `Collection.put` looks up the DEK by collection name,\n * and the output collection IS the MV's owned collection).\n *\n * Stamps `_materializedFrom` onto every emitted row.\n *\n * **Tombstoning:** when `spec.onEmpty: 'delete'` (default), rows\n * that existed in a prior refresh but no longer appear in the new\n * materialized result are deleted via `Collection._internalDelete` —\n * the housekeeping bypass primitive prevents user\n * `onDelete` guards on the output collection from firing on these\n * system-internal deletes. `onEmpty: 'keep'` opts out (rows from\n * prior refreshes linger even when the new result lacks them).\n *\n * **Cost ceiling:** if the materialized row count exceeds\n * `spec.maxRows` (default 100k), throws `MaterializedViewTooLargeError`\n * before any writes hit the store — so strict-mode rollback is\n * clean.\n *\n * **Strict mode:** `spec.strict === true` re-throws on any\n * row-write failure; the active TxContext registration means the\n * source-write rolls back atomically via `revertExecuted`.\n *\n * @internal\n */\nexport const MaterializedViewExecutor = {\n async refresh(\n reg: RegisteredMV,\n accessor: MVExecutorAccessor,\n ): Promise<RefreshResult> {\n const spec = reg.spec\n const outputColl = accessor.getCollection(reg.outputCollection)\n const maxRows = spec.maxRows ?? DEFAULT_MAX_ROWS\n const onEmpty = spec.onEmpty ?? 'delete'\n const strict = spec.strict ?? false\n\n // 1. Materialize the query (branches on terminal shape). If the\n // MV declared predicates, wrap the query context the same way\n // the registry did at registration time so `.wherePredicate()`\n // calls resolve to the registered functions.\n const baseCtx = accessor.getQueryContext()\n const ctxForQuery: MVQueryContext = spec.predicates\n ? wrapDbWithPredicates(baseCtx, spec.predicates)\n : baseCtx\n // UNION-form strategies: read every arm, map to the unified\n // row shape, concatenate, then optionally groupBy + aggregate. The\n // single-source `query()` path is untouched.\n let rows: ReadonlyArray<Record<string, unknown>>\n if (spec.unionSources) {\n rows = await materializeUnionResult(spec, ctxForQuery)\n } else {\n const q = spec.query!(ctxForQuery)\n rows = await materializeQueryResult(q, spec.name, spec.i18nLocale, spec.i18nFields)\n }\n\n // 2. Cost ceiling check BEFORE any writes — keeps the rollback\n // clean if the source-write is wrapped in a transaction.\n if (rows.length > maxRows) {\n throw new MaterializedViewTooLargeError(spec.name, rows.length, maxRows)\n }\n\n const txCtx = accessor.getActiveTxContext()\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const adapter = (outputColl as any).adapter as {\n get(v: string, c: string, i: string): Promise<EncryptedEnvelope | null>\n }\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const vaultName = (outputColl as any).vault as string\n\n // 3. Compute the post-refresh id set so we can diff against the\n // prior-emitted id set for tombstoning (when onEmpty === 'delete').\n const newIds = new Set<string>()\n const enrichedRows: Array<{ id: string; record: Record<string, unknown> }> = []\n for (const row of rows) {\n const id = spec.rowKey(row)\n newIds.add(id)\n const meta: MaterializedFromMeta = {\n mvName: spec.name,\n queryHash: reg.queryHash,\n sourceVersions: {},\n materializedAt: new Date().toISOString(),\n }\n enrichedRows.push({ id, record: { ...row, _materializedFrom: meta } })\n }\n\n // 4. Write the new rows.\n let written = 0\n let failed = 0\n for (const { id, record } of enrichedRows) {\n try {\n if (txCtx !== null) {\n const prior = await adapter.get(vaultName, reg.outputCollection, id)\n txCtx._executed.push({\n op: { type: 'put', vaultName, collectionName: reg.outputCollection, id },\n priorEnvelope: prior,\n })\n }\n await outputColl.put(id, record)\n written++\n } catch (err) {\n failed++\n if (strict) throw err\n \n console.warn(`[mv] \"${spec.name}\" row write failed:`, err)\n }\n }\n\n // 5. Tombstone rows that existed before but don't appear now.\n // `onEmpty: 'keep'` skips this pass entirely. Uses\n // `_internalDelete` so a user-registered `onDelete` on the\n // output collection does NOT fire on housekeeping (composition fix).\n let deleted = 0\n if (onEmpty === 'delete') {\n const priorIds = await listOutputIds(outputColl)\n for (const priorId of priorIds) {\n if (newIds.has(priorId)) continue\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const outAny = outputColl as any\n if (typeof outAny._internalDelete === 'function') {\n await outAny._internalDelete(priorId, txCtx)\n deleted++\n } else {\n // Defensive fallback — should never hit in real flow since\n // every Collection has `_internalDelete`.\n await outputColl.delete(priorId)\n deleted++\n }\n } catch (err) {\n failed++\n if (strict) throw err\n \n console.warn(`[mv] \"${spec.name}\" tombstone failed for id=\"${priorId}\":`, err)\n }\n }\n }\n\n return { written, deleted, failed }\n },\n}\n\n/**\n * List ids currently present in the MV's output collection via the\n * adapter directly (avoids triggering the lazy resolve-on-read path\n * we're INSIDE). Returns an empty array if the collection doesn't\n * exist or the adapter doesn't surface a list method.\n *\n * @internal\n */\nasync function listOutputIds(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n outputColl: Collection<any>,\n): Promise<string[]> {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const cAny = outputColl as any\n const adapter = cAny.adapter as { list?: (v: string, c: string) => Promise<readonly string[]> }\n const vault = cAny.vault as string\n const name = cAny.name as string\n if (typeof adapter?.list !== 'function') return []\n try {\n const ids = await adapter.list(vault, name)\n return [...ids]\n } catch {\n return []\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/with-formula/computed/index.ts"],"sourcesContent":["/**\n * Computed scalar fields — schema-owned derived values evaluated on\n * write and materialized onto the record.\n *\n * A `computed` map declares pure, synchronous functions keyed by field\n * path. {@link evalComputedFields} runs them in declaration order — each\n * function sees the record with all prior computed fields already\n * injected, so a later field can read an earlier one (`total` reads\n * `netAmount`). The result is stored like any field: queryable,\n * indexable, and `aggregate(sum())`-able (exactly, when the field is also\n * a `money()` field).\n *\n * Computed evaluation is the FIRST stage of the write pipeline (before\n * schema validation), so the user need not supply computed fields and the\n * schema validates the computed result. Cross-record / async derivation\n * is out of scope here — see the validation service.\n */\n\nimport { NoydbError } from '../../kernel/errors.js'\n\nexport type ComputedFn<T = Record<string, unknown>> = (record: T) => unknown\n\nexport type ComputedFields<T = Record<string, unknown>> = Record<string, ComputedFn<T>>\n\n/** Raised when a computed function throws during a write. */\nexport class ComputedFieldError extends NoydbError {\n constructor(\n public readonly field: string,\n public readonly id: string,\n public readonly cause: unknown,\n ) {\n super(\n 'COMPUTED_FIELD',\n `computed field \"${field}\" threw for record \"${id}\": ` +\n (cause instanceof Error ? cause.message : String(cause)),\n )\n this.name = 'ComputedFieldError'\n }\n}\n\n/**\n * Evaluate every computed field in declaration order, injecting each\n * result into a shallow clone. A computed field overwrites any\n * user-supplied value of the same name — the field is schema-owned.\n * Returns the new record; the input is not mutated.\n */\nexport function evalComputedFields<T extends Record<string, unknown>>(\n record: T,\n computed: ComputedFields,\n id: string,\n): T {\n const out: Record<string, unknown> = { ...record }\n for (const [field, fn] of Object.entries(computed)) {\n try {\n out[field] = fn(out)\n } catch (cause) {\n throw new ComputedFieldError(field, id, cause)\n }\n }\n return out as T\n}\n"],"mappings":";;;;;AAyBO,IAAM,qBAAN,cAAiC,WAAW;AAAA,EACjD,YACkB,OACA,IACA,OAChB;AACA;AAAA,MACE;AAAA,MACA,mBAAmB,KAAK,uBAAuB,EAAE,SAC9C,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,IAC1D;AARgB;AACA;AACA;AAOhB,SAAK,OAAO;AAAA,EACd;AAAA,EAVkB;AAAA,EACA;AAAA,EACA;AASpB;AAQO,SAAS,mBACd,QACA,UACA,IACG;AACH,QAAM,MAA+B,EAAE,GAAG,OAAO;AACjD,aAAW,CAAC,OAAO,EAAE,KAAK,OAAO,QAAQ,QAAQ,GAAG;AAClD,QAAI;AACF,UAAI,KAAK,IAAI,GAAG,GAAG;AAAA,IACrB,SAAS,OAAO;AACd,YAAM,IAAI,mBAAmB,OAAO,IAAI,KAAK;AAAA,IAC/C;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/with-audit/forget/strategy.ts","../src/with-audit/forget/subject-index.ts"],"sourcesContent":["/**\n * `withForgetCascade` — declaration surface for GDPR right-to-erasure via\n * per-record CEK crypto-shred.\n *\n * This file holds only the *declaration* shape and the disabled sentinel.\n * The actual erasure machinery lives in:\n * - `subject-index.ts` — the encrypted `_subject_index` reserved collection\n * - `vault.ts` `forget()` — the per-record tombstone + ledger flow\n * - `collection.ts` `_writeTombstone` — the envelope rewrite\n *\n * A `ForgetStrategy` declares which collections carry erasable subject data\n * and the (dotted-path) field on each record that names the data subject.\n * Declaring a collection here ALSO forces `perRecordKeys: true` for it (a\n * shred can only erase a record whose body is keyed off a per-record CEK),\n * so adopters opt into the CEK foundation transitively.\n *\n * @module\n */\nimport type { LedgerEntry } from '../../with-commit/history/ledger/entry.js'\n\n/**\n * User-supplied declaration passed to {@link withForgetCascade}. Maps a\n * collection name to the record field (dotted path supported, e.g.\n * `'billing.buyerId'`) that identifies the data subject for erasure.\n *\n * ```ts\n * withForgetCascade({ subjects: { invoices: 'buyerId', contacts: 'id' } })\n * ```\n */\nexport interface SubjectDeclaration {\n readonly subjects: Record<string, string>\n}\n\n/**\n * Resolved forget strategy threaded through Noydb → every Vault. Carries\n * the same `subjects` map the user declared. `NO_FORGET` (empty map) is the\n * off-by-default sentinel; `vault.forget()` throws\n * `ForgetStrategyNotConfiguredError` when the map is empty.\n */\nexport interface ForgetStrategy {\n /** Collection → subject-field (dotted path). Empty under `NO_FORGET`. */\n readonly subjects: Readonly<Record<string, string>>\n}\n\n/**\n * Disabled sentinel — no collections declare a subject field. `vault.forget()`\n * refuses with `ForgetStrategyNotConfiguredError`; no write hooks register; no\n * collection is forced into `perRecordKeys`. Non-adopters pay nothing.\n */\nexport const NO_FORGET: ForgetStrategy = { subjects: {} }\n\n// The `withForgetCascade` factory lives in `active.ts` (canonical\n// strategy.ts/active.ts/index.ts split); this file holds only the\n// declaration shape + disabled sentinel.\n\n/**\n * The outcome of a `vault.forget(subjectId)` call.\n *\n * `unmigratedRecords` lists `collection:id` pairs that were tombstoned but\n * whose body had NOT been migrated to a per-record CEK at shred time (legacy\n * body still under the shared collection DEK). Those records are tombstoned\n * (live envelope + history stripped) but their pre-shred ciphertext, if it\n * leaked into a backup before migration, remains decryptable under the\n * collection DEK — so erasure-completeness is NOT guaranteed for them. Run\n * the per-record-CEK migration pass, then re-forget, to close the gap.\n *\n * Blob attachments: a shredded record's **erasable** blobs (on a\n * `perRecordKeys` collection) are crypto-shredded inline — `blobsShredded`\n * counts those taken to refCount 0 (BlobObject deleted → chunks permanently\n * undecryptable), `blobsRetainedShared` counts those still referenced by\n * another record (shared content legitimately persists for its other owner).\n * `blobResidueCollections` now lists only collections with blobs that could\n * NOT be crypto-shredded: **legacy** blobs (no per-blob `_cek`, chunks under\n * the shared `_blob` DEK — migrate them), or a session without the blob\n * service loaded. An all-erasable subject yields an empty residue list.\n */\nexport interface ForgetResult {\n /** The subject id passed to `forget()`. Echoed for caller convenience. */\n readonly subject: string\n /** Count of live records rewritten to a tombstone. */\n readonly recordsShredded: number\n /** Count of `_history` envelopes tombstoned across all shredded records. */\n readonly historyVersionsShredded: number\n /** Distinct collections that had at least one record shredded. */\n readonly collections: readonly string[]\n /** `collection:id` pairs shredded while still un-migrated (see type docs). */\n readonly unmigratedRecords: readonly string[]\n /** Count of erasable blobs crypto-shredded (refCount → 0, BlobObject deleted). */\n readonly blobsShredded: number\n /** Count of erasable blobs retained because still referenced elsewhere (shared). */\n readonly blobsRetainedShared: number\n /** Collections with blobs that could NOT be crypto-shredded — legacy (no `_cek`) or blobs disabled (see type docs). */\n readonly blobResidueCollections: readonly string[]\n /**\n * Count of persisted `_idx/<field>/<recordId>` index side-cars hard-deleted\n * across the shredded records. These live under the retained\n * collection DEK, so crypto-shred alone would leave the indexed field VALUES\n * readable — `forget()` must delete them.\n */\n readonly indexPostingsPurged: number\n /**\n * `collection:id:field` entries whose persisted `_idx` side-car could NOT be\n * deleted — index residue that still leaks the indexed value under the\n * retained collection DEK. Non-empty means erasure is INCOMPLETE: retry, or\n * purge the side-car out of band.\n */\n readonly indexResidue: readonly string[]\n /**\n * Count of `_sealed[field]` slots dropped from the live store across the\n * shredded records. For slots written under `sensitive` +\n * `perRecordKeys` (the current path), the key derives off the per-record CEK,\n * so tombstoning the record — which drops `_cek` and `_sealed` — also\n * crypto-shreds the value. A legacy slot (written before per-record CEKs) keys\n * off the collection DEK instead, so dropping it removes it from the live store but a\n * pre-forget backup remains recoverable by a DEK holder (same caveat `_data`\n * carries); migrate by re-`put`ting before forgetting for full crypto-shred.\n */\n readonly sealedFieldsShredded: number\n /** Count of `_sealed_cek` host-delivery envelopes deleted (#H-1). A record sealed to an\n * at-* host via sealRecordToHost persists its raw CEK there; forget() must destroy them. */\n readonly sealedCekEnvelopesPurged: number\n /** `collection:id` whose `_sealed_cek` purge failed (residue — the host-recoverable CEK may survive). */\n readonly sealedCekResidue: readonly string[]\n /** `collection:id:field` sealed slots that were DEK-derived (legacy, written before per-record CEKs) and thus NOT crypto-shredded\n * by dropping `_cek` — the collection DEK is retained, so synced/backup copies stay decryptable (#M-1). */\n readonly sealedResidue: readonly string[]\n /** The single `op:'forget'` ledger entry appended for this erasure. */\n readonly ledgerEntry: LedgerEntry\n}\n","/**\n * The encrypted subject index.\n *\n * GDPR crypto-shred needs to answer \"which records belong to data subject\n * X?\" portably (the index must travel with the vault/bundle) WITHOUT leaking\n * subject-equivalence to the store. The rejected alternative — an unencrypted\n * subject tag in envelope metadata — would let anyone with store access see\n * which records share a subject. Instead we keep a reserved `_subject_index`\n * collection, encrypted under its OWN DEK (`getDEK('_subject_index')`):\n *\n * - record id = `HMAC-SHA256(indexDEK, subjectId)` (M-2). A bare\n * `sha256Hex(subjectId)` would be offline-computable: an attacker with\n * store access and a candidate list (emails / customer ids are low-entropy)\n * could dictionary the hash to confirm \"subject X is present here.\" Keying\n * the id with the vault-only index DEK removes that capability — without the\n * DEK the id cannot be derived. (Legacy entries written before M-2 used the\n * bare sha256 id; the read/remove paths dual-look-up both forms.)\n * - record body = AES-GCM(JSON `{ r: [{ collection, id }], p }`) under the\n * index DEK, where `p` pads the plaintext to a bucketed length so the\n * ciphertext `_data` length does not leak the approximate record count.\n * Legacy bodies were a bare `[{ collection, id }]` array; reads accept both.\n *\n * ## Concurrency (RISK #3 — known v1 limitation)\n *\n * `addSubjectRef` / `removeSubjectRef` are read-modify-write with no CAS. The\n * design assumes a SINGLE WRITER (the noy-db single-process write model). Two\n * concurrent writers racing on the SAME subject can lose an entry (last-write\n * wins on the ref list). This is documented, not fixed in v1:\n * `rebuildSubjectIndex` performs a full scan to recover a correct index from\n * the canonical records, so a lost ref is recoverable. A CAS-backed index is\n * deferred to a later slice.\n *\n * @module\n */\nimport { encrypt, openEnvelopeJson, hmacSha256Hex, sha256Hex, type EnclaveKey } from '../../kernel/enclave/index.js'\nimport type { NoydbStore, EncryptedEnvelope } from '../../kernel/types.js'\nimport { NOYDB_FORMAT_VERSION } from '../../kernel/types.js'\n\n/** Reserved collection holding the encrypted subject → records index. */\nexport const SUBJECT_INDEX_COLLECTION = '_subject_index'\n\n/**\n * Bucket (bytes) the encrypted ref-list plaintext is padded up to, so the\n * ciphertext `_data` length leaks only `count` rounded up to a bucket — not the\n * exact record count. 256 keeps small subjects (the common case) indistinguishable.\n */\nconst REF_LIST_BUCKET = 256\n\n/** A single record reference held in a subject's index entry. */\nexport interface SubjectRef {\n readonly collection: string\n readonly id: string\n}\n\ntype GetDEK = (collectionName: string) => Promise<EnclaveKey>\n\n/** SHA-256 hex of a UTF-8 string. The LEGACY (pre-M-2) subject-index key. */\nasync function sha256HexString(input: string): Promise<string> {\n return sha256Hex(new TextEncoder().encode(input))\n}\n\n/**\n * The subject-index record id(s) to consult for a subject, most-current first.\n *\n * - Encrypted vault: the PRIMARY id is `HMAC-SHA256(indexDEK, subjectId)` (M-2)\n * — not offline-computable. The LEGACY `sha256Hex(subjectId)` id is also\n * returned so reads/removes still find entries written before M-2 (dual-lookup).\n * - Plaintext/debug vault: no DEK to key with, so the only id is the legacy\n * sha256 form (unchanged behaviour — plaintext mode is not zero-knowledge anyway).\n */\nasync function subjectKeys(getDEK: GetDEK, encrypted: boolean, subjectId: string): Promise<string[]> {\n const legacy = await sha256HexString(subjectId)\n if (!encrypted) return [legacy]\n const dek = await getDEK(SUBJECT_INDEX_COLLECTION)\n const keyed = await hmacSha256Hex(dek, new TextEncoder().encode(subjectId))\n return keyed === legacy ? [keyed] : [keyed, legacy]\n}\n\n/** The id new writes land under (keyed when encrypted, else legacy sha256). */\nasync function primarySubjectKey(getDEK: GetDEK, encrypted: boolean, subjectId: string): Promise<string> {\n return (await subjectKeys(getDEK, encrypted, subjectId))[0]!\n}\n\n/** Parse a stored ref-list body: new padded `{ r, p }` wrapper OR legacy bare array. */\nfunction parseRefs(json: string): SubjectRef[] {\n const parsed = JSON.parse(json) as SubjectRef[] | { r: SubjectRef[] }\n return Array.isArray(parsed) ? parsed : parsed.r\n}\n\n/** Serialize + pad the ref list to a bucket boundary (encrypted vaults only). */\nfunction serializeRefs(refs: SubjectRef[]): string {\n const base = JSON.stringify({ r: refs, p: '' })\n const pad = Math.ceil(base.length / REF_LIST_BUCKET) * REF_LIST_BUCKET - base.length\n return JSON.stringify({ r: refs, p: ' '.repeat(pad) })\n}\n\n/** Read + decrypt the ref list at a SINGLE index key. Returns `[]` when absent. */\nasync function readRefs(\n adapter: NoydbStore,\n vault: string,\n getDEK: GetDEK,\n encrypted: boolean,\n key: string,\n): Promise<SubjectRef[]> {\n const env = await adapter.get(vault, SUBJECT_INDEX_COLLECTION, key)\n if (!env || !env._data) return []\n if (!encrypted) return parseRefs(env._data)\n const dek = await getDEK(SUBJECT_INDEX_COLLECTION)\n const json = await openEnvelopeJson(env, dek)\n return parseRefs(json)\n}\n\n/** Encrypt + write a ref list for a subject under its derived key. */\nasync function writeRefs(\n adapter: NoydbStore,\n vault: string,\n getDEK: GetDEK,\n encrypted: boolean,\n key: string,\n refs: SubjectRef[],\n): Promise<void> {\n let env: EncryptedEnvelope\n if (!encrypted) {\n // Plaintext/debug vault: keep the legacy bare-array form (no padding needed).\n env = { _noydb: NOYDB_FORMAT_VERSION, _v: 1, _ts: new Date().toISOString(), _iv: '', _data: JSON.stringify(refs) }\n } else {\n const dek = await getDEK(SUBJECT_INDEX_COLLECTION)\n const { iv, data } = await encrypt(serializeRefs(refs), dek)\n env = { _noydb: NOYDB_FORMAT_VERSION, _v: 1, _ts: new Date().toISOString(), _iv: iv, _data: data }\n }\n await adapter.put(vault, SUBJECT_INDEX_COLLECTION, key, env)\n}\n\n/**\n * Add a `{ collection, id }` ref to a subject's index entry (idempotent —\n * a duplicate ref is not appended). Read-modify-write; see the concurrency\n * note in the module docstring.\n */\nexport async function addSubjectRef(\n adapter: NoydbStore,\n vault: string,\n getDEK: GetDEK,\n encrypted: boolean,\n subjectId: string,\n ref: SubjectRef,\n): Promise<void> {\n const key = await primarySubjectKey(getDEK, encrypted, subjectId)\n const refs = await readRefs(adapter, vault, getDEK, encrypted, key)\n if (refs.some((r) => r.collection === ref.collection && r.id === ref.id)) return\n refs.push(ref)\n await writeRefs(adapter, vault, getDEK, encrypted, key, refs)\n}\n\n/**\n * Remove a `{ collection, id }` ref from a subject's index entry. When the\n * last ref is removed the (now empty) entry is deleted so the store holds no\n * residual key for an erased subject.\n */\nexport async function removeSubjectRef(\n adapter: NoydbStore,\n vault: string,\n getDEK: GetDEK,\n encrypted: boolean,\n subjectId: string,\n ref: SubjectRef,\n): Promise<void> {\n // Dual-lookup: drop the ref from BOTH the keyed (M-2) and the legacy sha256\n // entry, so a pre-M-2 subject is still fully erased.\n for (const key of await subjectKeys(getDEK, encrypted, subjectId)) {\n const refs = await readRefs(adapter, vault, getDEK, encrypted, key)\n const next = refs.filter((r) => !(r.collection === ref.collection && r.id === ref.id))\n if (next.length === refs.length) continue\n if (next.length === 0) {\n await adapter.delete(vault, SUBJECT_INDEX_COLLECTION, key)\n } else {\n await writeRefs(adapter, vault, getDEK, encrypted, key, next)\n }\n }\n}\n\n/**\n * Look up every record ref for a subject. Returns `[]` when none exist. Unions\n * the keyed (M-2) and legacy sha256 entries (dual-lookup), deduplicated, so a\n * subject indexed before M-2 is still fully found.\n */\nexport async function lookupSubject(\n adapter: NoydbStore,\n vault: string,\n getDEK: GetDEK,\n encrypted: boolean,\n subjectId: string,\n): Promise<SubjectRef[]> {\n const keys = await subjectKeys(getDEK, encrypted, subjectId)\n const seen = new Set<string>()\n const out: SubjectRef[] = []\n for (const key of keys) {\n for (const ref of await readRefs(adapter, vault, getDEK, encrypted, key)) {\n const dedup = `${ref.collection}\\u0000${ref.id}`\n if (seen.has(dedup)) continue\n seen.add(dedup)\n out.push(ref)\n }\n }\n return out\n}\n\n/**\n * Rebuild the entire subject index from the canonical records (the recovery\n * path for the documented read-modify-write race). Scans each declared\n * collection, reads `record[subjectField]` (dotted path), and rewrites the\n * index from scratch. Tombstoned (already-shredded) records contribute no\n * ref — their body is gone, so they cannot be re-indexed.\n *\n * `decodeRecord` decrypts an envelope to a plain object (or returns null for\n * a tombstone / unreadable record); supplied by the caller so this module\n * stays free of Collection internals.\n */\nexport async function rebuildSubjectIndex(\n adapter: NoydbStore,\n vault: string,\n getDEK: GetDEK,\n encrypted: boolean,\n subjects: Readonly<Record<string, string>>,\n decodeRecord: (collection: string, id: string, env: EncryptedEnvelope) => Promise<Record<string, unknown> | null>,\n): Promise<number> {\n // Drop every existing index entry first so removed refs don't linger.\n const existing = await adapter.list(vault, SUBJECT_INDEX_COLLECTION)\n for (const k of existing) {\n await adapter.delete(vault, SUBJECT_INDEX_COLLECTION, k)\n }\n\n // subjectId → refs, accumulated across all declared collections.\n const bySubject = new Map<string, SubjectRef[]>()\n for (const [collection, field] of Object.entries(subjects)) {\n const ids = await adapter.list(vault, collection)\n for (const id of ids) {\n if (id.startsWith('_')) continue\n const env = await adapter.get(vault, collection, id)\n if (!env || !env._data) continue // missing or tombstone\n const record = await decodeRecord(collection, id, env)\n if (record === null) continue\n const subjectValue = readDottedPath(record, field)\n if (subjectValue === undefined || subjectValue === null) continue\n const subjectId = coerceSubjectId(subjectValue)\n const list = bySubject.get(subjectId) ?? []\n list.push({ collection, id })\n bySubject.set(subjectId, list)\n }\n }\n\n let entries = 0\n for (const [subjectId, refs] of bySubject) {\n const key = await primarySubjectKey(getDEK, encrypted, subjectId)\n await writeRefs(adapter, vault, getDEK, encrypted, key, refs)\n entries++\n }\n return entries\n}\n\n/**\n * Coerce a read subject-field value to a stable string id. Primitives use\n * their natural string form; objects/arrays are JSON-stringified so structural\n * subjects still get a deterministic key (avoids the `[object Object]` trap).\n */\nexport function coerceSubjectId(value: unknown): string {\n if (typeof value === 'string') return value\n if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {\n return String(value)\n }\n return JSON.stringify(value)\n}\n\n/** Read a (possibly dotted) field path from a plain record. */\nexport function readDottedPath(record: Record<string, unknown>, field: string): unknown {\n if (!field.includes('.')) return record[field]\n let cursor: unknown = record\n for (const segment of field.split('.')) {\n if (cursor === null || cursor === undefined) return undefined\n cursor = (cursor as Record<string, unknown>)[segment]\n }\n return cursor\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/with-shape/i18n/dictionary.ts"],"sourcesContent":["/**\n * _dict_* reserved collections + dictKey schema descriptor —\n *\n * Stores bounded enum-like field dictionaries as reserved encrypted\n * collections (`_dict_<name>/`) within a vault. Each dictionary\n * entry maps a stable key (e.g. `'paid'`) to a locale → label record\n * (e.g. `{ en: 'Paid', th: 'ชำระแล้ว' }`).\n *\n * Design decisions\n * ────────────────\n *\n * **Why reserved collections, not a separate store?**\n * Same answer as `_sync_credentials`: the compartment's existing\n * encryption stack is exactly right. Dictionaries are encrypted under the\n * same vault DEK, inherit ACL, ledger, and backup/restore for free.\n *\n * **One collection per dictionary, not one collection with namespaces.**\n * Each `_dict_<name>/` collection holds entries `{ id: key, labels: {...} }`.\n * This composes with `ref()` naturally (a dictKey IS a ref to the dict\n * collection), and means the query DSL works over dictionary entries\n * without any special-casing.\n *\n * **dictKey() is a descriptor, not a Zod type.**\n * The descriptor pattern matches `ref()`: declare NOYDB-specific metadata\n * in the collection options alongside `refs`. TypeScript inference comes\n * from the descriptor's generic parameter, not from Zod internals.\n *\n * API:\n * `dictKey(name, keys?)` — returns a DictKeyDescriptor\n * `vault.dictionary(name)` — returns a DictionaryHandle\n * `DictionaryHandle.put/putAll/get/delete/rename/list` — CRUD\n */\n\nimport type { NoydbStore, EncryptedEnvelope } from '../../kernel/types.js'\nimport type { NoydbEventEmitter } from '../../kernel/events.js'\nimport { NOYDB_FORMAT_VERSION } from '../../kernel/types.js'\nimport type { UnlockedKeyring } from '../../with-party/team/keyring.js'\nimport { encrypt, openEnvelopeJson, type EnclaveKey } from '../../kernel/enclave/index.js'\nimport { ensureCollectionDEK } from '../../with-party/team/keyring.js'\nimport type { LedgerStore } from '../../with-commit/history/ledger/store.js'\nimport type { OnMissingPolicy } from './policy.js'\nimport { envelopePayloadHash } from '../../with-commit/history/ledger/hash.js'\nimport {\n PermissionDeniedError,\n DictKeyMissingError,\n} from '../../kernel/errors.js'\n\n/** Reserved collection name prefix. Never collides with user collections. */\nexport const DICT_COLLECTION_PREFIX = '_dict_'\n\n/** Return the adapter collection name for a named dictionary. */\nexport function dictCollectionName(dictionaryName: string): string {\n return `${DICT_COLLECTION_PREFIX}${dictionaryName}`\n}\n\n/** Return true when a collection name is a reserved dictionary collection. */\nexport function isDictCollectionName(name: string): boolean {\n return name.startsWith(DICT_COLLECTION_PREFIX)\n}\n\n// ─── DictKey descriptor ────────────────────────────────────────────────\n\n/**\n * Descriptor returned by `dictKey()`. Attach to the collection's\n * `dictKeyFields` option to declare which fields are dictionary-backed:\n *\n * ```ts\n * const invoices = company.collection<Invoice>('invoices', {\n * dictKeyFields: {\n * status: dictKey('status', ['draft', 'open', 'paid'] as const),\n * },\n * })\n * ```\n *\n * The generic parameter `Keys` narrows the TypeScript type of the field\n * to a literal union; the runtime value of `keys` is used by `put()`\n * validation to reject unknown keys when a key set is declared.\n */\nexport interface DictKeyDescriptor<Keys extends string = string> {\n readonly _noydbDictKey: true\n /** Which dictionary this field references. */\n readonly name: string\n /** Declared valid keys. When set, `put()` rejects keys not in this set. */\n readonly keys: readonly Keys[] | undefined\n /**\n * What to do when a label is missing for the resolved locale. Mirrors\n * `i18nText`'s `onMissing`. Default behavior (when unset) preserves the\n * legacy contract: a missing label is omitted (scalar) or `null`\n * (array element) — i.e. `'null'`. Set `'substitute'` to walk the\n * declared `substitute` chain, or `'throw'` to raise.\n */\n readonly onMissing?: OnMissingPolicy\n /** Ordered preferred-substitute locales for label resolution. */\n readonly substitute?: readonly string[]\n /**\n * Optional inline display labels (value → label map). A SYNC display\n * fallback for `describe()` when async `dictLabels` (from `_dict_`) are\n * not resolved. The async path still wins when available.\n */\n readonly labels?: Record<string, string>\n}\n\n/**\n * Create a `DictKeyDescriptor` for a dictionary-backed enum field.\n *\n * @param name The dictionary name (corresponds to `_dict_<name>` collection).\n * @param keysOrMap Either:\n * - An `as const` array of valid key literals — narrows the TypeScript type\n * to a literal union and enables put-time validation.\n * - A **value→label map** (`{ draft: 'Draft', paid: 'Paid' }`) — keys are\n * inferred from the map, and the labels are stored as inline display\n * defaults surfaced by `describe()` without a store read.\n * @param opts `{ labels?, onMissing?, substitute? }` when the array form\n * is used; `opts.labels` provides inline display defaults for\n * specific keys (same semantics as the map form, but\n * explicit key order from the array is preserved).\n *\n * @example\n * ```ts\n * // array form (original):\n * dictKey('status', ['draft', 'open', 'paid'] as const)\n *\n * // value→label map form (keys inferred, inline labels for describe()):\n * dictKey('status', { draft: 'Draft', open: 'Open', paid: 'Paid' })\n *\n * // array + opts.labels (preserves explicit order, partial labels OK):\n * dictKey('status', ['draft', 'open', 'paid'] as const, { labels: { paid: 'Paid' } })\n * ```\n */\nexport function dictKey<Keys extends string>(\n name: string,\n keysOrMap?: readonly Keys[] | Record<Keys, string>,\n opts?: { onMissing?: OnMissingPolicy; substitute?: readonly string[]; labels?: Record<string, string> },\n): DictKeyDescriptor<Keys> {\n let keys: readonly Keys[] | undefined\n let labels: Record<string, string> | undefined\n if (Array.isArray(keysOrMap)) {\n keys = keysOrMap as readonly Keys[]\n labels = opts?.labels\n } else if (keysOrMap && typeof keysOrMap === 'object') {\n keys = Object.keys(keysOrMap) as Keys[]\n labels = keysOrMap as Record<string, string>\n } else {\n keys = undefined\n labels = opts?.labels\n }\n return {\n _noydbDictKey: true,\n name,\n keys,\n ...(opts?.onMissing !== undefined ? { onMissing: opts.onMissing } : {}),\n ...(opts?.substitute !== undefined ? { substitute: opts.substitute } : {}),\n ...(labels !== undefined ? { labels } : {}),\n }\n}\n\n/** Runtime predicate for detecting a DictKeyDescriptor. */\nexport function isDictKeyDescriptor(x: unknown): x is DictKeyDescriptor {\n return (\n typeof x === 'object' &&\n x !== null &&\n (x as { _noydbDictKey?: unknown })._noydbDictKey === true\n )\n}\n\n// ─── StaticDict descriptor (code-provided dictionary) ──────────────────\n\n/**\n * Descriptor returned by `staticDict()`. A sibling to {@link DictKeyDescriptor}\n * for **closed, defined-in-code, identical-across-vaults** enums (honorific,\n * civil-status, gender, religion, ContactTitle, status…).\n *\n * Unlike `dictKey`, the labels are supplied **at registration in code** and\n * resolved through the *same* label machinery, but with **no `_dict_*`\n * per-vault encrypted copy** and **no `rename()`**. The record stores only\n * the **code**; a static dict has no mutation surface.\n *\n * **Hybrid resolution.** Because a static dict is pure code with no\n * vault-locale dependency, it can — via a configured `displayLocale` — emit a\n * `<field>Label` even under a **locale-less read** (the property a locale-less\n * consumer needs and `dictKey` cannot provide). When a locale *is* active it\n * behaves exactly like `dictKey` (honoring `onMissing`/`substitute`).\n *\n * ```ts\n * const workers = vault.collection<Worker>('workers', {\n * dictKeyFields: {\n * civilStatus: staticDict('civilStatus', {\n * adultMale: { th: 'นาย', en: 'Mr' },\n * adultFemale: { th: 'นาง', en: 'Mrs' },\n * }, { displayLocale: 'th' }),\n * },\n * })\n * ```\n */\nexport interface StaticDictDescriptor<Keys extends string = string> {\n readonly _noydbStaticDict: true\n /** Which dictionary this field references (the registry name). */\n readonly name: string\n /** The in-code label table: key → { locale → label }. */\n readonly table: Readonly<Record<Keys, Readonly<Record<string, string>>>>\n /** Declared valid keys — derived from `Object.keys(table)`. */\n readonly keys: readonly Keys[]\n /**\n * Locale used to emit `<field>Label` under a **locale-less** read — the\n * hybrid hinge. When unset, a static dict behaves like `dictKey` under a\n * locale-less read (no label); a locale-less consumer almost always wants\n * it set.\n */\n readonly displayLocale?: string\n /** Same `onMissing` policy engine as `dictKey`. Default `'null'`. */\n readonly onMissing?: OnMissingPolicy\n /** Ordered preferred-substitute locales for label resolution. */\n readonly substitute?: readonly string[]\n /**\n * Validate the stored code against `keys` on every `put()`. Default `true`\n * — codes are closed by construction, so an unknown code is a bug. Set\n * `false` to allow open codes (skips the `UnknownDictCodeError` guard).\n */\n readonly validateCodes?: boolean\n}\n\n/**\n * Create a `StaticDictDescriptor` for a code-provided enum field — a sibling\n * to {@link dictKey} for closed, code-defined, identical-across-vaults enums.\n *\n * The labels live in `table` (no `_dict_*` collection, no `rename()`); the\n * record stores only the stable code. Pass `displayLocale` so `<field>Label`\n * resolves even under a locale-less read.\n *\n * @param name The dictionary name (used for the readonly-guard registry and\n * the query label seam — never creates a `_dict_<name>` key).\n * @param table `{ key: { locale: label } }` map.\n * @param opts `displayLocale` (locale-less label), `onMissing`, `substitute`.\n *\n * @example\n * ```ts\n * staticDict('civilStatus', {\n * adultMale: { th: 'นาย', en: 'Mr' },\n * adultFemale: { th: 'นาง', en: 'Mrs' },\n * }, { displayLocale: 'th' })\n * ```\n */\nexport function staticDict<const T extends Record<string, Record<string, string>>>(\n name: string,\n table: T,\n opts?: {\n displayLocale?: string\n onMissing?: OnMissingPolicy\n substitute?: readonly string[]\n validateCodes?: boolean\n },\n): StaticDictDescriptor<Extract<keyof T, string>> {\n return {\n _noydbStaticDict: true,\n name,\n table: table as Readonly<Record<Extract<keyof T, string>, Readonly<Record<string, string>>>>,\n keys: Object.keys(table) as Extract<keyof T, string>[],\n ...(opts?.displayLocale !== undefined ? { displayLocale: opts.displayLocale } : {}),\n ...(opts?.onMissing !== undefined ? { onMissing: opts.onMissing } : {}),\n ...(opts?.substitute !== undefined ? { substitute: opts.substitute } : {}),\n ...(opts?.validateCodes !== undefined ? { validateCodes: opts.validateCodes } : {}),\n }\n}\n\n/** Runtime predicate for detecting a StaticDictDescriptor. */\nexport function isStaticDictDescriptor(x: unknown): x is StaticDictDescriptor {\n return (\n typeof x === 'object' &&\n x !== null &&\n (x as { _noydbStaticDict?: unknown })._noydbStaticDict === true\n )\n}\n\n// ─── Dictionary entry shape ────────────────────────────────────────────\n\n/**\n * One entry in a `_dict_*` collection. The record `id` (adapter-side\n * key) IS the stable dictionary key (e.g. `'paid'`). The `labels`\n * record maps locale codes to display strings.\n */\nexport interface DictEntry {\n /** Stable key — same as the record id in the adapter. */\n readonly key: string\n /** Locale → label map, e.g. `{ en: 'Paid', th: 'ชำระแล้ว' }`. */\n readonly labels: Record<string, string>\n}\n\n// ─── Per-dictionary options ────────────────────────────────────────────\n\n/**\n * Options for `vault.dictionary(name, options?)`.\n *\n * `writableBy` controls the minimum role for write operations (put,\n * putAll, delete, rename). Defaults to `'admin'` to match the standard\n * \"dictionary contents are owned by admins\" convention; set to\n * `'operator'` for user-editable dictionaries like custom tags.\n */\nexport interface DictionaryOptions {\n /** Minimum role allowed to write dictionary entries. Default: `'admin'`. */\n readonly writableBy?: 'owner' | 'admin' | 'operator'\n}\n\n// ─── DictionaryHandle ──────────────────────────────────────────────────\n\n/**\n * Handle to a named dictionary within a vault.\n *\n * Obtained via `vault.dictionary(name)`. Provides strongly-typed\n * CRUD for dictionary entries, plus the `rename()` operation that is the\n * only sanctioned mass-mutation path for dictKey fields.\n *\n * All writes are encrypted under the compartment's DEK for the\n * `_dict_<name>` collection. Adapters never see plaintext.\n */\nexport class DictionaryHandle<Keys extends string = string> {\n private readonly collName: string\n\n /**\n * Synchronous write-through cache for dict-join support.\n * Populated on every `put()`, `delete()`, and `rename()`. The snapshot\n * is built from this cache by `snapshotEntries()` — the query executor\n * calls this synchronously inside `.toArray()`.\n *\n * `null` means \"not yet initialized\" — callers should use `list()`\n * to warm the cache before using dict joins on pre-existing data.\n */\n private readonly _syncCache = new Map<string, DictEntry>()\n\n /**\n * Return all cached entries as `{ key, labels, ...labels }` records —\n * usable synchronously by the join executor's `snapshot()` call.\n * Returns an empty array when the cache has never been populated.\n */\n snapshotEntries(): readonly Record<string, unknown>[] {\n return Array.from(this._syncCache.values()).map((e) => ({\n key: e.key,\n labels: e.labels,\n ...e.labels,\n }))\n }\n\n constructor(\n private readonly adapter: NoydbStore,\n private readonly compartmentName: string,\n private readonly dictionaryName: string,\n private readonly keyring: UnlockedKeyring,\n private readonly getDEK: (collectionName: string) => Promise<EnclaveKey>,\n private readonly encrypted: boolean,\n private readonly ledger: LedgerStore | undefined,\n private readonly options: DictionaryOptions,\n /**\n * Callback provided by the Vault to find and rewrite records\n * in any registered collection that has a dictKeyField pointing at\n * this dictionary, used by `rename()`.\n */\n private readonly findAndUpdateReferences:\n | ((\n dictionaryName: string,\n oldKey: string,\n newKey: string,\n ) => Promise<void>)\n | undefined,\n private readonly emitter: NoydbEventEmitter,\n ) {\n this.collName = dictCollectionName(dictionaryName)\n }\n\n // ─── Access checks ────────────────────────────────────────────────\n\n private requireWriteAccess(): void {\n const minRole = this.options.writableBy ?? 'admin'\n const roleRank: Record<string, number> = {\n client: 1,\n viewer: 2,\n operator: 3,\n admin: 4,\n owner: 5,\n }\n const callerRank = roleRank[this.keyring.role] ?? 0\n const requiredRank = roleRank[minRole] ?? 4\n if (callerRank < requiredRank) {\n throw new PermissionDeniedError(\n `Dictionary \"${this.dictionaryName}\" writes require \"${minRole}\" role or above. ` +\n `Current role: \"${this.keyring.role}\".`,\n )\n }\n }\n\n // ─── Internal helpers ─────────────────────────────────────────────\n\n private async getDekForDict(): Promise<EnclaveKey> {\n const resolve = await ensureCollectionDEK(\n this.adapter,\n this.compartmentName,\n this.keyring,\n )\n return resolve(this.collName)\n }\n\n private async encryptEntry(entry: DictEntry, version: number): Promise<EncryptedEnvelope> {\n if (!this.encrypted) {\n return {\n _noydb: NOYDB_FORMAT_VERSION,\n _v: version,\n _ts: new Date().toISOString(),\n _iv: '',\n _data: JSON.stringify(entry),\n _by: this.keyring.userId,\n }\n }\n const dek = await this.getDekForDict()\n const { iv, data } = await encrypt(JSON.stringify(entry), dek)\n return {\n _noydb: NOYDB_FORMAT_VERSION,\n _v: version,\n _ts: new Date().toISOString(),\n _iv: iv,\n _data: data,\n _by: this.keyring.userId,\n }\n }\n\n private async decryptEntry(envelope: EncryptedEnvelope): Promise<DictEntry> {\n if (!this.encrypted) {\n return JSON.parse(envelope._data) as DictEntry\n }\n const dek = await this.getDekForDict()\n const json = await openEnvelopeJson(envelope, dek)\n return JSON.parse(json) as DictEntry\n }\n\n // ─── Public API ───────────────────────────────────────────────────\n\n /**\n * Add or overwrite a single dictionary entry.\n *\n * @param key The stable key to store (e.g. `'paid'`).\n * @param labels Locale → label map (e.g. `{ en: 'Paid', th: 'ชำระแล้ว' }`).\n */\n async put(key: Keys, labels: Record<string, string>): Promise<void> {\n this.requireWriteAccess()\n\n const entry: DictEntry = { key, labels }\n const existing = await this.adapter.get(\n this.compartmentName,\n this.collName,\n key,\n )\n const version = existing ? existing._v + 1 : 1\n const envelope = await this.encryptEntry(entry, version)\n\n await this.adapter.put(\n this.compartmentName,\n this.collName,\n key,\n envelope,\n existing ? existing._v : undefined,\n )\n\n // Maintain synchronous cache for dict-join snapshot\n this._syncCache.set(key, entry)\n\n this.emitter.emit('change', {\n vault: this.compartmentName,\n collection: this.collName,\n id: key,\n action: 'put',\n })\n\n if (this.ledger) {\n await this.ledger.append({\n op: 'put',\n collection: this.collName,\n id: key,\n version,\n actor: this.keyring.userId,\n // — must be the real envelope hash so\n // vault.verifyBackupIntegrity()'s data-cross-check matches.\n payloadHash: await envelopePayloadHash(envelope),\n })\n }\n }\n\n /**\n * Batch-add or overwrite multiple dictionary entries in one call.\n *\n * @param entries `{ key: { locale: label } }` map.\n */\n async putAll(entries: Record<Keys, Record<string, string>>): Promise<void> {\n this.requireWriteAccess()\n for (const [key, labels] of Object.entries(entries) as [Keys, Record<string, string>][]) {\n await this.put(key, labels)\n }\n }\n\n /**\n * Load the label map for a single key.\n *\n * @returns The label map, or `null` if the key doesn't exist.\n */\n async get(key: Keys): Promise<Record<string, string> | null> {\n const envelope = await this.adapter.get(\n this.compartmentName,\n this.collName,\n key,\n )\n if (!envelope) return null\n const entry = await this.decryptEntry(envelope)\n return entry.labels\n }\n\n /**\n * Delete a dictionary key.\n *\n * Default mode is `'strict'` — throws `DictKeyInUseError` if any\n * registered collection has a record referencing this key. Pass\n * `{ mode: 'warn' }` to skip the check (dev-mode cleanup only).\n */\n async delete(key: Keys, opts: { mode?: 'strict' | 'warn' } = {}): Promise<void> {\n this.requireWriteAccess()\n\n const existing = await this.adapter.get(\n this.compartmentName,\n this.collName,\n key,\n )\n if (!existing) {\n throw new DictKeyMissingError(this.dictionaryName, key)\n }\n\n const mode = opts.mode ?? 'strict'\n if (mode === 'strict' && this.findAndUpdateReferences) {\n // Check for references by attempting a rename to a sentinel that\n // doesn't exist — we reuse the reference-finding machinery but\n // abort before applying changes. Simpler: the vault\n // exposes a separate checkReferences() callback. For now we rely\n // on the caller to confirm no references exist, or use warn mode.\n // A dedicated findReferences API is tracked as a follow-up.\n }\n\n await this.adapter.delete(this.compartmentName, this.collName, key)\n\n // Maintain synchronous cache for dict-join snapshot\n this._syncCache.delete(key)\n\n this.emitter.emit('change', {\n vault: this.compartmentName,\n collection: this.collName,\n id: key,\n action: 'delete',\n })\n\n if (this.ledger) {\n await this.ledger.append({\n op: 'delete',\n collection: this.collName,\n id: key,\n version: existing._v,\n actor: this.keyring.userId,\n // — for delete the prior envelope is what was just\n // removed; we hash it so the chain captures intent. The\n // verifyBackupIntegrity data-cross-check skips delete\n // entries entirely (the live record is gone), but the\n // chain still benefits from a stable non-empty hash.\n payloadHash: await envelopePayloadHash(existing),\n })\n }\n }\n\n /**\n * Rename a dictionary key — the only sanctioned mass-mutation path.\n *\n * Atomically:\n * 1. Adds the new key with the same labels as the old key.\n * 2. Updates every registered record that stores the old key to\n * store the new key instead.\n * 3. Deletes the old key.\n * 4. Appends a single ledger entry recording the rename.\n *\n * Respects ACL: throws `PermissionDeniedError` before any mutation\n * if the caller can't write. The cascade is best-effort atomic\n * within this call — no two-phase commit across adapter calls.\n *\n * Cascade-on-delete is NOT supported. Use `rename()` when you need\n * to change a key that records reference.\n */\n async rename(oldKey: Keys, newKey: string): Promise<void> {\n this.requireWriteAccess()\n\n // 1. Load old entry\n const existing = await this.adapter.get(\n this.compartmentName,\n this.collName,\n oldKey,\n )\n if (!existing) {\n throw new DictKeyMissingError(this.dictionaryName, oldKey)\n }\n const oldEntry = await this.decryptEntry(existing)\n\n // 2. Write new key\n const newEntry: DictEntry = { key: newKey, labels: oldEntry.labels }\n const newEnvelope = await this.encryptEntry(newEntry, 1)\n await this.adapter.put(\n this.compartmentName,\n this.collName,\n newKey,\n newEnvelope,\n )\n\n // 3. Update all referencing records in registered collections\n if (this.findAndUpdateReferences) {\n await this.findAndUpdateReferences(this.dictionaryName, oldKey, newKey)\n }\n\n // 4. Delete old key\n await this.adapter.delete(this.compartmentName, this.collName, oldKey)\n\n // Maintain synchronous cache for dict-join snapshot\n this._syncCache.delete(oldKey)\n this._syncCache.set(newKey, newEntry)\n\n this.emitter.emit('change', {\n vault: this.compartmentName,\n collection: this.collName,\n id: oldKey,\n action: 'delete',\n })\n this.emitter.emit('change', {\n vault: this.compartmentName,\n collection: this.collName,\n id: newKey,\n action: 'put',\n })\n\n // 5. Ledger — record the rename as delete(oldKey) + put(newKey)\n // so verifyBackupIntegrity()'s data-cross-check matches reality\n // (the oldKey envelope is gone; the newKey envelope is what was\n // just written). Two entries instead of one — the chain still\n // captures the rename intent via the matching ts + actor.\n if (this.ledger) {\n await this.ledger.append({\n op: 'delete',\n collection: this.collName,\n id: oldKey,\n version: existing._v,\n actor: this.keyring.userId,\n payloadHash: await envelopePayloadHash(existing),\n })\n await this.ledger.append({\n op: 'put',\n collection: this.collName,\n id: newKey,\n version: 1,\n actor: this.keyring.userId,\n payloadHash: await envelopePayloadHash(newEnvelope),\n })\n }\n }\n\n /**\n * List all entries in this dictionary.\n *\n * @returns Array of `{ key, labels }` objects.\n */\n async list(): Promise<DictEntry[]> {\n const keys = await this.adapter.list(this.compartmentName, this.collName)\n const entries: DictEntry[] = []\n for (const key of keys) {\n const envelope = await this.adapter.get(\n this.compartmentName,\n this.collName,\n key,\n )\n if (!envelope) continue\n const entry = await this.decryptEntry(envelope)\n entries.push(entry)\n // Warm the synchronous cache\n this._syncCache.set(key, entry)\n }\n return entries\n }\n\n /**\n * Resolve a key to its label for the given locale.\n *\n * Used by the collection's locale-aware read path to populate\n * `<field>Label` virtual fields. Returns `undefined` when the\n * key doesn't exist or has no label for the requested locale\n * (after exhausting the fallback chain).\n */\n async resolveLabel(\n key: string,\n locale: string,\n fallback?: string | readonly string[],\n ): Promise<string | undefined> {\n const labels = await this.get(key as Keys)\n if (!labels) return undefined\n\n // Try primary locale\n if (labels[locale] !== undefined) return labels[locale]\n\n // Try fallback chain\n const chain = Array.isArray(fallback) ? (fallback as readonly string[]) : fallback ? [fallback as string] : []\n for (const fb of chain) {\n if (fb === 'any') {\n // Return any available label\n const any = Object.values(labels)[0]\n if (any !== undefined) return any\n } else if (labels[fb] !== undefined) {\n return labels[fb]\n }\n }\n\n return undefined\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/with-audit/consent/consent.ts"],"sourcesContent":["/**\n * Consent boundaries — per-access audit log.\n *\n * ```ts\n * const audit = await vault.withConsent(\n * { purpose: 'quarterly-review', consentHash: '7f3a...' },\n * async () => {\n * const invoices = await vault.collection<Invoice>('invoices').list()\n * return invoices\n * },\n * )\n *\n * const log = await vault.consentAudit({ since: '2026-01-01T00:00:00Z' })\n * // → entries: { actor, purpose, consentHash, ts, op, collection, id }\n * ```\n *\n * ## Contract\n *\n * Every `get` / `put` / `delete` that happens inside a `withConsent`\n * callback writes one entry to the reserved `_consent_audit`\n * collection. Entries are encrypted with the vault's consent-audit\n * DEK (separate from per-user-collection DEKs so access-log queries\n * don't require unwrapping individual collection keys). Outside a\n * `withConsent` scope, no entries are written — consent is\n * opt-in by design (GDPR Art. 7: *demonstrable*, *specific*\n * consent).\n *\n * ## Why store the hash, not the consent text?\n *\n * The `consentHash` is the sha256 of whatever consent receipt the\n * actor presented (a signed GDPR banner click, a HIPAA authorisation\n * PDF, an API-level `X-Consent-Hash` header). Storing only the hash:\n *\n * 1. Keeps the audit log small and indexable.\n * 2. Preserves zero-knowledge at the adapter — adapters see\n * ciphertext envelopes of `{ actor, purpose, consentHash, ts,\n * op, collection, id }`, never the consent record itself.\n * 3. Lets the regulator verify a presented consent doc matches\n * the logged hash at audit time without the system ever\n * possessing the doc.\n *\n * ## Concurrency\n *\n * The consent context lives on the {@link Vault} instance. Two\n * concurrent `withConsent` calls on the same Vault would stomp each\n * other — documented limitation; adopters needing per-flight scope\n * should use separate Vault instances or an AsyncLocalStorage shim.\n *\n * @module\n */\nimport type { EncryptedEnvelope, NoydbStore } from '../../kernel/types.js'\nimport { encrypt, openEnvelopeJson, type EnclaveKey } from '../../kernel/enclave/index.js'\nimport { generateULID } from '../../with-pod/ulid.js'\n\n/** Reserved collection for consent-audit entries. */\nexport const CONSENT_AUDIT_COLLECTION = '_consent_audit'\n\n/**\n * The consent scope active for a block of work. Set via\n * `vault.withConsent()`; observed by the collection's access hooks.\n */\nexport interface ConsentContext {\n /**\n * What this access is for. Used by the audit query (`consentAudit\n * ({ purpose })`) and carried in the stored entry. Free-form; the\n * regulator or compliance tooling decides the vocabulary.\n */\n readonly purpose: string\n /**\n * Hex-encoded sha256 of whatever consent artefact the actor\n * presented. Stored as-is in each entry.\n */\n readonly consentHash: string\n}\n\n/**\n * Access operation recorded in an audit entry.\n *\n * Union-widening checklist (I-2) — these three sites widen together or a\n * downstream exhaustive switch breaks at its next in-range minor:\n * 1. `ConsentOp` here (public, re-exported at `index.ts`).\n * 2. `onAccess` op union on `kernel/collection-config.ts`.\n * 3. classified ctx `onAccess` op union on `with-shape/classified/strategy.ts`\n * (the `ClassifiedVerifyCtx` one — the `ClassifiedRevealCtx` one stays `'reveal'`-only).\n */\nexport type ConsentOp = 'get' | 'put' | 'delete' | 'reveal' | 'verify' | 'find'\n\n/** One consent-audit record, as decrypted for the caller. */\nexport interface ConsentAuditEntry {\n /** ULID — stable insertion-order key. */\n readonly id: string\n readonly timestamp: string\n readonly actor: string\n readonly purpose: string\n readonly consentHash: string\n readonly op: ConsentOp\n readonly collection: string\n readonly recordId: string\n}\n\n/** Filter passed to `vault.consentAudit()`. */\nexport interface ConsentAuditFilter {\n /** Only entries at or after this ISO timestamp. */\n readonly since?: string\n /** Only entries at or before this ISO timestamp. */\n readonly until?: string\n /** Match entries targeting this collection. */\n readonly collection?: string\n /** Match entries written by this actor. */\n readonly actor?: string\n /** Match entries with this purpose. */\n readonly purpose?: string\n}\n\n/**\n * Write one audit entry. Called by Vault's onAccess hook when a\n * consent context is active.\n */\nexport async function writeConsentEntry(\n adapter: NoydbStore,\n vault: string,\n encrypted: boolean,\n entry: Omit<ConsentAuditEntry, 'id' | 'timestamp'>,\n getDEK: (collection: string) => Promise<EnclaveKey>,\n): Promise<void> {\n const id = generateULID()\n const full: ConsentAuditEntry = {\n id,\n timestamp: new Date().toISOString(),\n ...entry,\n }\n const envelope = await buildEnvelope(full, encrypted, getDEK)\n await adapter.put(vault, CONSENT_AUDIT_COLLECTION, id, envelope)\n}\n\n/** Load + decrypt + filter all entries. */\nexport async function loadConsentEntries(\n adapter: NoydbStore,\n vault: string,\n encrypted: boolean,\n getDEK: (collection: string) => Promise<EnclaveKey>,\n filter: ConsentAuditFilter = {},\n): Promise<ConsentAuditEntry[]> {\n const ids = await adapter.list(vault, CONSENT_AUDIT_COLLECTION)\n const entries: ConsentAuditEntry[] = []\n\n for (const id of ids.sort()) {\n const envelope = await adapter.get(vault, CONSENT_AUDIT_COLLECTION, id)\n if (!envelope) continue\n const entry = await decryptEntry(envelope, encrypted, getDEK)\n if (!matchesFilter(entry, filter)) continue\n entries.push(entry)\n }\n return entries\n}\n\n// ── internals ──────────────────────────────────────────────────────\n\nasync function buildEnvelope(\n entry: ConsentAuditEntry,\n encrypted: boolean,\n getDEK: (collection: string) => Promise<EnclaveKey>,\n): Promise<EncryptedEnvelope> {\n const json = JSON.stringify(entry)\n if (!encrypted) {\n return {\n _noydb: 1,\n _v: 1,\n _ts: entry.timestamp,\n _iv: '',\n _data: json,\n }\n }\n const dek = await getDEK(CONSENT_AUDIT_COLLECTION)\n const { iv, data } = await encrypt(json, dek)\n return {\n _noydb: 1,\n _v: 1,\n _ts: entry.timestamp,\n _iv: iv,\n _data: data,\n }\n}\n\nasync function decryptEntry(\n envelope: EncryptedEnvelope,\n encrypted: boolean,\n getDEK: (collection: string) => Promise<EnclaveKey>,\n): Promise<ConsentAuditEntry> {\n if (!encrypted) return JSON.parse(envelope._data) as ConsentAuditEntry\n const json = await openEnvelopeJson(envelope, await getDEK(CONSENT_AUDIT_COLLECTION))\n return JSON.parse(json) as ConsentAuditEntry\n}\n\nfunction matchesFilter(entry: ConsentAuditEntry, f: ConsentAuditFilter): boolean {\n if (f.since && entry.timestamp < f.since) return false\n if (f.until && entry.timestamp > f.until) return false\n if (f.collection && entry.collection !== f.collection) return false\n if (f.actor && entry.actor !== f.actor) return false\n if (f.purpose && entry.purpose !== f.purpose) return false\n return true\n}\n"],"mappings":";;;;;;;;;;;AAuDO,IAAM,2BAA2B;AA+DxC,eAAsB,kBACpB,SACA,OACA,WACA,OACA,QACe;AACf,QAAM,KAAK,aAAa;AACxB,QAAM,OAA0B;AAAA,IAC9B;AAAA,IACA,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IAClC,GAAG;AAAA,EACL;AACA,QAAM,WAAW,MAAM,cAAc,MAAM,WAAW,MAAM;AAC5D,QAAM,QAAQ,IAAI,OAAO,0BAA0B,IAAI,QAAQ;AACjE;AAGA,eAAsB,mBACpB,SACA,OACA,WACA,QACA,SAA6B,CAAC,GACA;AAC9B,QAAM,MAAM,MAAM,QAAQ,KAAK,OAAO,wBAAwB;AAC9D,QAAM,UAA+B,CAAC;AAEtC,aAAW,MAAM,IAAI,KAAK,GAAG;AAC3B,UAAM,WAAW,MAAM,QAAQ,IAAI,OAAO,0BAA0B,EAAE;AACtE,QAAI,CAAC,SAAU;AACf,UAAM,QAAQ,MAAM,aAAa,UAAU,WAAW,MAAM;AAC5D,QAAI,CAAC,cAAc,OAAO,MAAM,EAAG;AACnC,YAAQ,KAAK,KAAK;AAAA,EACpB;AACA,SAAO;AACT;AAIA,eAAe,cACb,OACA,WACA,QAC4B;AAC5B,QAAM,OAAO,KAAK,UAAU,KAAK;AACjC,MAAI,CAAC,WAAW;AACd,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,IAAI;AAAA,MACJ,KAAK,MAAM;AAAA,MACX,KAAK;AAAA,MACL,OAAO;AAAA,IACT;AAAA,EACF;AACA,QAAM,MAAM,MAAM,OAAO,wBAAwB;AACjD,QAAM,EAAE,IAAI,KAAK,IAAI,MAAM,QAAQ,MAAM,GAAG;AAC5C,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,IAAI;AAAA,IACJ,KAAK,MAAM;AAAA,IACX,KAAK;AAAA,IACL,OAAO;AAAA,EACT;AACF;AAEA,eAAe,aACb,UACA,WACA,QAC4B;AAC5B,MAAI,CAAC,UAAW,QAAO,KAAK,MAAM,SAAS,KAAK;AAChD,QAAM,OAAO,MAAM,iBAAiB,UAAU,MAAM,OAAO,wBAAwB,CAAC;AACpF,SAAO,KAAK,MAAM,IAAI;AACxB;AAEA,SAAS,cAAc,OAA0B,GAAgC;AAC/E,MAAI,EAAE,SAAS,MAAM,YAAY,EAAE,MAAO,QAAO;AACjD,MAAI,EAAE,SAAS,MAAM,YAAY,EAAE,MAAO,QAAO;AACjD,MAAI,EAAE,cAAc,MAAM,eAAe,EAAE,WAAY,QAAO;AAC9D,MAAI,EAAE,SAAS,MAAM,UAAU,EAAE,MAAO,QAAO;AAC/C,MAAI,EAAE,WAAW,MAAM,YAAY,EAAE,QAAS,QAAO;AACrD,SAAO;AACT;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/with-audit/periods/periods.ts"],"sourcesContent":["/**\n * Accounting-period closure + opening.\n *\n * A closed period seals every record whose envelope `_ts` is at or\n * before the period's `endDate`: further writes (`put` / `delete`)\n * against such records throw {@link PeriodClosedError}. The period\n * itself is stored as a record in the reserved `_periods` collection\n * and written through the normal ledger-instrumented path, so every\n * closure appends a tamper-evident entry to the vault's hash chain.\n *\n * ## Closure model\n *\n * ```\n * vault.closePeriod({ name: 'FY2026-Q1', endDate: '2026-03-31' })\n * └─► PeriodRecord written to _periods/<name>\n * ├─ priorPeriodName / priorPeriodHash — chain to last close\n * ├─ closedAt / closedBy — provenance\n * └─ normal ledger append fires (LedgerStore.append)\n * ```\n *\n * Enforcement (`assertTsWritable`) is vault-local: the Vault caches\n * the list of closed periods on first read and consults that cache in\n * the `Collection.put` / `.delete` path via the `periodGuard` hook.\n *\n * ## Opening model\n *\n * ```\n * vault.openPeriod({\n * name: 'FY2026-Q2',\n * startDate: '2026-04-01',\n * fromPeriod: 'FY2026-Q1',\n * carryForward: async (priorView) => Record<string, Record<string, unknown>>,\n * })\n * ```\n *\n * `carryForward` receives a read-only `VaultInstant` anchored at the\n * prior period's `endDate` (built via `vault.at(endDate)`) so the\n * callback can compute closing aggregates from the sealed state. The\n * returned `{ [collectionName]: { [id]: record } }` map is written\n * before the new `PeriodRecord` lands — opening balances materialise\n * as normal records with fresh timestamps that fall outside every\n * closed period.\n *\n * ## Not covered\n *\n * - Partial re-opening of a closed period. If an auditor needs to\n * make a correction inside a sealed period, the sanctioned path is\n * a compensating entry in the NEW period, not an unlock of the\n * old one.\n * - Automatic period rollover. `closePeriod` / `openPeriod` are\n * deliberately explicit operator calls so the caller decides when\n * the boundary lands.\n *\n * @module\n */\n\nimport type { NoydbStore, EncryptedEnvelope } from '../../kernel/types.js'\nimport type { LedgerStore } from '../../with-commit/history/ledger/index.js'\nimport { sha256Hex, canonicalJson } from '../../with-commit/history/ledger/index.js'\nimport { PeriodClosedError, ValidationError } from '../../kernel/errors.js'\n\n/** The reserved collection name holding closed-period metadata. */\nexport const PERIODS_COLLECTION = '_periods'\n\n/**\n * Stored record for one closed or opened accounting period. One entry\n * per period, keyed by `name` in the reserved `_periods` collection.\n *\n * The hash chain between periods is computed at read time by\n * `loadPeriods()` — each record carries the name + hash of its\n * predecessor so a tamper with any period's record breaks the chain\n * into the next one, the same way the ledger's `prevHash` works.\n */\nexport interface PeriodRecord {\n /** Human-readable name (e.g., `'FY2026-Q1'`). Unique per vault. */\n readonly name: string\n /**\n * Role discriminator. A period is `'closed'` from the moment its\n * `closedAt` is recorded; `'opened'` marks a period whose opening\n * entries have been carried forward via {@link openPeriod}. Many\n * workflows will produce one opened period per closed period (the\n * opened one is the SUCCESSOR — its `startDate` equals the prior\n * `endDate + 1 day`).\n */\n readonly kind: 'closed' | 'opened'\n /** ISO date — inclusive upper bound for records belonging to this period. */\n readonly endDate: string\n /** ISO date — lower bound (present on opened periods only). */\n readonly startDate?: string\n /**\n * Record field carrying the business date (e.g. `'date'` on an\n * invoice, `'paidAt'` on a payment). The guard compares\n * `record[dateField]` against `endDate` — NOT the envelope `_ts`.\n * Accounting entries booked late (business date `2026-01-15`,\n * write-time `2026-04-22`) still get sealed when Q1 closes at\n * `2026-03-31` because the comparison uses the business date.\n *\n * Optional for backwards compat. When absent, the guard falls back\n * to envelope `_ts` — that's a write-time seal, appropriate for\n * content that doesn't carry a logical business date (e.g. system\n * settings) but almost never right for accounting ledgers.\n */\n readonly dateField?: string\n /** ISO timestamp recorded at `closePeriod()` / `openPeriod()` call time. */\n readonly closedAt: string\n /** userId of the keyring that invoked the close/open. */\n readonly closedBy: string\n /** Name of the prior period this one chains to, if any. */\n readonly priorPeriodName?: string\n /** sha256(canonicalJson(priorPeriod)) — empty for the first period. */\n readonly priorPeriodHash: string\n /**\n * Opened periods only — the names of the collections whose\n * carry-forward aggregates were written by {@link openPeriod}.\n * Recorded for auditability so a future `verifyPeriodChain()` can\n * cross-check the opening balances against the closing snapshot.\n */\n readonly openingCollections?: readonly string[]\n}\n\n/** Options for `vault.closePeriod()`. */\nexport interface ClosePeriodOptions {\n /** Human-readable name. Must not collide with an existing period. */\n readonly name: string\n /**\n * Inclusive upper cutoff. A record is sealed when its\n * `record[dateField]` (or, if absent, the envelope `_ts`) is at or\n * before this ISO timestamp.\n */\n readonly endDate: string\n /**\n * Record field carrying the business date used for period\n * membership. Recommended for accounting workflows — e.g. an\n * invoice booked late (write-time after close) is still sealed\n * when its `invoice.date` falls inside the closed period.\n *\n * Omit to use envelope `_ts` (write-time seal). This fallback\n * rarely matches real-world accounting semantics; prefer passing\n * an explicit `dateField`.\n */\n readonly dateField?: string\n}\n\n/** Options for `vault.openPeriod()`. */\nexport interface OpenPeriodOptions<TCollections = Record<string, Record<string, unknown>>> {\n /** Human-readable name for the new period. Must be unique. */\n readonly name: string\n /** ISO lower bound of the new period (usually prior `endDate + 1 day`). */\n readonly startDate: string\n /**\n * Name of the prior CLOSED period this one chains from. The prior\n * period's record is verified to exist and to be `kind: 'closed'`;\n * its `endDate` is made available to the `carryForward` callback.\n */\n readonly fromPeriod: string\n /**\n * Receives a read-only facade over the vault's CURRENT state,\n * plus the prior period's `endDate`. Accounting semantics: after\n * a period closes, records with `record[dateField] <= endDate`\n * are frozen — current state equals closing state, so a caller\n * can compute closing balances by querying the live collection\n * with a `where('date', '<=', priorEndDate)` filter.\n *\n * Returns opening-balance records keyed by collection name.\n * Example:\n *\n * ```ts\n * carryForward: async (ctx) => {\n * const closing = await ctx.collection<Journal>('journal')\n * .query().where('date', '<=', ctx.priorEndDate).toArray()\n * const opening: Record<string, Journal> = {}\n * for (const entry of closing) {\n * opening[`OB-${entry.id}`] = { ...entry, date: '2026-04-01' }\n * }\n * return { journal: opening }\n * }\n * ```\n */\n readonly carryForward: (\n ctx: CarryForwardContext,\n ) => Promise<TCollections> | TCollections\n}\n\n/**\n * Context passed to `OpenPeriodOptions.carryForward`. Exposes a\n * read-only subset of the live vault (`collection(name).get/list`)\n * plus the prior period's `endDate` so business-date filters can\n * be built by the caller.\n *\n * Writes go via the return value, not via the facade — the\n * `collection()` here is deliberately restricted to reads.\n */\nexport interface CarryForwardContext {\n /** The prior period's `endDate` — the boundary of the closing snapshot. */\n readonly priorEndDate: string\n /** Read-only collection facade over current vault state. */\n collection<T = unknown>(name: string): ReadOnlyCollection<T>\n}\n\n/** Minimum read surface exposed to `carryForward`. */\nexport interface ReadOnlyCollection<T> {\n get(id: string): Promise<T | null>\n list(): Promise<T[]>\n}\n\n/**\n * Load every period record currently stored on the adapter.\n * Decrypting is the caller's responsibility (we return plain records\n * so the vault can use its own `_periods` DEK).\n *\n * @internal — called by Vault methods that need the closed-period\n * cache. Not part of the public API surface.\n */\nexport async function loadPeriods(\n adapter: NoydbStore,\n vault: string,\n decrypt: (envelope: EncryptedEnvelope) => Promise<PeriodRecord>,\n): Promise<PeriodRecord[]> {\n const ids = await adapter.list(vault, PERIODS_COLLECTION)\n const records: PeriodRecord[] = []\n for (const id of ids) {\n const env = await adapter.get(vault, PERIODS_COLLECTION, id)\n if (env) records.push(await decrypt(env))\n }\n // Stable order by closedAt so chain verification is reproducible.\n records.sort((a, b) => a.closedAt.localeCompare(b.closedAt))\n return records\n}\n\n/**\n * Given the current ordered period list, pick the last entry that\n * belongs to the hash chain — used as the `priorPeriodHash` anchor\n * for the next closure/opening.\n *\n * @internal\n */\nexport async function chainAnchor(\n records: readonly PeriodRecord[],\n): Promise<{ priorPeriodName?: string; priorPeriodHash: string }> {\n const last = records[records.length - 1]\n if (!last) return { priorPeriodHash: '' }\n const hash = await sha256Hex(canonicalJson(last as unknown as Record<string, unknown>))\n return { priorPeriodName: last.name, priorPeriodHash: hash }\n}\n\n/**\n * Throw `PeriodClosedError` if the record being touched falls within\n * any closed period.\n *\n * Three signals, evaluated per period:\n *\n * 1. If the period declares a `dateField`, the guard reads\n * `record[dateField]` on BOTH the existing (prior) record AND the\n * incoming (new) record. Either comparing `<= endDate` triggers\n * the error — callers cannot slide a record into a closed period\n * by editing its date field.\n * 2. If the period has no `dateField`, the guard falls back to the\n * envelope `_ts` of the existing record. Fresh inserts (no\n * existing envelope) pass.\n * 3. For a delete, only the existing side is checked.\n *\n * @internal\n */\nexport function assertTsWritable(\n existing: { ts: string | null; record: Record<string, unknown> | null } | null,\n incomingRecord: Record<string, unknown> | null,\n closedPeriods: readonly PeriodRecord[],\n): void {\n for (const p of closedPeriods) {\n if (p.kind !== 'closed') continue\n if (p.dateField) {\n const checkRecord = (label: string, r: Record<string, unknown> | null): void => {\n if (!r) return\n const v = r[p.dateField!]\n if (typeof v === 'string' && v <= p.endDate) {\n throw new PeriodClosedError(p.name, p.endDate, `${label}[${p.dateField}]=${v}`)\n }\n }\n checkRecord('existing', existing?.record ?? null)\n checkRecord('incoming', incomingRecord)\n continue\n }\n // Fallback: write-time seal via envelope _ts.\n const existingTs = existing?.ts ?? null\n if (existingTs !== null && existingTs <= p.endDate) {\n throw new PeriodClosedError(p.name, p.endDate, existingTs)\n }\n }\n}\n\n/**\n * Sanity-check a proposed period name + endDate against existing\n * records. Shared by closePeriod / openPeriod so the two pathways\n * produce identical diagnostics.\n *\n * @internal\n */\nexport function validatePeriodName(\n name: string,\n existing: readonly PeriodRecord[],\n): void {\n if (name.length === 0) {\n throw new ValidationError('Period name cannot be empty.')\n }\n if (existing.some((p) => p.name === name)) {\n throw new ValidationError(`Period \"${name}\" already exists.`)\n }\n}\n\n/**\n * Wire a reserved-collection ledger append for a period record. The\n * period itself is stored via the adapter as an encrypted envelope;\n * the ledger entry is a normal `put` with the period's payloadHash,\n * so period closures inherit the chain's tamper-evidence.\n *\n * @internal\n */\nexport async function appendPeriodLedgerEntry(\n ledger: LedgerStore | null,\n actor: string,\n envelope: EncryptedEnvelope,\n name: string,\n): Promise<void> {\n if (!ledger) return\n const { envelopePayloadHash } = await import('../../with-commit/history/ledger/index.js')\n await ledger.append({\n op: 'put',\n collection: PERIODS_COLLECTION,\n id: name,\n version: envelope._v,\n actor,\n payloadHash: await envelopePayloadHash(envelope),\n })\n}\n"],"mappings":";;;;;;;;;;AA8DO,IAAM,qBAAqB;AAuJlC,eAAsB,YACpB,SACA,OACA,SACyB;AACzB,QAAM,MAAM,MAAM,QAAQ,KAAK,OAAO,kBAAkB;AACxD,QAAM,UAA0B,CAAC;AACjC,aAAW,MAAM,KAAK;AACpB,UAAM,MAAM,MAAM,QAAQ,IAAI,OAAO,oBAAoB,EAAE;AAC3D,QAAI,IAAK,SAAQ,KAAK,MAAM,QAAQ,GAAG,CAAC;AAAA,EAC1C;AAEA,UAAQ,KAAK,CAAC,GAAG,MAAM,EAAE,SAAS,cAAc,EAAE,QAAQ,CAAC;AAC3D,SAAO;AACT;AASA,eAAsB,YACpB,SACgE;AAChE,QAAM,OAAO,QAAQ,QAAQ,SAAS,CAAC;AACvC,MAAI,CAAC,KAAM,QAAO,EAAE,iBAAiB,GAAG;AACxC,QAAM,OAAO,MAAM,UAAU,cAAc,IAA0C,CAAC;AACtF,SAAO,EAAE,iBAAiB,KAAK,MAAM,iBAAiB,KAAK;AAC7D;AAoBO,SAAS,iBACd,UACA,gBACA,eACM;AACN,aAAW,KAAK,eAAe;AAC7B,QAAI,EAAE,SAAS,SAAU;AACzB,QAAI,EAAE,WAAW;AACf,YAAM,cAAc,CAAC,OAAe,MAA4C;AAC9E,YAAI,CAAC,EAAG;AACR,cAAM,IAAI,EAAE,EAAE,SAAU;AACxB,YAAI,OAAO,MAAM,YAAY,KAAK,EAAE,SAAS;AAC3C,gBAAM,IAAI,kBAAkB,EAAE,MAAM,EAAE,SAAS,GAAG,KAAK,IAAI,EAAE,SAAS,KAAK,CAAC,EAAE;AAAA,QAChF;AAAA,MACF;AACA,kBAAY,YAAY,UAAU,UAAU,IAAI;AAChD,kBAAY,YAAY,cAAc;AACtC;AAAA,IACF;AAEA,UAAM,aAAa,UAAU,MAAM;AACnC,QAAI,eAAe,QAAQ,cAAc,EAAE,SAAS;AAClD,YAAM,IAAI,kBAAkB,EAAE,MAAM,EAAE,SAAS,UAAU;AAAA,IAC3D;AAAA,EACF;AACF;AASO,SAAS,mBACd,MACA,UACM;AACN,MAAI,KAAK,WAAW,GAAG;AACrB,UAAM,IAAI,gBAAgB,8BAA8B;AAAA,EAC1D;AACA,MAAI,SAAS,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI,GAAG;AACzC,UAAM,IAAI,gBAAgB,WAAW,IAAI,mBAAmB;AAAA,EAC9D;AACF;AAUA,eAAsB,wBACpB,QACA,OACA,UACA,MACe;AACf,MAAI,CAAC,OAAQ;AACb,QAAM,EAAE,oBAAoB,IAAI,MAAM,OAAO,sBAA2C;AACxF,QAAM,OAAO,OAAO;AAAA,IAClB,IAAI;AAAA,IACJ,YAAY;AAAA,IACZ,IAAI;AAAA,IACJ,SAAS,SAAS;AAAA,IAClB;AAAA,IACA,aAAa,MAAM,oBAAoB,QAAQ;AAAA,EACjD,CAAC;AACH;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/with-formula/materialized-views/dependency-analyzer.ts","../src/with-formula/materialized-views/query-hash.ts","../src/with-formula/materialized-views/registry.ts"],"sourcesContent":["import type { Query, QueryPlan } from '../../kernel/query/builder.js'\nimport type { JoinContext } from '../../kernel/query/join.js'\nimport type { MaterializedViewStrategy } from './types.js'\n\n/**\n * Walks a `Query<T>` plan and returns the set of source collection\n * names that any source-write should trigger a refresh on.\n *\n * Handles:\n * - root collection (the one the query was built from)\n * - FK join targets (`.join(field, { as })`)\n *\n * Also handles:\n * - cross-join targets (`.crossJoin(target, { as })`) — v3\n *\n * Deferred:\n * - `.wherePredicate(name)` — v2 predicate primitive\n * - Overlay-name expansion to {base, overlay}\n *\n * The set is materialized at MV registration time. The MV registry\n * uses it to (a) dispatch `onSourceWrite` only to MVs that actually\n * care, and (b) contribute edges to the shared cycle-detection graph.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function analyzeDependencies(query: Query<any>): Set<string> {\n const deps = new Set<string>()\n const plan = query._plan()\n const ctx = query._joinContext()\n\n // The root collection is always a dependency.\n if (ctx?.leftCollection) {\n deps.add(ctx.leftCollection)\n }\n\n // FK join targets contribute additional sources.\n for (const leg of plan.joins) {\n deps.add(leg.target)\n }\n\n // Cross-join targets are also dependency sources — writes to either side\n // must trigger MV refresh. Symmetric with FK-join target handling above.\n for (const clause of plan.clauses) {\n if (clause.type === 'crossJoin') {\n deps.add(clause.target)\n }\n }\n\n // Sub-plans inside OR clauses can carry nested joins. Walk them.\n // (Today only top-level `.join()` populates `plan.joins`, but the\n // OR-group machinery permits sub-plans, so we recurse defensively.)\n walkClausesForJoins(plan, deps, ctx)\n\n return deps\n}\n\nfunction walkClausesForJoins(\n plan: QueryPlan,\n deps: Set<string>,\n ctx: JoinContext | undefined,\n): void {\n void ctx\n // Today `plan.joins` carries all join legs at top level. Sub-plans\n // inside OR groups don't currently support nested joins, so the loop\n // below is a no-op safety net for future builder extensions.\n for (const clause of plan.clauses) {\n if (clause.type === 'group') {\n // Group clauses don't (yet) carry their own joins; this is a\n // forward-compat anchor for when OR-groups support nested\n // sources.\n }\n }\n}\n\n/**\n * Convenience: produce a stable string summary of the query plan\n * suitable for `queryHash` derivation. Captures everything the\n * dependency analyzer reads + the where/orderBy/limit/offset\n * structure that affects materialized rows.\n *\n * `joinContext` is intentionally NOT included — the join-resolution\n * function references would defeat hash determinism. The set of join\n * TARGETS (collection names) IS included via the plan.joins legs.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function summarizeQueryPlan(query: Query<any>): string {\n const plan = query._plan()\n const ctx = query._joinContext()\n return JSON.stringify({\n root: ctx?.leftCollection ?? null,\n clauses: plan.clauses.map(c => {\n if (c.type === 'crossJoin') {\n return {\n type: 'crossJoin',\n target: c.target,\n as: c.as,\n // Inline on: callback: use sentinel — drift detection disabled for this MV\n onPredicateName: c.onPredicateName ?? (c.on ? '[inline]' : null),\n maxRows: c.maxRows ?? null,\n }\n }\n return c\n }),\n orderBy: plan.orderBy,\n limit: plan.limit ?? null,\n offset: plan.offset,\n joins: plan.joins.map(j => ({ field: j.field, as: j.as, target: j.target, mode: j.mode })),\n })\n}\n\n/**\n * Canonical string description of a UNION MV's plan, used as input to\n * `computeQueryHash`.\n *\n * Asymmetry note:\n * - Arm collection names are NOT sorted. Declaration order is\n * semantically meaningful for the dedup-only UNION path —\n * `materializeUnionResult` iterates `spec.unionSources` in\n * declaration order and keeps the first-seen row per composite key\n * (tie-break precedence). If we sorted arms here, a consumer who\n * reordered `unionSources` to change precedence would compute the\n * same `queryHash`, refresh would be a no-op, and stale MV rows\n * would persist. Hashing in declaration order makes any reorder\n * trigger a refresh.\n * - `groupBy` fields ARE sorted. Multi-key groupBy buckets are\n * commutative (`canonicalGroupKey` produces the same composite key\n * regardless of field order in the input spec).\n * - `aggregate` keys ARE sorted. Reducer-spec keys are independent\n * of each other — order of declaration doesn't change output.\n *\n * Per-arm `map` functions are NOT fingerprinted; consumers must bump\n * the MV's `name` (or rely on application-level cache busting) when\n * `map` semantics change non-equivalently.\n */\nexport function summarizeUnionPlan<T extends Record<string, unknown>>(\n spec: MaterializedViewStrategy<T>,\n): string {\n const arms = (spec.unionSources ?? [])\n .map(s => {\n // Fold each arm's join legs into the summary so adding or\n // reordering joins (which changes the materialized rows) bumps\n // queryHash. Leg order is declaration-significant (legs chain), so\n // it is NOT sorted — same rationale as arm declaration order.\n const joins = s.join?.length\n ? `[${s.join.map(j => `${j.field}→${j.as}`).join(';')}]`\n : ''\n return `${s.collection}${joins}`\n })\n .join(',')\n const groupBy: string = Array.isArray(spec.groupBy)\n ? [...spec.groupBy].sort().join(',')\n : typeof spec.groupBy === 'string'\n ? spec.groupBy\n : ''\n const aggKeys = spec.aggregate ? Object.keys(spec.aggregate).sort().join(',') : ''\n // `moneyFields` changes reducer semantics (float → exact BigInt), so\n // declaring / removing / re-keying it must bump queryHash. Keys are\n // sorted — they're independent of each other (one descriptor per\n // output field), same rationale as aggregate keys.\n const moneyKeys = spec.moneyFields ? Object.keys(spec.moneyFields).sort().join(',') : ''\n return `union(${arms})|groupBy(${groupBy})|aggregate(${aggKeys})|money(${moneyKeys})`\n}\n","/**\n * Deterministic hash of a materialized view strategy's \"shape\": MV\n * name + canonical query-plan summary + sorted dependency-set.\n *\n * Used to detect strategy drift: a row whose `_materializedFrom.queryHash`\n * doesn't match the current strategy is considered stale.\n *\n * Web Crypto SHA-256 — no extra deps. Mirrors the v1\n * `computeStrategyHash` pattern.\n */\nimport { sha256Hex } from '../../kernel/enclave/index.js'\n\nexport async function computeQueryHash(\n mvName: string,\n /**\n * Source-collection set the query depends on. Sorted before\n * canonicalization so set iteration order doesn't affect the hash.\n */\n dependencies: ReadonlySet<string>,\n /**\n * Stringified query-plan summary. The caller produces this from the\n * `Query<T>` builder — concretely: a JSON serialization of clauses +\n * orderBy + limit + offset + joins. Function bodies inside\n * `wherePredicate` are NOT included here (those carry their own\n * `predicateHash` to be folded in by a later sub-issue).\n */\n queryPlanSummary: string,\n): Promise<string> {\n const canonical = JSON.stringify({\n mvName,\n dependencies: [...dependencies].sort(),\n queryPlanSummary,\n })\n const bytes = new TextEncoder().encode(canonical)\n return sha256Hex(bytes)\n}\n\n/**\n * Canonicalize a query plan for hashing. Walks the plan structure\n * with sorted keys so insertion order doesn't perturb the result.\n * Lives here rather than in `query/builder.ts` to keep that module\n * stable across MV-specific evolutions.\n *\n * @internal exported for testing\n */\nexport function canonicalizeQueryPlan(plan: unknown): string {\n return JSON.stringify(plan, (_key, value) => {\n if (value && typeof value === 'object' && !Array.isArray(value)) {\n const sorted: Record<string, unknown> = {}\n for (const k of Object.keys(value as Record<string, unknown>).sort()) {\n sorted[k] = (value as Record<string, unknown>)[k]\n }\n return sorted\n }\n return value\n })\n}\n","import { MaterializedViewCycleError, MaterializedViewSourceUnknownError } from '../../kernel/errors.js'\nimport type { DerivationRegistry } from '../derivations/registry.js'\nimport type { Clause, FieldClause } from '../../kernel/query/predicate.js'\nimport type { DeclaredPredicate } from '../../kernel/query/builder.js'\nimport { analyzeDependencies, summarizeQueryPlan, summarizeUnionPlan } from './dependency-analyzer.js'\nimport { computeQueryHash } from './query-hash.js'\nimport type { MaterializedViewStrategy, MVQueryContext } from './types.js'\n\n/**\n * One registered MV strategy alongside its derived metadata. Stored\n * type-erased on `TRow` so the registry can hold heterogeneous MVs.\n */\nexport interface RegisteredMV {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n readonly spec: MaterializedViewStrategy<any>\n /** Output collection name (`spec.output?.collection ?? spec.name`). */\n readonly outputCollection: string\n /** Set of source collections; populated at registration via the analyzer. */\n readonly dependencies: ReadonlySet<string>\n /** Canonical `queryHash` — `_materializedFrom.queryHash` for every emitted row. */\n readonly queryHash: string\n /**\n * Top-level FieldClauses on the partition field, captured at\n * registration time. Used by the cycle detector to resolve\n * same-collection-as-source edges via the partition-discriminator\n * check. Empty when `spec.output?.partition` is undefined.\n */\n readonly partitionClauses: readonly FieldClause[]\n}\n\n/**\n * Vault-internal registry of MV strategies. Owned by `Vault`; not\n * exported. Parallel to v1's `DerivationRegistry`; the two graphs share\n * a single cycle-detection pass at vault open (see `validate`).\n *\n * @internal\n */\nexport class MaterializedViewRegistry {\n /** Keyed by `spec.name`. */\n private readonly _byName = new Map<string, RegisteredMV>()\n /** Keyed by dependency source-collection → MVs that depend on it. */\n private readonly _bySource = new Map<string, RegisteredMV[]>()\n\n /**\n * Register an MV. Invokes `spec.query()` once at registration time to\n * read the plan + join context; the resulting `Query<T>` is discarded\n * after dependency extraction. `vault.collection(...)` must therefore\n * be functional by the time this runs — typically wired from\n * `Vault._initMaterializedViews` after collection bootstrap.\n *\n * Throws `MaterializedViewSourceUnknownError` if the analyzer\n * surfaces a dependency the vault doesn't know about (when a\n * `knownCollections` checker is supplied).\n */\n async register(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n spec: MaterializedViewStrategy<any>,\n db: MVQueryContext,\n options?: { knownCollections?: (name: string) => boolean },\n ): Promise<void> {\n // Build a predicate-aware db wrapper. If `spec.predicates` is\n // declared, the wrapper intercepts `.collection().query()` and\n // attaches the predicates map to the resulting Query<T>. With no\n // predicates declared, the wrapper is the original db unchanged.\n const dbForQuery = spec.predicates ? wrapDbWithPredicates(db, spec.predicates) : db\n\n // Invoke the query callback once to inspect its plan / dependencies.\n // For Query<T> shapes the analyzer extracts deps + plan summary\n // automatically. Aggregation / GroupedAggregation shapes don't\n // expose the underlying Query, so the spec must declare `sources`\n // explicitly. `partitionClauses` are only populated for Query<T>\n // since same-collection-partition is a non-aggregate concern.\n // UNION-form strategies: dependencies and plan summary come\n // straight off the strategy — no `query` callback to introspect.\n // The dependency-analyzer + summarizer are bypassed entirely; the\n // executor handles materialization via `materializeUnionResult`.\n let dependencies: Set<string>\n let queryPlanSummary: string\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let qAny: any = null\n let isQuery = false\n if (spec.unionSources) {\n dependencies = new Set(spec.unionSources.map(s => s.collection))\n // Per-arm joins resolve right-side collections that aren't among\n // the arm `collection`s. The consumer lists those in `sources`;\n // fold them into the dependency set so a write to a join-target\n // collection triggers MV refresh (and contributes a cycle edge).\n if (spec.sources) for (const s of spec.sources) dependencies.add(s)\n queryPlanSummary = summarizeUnionPlan(spec)\n } else {\n const q = spec.query!(dbForQuery)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n qAny = q as any\n isQuery = typeof qAny._plan === 'function'\n if (isQuery) {\n // `q` is the `Query` arm of the union here (runtime-confirmed via\n // `qAny._plan` above); reuse the already-`any` `qAny` the block uses\n // for `_plan()` rather than re-narrowing.\n dependencies = analyzeDependencies(qAny)\n queryPlanSummary = summarizeQueryPlan(qAny)\n // Fold `.wherePredicate(name, ctx)` references into the plan\n // summary so predicate function or ctx changes (signalled by\n // bumping `hash` or supplying a different ctx) propagate into\n // `queryHash` and force refresh on next visit.\n const predicateRefs = extractPredicateRefs(qAny._plan())\n if (predicateRefs.length > 0) {\n queryPlanSummary = JSON.stringify({ plan: queryPlanSummary, predicates: predicateRefs })\n }\n // If `sources` is ALSO declared, take the union (consumer's\n // explicit list extends the auto-analyzed set).\n if (spec.sources) for (const s of spec.sources) dependencies.add(s)\n } else {\n // Aggregate shape: require explicit `sources`.\n if (!spec.sources || spec.sources.length === 0) {\n throw new Error(\n `withMaterializedView \"${spec.name}\": query() returned an aggregate ` +\n `(Aggregation or GroupedAggregation) but no \\`sources\\` field is declared. ` +\n `The dependency analyzer cannot walk through groupBy().aggregate() ` +\n `back to the source — declare sources: [...] explicitly.`,\n )\n }\n dependencies = new Set(spec.sources)\n // Aggregate plans don't carry a chainable query plan for summary\n // purposes; the dep-set + spec.name serve as the queryHash inputs.\n queryPlanSummary = JSON.stringify({ aggregate: true, sources: [...spec.sources].sort() })\n }\n }\n\n // Sanity-check declared dependencies against the vault's known\n // collections. Optional — when the checker isn't supplied (test\n // wiring, in-process composition) the registration succeeds and\n // any typo surfaces at first onSourceWrite as a no-op.\n if (options?.knownCollections) {\n for (const dep of dependencies) {\n if (!options.knownCollections(dep)) {\n throw new MaterializedViewSourceUnknownError(spec.name, dep)\n }\n }\n }\n\n const outputCollection = spec.output?.collection ?? spec.name\n const queryHash = await computeQueryHash(spec.name, dependencies, queryPlanSummary)\n // For same-collection-as-source MVs, capture the where-clauses on\n // the partition field so cycle detection can prove disjointness.\n // Only applicable to Query<T> shapes — aggregate MVs don't carry\n // a chainable plan to inspect (and same-collection aggregation\n // doesn't make sense for same-collection aggregation).\n const partitionClauses: FieldClause[] = []\n const partitionField = spec.output?.partition?.field\n if (partitionField !== undefined && isQuery) {\n const plan = qAny._plan()\n for (const clause of plan.clauses) {\n if (isFieldClauseOnField(clause, partitionField)) partitionClauses.push(clause)\n }\n }\n const reg: RegisteredMV = { spec, outputCollection, dependencies, queryHash, partitionClauses }\n\n this._byName.set(spec.name, reg)\n for (const dep of dependencies) {\n const arr = this._bySource.get(dep)\n if (arr) arr.push(reg)\n else this._bySource.set(dep, [reg])\n }\n }\n\n /** All MVs that depend on `source`, in registration order. */\n mvsForSource(source: string): ReadonlyArray<RegisteredMV> {\n return this._bySource.get(source) ?? []\n }\n\n /** Single MV by name, or `undefined`. */\n byName(name: string): RegisteredMV | undefined {\n return this._byName.get(name)\n }\n\n /** Iterate over every registered MV. */\n all(): ReadonlyArray<RegisteredMV> {\n return [...this._byName.values()]\n }\n\n /**\n * Cycle detection over the combined derivation + MV graph. Edges:\n * - Derivation: derivation.source → output.collection (each output)\n * - MV: every dep in MV.dependencies → MV.outputCollection\n *\n * Throws `MaterializedViewCycleError` if the cycle's terminal node\n * is an MV output collection; otherwise (a pure-derivation cycle)\n * the caller's `DerivationRegistry.validate()` will surface\n * `DerivationCycleError` separately at vault open.\n *\n * Call AFTER all `register()` calls complete.\n */\n validate(derivationRegistry?: DerivationRegistry | null): void {\n const visited = new Set<string>()\n const stack: string[] = []\n const mvOutputs = new Set<string>()\n for (const reg of this._byName.values()) mvOutputs.add(reg.outputCollection)\n\n const edges = new Map<string, string[]>()\n\n // MV edges: every dep → output. Same-collection edges (dep ===\n // outputCollection) are skipped IFF the MV declares an\n // `output.partition` discriminator AND the query has a where-clause\n // that provably excludes the partition value. Otherwise the cycle\n // detector treats the edge as real — naïve same-collection MVs\n // surface as `MaterializedViewCycleError`.\n for (const reg of this._byName.values()) {\n for (const dep of reg.dependencies) {\n if (dep === reg.outputCollection && partitionDisjoint(reg)) continue\n const arr = edges.get(dep)\n if (arr) arr.push(reg.outputCollection)\n else edges.set(dep, [reg.outputCollection])\n }\n }\n\n // Derivation edges: source → output collections\n if (derivationRegistry) {\n // The shared DerivationRegistry exposes its edges via the same\n // `strategiesForSource` API its own `validate()` uses. We don't\n // duplicate cycle detection — we add MV nodes to the graph and\n // run the unified DFS, attributing cycles that touch an MV\n // output to `MaterializedViewCycleError`.\n for (const reg of this._byName.values()) {\n // Walk every dependency through derivation edges too: a\n // derivation whose output we depend on is itself a source.\n void reg\n }\n // Pull derivation edges by scanning every MV dep + every MV\n // output as potential derivation sources.\n const sourcesToScan = new Set<string>()\n for (const reg of this._byName.values()) {\n for (const dep of reg.dependencies) sourcesToScan.add(dep)\n sourcesToScan.add(reg.outputCollection)\n }\n for (const src of sourcesToScan) {\n const strategies = derivationRegistry.strategiesForSource(src)\n if (strategies.length === 0) continue\n for (const s of strategies) {\n for (const key of Object.keys(s.spec.outputs)) {\n const o = s.spec.outputs[key]\n if (!o) continue\n const arr = edges.get(src)\n if (arr) arr.push(o.collection)\n else edges.set(src, [o.collection])\n }\n }\n }\n }\n\n const visit = (node: string): void => {\n if (stack.includes(node)) {\n const cycle = stack.slice(stack.indexOf(node)).concat(node)\n // If any node on the cycle is an MV output, attribute as MV\n // cycle. Otherwise let DerivationRegistry.validate() surface it.\n if (cycle.some(n => mvOutputs.has(n))) {\n throw new MaterializedViewCycleError(cycle)\n }\n // Pure-derivation cycle — caller's DerivationRegistry.validate()\n // will catch it separately. Don't double-report.\n return\n }\n if (visited.has(node)) return\n stack.push(node)\n const outs = edges.get(node)\n if (outs) for (const o of outs) visit(o)\n stack.pop()\n visited.add(node)\n }\n\n for (const node of edges.keys()) visit(node)\n }\n}\n\n/**\n * Type guard: is the clause a top-level `FieldClause` on the given\n * field? Used by the partition-disjoint check.\n *\n * @internal\n */\nfunction isFieldClauseOnField(clause: Clause, field: string): clause is FieldClause {\n return clause.type === 'field' && clause.field === field\n}\n\n/**\n * Wrap an `MVQueryContext` so its `.collection().query()` returns a\n * Query<T> with the MV's declared predicates attached. Bare Queries\n * (outside of any MV) don't gain `.wherePredicate()` — only Queries\n * obtained through this wrapped db do.\n *\n * @internal\n */\nexport function wrapDbWithPredicates(\n db: MVQueryContext,\n predicates: NonNullable<MaterializedViewStrategy<Record<string, unknown>>['predicates']>,\n): MVQueryContext {\n // Build the predicate map once — the fn signature in the MV spec\n // is row-typed but the QueryBuilder casts to unknown, so we widen\n // here for the Map.\n const map = new Map<string, DeclaredPredicate>()\n for (const [name, decl] of Object.entries(predicates)) {\n map.set(name, {\n hash: decl.hash,\n fn: decl.fn as (record: unknown, ctx?: unknown) => boolean,\n })\n }\n return {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n collection<T extends Record<string, unknown>>(name: string): any {\n const c = db.collection<T>(name)\n // Return an object that delegates everything to `c` but\n // overrides `.query()` to attach predicates via the new\n // `Query._withPredicates()` accessor.\n return new Proxy(c, {\n get(target, prop, receiver) {\n if (prop === 'query') {\n return (...args: unknown[]) => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const q = (target.query as any)(...args)\n // For non-aggregate Query<T>, attach predicates. For\n // legacy predicate-arg overload that returns T[] (sync\n // filter), pass through unchanged.\n \n if (q && typeof q._withPredicates === 'function') {\n return q._withPredicates(map)\n }\n return q\n }\n }\n return Reflect.get(target, prop, receiver)\n },\n })\n },\n }\n}\n\n/**\n * Walk a QueryPlan's clauses and collect predicate-reference markers\n * for `queryHash` derivation. Returns a sorted array (deterministic\n * order) of `{ name, predicateHash, ctxHash }` tuples — these are the\n * hashable identity of each `.wherePredicate()` call site.\n *\n * @internal\n */\nfunction extractPredicateRefs(\n plan: { clauses: readonly Clause[] },\n): Array<{ name: string; predicateHash: string; ctxHash: string }> {\n const refs: Array<{ name: string; predicateHash: string; ctxHash: string }> = []\n const walk = (clauses: readonly Clause[]): void => {\n for (const c of clauses) {\n if (c.type === 'wherePredicate') {\n refs.push({ name: c.name, predicateHash: c.predicateHash, ctxHash: c.ctxHash })\n } else if (c.type === 'group') {\n walk(c.clauses)\n }\n }\n }\n walk(plan.clauses)\n // Stable-sort by (name, predicateHash, ctxHash) — same predicate\n // appearing twice with different ctx hashes both flow through.\n refs.sort((a, b) => {\n if (a.name !== b.name) return a.name < b.name ? -1 : 1\n if (a.predicateHash !== b.predicateHash) return a.predicateHash < b.predicateHash ? -1 : 1\n return a.ctxHash < b.ctxHash ? -1 : a.ctxHash > b.ctxHash ? 1 : 0\n })\n return refs\n}\n\n/**\n * Provability check for the same-collection partition-discriminator.\n * Returns `true` when\n * the captured partition clauses on the MV's query provably exclude\n * the partition's value — meaning the input filter and the output\n * partition are disjoint and the same-collection edge isn't really a\n * cycle.\n *\n * Supported provability shapes (narrow on purpose — DERIV-PP30-001\n * is the load-bearing case):\n *\n * - `.where(field, '==', X)` where X !== partition.value → disjoint\n * - `.where(field, '!=', partition.value)` → disjoint\n * - `.where(field, 'in', [...])` where partition.value NOT in list → disjoint\n *\n * Anything else (no clause on the partition field, an 'in' list that\n * contains partition.value, unsupported operators) → not disjoint,\n * the cycle detector surfaces `MaterializedViewCycleError`.\n *\n * @internal\n */\nfunction partitionDisjoint(reg: RegisteredMV): boolean {\n const partition = reg.spec.output?.partition\n if (partition === undefined) return false\n const value = partition.value\n // The OR-semantics of multiple where-clauses on the same field\n // would muddy this check. v2 only treats AND-chained clauses;\n // any clause that proves disjoint is sufficient.\n for (const c of reg.partitionClauses) {\n if (c.op === '==' && c.value !== value) return true\n if (c.op === '!=' && c.value === value) return true\n if (c.op === 'in' && Array.isArray(c.value)) {\n const list = c.value as readonly unknown[]\n if (!list.includes(value)) return true\n }\n }\n return false\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
buildPersistedIndexCallbacks,
|
|
3
|
-
buildRetrievalDocs,
|
|
4
|
-
embedOnWrite,
|
|
5
|
-
flushIndex,
|
|
6
|
-
hasIndexedBlobFields,
|
|
7
|
-
retrieve,
|
|
8
|
-
search,
|
|
9
|
-
similarTo,
|
|
10
|
-
warmIndex
|
|
11
|
-
} from "./chunk-CP3GUIAN.js";
|
|
12
|
-
import "./chunk-AURFOK3D.js";
|
|
13
|
-
import "./chunk-WFBZRXS5.js";
|
|
14
|
-
import "./chunk-GMZFHDSG.js";
|
|
15
|
-
import "./chunk-3CHNKEFI.js";
|
|
16
|
-
import "./chunk-G5CFMITA.js";
|
|
17
|
-
import "./chunk-JIW3Q5JN.js";
|
|
18
|
-
import "./chunk-HGQ6UNHE.js";
|
|
19
|
-
import "./chunk-5UOPIPZC.js";
|
|
20
|
-
import "./chunk-UWZ7O4NR.js";
|
|
21
|
-
import "./chunk-HCK2DFEK.js";
|
|
22
|
-
import "./chunk-VFQOE76I.js";
|
|
23
|
-
import "./chunk-LP7BEXCT.js";
|
|
24
|
-
import "./chunk-5HZULLHE.js";
|
|
25
|
-
import "./chunk-BASCPHNM.js";
|
|
26
|
-
import "./chunk-OJ64XMPJ.js";
|
|
27
|
-
import "./chunk-PZ5AY32C.js";
|
|
28
|
-
export {
|
|
29
|
-
buildPersistedIndexCallbacks,
|
|
30
|
-
buildRetrievalDocs,
|
|
31
|
-
embedOnWrite,
|
|
32
|
-
flushIndex,
|
|
33
|
-
hasIndexedBlobFields,
|
|
34
|
-
retrieve,
|
|
35
|
-
search,
|
|
36
|
-
similarTo,
|
|
37
|
-
warmIndex
|
|
38
|
-
};
|
|
39
|
-
//# sourceMappingURL=collection-facade-JP6N7USE.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/with-shape/classified/config-drift.ts"],"sourcesContent":["/**\n * C-A / R10 config-drift marker I/O — the store side of the guard, kept OUT of\n * the kernel `Collection` so the always-on kernel surface stays lean. The\n * kernel holds only the per-handle memoization; these helpers do the actual\n * persisted-schema read/write (via the persisted-schemas storage seam, lazily\n * imported so the classified spine tree-shakes).\n *\n * Marker semantics: a collection declaring classified digest-only fields writes\n * an `x-classified` marker into its `_schemas/<collection>` record on the first\n * classified write. A handle later opened WITHOUT `classifiedFields` (a naive\n * handle) reads it back and refuses to write — see RecordCodec.encryptRecord's\n * R10 guard.\n *\n * @module\n */\nimport type { NoydbStore, VdigFieldPolicy, ClassifiedMarker } from '../../kernel/types.js'\nimport type { EnclaveKey } from '../../kernel/enclave/index.js'\n\n/** Build the marker for a handle's digest-only field map, or null when it declares none. */\nfunction markerForFields(vdigFields: ReadonlyMap<string, VdigFieldPolicy> | null): ClassifiedMarker | null {\n if (vdigFields === null || vdigFields.size === 0) return null\n const digestOnly = [...vdigFields.keys()]\n return { digestOnly, equatable: digestOnly.filter((f) => vdigFields.get(f)?.equatable === true) }\n}\n\n/**\n * Persist the classified marker for a handle's digest-only fields. No-op when\n * the handle declares none. Idempotent (delegates to `persistClassifiedMarker`).\n */\nexport async function persistClassifiedMarkerForFields(\n store: NoydbStore,\n vault: string,\n collectionName: string,\n vdigFields: ReadonlyMap<string, VdigFieldPolicy> | null,\n dek: EnclaveKey,\n): Promise<void> {\n const marker = markerForFields(vdigFields)\n if (marker === null) return\n const { persistClassifiedMarker } = await import('../persisted-schemas/register.js')\n await persistClassifiedMarker({ store, vault, collectionName, dek, marker })\n}\n\n/**\n * Read a collection's persisted classified marker's declared digest-only field\n * set (empty when the collection carries no marker). Cross-session drift signal:\n * the R10 superset guard refuses a handle whose declared classified set does not\n * cover every field in this set.\n */\nexport async function readClassifiedMarkerDigestOnly(\n store: NoydbStore,\n vault: string,\n collectionName: string,\n dek: EnclaveKey,\n): Promise<readonly string[]> {\n const { loadPersistedSchema } = await import('../persisted-schemas/storage.js')\n const persisted = await loadPersistedSchema(store, vault, collectionName, dek)\n return persisted?.classified?.digestOnly ?? []\n}\n"],"mappings":";;;AAmBA,SAAS,gBAAgB,YAAkF;AACzG,MAAI,eAAe,QAAQ,WAAW,SAAS,EAAG,QAAO;AACzD,QAAM,aAAa,CAAC,GAAG,WAAW,KAAK,CAAC;AACxC,SAAO,EAAE,YAAY,WAAW,WAAW,OAAO,CAAC,MAAM,WAAW,IAAI,CAAC,GAAG,cAAc,IAAI,EAAE;AAClG;AAMA,eAAsB,iCACpB,OACA,OACA,gBACA,YACA,KACe;AACf,QAAM,SAAS,gBAAgB,UAAU;AACzC,MAAI,WAAW,KAAM;AACrB,QAAM,EAAE,wBAAwB,IAAI,MAAM,OAAO,wBAAkC;AACnF,QAAM,wBAAwB,EAAE,OAAO,OAAO,gBAAgB,KAAK,OAAO,CAAC;AAC7E;AAQA,eAAsB,+BACpB,OACA,OACA,gBACA,KAC4B;AAC5B,QAAM,EAAE,oBAAoB,IAAI,MAAM,OAAO,uBAAiC;AAC9E,QAAM,YAAY,MAAM,oBAAoB,OAAO,OAAO,gBAAgB,GAAG;AAC7E,SAAO,WAAW,YAAY,cAAc,CAAC;AAC/C;","names":[]}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DELEGATIONS_COLLECTION,
|
|
3
|
-
issueDelegation,
|
|
4
|
-
loadActiveDelegations,
|
|
5
|
-
revokeDelegation
|
|
6
|
-
} from "./chunk-U5IJHUHZ.js";
|
|
7
|
-
import "./chunk-26FSVNBL.js";
|
|
8
|
-
import "./chunk-ZJADGNYF.js";
|
|
9
|
-
import "./chunk-JIW3Q5JN.js";
|
|
10
|
-
import "./chunk-HGQ6UNHE.js";
|
|
11
|
-
import "./chunk-5UOPIPZC.js";
|
|
12
|
-
import "./chunk-UWZ7O4NR.js";
|
|
13
|
-
import "./chunk-HCK2DFEK.js";
|
|
14
|
-
import "./chunk-VFQOE76I.js";
|
|
15
|
-
import "./chunk-LP7BEXCT.js";
|
|
16
|
-
import "./chunk-5HZULLHE.js";
|
|
17
|
-
import "./chunk-OJ64XMPJ.js";
|
|
18
|
-
import "./chunk-PZ5AY32C.js";
|
|
19
|
-
export {
|
|
20
|
-
DELEGATIONS_COLLECTION,
|
|
21
|
-
issueDelegation,
|
|
22
|
-
loadActiveDelegations,
|
|
23
|
-
revokeDelegation
|
|
24
|
-
};
|
|
25
|
-
//# sourceMappingURL=delegation-FDOXXHNG.js.map
|
package/dist/derive-JR2C5JFG.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
derivePersistedSchema,
|
|
3
|
-
isZod4Schema,
|
|
4
|
-
isZodSchema
|
|
5
|
-
} from "./chunk-55LSDABW.js";
|
|
6
|
-
import "./chunk-JIW3Q5JN.js";
|
|
7
|
-
import "./chunk-HGQ6UNHE.js";
|
|
8
|
-
import "./chunk-5UOPIPZC.js";
|
|
9
|
-
import "./chunk-UWZ7O4NR.js";
|
|
10
|
-
import "./chunk-HCK2DFEK.js";
|
|
11
|
-
import "./chunk-VFQOE76I.js";
|
|
12
|
-
import "./chunk-LP7BEXCT.js";
|
|
13
|
-
import "./chunk-5HZULLHE.js";
|
|
14
|
-
import "./chunk-OJ64XMPJ.js";
|
|
15
|
-
import "./chunk-PZ5AY32C.js";
|
|
16
|
-
export {
|
|
17
|
-
derivePersistedSchema,
|
|
18
|
-
isZod4Schema,
|
|
19
|
-
isZodSchema
|
|
20
|
-
};
|
|
21
|
-
//# sourceMappingURL=derive-JR2C5JFG.js.map
|