@noy-db/hub 0.2.0-pre.2 → 0.2.0-pre.21
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/README.md +126 -0
- package/dist/aggregate/index.cjs +643 -37
- package/dist/aggregate/index.cjs.map +1 -1
- package/dist/aggregate/index.d.cts +3 -2
- package/dist/aggregate/index.d.ts +3 -2
- package/dist/aggregate/index.js +9 -8
- package/dist/aggregate/index.js.map +1 -1
- package/dist/attestation/index.cjs.map +1 -1
- package/dist/attestation/index.d.cts +7 -5
- package/dist/attestation/index.d.ts +7 -5
- package/dist/attestation/index.js +6 -6
- package/dist/blobs/index.cjs +509 -22
- package/dist/blobs/index.cjs.map +1 -1
- package/dist/blobs/index.d.cts +9 -7
- package/dist/blobs/index.d.ts +9 -7
- package/dist/blobs/index.js +11 -6
- package/dist/blobs/index.js.map +1 -1
- package/dist/bundle/index.cjs +7886 -841
- package/dist/bundle/index.cjs.map +1 -1
- package/dist/bundle/index.d.cts +20 -18
- package/dist/bundle/index.d.ts +20 -18
- package/dist/bundle/index.js +24 -13
- package/dist/bundle/index.js.map +1 -1
- package/dist/{chunk-PFSNOPBQ.js → chunk-2XA2ZML4.js} +31 -3
- package/dist/chunk-2XA2ZML4.js.map +1 -0
- package/dist/{chunk-2PAQNPE3.js → chunk-37VGJM3T.js} +37 -2
- package/dist/chunk-37VGJM3T.js.map +1 -0
- package/dist/{chunk-7BRE6EUA.js → chunk-3HNKR65T.js} +4 -4
- package/dist/chunk-3HNKR65T.js.map +1 -0
- package/dist/{chunk-Y2RKOPNC.js → chunk-5YTXYPES.js} +46 -10
- package/dist/chunk-5YTXYPES.js.map +1 -0
- package/dist/{chunk-OVZDFEOR.js → chunk-6QAZ5O6X.js} +2 -2
- package/dist/chunk-6QAZ5O6X.js.map +1 -0
- package/dist/{chunk-RTZVQAJ7.js → chunk-6QE4DUYC.js} +19 -4
- package/dist/chunk-6QE4DUYC.js.map +1 -0
- package/dist/{chunk-7Q5PLD5C.js → chunk-7MRT7EPB.js} +3 -3
- package/dist/{chunk-E535SAN4.js → chunk-7PH4OPBZ.js} +4258 -520
- package/dist/chunk-7PH4OPBZ.js.map +1 -0
- package/dist/{chunk-PEULZC6M.js → chunk-A3JMGXPG.js} +8 -1
- package/dist/chunk-A3JMGXPG.js.map +1 -0
- package/dist/{chunk-UMLVJTYV.js → chunk-ADB7GPM3.js} +7 -4
- package/dist/chunk-ADB7GPM3.js.map +1 -0
- package/dist/{chunk-G6FRSBKK.js → chunk-AI4USDRI.js} +4 -4
- package/dist/chunk-BZW5IL43.js +151 -0
- package/dist/chunk-BZW5IL43.js.map +1 -0
- package/dist/chunk-C2RJVZZL.js +123 -0
- package/dist/chunk-C2RJVZZL.js.map +1 -0
- package/dist/{chunk-UND4XIB6.js → chunk-C6W5KVDV.js} +52 -38
- package/dist/chunk-C6W5KVDV.js.map +1 -0
- package/dist/chunk-CQYEDODS.js +125 -0
- package/dist/chunk-CQYEDODS.js.map +1 -0
- package/dist/{chunk-NWZ3I6R6.js → chunk-EYK72OTL.js} +5 -5
- package/dist/{chunk-7BUTTVMR.js → chunk-F5GWNSE2.js} +2 -2
- package/dist/{chunk-AHPFONIL.js → chunk-F5ILTHMU.js} +5 -5
- package/dist/{chunk-Q6W2CMEJ.js → chunk-FRRJIUSI.js} +18 -5
- package/dist/chunk-FRRJIUSI.js.map +1 -0
- package/dist/{chunk-YMYK7US4.js → chunk-GJTKMME7.js} +2 -2
- package/dist/chunk-GJTKMME7.js.map +1 -0
- package/dist/{chunk-EUYOGYGV.js → chunk-HYJMAV53.js} +6 -6
- package/dist/chunk-HYJMAV53.js.map +1 -0
- package/dist/{chunk-QPEXPHJR.js → chunk-I3IYTUUI.js} +4 -4
- package/dist/{chunk-3QAKZ37R.js → chunk-IVZWHIEK.js} +5 -5
- package/dist/{chunk-PLI5TV7N.js → chunk-IW4L4X65.js} +2 -2
- package/dist/chunk-IW4L4X65.js.map +1 -0
- package/dist/{chunk-3Z2TPHC4.js → chunk-IY24WS2P.js} +69 -5
- package/dist/chunk-IY24WS2P.js.map +1 -0
- package/dist/{chunk-HXJXPZRE.js → chunk-J6RGRZOY.js} +10 -3
- package/dist/chunk-J6RGRZOY.js.map +1 -0
- package/dist/{chunk-3S4BJX25.js → chunk-JBBWALNI.js} +2 -2
- package/dist/chunk-JBBWALNI.js.map +1 -0
- package/dist/{chunk-7Z23ZFLV.js → chunk-JDCPRJVS.js} +5 -5
- package/dist/chunk-JDCPRJVS.js.map +1 -0
- package/dist/{chunk-243PNUA6.js → chunk-JOK73NDT.js} +3 -3
- package/dist/chunk-JTI57WRT.js +164 -0
- package/dist/chunk-JTI57WRT.js.map +1 -0
- package/dist/{chunk-VRBCTEKQ.js → chunk-JYNH4FIM.js} +233 -11
- package/dist/chunk-JYNH4FIM.js.map +1 -0
- package/dist/{chunk-TBKOGSYR.js → chunk-KOAJ3TZM.js} +27 -5
- package/dist/chunk-KOAJ3TZM.js.map +1 -0
- package/dist/{chunk-YTXSFG3C.js → chunk-MBXKRHSS.js} +50 -20
- package/dist/chunk-MBXKRHSS.js.map +1 -0
- package/dist/{chunk-MUWOSVEP.js → chunk-NSXNXLYM.js} +10 -2
- package/dist/chunk-NSXNXLYM.js.map +1 -0
- package/dist/{chunk-J4KLMEUL.js → chunk-NV4IHBZS.js} +664 -51
- package/dist/chunk-NV4IHBZS.js.map +1 -0
- package/dist/{chunk-LRAZDV5X.js → chunk-O5XKZCUD.js} +31 -8
- package/dist/chunk-O5XKZCUD.js.map +1 -0
- package/dist/{chunk-W3XXT26A.js → chunk-OTWT6BAJ.js} +358 -3
- package/dist/chunk-OTWT6BAJ.js.map +1 -0
- package/dist/{chunk-XG3PTSCD.js → chunk-PDVP3C2I.js} +1 -1
- package/dist/chunk-PDVP3C2I.js.map +1 -0
- package/dist/{chunk-GIV6DWBG.js → chunk-S45MDEEF.js} +44 -5
- package/dist/chunk-S45MDEEF.js.map +1 -0
- package/dist/{chunk-VK5EER6C.js → chunk-SQKAECUL.js} +2 -2
- package/dist/{chunk-FAQVNJD4.js → chunk-SQOK5UM6.js} +12 -2
- package/dist/{chunk-FAQVNJD4.js.map → chunk-SQOK5UM6.js.map} +1 -1
- package/dist/chunk-STNPB3UM.js +9 -0
- package/dist/chunk-STNPB3UM.js.map +1 -0
- package/dist/{chunk-YS3POABP.js → chunk-TA6HPKWQ.js} +1 -1
- package/dist/chunk-TA6HPKWQ.js.map +1 -0
- package/dist/{chunk-4HIL6AHQ.js → chunk-TAMRU7A2.js} +4 -4
- package/dist/{chunk-QXQRKXCU.js → chunk-TGIJTNM3.js} +2 -2
- package/dist/chunk-TNH5SLCD.js +361 -0
- package/dist/chunk-TNH5SLCD.js.map +1 -0
- package/dist/{chunk-VPSUZLOJ.js → chunk-TYMDCIQM.js} +31 -5
- package/dist/chunk-TYMDCIQM.js.map +1 -0
- package/dist/chunk-U2XSUCDF.js +524 -0
- package/dist/chunk-U2XSUCDF.js.map +1 -0
- package/dist/{chunk-3Y53S2SA.js → chunk-UU6M64HI.js} +4 -4
- package/dist/{chunk-VCGTOS2A.js → chunk-WE2BUQD2.js} +3 -3
- package/dist/chunk-WE2BUQD2.js.map +1 -0
- package/dist/{chunk-JYQTXEIO.js → chunk-WWVJXBOT.js} +449 -29
- package/dist/chunk-WWVJXBOT.js.map +1 -0
- package/dist/chunk-YPIOFSN3.js +129 -0
- package/dist/chunk-YPIOFSN3.js.map +1 -0
- package/dist/chunk-ZC7J6ZYV.js +7 -0
- package/dist/chunk-ZC7J6ZYV.js.map +1 -0
- package/dist/{chunk-5ZGZ6HIZ.js → chunk-ZONKSLF2.js} +30 -7
- package/dist/chunk-ZONKSLF2.js.map +1 -0
- package/dist/consent/index.cjs.map +1 -1
- package/dist/consent/index.d.cts +8 -6
- package/dist/consent/index.d.ts +8 -6
- package/dist/consent/index.js +3 -3
- package/dist/{crypto-5ZDIY3NG.js → crypto-456N7UVX.js} +7 -3
- package/dist/{delegation-QYXZW25W.js → delegation-DP4COTXB.js} +5 -5
- package/dist/derivations/index.cjs +124 -6
- package/dist/derivations/index.cjs.map +1 -1
- package/dist/derivations/index.d.cts +11 -9
- package/dist/derivations/index.d.ts +11 -9
- package/dist/derivations/index.js +8 -6
- package/dist/{dev-unlock-DQCNDfFp.d.cts → dev-unlock-CY0HIZA0.d.cts} +1 -1
- package/dist/{dev-unlock-utkybTKb.d.ts → dev-unlock-CpKSkl2c.d.ts} +1 -1
- package/dist/discriminant-BN9REW3o.d.cts +60 -0
- package/dist/discriminant-BN9REW3o.d.ts +60 -0
- package/dist/errors-Dkc_fi-S.d.cts +1467 -0
- package/dist/errors-Dkc_fi-S.d.ts +1467 -0
- package/dist/executor-4IEW4KG5.js +8 -0
- package/dist/executor-KYJCJCIN.js +12 -0
- package/dist/executor-W7VIBOBZ.js +8 -0
- package/dist/{fanout-sidecar-VJ52RIEY.js → fanout-sidecar-YXNAEZ33.js} +2 -2
- package/dist/fanout-sidecar-YXNAEZ33.js.map +1 -0
- package/dist/forget/index.cjs +43 -0
- package/dist/forget/index.cjs.map +1 -0
- package/dist/forget/index.d.cts +1 -0
- package/dist/forget/index.d.ts +1 -0
- package/dist/forget/index.js +14 -0
- package/dist/guards/index.cjs +144 -4
- package/dist/guards/index.cjs.map +1 -1
- package/dist/guards/index.d.cts +16 -8
- package/dist/guards/index.d.ts +16 -8
- package/dist/guards/index.js +13 -7
- package/dist/{hash-jDowCrK2.d.cts → hash-BSd0-_L8.d.cts} +1 -1
- package/dist/{hash-DcoYWfJ_.d.ts → hash-BnBQx39y.d.ts} +1 -1
- package/dist/history/index.cjs +28 -5
- package/dist/history/index.cjs.map +1 -1
- package/dist/history/index.d.cts +9 -7
- package/dist/history/index.d.ts +9 -7
- package/dist/history/index.js +9 -7
- package/dist/history/index.js.map +1 -1
- package/dist/i18n/index.cjs +356 -26
- package/dist/i18n/index.cjs.map +1 -1
- package/dist/i18n/index.d.cts +8 -6
- package/dist/i18n/index.d.ts +8 -6
- package/dist/i18n/index.js +36 -15
- package/dist/i18n/index.js.map +1 -1
- package/dist/index-BMmajblo.d.cts +362 -0
- package/dist/index-BMmajblo.d.ts +362 -0
- package/dist/{index-BCKdioeh.d.ts → index-Bm9hIY7t.d.ts} +169 -1127
- package/dist/{index-BMjrzNZr.d.cts → index-tZqVB9g5.d.cts} +169 -1127
- package/dist/index.cjs +10286 -2168
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +258 -23
- package/dist/index.d.ts +258 -23
- package/dist/index.js +443 -110
- package/dist/index.js.map +1 -1
- package/dist/indexing/index.cjs +97 -32
- package/dist/indexing/index.cjs.map +1 -1
- package/dist/indexing/index.d.cts +3 -3
- package/dist/indexing/index.d.ts +3 -3
- package/dist/indexing/index.js +4 -4
- package/dist/issue-JXC6T2QR.js +12 -0
- package/dist/{lazy-builder-Rpd-V3jP.d.ts → lazy-builder-ChSqcF5t.d.ts} +2 -2
- package/dist/{lazy-builder-C-rPfWG0.d.cts → lazy-builder-eYZzLEL1.d.cts} +2 -2
- package/dist/{ledger-3IU5GMXA.js → ledger-I7JUYP4L.js} +6 -6
- package/dist/materialized-views/index.cjs +687 -13
- package/dist/materialized-views/index.cjs.map +1 -1
- package/dist/materialized-views/index.d.cts +23 -20
- package/dist/materialized-views/index.d.ts +23 -20
- package/dist/materialized-views/index.js +8 -7
- package/dist/mime-magic-BnJCGJzB.d.cts +103 -0
- package/dist/mime-magic-CjSyakO4.d.ts +103 -0
- package/dist/noydb-ZZCRF6TE.js +38 -0
- package/dist/overlay-views/index.cjs +58 -18
- package/dist/overlay-views/index.cjs.map +1 -1
- package/dist/overlay-views/index.d.cts +32 -12
- package/dist/overlay-views/index.d.ts +32 -12
- package/dist/overlay-views/index.js +6 -6
- package/dist/periods/index.cjs.map +1 -1
- package/dist/periods/index.d.cts +8 -6
- package/dist/periods/index.d.ts +8 -6
- package/dist/periods/index.js +6 -6
- package/dist/{predicate-Dnu81tsS.d.cts → predicate-BmhBSPCH.d.cts} +87 -5
- package/dist/{predicate-Dnu81tsS.d.ts → predicate-BmhBSPCH.d.ts} +87 -5
- package/dist/{public-envelope-U3CMEOMV.js → public-envelope-5XRTUNKF.js} +4 -4
- package/dist/query/index.cjs +1438 -130
- package/dist/query/index.cjs.map +1 -1
- package/dist/query/index.d.cts +4 -3
- package/dist/query/index.d.ts +4 -3
- package/dist/query/index.js +13 -6
- package/dist/read-only-facade-EX6WZZBP.js +7 -0
- package/dist/registry-ATRHOG5B.js +8 -0
- package/dist/registry-DKEXOJVO.js +7 -0
- package/dist/registry-LEHB26TY.js +8 -0
- package/dist/{registry-3ALP62P6.js → registry-NWHOLD5M.js} +3 -3
- package/dist/{revoke-KY2GB4KP.js → revoke-5IEK22KT.js} +6 -6
- package/dist/sealed-record/index.cjs +139 -0
- package/dist/sealed-record/index.cjs.map +1 -0
- package/dist/sealed-record/index.d.cts +123 -0
- package/dist/sealed-record/index.d.ts +123 -0
- package/dist/sealed-record/index.js +42 -0
- package/dist/sealed-record/index.js.map +1 -0
- package/dist/session/index.cjs.map +1 -1
- package/dist/session/index.d.cts +9 -7
- package/dist/session/index.d.ts +9 -7
- package/dist/session/index.js +3 -3
- package/dist/shadow/index.cjs.map +1 -1
- package/dist/shadow/index.d.cts +8 -6
- package/dist/shadow/index.d.ts +8 -6
- package/dist/shadow/index.js +2 -2
- package/dist/{signer-GRI5TZKH.js → signer-I6YARZQA.js} +5 -5
- package/dist/snapshots/index.cjs +937 -0
- package/dist/snapshots/index.cjs.map +1 -0
- package/dist/snapshots/index.d.cts +30 -0
- package/dist/snapshots/index.d.ts +30 -0
- package/dist/snapshots/index.js +152 -0
- package/dist/snapshots/index.js.map +1 -0
- package/dist/{stale-OTOF3FH7.js → stale-CPESGAPL.js} +2 -2
- package/dist/stale-CPESGAPL.js.map +1 -0
- package/dist/state-vault-JR3CFGNP.js +14 -0
- package/dist/state-vault-JR3CFGNP.js.map +1 -0
- package/dist/store/index.cjs +8 -0
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +15 -6
- package/dist/store/index.d.ts +15 -6
- package/dist/store/index.js +2 -2
- package/dist/{strategy-DSTrsZ8t.d.ts → strategy-54eIwox5.d.ts} +456 -7
- package/dist/{strategy-DSTrsZ8t.d.cts → strategy-WtB-jXYv.d.cts} +456 -7
- package/dist/sync/index.cjs.map +1 -1
- package/dist/sync/index.d.cts +7 -5
- package/dist/sync/index.d.ts +7 -5
- package/dist/sync/index.js +4 -4
- package/dist/team/index.cjs +1 -1
- package/dist/team/index.cjs.map +1 -1
- package/dist/team/index.d.cts +8 -6
- package/dist/team/index.d.ts +8 -6
- package/dist/team/index.js +8 -8
- package/dist/transition-guard-D4bfIAiW.d.ts +165 -0
- package/dist/transition-guard-Dmpqzg-_.d.cts +165 -0
- package/dist/tx/index.cjs +155 -5
- package/dist/tx/index.cjs.map +1 -1
- package/dist/tx/index.d.cts +27 -9
- package/dist/tx/index.d.ts +27 -9
- package/dist/tx/index.js +61 -4
- package/dist/tx/index.js.map +1 -1
- package/dist/{types-BoFFiskX.d.ts → types-DLfWFr6U.d.ts} +3997 -1262
- package/dist/{types-DJG8HG6F.d.cts → types-DyOI6XZ_.d.cts} +3997 -1262
- package/dist/{ulid-BmBgooGm.d.ts → ulid-B2L_aqVA.d.ts} +19 -19
- package/dist/{ulid-C7ms9oli.d.cts → ulid-LaxfH2tK.d.cts} +19 -19
- package/dist/util/index.cjs +7 -0
- package/dist/util/index.cjs.map +1 -1
- package/dist/util/index.d.cts +2 -0
- package/dist/util/index.d.ts +2 -0
- package/dist/util/index.js +5 -1
- package/dist/util/index.js.map +1 -1
- package/dist/vault-group-BB246VIM.js +804 -0
- package/dist/vault-group-BB246VIM.js.map +1 -0
- package/dist/{with-materialized-view-CqnRwI2S.d.ts → with-materialized-view-CeZYGJVf.d.cts} +2 -2
- package/dist/{with-materialized-view-BbEPFIIJ.d.cts → with-materialized-view-DNULSxoP.d.ts} +2 -2
- package/dist/{with-overlayed-view-Ct1fSJt-.d.ts → with-overlayed-view-C9joG7UZ.d.ts} +2 -2
- package/dist/{with-overlayed-view-bwlmmFjx.d.cts → with-overlayed-view-kdcPGHih.d.cts} +2 -2
- package/dist/with-rollup-DJDbrxjf.d.ts +47 -0
- package/dist/with-rollup-s58XAeWO.d.cts +47 -0
- package/package.json +35 -4
- package/dist/chunk-2PAQNPE3.js.map +0 -1
- package/dist/chunk-3S4BJX25.js.map +0 -1
- package/dist/chunk-3XHOCQK4.js +0 -118
- package/dist/chunk-3XHOCQK4.js.map +0 -1
- package/dist/chunk-3Z2TPHC4.js.map +0 -1
- package/dist/chunk-5ZGZ6HIZ.js.map +0 -1
- package/dist/chunk-7BRE6EUA.js.map +0 -1
- package/dist/chunk-7Z23ZFLV.js.map +0 -1
- package/dist/chunk-CXSCDO5T.js +0 -51
- package/dist/chunk-CXSCDO5T.js.map +0 -1
- package/dist/chunk-E535SAN4.js.map +0 -1
- package/dist/chunk-EUYOGYGV.js.map +0 -1
- package/dist/chunk-GIV6DWBG.js.map +0 -1
- package/dist/chunk-HXJXPZRE.js.map +0 -1
- package/dist/chunk-J4KLMEUL.js.map +0 -1
- package/dist/chunk-JYQTXEIO.js.map +0 -1
- package/dist/chunk-LRAZDV5X.js.map +0 -1
- package/dist/chunk-MRIBLZL3.js +0 -86
- package/dist/chunk-MRIBLZL3.js.map +0 -1
- package/dist/chunk-MUWOSVEP.js.map +0 -1
- package/dist/chunk-OVZDFEOR.js.map +0 -1
- package/dist/chunk-PEULZC6M.js.map +0 -1
- package/dist/chunk-PFSNOPBQ.js.map +0 -1
- package/dist/chunk-PLI5TV7N.js.map +0 -1
- package/dist/chunk-Q6W2CMEJ.js.map +0 -1
- package/dist/chunk-RTZVQAJ7.js.map +0 -1
- package/dist/chunk-TBKOGSYR.js.map +0 -1
- package/dist/chunk-UMLVJTYV.js.map +0 -1
- package/dist/chunk-UND4XIB6.js.map +0 -1
- package/dist/chunk-VCGTOS2A.js.map +0 -1
- package/dist/chunk-VE6YVP32.js +0 -19
- package/dist/chunk-VE6YVP32.js.map +0 -1
- package/dist/chunk-VPSUZLOJ.js.map +0 -1
- package/dist/chunk-VRBCTEKQ.js.map +0 -1
- package/dist/chunk-W3XXT26A.js.map +0 -1
- package/dist/chunk-XG3PTSCD.js.map +0 -1
- package/dist/chunk-Y2RKOPNC.js.map +0 -1
- package/dist/chunk-YMYK7US4.js.map +0 -1
- package/dist/chunk-YS3POABP.js.map +0 -1
- package/dist/chunk-YTXSFG3C.js.map +0 -1
- package/dist/executor-AS2IDHKZ.js +0 -11
- package/dist/executor-HLXFXNFM.js +0 -8
- package/dist/executor-HN6YBHZ5.js +0 -8
- package/dist/fanout-sidecar-VJ52RIEY.js.map +0 -1
- package/dist/issue-ORP37MVW.js +0 -12
- package/dist/mime-magic-CBBSOkjm.d.cts +0 -50
- package/dist/mime-magic-CBBSOkjm.d.ts +0 -50
- package/dist/noydb-5H3C24GG.js +0 -34
- package/dist/read-only-facade-ITU6L7BL.js +0 -7
- package/dist/registry-7HE6VJGC.js +0 -8
- package/dist/registry-PSIPG2QR.js +0 -8
- package/dist/registry-RFGGMVNJ.js +0 -7
- package/dist/with-derivation-BKXXa8Vt.d.ts +0 -13
- package/dist/with-derivation-BjQ7q4NE.d.cts +0 -13
- package/dist/with-guard-C25yNjzd.d.ts +0 -18
- package/dist/with-guard-DQme5DKE.d.cts +0 -18
- /package/dist/{chunk-7Q5PLD5C.js.map → chunk-7MRT7EPB.js.map} +0 -0
- /package/dist/{chunk-G6FRSBKK.js.map → chunk-AI4USDRI.js.map} +0 -0
- /package/dist/{chunk-NWZ3I6R6.js.map → chunk-EYK72OTL.js.map} +0 -0
- /package/dist/{chunk-7BUTTVMR.js.map → chunk-F5GWNSE2.js.map} +0 -0
- /package/dist/{chunk-AHPFONIL.js.map → chunk-F5ILTHMU.js.map} +0 -0
- /package/dist/{chunk-QPEXPHJR.js.map → chunk-I3IYTUUI.js.map} +0 -0
- /package/dist/{chunk-3QAKZ37R.js.map → chunk-IVZWHIEK.js.map} +0 -0
- /package/dist/{chunk-243PNUA6.js.map → chunk-JOK73NDT.js.map} +0 -0
- /package/dist/{chunk-VK5EER6C.js.map → chunk-SQKAECUL.js.map} +0 -0
- /package/dist/{chunk-4HIL6AHQ.js.map → chunk-TAMRU7A2.js.map} +0 -0
- /package/dist/{chunk-QXQRKXCU.js.map → chunk-TGIJTNM3.js.map} +0 -0
- /package/dist/{chunk-3Y53S2SA.js.map → chunk-UU6M64HI.js.map} +0 -0
- /package/dist/{crypto-5ZDIY3NG.js.map → crypto-456N7UVX.js.map} +0 -0
- /package/dist/{delegation-QYXZW25W.js.map → delegation-DP4COTXB.js.map} +0 -0
- /package/dist/{executor-AS2IDHKZ.js.map → executor-4IEW4KG5.js.map} +0 -0
- /package/dist/{executor-HLXFXNFM.js.map → executor-KYJCJCIN.js.map} +0 -0
- /package/dist/{executor-HN6YBHZ5.js.map → executor-W7VIBOBZ.js.map} +0 -0
- /package/dist/{issue-ORP37MVW.js.map → forget/index.js.map} +0 -0
- /package/dist/{ledger-3IU5GMXA.js.map → issue-JXC6T2QR.js.map} +0 -0
- /package/dist/{noydb-5H3C24GG.js.map → ledger-I7JUYP4L.js.map} +0 -0
- /package/dist/{public-envelope-U3CMEOMV.js.map → noydb-ZZCRF6TE.js.map} +0 -0
- /package/dist/{read-only-facade-ITU6L7BL.js.map → public-envelope-5XRTUNKF.js.map} +0 -0
- /package/dist/{registry-3ALP62P6.js.map → read-only-facade-EX6WZZBP.js.map} +0 -0
- /package/dist/{registry-7HE6VJGC.js.map → registry-ATRHOG5B.js.map} +0 -0
- /package/dist/{registry-PSIPG2QR.js.map → registry-DKEXOJVO.js.map} +0 -0
- /package/dist/{registry-RFGGMVNJ.js.map → registry-LEHB26TY.js.map} +0 -0
- /package/dist/{revoke-KY2GB4KP.js.map → registry-NWHOLD5M.js.map} +0 -0
- /package/dist/{signer-GRI5TZKH.js.map → revoke-5IEK22KT.js.map} +0 -0
- /package/dist/{stale-OTOF3FH7.js.map → signer-I6YARZQA.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/blobs/export-blobs.ts","../src/blobs/blob-compaction.ts"],"sourcesContent":["/**\n * `vault.exportBlobs()` — bulk blob extraction primitive.\n *\n * Async-iterable handle over every blob attached to records in a\n * vault, optionally filtered by collection allowlist and per-record\n * predicate. Emits tuples of `{ blobId, recordRef, bytes, meta }` so\n * the consumer can pipe into any sink (zip stream, S3 multipart, USB\n * copy, cold-storage tape) without pulling the whole export into\n * memory.\n *\n * ## Auth + audit\n *\n * - Capability check runs **once** at handle creation via\n * `Vault.assertCanExport('plaintext', 'blob')`. An operator whose\n * keyring lacks that bit fails before a single byte of ciphertext\n * is decrypted.\n * - Audit entry lands in `_export_audit` at handle creation: the\n * actor, start timestamp, target collections, predicate presence,\n * and batch mechanism. **No content hashes** — per the spec\n * non-correlation invariant.\n *\n * ## Abort + resume\n *\n * - `handle.abort()` flips the internal signal; the next iteration\n * boundary throws `AbortError`. Consumers already in `for await`\n * can catch and exit cleanly.\n * - Restart after a partial failure with `{ afterBlobId }` — the\n * iterator skips tuples up to (and including) that blob id before\n * yielding again. Combined with a blob-count ceiling it supports\n * idempotent batch re-runs.\n *\n * @module\n */\n\nimport type { Collection } from '../collection.js'\nimport type { SlotInfo } from '../types.js'\n\n// ─── Types ──────────────────────────────────────────────────────────────\n\nexport interface ExportBlobsOptions {\n /**\n * Collection allowlist. Omit to export blobs from every collection\n * the caller has read access to.\n */\n readonly collections?: readonly string[]\n /**\n * Per-record predicate. Called on the decrypted record BEFORE any\n * blob bytes are read for that record — returning false skips the\n * record and all its slots without touching their chunks.\n */\n readonly where?: (record: unknown, context: { collection: string; id: string }) => boolean\n /**\n * Resume after a specific blob id. The iterator skips tuples up to\n * and including this id, then yields. Format of the id is the same\n * as `ExportedBlob.blobId` (the HMAC-keyed eTag).\n */\n readonly afterBlobId?: string\n /**\n * External abort signal. When fired, the next iterator tick throws\n * `ExportBlobsAbortedError`. Honored alongside `handle.abort()`.\n */\n readonly signal?: AbortSignal\n}\n\nexport interface ExportedBlob {\n /** Opaque blob identifier — HMAC-keyed eTag, stable across vaults. */\n readonly blobId: string\n /** Where this blob came from in the vault. */\n readonly recordRef: {\n readonly collection: string\n readonly id: string\n readonly slot: string\n }\n /** Decrypted plaintext bytes. */\n readonly bytes: Uint8Array\n /** Best-effort metadata (from the blob slot record). */\n readonly meta: {\n readonly size: number\n /**\n * User-visible filename stored on the slot. Often equal to the\n * slot name; differs when the caller supplied an explicit\n * `filename` to `BlobSet.put()`.\n */\n readonly filename: string\n readonly mimeType?: string\n readonly createdAt?: string\n }\n}\n\nexport interface ExportBlobsHandle extends AsyncIterable<ExportedBlob> {\n /** Abort the export. Safe to call multiple times. */\n abort(): void\n /** True once `abort()` has fired or the external signal aborted. */\n readonly aborted: boolean\n}\n\nexport class ExportBlobsAbortedError extends Error {\n constructor(reason: string) {\n super(`exportBlobs aborted: ${reason}`)\n this.name = 'ExportBlobsAbortedError'\n }\n}\n\n// ─── Audit ──────────────────────────────────────────────────────────────\n\nexport const EXPORT_AUDIT_COLLECTION = '_export_audit'\n\nexport interface ExportBlobsAuditEntry {\n readonly id: string\n readonly mechanism: 'exportBlobs'\n readonly actor: string\n readonly startedAt: string\n readonly collections: readonly string[] | null\n readonly predicate: boolean\n readonly afterBlobId: string | null\n}\n\n// ─── Implementation ─────────────────────────────────────────────────────\n\n/**\n * Build the handle. Factored out of `Vault.exportBlobs` so the\n * implementation can be unit-tested without going through the\n * compartment lifecycle.\n */\nexport function createExportBlobsHandle(\n actor: string,\n listAccessibleCollections: () => Promise<string[]>,\n getCollection: <T>(name: string) => Collection<T>,\n writeAudit: (entry: ExportBlobsAuditEntry) => Promise<void>,\n options: ExportBlobsOptions,\n): ExportBlobsHandle {\n let aborted = false\n\n const abort = (): void => {\n aborted = true\n }\n\n if (options.signal) {\n if (options.signal.aborted) aborted = true\n options.signal.addEventListener('abort', () => { aborted = true })\n }\n\n function assertLive(): void {\n if (aborted) throw new ExportBlobsAbortedError('aborted by caller')\n }\n\n const allowlist = options.collections ? new Set(options.collections) : null\n\n // Write the audit entry BEFORE the first yield so a blocked\n // iteration still leaves an audit trail that the export started.\n let auditPromise: Promise<void> | null = null\n function writeAuditOnce(): Promise<void> {\n if (!auditPromise) {\n auditPromise = writeAudit({\n id: generateBatchId(),\n mechanism: 'exportBlobs',\n actor,\n startedAt: new Date().toISOString(),\n collections: options.collections ?? null,\n predicate: Boolean(options.where),\n afterBlobId: options.afterBlobId ?? null,\n })\n }\n return auditPromise\n }\n\n async function* generate(): AsyncGenerator<ExportedBlob> {\n await writeAuditOnce()\n assertLive()\n\n // Resolve target collections lazily — also keeps the call async.\n const allCollections = await listAccessibleCollections()\n const targets = allCollections.filter(name => {\n if (name.startsWith('_')) return false\n if (allowlist && !allowlist.has(name)) return false\n return true\n })\n\n let resumeCursorHit = options.afterBlobId === undefined\n\n for (const collectionName of targets) {\n if (aborted) return\n\n const coll = getCollection<Record<string, unknown>>(collectionName)\n const records = await coll.list().catch(() => [])\n for (const record of records) {\n if (aborted) return\n assertLive()\n\n const idField = (record as { id?: unknown }).id\n if (typeof idField !== 'string') continue\n\n if (options.where && !options.where(record, { collection: collectionName, id: idField })) continue\n\n const blobSet = coll.blob(idField)\n const slots = await blobSet.list().catch(() => [] as SlotInfo[])\n for (const slot of slots) {\n if (aborted) return\n\n if (!resumeCursorHit) {\n if (slot.eTag === options.afterBlobId) {\n resumeCursorHit = true\n }\n continue\n }\n\n const bytes = await blobSet.get(slot.name)\n if (!bytes) continue\n\n const item: ExportedBlob = {\n blobId: slot.eTag,\n recordRef: { collection: collectionName, id: idField, slot: slot.name },\n bytes,\n meta: {\n size: slot.size,\n filename: slot.filename,\n ...(slot.mimeType !== undefined && { mimeType: slot.mimeType }),\n ...(slot.uploadedAt !== undefined && { createdAt: slot.uploadedAt }),\n },\n }\n yield item\n }\n }\n }\n }\n\n const handle: ExportBlobsHandle = {\n abort,\n get aborted() { return aborted },\n [Symbol.asyncIterator]: () => generate(),\n }\n return handle\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────\n\nfunction generateBatchId(): string {\n // 16 bytes of crypto randomness, URL-safe base64, no padding.\n const raw = globalThis.crypto.getRandomValues(new Uint8Array(16))\n let s = ''\n for (const b of raw) s += b.toString(16).padStart(2, '0')\n return `batch-${Date.now().toString(36)}-${s.slice(0, 12)}`\n}\n","/**\n * Blob retention + compaction.\n *\n * Declarative per-collection / per-slot eviction policy. Two\n * triggers:\n *\n * - **`retainDays`** — age-based TTL. A slot uploaded more than N\n * days ago is evicted.\n * - **`evictWhen(record)`** — predicate over the **decrypted**\n * record. Lets consumers express \"the image is safe to drop once\n * the structured invoice has been reviewed and confirmed.\"\n *\n * Either trigger (or both) causes the slot to evict. Eviction removes\n * the slot entry from `_blob_slots_{collection}`, decrements the\n * blob's refCount (so unreferenced chunks can be GC'd by the next\n * sweep), and writes one entry to the `_blob_eviction_audit`\n * collection for tamper-evident record-keeping.\n *\n * The audit entry carries the eTag of the evicted blob (opaque HMAC\n * of plaintext under the vault's `_blob` DEK) — no plaintext leakage,\n * per the SPEC non-correlation invariant. Consumers reconstructing\n * \"what used to be attached\" can look up the audit entry by record\n * id.\n *\n * Compaction is **consumer-scheduled** — noy-db never runs a\n * background daemon. Call `vault.compact()` whenever your workflow\n * allows (cron, manual \"tidy\" button, cold-storage export prep, …).\n *\n * @module\n */\n\nimport type { NoydbStore, EncryptedEnvelope, SlotInfo } from '../types.js'\nimport { NOYDB_FORMAT_VERSION } from '../types.js'\nimport { encrypt } from '../crypto.js'\n\n// ─── Config types ───────────────────────────────────────────────────────\n\nexport interface BlobFieldPolicy<T = unknown> {\n /**\n * Age-based TTL in days. A slot whose `uploadedAt` is older than\n * `now - retainDays × 86400s` evicts on the next `vault.compact()`.\n * Omit to disable age-based eviction.\n */\n readonly retainDays?: number\n /**\n * Predicate evaluated against the decrypted record. When it returns\n * `true`, every matching slot on that record evicts. Omit to\n * disable predicate-based eviction.\n */\n readonly evictWhen?: (record: T) => boolean\n /**\n * **Legal hold.** When this predicate returns `true`, the slot is\n * never evicted — `retainDays`/`evictWhen` are overridden. Use for a\n * litigation / audit hold on a fiscal document: the blob stays until\n * the predicate returns `false` (the hold is released). Fail-closed:\n * if the predicate throws, the slot is treated as held.\n */\n readonly legalHold?: (record: T) => boolean\n /**\n * **Period-bound retention.** Returns the date (Date / ISO string /\n * epoch ms) until which the slot must be retained — typically derived\n * from the record's fiscal period (e.g. period end + 10 years). While\n * `now < retainUntil`, the slot is never evicted, regardless of\n * `retainDays`. Return `null`/`undefined` to impose no floor.\n * Fail-closed: a throwing function holds the slot.\n */\n readonly retainUntil?: (record: T) => Date | string | number | null | undefined\n /**\n * **External projection.** When `true`, this field's bytes are stored in the\n * vault's `ObjectProjection` (`createNoydb({ objectStore })`) as a single raw,\n * **unencrypted** object — servable directly from S3/CDN and processable by\n * native tooling — instead of the encrypted-chunk path. The encrypted slot\n * record remains the catalog (anchoring invariant). Requires an `objectStore`;\n * **outside the zero-knowledge guarantee** — use only for assets meant to\n * leave the vault. See the as-aws-s3 design spec.\n */\n readonly external?: boolean\n /**\n * For an `external` field: make the object world-readable (CDN origin) rather\n * than presigned-only. Default `false` (presigned). Ignored unless `external`.\n */\n readonly public?: boolean\n /**\n * For an `external` field: how to stamp a **backlink** (this record's\n * vault/collection/id/field) onto the object's metadata — the self-describing\n * \"secondary store\" that powers reconcile / DR / import re-pairing.\n * - `'opaque-token'` (default): a random id; preserves the opaque-bucket\n * property (no names leak); the token is also recorded on the slot.\n * - `'encrypted'`: the reference encrypted under the blob DEK (ZK-preserving;\n * falls back to `'opaque-token'` on a plaintext vault).\n * - `'plain'`: the reference in cleartext metadata — **leaks structure** to\n * bucket readers; only for non-sensitive deployments.\n * - `'none'`: no backlink.\n */\n readonly backlink?: 'opaque-token' | 'encrypted' | 'plain' | 'none'\n}\n\nexport type BlobFieldsConfig<T = unknown> = Record<string, BlobFieldPolicy<T>>\n\n// ─── Audit collection ──────────────────────────────────────────────────\n\nexport const BLOB_EVICTION_AUDIT_COLLECTION = '_blob_eviction_audit'\n\nexport interface BlobEvictionEntry {\n readonly id: string\n readonly collection: string\n readonly recordId: string\n readonly slotName: string\n readonly blobHash: string\n readonly reason: 'ttl' | 'predicate' | 'both'\n readonly evictedAt: string\n readonly actor: string\n}\n\n// ─── Compaction result ──────────────────────────────────────────────────\n\nexport interface CompactionResult {\n /** Number of blob slots evicted across all collections. */\n readonly evicted: number\n /** Number of records touched (iterated + policy checked). */\n readonly records: number\n /** Number of collections with `blobFields` configured. */\n readonly collections: number\n /** Number of audit entries written. Equal to `evicted`. */\n readonly auditEntries: number\n /**\n * Number of slots that would have evicted (TTL/predicate triggered)\n * but were retained by a `legalHold` or `retainUntil` floor.\n */\n readonly held: number\n /** Per-collection breakdown for diagnostics. */\n readonly byCollection: Record<string, { records: number; evicted: number }>\n}\n\n// ─── Core ──────────────────────────────────────────────────────────────\n\nexport interface CompactRunOptions {\n /** Override \"now\" for deterministic testing. */\n readonly now?: Date\n /**\n * Stop after this many evictions. Useful for capped batches / cron\n * jobs that need to fit in a time window. `undefined` = unbounded.\n */\n readonly maxEvictions?: number\n /**\n * Dry-run — evaluate policies and return the counts, but do NOT\n * delete slots or write audit entries. Lets a consumer preview\n * what would happen.\n */\n readonly dryRun?: boolean\n}\n\nexport interface CompactionContext {\n readonly adapter: NoydbStore\n readonly vault: string\n readonly actor: string\n readonly encrypted: boolean\n readonly getDEK: (collection: string) => Promise<CryptoKey>\n /**\n * Resolve a collection's declared `blobFields` config. Returns an\n * empty map for collections without the config — the walk skips\n * those.\n */\n readonly getBlobFields: <T>(collection: string) => BlobFieldsConfig<T> | null\n /** List collection names in the vault. */\n readonly listCollections: () => Promise<string[]>\n /** List record ids in a collection. */\n readonly listRecords: (collection: string) => Promise<string[]>\n /** Decrypt and return the record. Null when absent. */\n readonly getRecord: <T>(collection: string, id: string) => Promise<T | null>\n /** Return the BlobSet-like handle for a record's slots. */\n readonly listSlots: (collection: string, id: string) => Promise<SlotInfo[]>\n /** Delete a slot and decrement its blob's refCount. */\n readonly deleteSlot: (collection: string, id: string, slotName: string) => Promise<void>\n}\n\nexport async function runCompaction(\n ctx: CompactionContext,\n options: CompactRunOptions = {},\n): Promise<CompactionResult> {\n const now = options.now ?? new Date()\n const maxEvictions = options.maxEvictions ?? Infinity\n const dryRun = options.dryRun === true\n\n const allCollections = await ctx.listCollections()\n const byCollection: Record<string, { records: number; evicted: number }> = {}\n let evicted = 0\n let records = 0\n let auditEntries = 0\n let held = 0\n let collectionsWithPolicy = 0\n\n outer: for (const collectionName of allCollections) {\n if (collectionName.startsWith('_')) continue\n const config = ctx.getBlobFields(collectionName)\n if (!config) continue\n const configuredSlots = Object.keys(config)\n if (configuredSlots.length === 0) continue\n collectionsWithPolicy += 1\n byCollection[collectionName] = { records: 0, evicted: 0 }\n\n const ids = await ctx.listRecords(collectionName)\n for (const recordId of ids) {\n if (evicted >= maxEvictions) break outer\n\n const record = await ctx.getRecord(collectionName, recordId).catch(() => null)\n if (record === null) continue\n records += 1\n byCollection[collectionName].records += 1\n\n const slots = await ctx.listSlots(collectionName, recordId).catch(() => [])\n for (const slot of slots) {\n if (evicted >= maxEvictions) break outer\n const policy = config[slot.name]\n if (!policy) continue\n\n const reason = evaluatePolicy(policy, record, slot, now)\n if (!reason) continue\n\n // Retention floor: a legal hold or period-bound retainUntil\n // blocks an otherwise-due eviction. Counted, never evicted.\n if (isHeld(policy, record, now)) {\n held += 1\n continue\n }\n\n if (!dryRun) {\n await ctx.deleteSlot(collectionName, recordId, slot.name)\n await writeAuditEntry(ctx, {\n id: generateEvictionId(collectionName, recordId, slot.name),\n collection: collectionName,\n recordId,\n slotName: slot.name,\n blobHash: slot.eTag,\n reason,\n evictedAt: now.toISOString(),\n actor: ctx.actor,\n })\n auditEntries += 1\n }\n evicted += 1\n byCollection[collectionName].evicted += 1\n }\n }\n }\n\n return {\n evicted,\n records,\n collections: collectionsWithPolicy,\n auditEntries,\n held,\n byCollection,\n }\n}\n\n/**\n * Whether a retention floor (legal hold or period-bound `retainUntil`)\n * currently blocks eviction of this record's slots. Fail-closed: a\n * throwing predicate holds the slot.\n */\nfunction isHeld<T>(policy: BlobFieldPolicy<T>, record: T, now: Date): boolean {\n if (policy.legalHold) {\n try {\n if (policy.legalHold(record)) return true\n } catch {\n return true\n }\n }\n if (policy.retainUntil) {\n try {\n const until = policy.retainUntil(record)\n if (until !== null && until !== undefined) {\n const t = until instanceof Date ? until.getTime() : typeof until === 'number' ? until : Date.parse(String(until))\n if (!Number.isFinite(t)) return true // fail-closed: unparseable retainUntil holds the slot\n if (t > now.getTime()) return true\n }\n } catch {\n return true\n }\n }\n return false\n}\n\nfunction evaluatePolicy<T>(\n policy: BlobFieldPolicy<T>,\n record: T,\n slot: SlotInfo,\n now: Date,\n): 'ttl' | 'predicate' | 'both' | null {\n let ttlTriggered = false\n let predicateTriggered = false\n\n if (policy.retainDays !== undefined && policy.retainDays > 0) {\n const uploadedAt = Date.parse(slot.uploadedAt)\n if (Number.isFinite(uploadedAt)) {\n const ageMs = now.getTime() - uploadedAt\n const limitMs = policy.retainDays * 86_400_000\n if (ageMs > limitMs) ttlTriggered = true\n }\n }\n\n if (policy.evictWhen) {\n try {\n if (policy.evictWhen(record)) predicateTriggered = true\n } catch {\n // Predicate error → do NOT evict. Fail closed.\n }\n }\n\n if (ttlTriggered && predicateTriggered) return 'both'\n if (ttlTriggered) return 'ttl'\n if (predicateTriggered) return 'predicate'\n return null\n}\n\nfunction generateEvictionId(collection: string, recordId: string, slotName: string): string {\n const rand = globalThis.crypto.getRandomValues(new Uint8Array(8))\n let suffix = ''\n for (const b of rand) suffix += b.toString(16).padStart(2, '0')\n return `${collection}__${recordId}__${slotName}__${suffix}`\n}\n\nasync function writeAuditEntry(ctx: CompactionContext, entry: BlobEvictionEntry): Promise<void> {\n const json = JSON.stringify(entry)\n let envelope: EncryptedEnvelope\n if (ctx.encrypted) {\n const dek = await ctx.getDEK(BLOB_EVICTION_AUDIT_COLLECTION)\n const { iv, data } = await encrypt(json, dek)\n envelope = {\n _noydb: NOYDB_FORMAT_VERSION,\n _v: 1,\n _ts: entry.evictedAt,\n _iv: iv,\n _data: data,\n _by: entry.actor,\n }\n } else {\n envelope = {\n _noydb: NOYDB_FORMAT_VERSION,\n _v: 1,\n _ts: entry.evictedAt,\n _iv: '',\n _data: json,\n _by: entry.actor,\n }\n }\n await ctx.adapter.put(ctx.vault, BLOB_EVICTION_AUDIT_COLLECTION, entry.id, envelope)\n}\n"],"mappings":";;;;;;;;AAgGO,IAAM,0BAAN,cAAsC,MAAM;AAAA,EACjD,YAAY,QAAgB;AAC1B,UAAM,wBAAwB,MAAM,EAAE;AACtC,SAAK,OAAO;AAAA,EACd;AACF;AAIO,IAAM,0BAA0B;AAmBhC,SAAS,wBACd,OACA,2BACA,eACA,YACA,SACmB;AACnB,MAAI,UAAU;AAEd,QAAM,QAAQ,MAAY;AACxB,cAAU;AAAA,EACZ;AAEA,MAAI,QAAQ,QAAQ;AAClB,QAAI,QAAQ,OAAO,QAAS,WAAU;AACtC,YAAQ,OAAO,iBAAiB,SAAS,MAAM;AAAE,gBAAU;AAAA,IAAK,CAAC;AAAA,EACnE;AAEA,WAAS,aAAmB;AAC1B,QAAI,QAAS,OAAM,IAAI,wBAAwB,mBAAmB;AAAA,EACpE;AAEA,QAAM,YAAY,QAAQ,cAAc,IAAI,IAAI,QAAQ,WAAW,IAAI;AAIvE,MAAI,eAAqC;AACzC,WAAS,iBAAgC;AACvC,QAAI,CAAC,cAAc;AACjB,qBAAe,WAAW;AAAA,QACxB,IAAI,gBAAgB;AAAA,QACpB,WAAW;AAAA,QACX;AAAA,QACA,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,QAClC,aAAa,QAAQ,eAAe;AAAA,QACpC,WAAW,QAAQ,QAAQ,KAAK;AAAA,QAChC,aAAa,QAAQ,eAAe;AAAA,MACtC,CAAC;AAAA,IACH;AACA,WAAO;AAAA,EACT;AAEA,kBAAgB,WAAyC;AACvD,UAAM,eAAe;AACrB,eAAW;AAGX,UAAM,iBAAiB,MAAM,0BAA0B;AACvD,UAAM,UAAU,eAAe,OAAO,UAAQ;AAC5C,UAAI,KAAK,WAAW,GAAG,EAAG,QAAO;AACjC,UAAI,aAAa,CAAC,UAAU,IAAI,IAAI,EAAG,QAAO;AAC9C,aAAO;AAAA,IACT,CAAC;AAED,QAAI,kBAAkB,QAAQ,gBAAgB;AAE9C,eAAW,kBAAkB,SAAS;AACpC,UAAI,QAAS;AAEb,YAAM,OAAO,cAAuC,cAAc;AAClE,YAAM,UAAU,MAAM,KAAK,KAAK,EAAE,MAAM,MAAM,CAAC,CAAC;AAChD,iBAAW,UAAU,SAAS;AAC5B,YAAI,QAAS;AACb,mBAAW;AAEX,cAAM,UAAW,OAA4B;AAC7C,YAAI,OAAO,YAAY,SAAU;AAEjC,YAAI,QAAQ,SAAS,CAAC,QAAQ,MAAM,QAAQ,EAAE,YAAY,gBAAgB,IAAI,QAAQ,CAAC,EAAG;AAE1F,cAAM,UAAU,KAAK,KAAK,OAAO;AACjC,cAAM,QAAQ,MAAM,QAAQ,KAAK,EAAE,MAAM,MAAM,CAAC,CAAe;AAC/D,mBAAW,QAAQ,OAAO;AACxB,cAAI,QAAS;AAEb,cAAI,CAAC,iBAAiB;AACpB,gBAAI,KAAK,SAAS,QAAQ,aAAa;AACrC,gCAAkB;AAAA,YACpB;AACA;AAAA,UACF;AAEA,gBAAM,QAAQ,MAAM,QAAQ,IAAI,KAAK,IAAI;AACzC,cAAI,CAAC,MAAO;AAEZ,gBAAM,OAAqB;AAAA,YACzB,QAAQ,KAAK;AAAA,YACb,WAAW,EAAE,YAAY,gBAAgB,IAAI,SAAS,MAAM,KAAK,KAAK;AAAA,YACtE;AAAA,YACA,MAAM;AAAA,cACJ,MAAM,KAAK;AAAA,cACX,UAAU,KAAK;AAAA,cACf,GAAI,KAAK,aAAa,UAAa,EAAE,UAAU,KAAK,SAAS;AAAA,cAC7D,GAAI,KAAK,eAAe,UAAa,EAAE,WAAW,KAAK,WAAW;AAAA,YACpE;AAAA,UACF;AACA,gBAAM;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,SAA4B;AAAA,IAChC;AAAA,IACA,IAAI,UAAU;AAAE,aAAO;AAAA,IAAQ;AAAA,IAC/B,CAAC,OAAO,aAAa,GAAG,MAAM,SAAS;AAAA,EACzC;AACA,SAAO;AACT;AAIA,SAAS,kBAA0B;AAEjC,QAAM,MAAM,WAAW,OAAO,gBAAgB,IAAI,WAAW,EAAE,CAAC;AAChE,MAAI,IAAI;AACR,aAAW,KAAK,IAAK,MAAK,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AACxD,SAAO,SAAS,KAAK,IAAI,EAAE,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,EAAE,CAAC;AAC3D;;;AC7IO,IAAM,iCAAiC;AA2E9C,eAAsB,cACpB,KACA,UAA6B,CAAC,GACH;AAC3B,QAAM,MAAM,QAAQ,OAAO,oBAAI,KAAK;AACpC,QAAM,eAAe,QAAQ,gBAAgB;AAC7C,QAAM,SAAS,QAAQ,WAAW;AAElC,QAAM,iBAAiB,MAAM,IAAI,gBAAgB;AACjD,QAAM,eAAqE,CAAC;AAC5E,MAAI,UAAU;AACd,MAAI,UAAU;AACd,MAAI,eAAe;AACnB,MAAI,OAAO;AACX,MAAI,wBAAwB;AAE5B,QAAO,YAAW,kBAAkB,gBAAgB;AAClD,QAAI,eAAe,WAAW,GAAG,EAAG;AACpC,UAAM,SAAS,IAAI,cAAc,cAAc;AAC/C,QAAI,CAAC,OAAQ;AACb,UAAM,kBAAkB,OAAO,KAAK,MAAM;AAC1C,QAAI,gBAAgB,WAAW,EAAG;AAClC,6BAAyB;AACzB,iBAAa,cAAc,IAAI,EAAE,SAAS,GAAG,SAAS,EAAE;AAExD,UAAM,MAAM,MAAM,IAAI,YAAY,cAAc;AAChD,eAAW,YAAY,KAAK;AAC1B,UAAI,WAAW,aAAc,OAAM;AAEnC,YAAM,SAAS,MAAM,IAAI,UAAU,gBAAgB,QAAQ,EAAE,MAAM,MAAM,IAAI;AAC7E,UAAI,WAAW,KAAM;AACrB,iBAAW;AACX,mBAAa,cAAc,EAAE,WAAW;AAExC,YAAM,QAAQ,MAAM,IAAI,UAAU,gBAAgB,QAAQ,EAAE,MAAM,MAAM,CAAC,CAAC;AAC1E,iBAAW,QAAQ,OAAO;AACxB,YAAI,WAAW,aAAc,OAAM;AACnC,cAAM,SAAS,OAAO,KAAK,IAAI;AAC/B,YAAI,CAAC,OAAQ;AAEb,cAAM,SAAS,eAAe,QAAQ,QAAQ,MAAM,GAAG;AACvD,YAAI,CAAC,OAAQ;AAIb,YAAI,OAAO,QAAQ,QAAQ,GAAG,GAAG;AAC/B,kBAAQ;AACR;AAAA,QACF;AAEA,YAAI,CAAC,QAAQ;AACX,gBAAM,IAAI,WAAW,gBAAgB,UAAU,KAAK,IAAI;AACxD,gBAAM,gBAAgB,KAAK;AAAA,YACzB,IAAI,mBAAmB,gBAAgB,UAAU,KAAK,IAAI;AAAA,YAC1D,YAAY;AAAA,YACZ;AAAA,YACA,UAAU,KAAK;AAAA,YACf,UAAU,KAAK;AAAA,YACf;AAAA,YACA,WAAW,IAAI,YAAY;AAAA,YAC3B,OAAO,IAAI;AAAA,UACb,CAAC;AACD,0BAAgB;AAAA,QAClB;AACA,mBAAW;AACX,qBAAa,cAAc,EAAE,WAAW;AAAA,MAC1C;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,aAAa;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAOA,SAAS,OAAU,QAA4B,QAAW,KAAoB;AAC5E,MAAI,OAAO,WAAW;AACpB,QAAI;AACF,UAAI,OAAO,UAAU,MAAM,EAAG,QAAO;AAAA,IACvC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACA,MAAI,OAAO,aAAa;AACtB,QAAI;AACF,YAAM,QAAQ,OAAO,YAAY,MAAM;AACvC,UAAI,UAAU,QAAQ,UAAU,QAAW;AACzC,cAAM,IAAI,iBAAiB,OAAO,MAAM,QAAQ,IAAI,OAAO,UAAU,WAAW,QAAQ,KAAK,MAAM,OAAO,KAAK,CAAC;AAChH,YAAI,CAAC,OAAO,SAAS,CAAC,EAAG,QAAO;AAChC,YAAI,IAAI,IAAI,QAAQ,EAAG,QAAO;AAAA,MAChC;AAAA,IACF,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,eACP,QACA,QACA,MACA,KACqC;AACrC,MAAI,eAAe;AACnB,MAAI,qBAAqB;AAEzB,MAAI,OAAO,eAAe,UAAa,OAAO,aAAa,GAAG;AAC5D,UAAM,aAAa,KAAK,MAAM,KAAK,UAAU;AAC7C,QAAI,OAAO,SAAS,UAAU,GAAG;AAC/B,YAAM,QAAQ,IAAI,QAAQ,IAAI;AAC9B,YAAM,UAAU,OAAO,aAAa;AACpC,UAAI,QAAQ,QAAS,gBAAe;AAAA,IACtC;AAAA,EACF;AAEA,MAAI,OAAO,WAAW;AACpB,QAAI;AACF,UAAI,OAAO,UAAU,MAAM,EAAG,sBAAqB;AAAA,IACrD,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,MAAI,gBAAgB,mBAAoB,QAAO;AAC/C,MAAI,aAAc,QAAO;AACzB,MAAI,mBAAoB,QAAO;AAC/B,SAAO;AACT;AAEA,SAAS,mBAAmB,YAAoB,UAAkB,UAA0B;AAC1F,QAAM,OAAO,WAAW,OAAO,gBAAgB,IAAI,WAAW,CAAC,CAAC;AAChE,MAAI,SAAS;AACb,aAAW,KAAK,KAAM,WAAU,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AAC9D,SAAO,GAAG,UAAU,KAAK,QAAQ,KAAK,QAAQ,KAAK,MAAM;AAC3D;AAEA,eAAe,gBAAgB,KAAwB,OAAyC;AAC9F,QAAM,OAAO,KAAK,UAAU,KAAK;AACjC,MAAI;AACJ,MAAI,IAAI,WAAW;AACjB,UAAM,MAAM,MAAM,IAAI,OAAO,8BAA8B;AAC3D,UAAM,EAAE,IAAI,KAAK,IAAI,MAAM,QAAQ,MAAM,GAAG;AAC5C,eAAW;AAAA,MACT,QAAQ;AAAA,MACR,IAAI;AAAA,MACJ,KAAK,MAAM;AAAA,MACX,KAAK;AAAA,MACL,OAAO;AAAA,MACP,KAAK,MAAM;AAAA,IACb;AAAA,EACF,OAAO;AACL,eAAW;AAAA,MACT,QAAQ;AAAA,MACR,IAAI;AAAA,MACJ,KAAK,MAAM;AAAA,MACX,KAAK;AAAA,MACL,OAAO;AAAA,MACP,KAAK,MAAM;AAAA,IACb;AAAA,EACF;AACA,QAAM,IAAI,QAAQ,IAAI,IAAI,OAAO,gCAAgC,MAAM,IAAI,QAAQ;AACrF;","names":[]}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
DecryptionError,
|
|
3
3
|
InvalidKeyError,
|
|
4
4
|
TamperedError
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-OTWT6BAJ.js";
|
|
6
6
|
|
|
7
7
|
// src/crypto.ts
|
|
8
8
|
var PBKDF2_ITERATIONS = 6e5;
|
|
@@ -58,6 +58,39 @@ async function unwrapKey(wrappedBase64, kek) {
|
|
|
58
58
|
throw new InvalidKeyError();
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
+
async function asKwKey(dek) {
|
|
62
|
+
const rawDek = await subtle.exportKey("raw", dek);
|
|
63
|
+
const hkdfKey = await subtle.importKey("raw", rawDek, "HKDF", false, ["deriveBits"]);
|
|
64
|
+
const salt = new TextEncoder().encode("noydb-cek-wrap");
|
|
65
|
+
const info = new TextEncoder().encode("v1");
|
|
66
|
+
const bits = await subtle.deriveBits(
|
|
67
|
+
{ name: "HKDF", hash: "SHA-256", salt, info },
|
|
68
|
+
hkdfKey,
|
|
69
|
+
KEY_BITS
|
|
70
|
+
);
|
|
71
|
+
return subtle.importKey("raw", bits, "AES-KW", false, ["wrapKey", "unwrapKey"]);
|
|
72
|
+
}
|
|
73
|
+
async function wrapCek(cek, dek) {
|
|
74
|
+
const kw = await asKwKey(dek);
|
|
75
|
+
const wrapped = await subtle.wrapKey("raw", cek, kw, "AES-KW");
|
|
76
|
+
return bufferToBase64(wrapped);
|
|
77
|
+
}
|
|
78
|
+
async function unwrapCek(wrappedBase64, dek) {
|
|
79
|
+
const kw = await asKwKey(dek);
|
|
80
|
+
try {
|
|
81
|
+
return await subtle.unwrapKey(
|
|
82
|
+
"raw",
|
|
83
|
+
base64ToBuffer(wrappedBase64),
|
|
84
|
+
kw,
|
|
85
|
+
"AES-KW",
|
|
86
|
+
{ name: "AES-GCM", length: KEY_BITS },
|
|
87
|
+
true,
|
|
88
|
+
["encrypt", "decrypt"]
|
|
89
|
+
);
|
|
90
|
+
} catch {
|
|
91
|
+
throw new InvalidKeyError();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
61
94
|
async function encrypt(plaintext, dek) {
|
|
62
95
|
const iv = generateIV();
|
|
63
96
|
const encoded = new TextEncoder().encode(plaintext);
|
|
@@ -256,6 +289,8 @@ export {
|
|
|
256
289
|
generateDEK,
|
|
257
290
|
wrapKey,
|
|
258
291
|
unwrapKey,
|
|
292
|
+
wrapCek,
|
|
293
|
+
unwrapCek,
|
|
259
294
|
encrypt,
|
|
260
295
|
decrypt,
|
|
261
296
|
encryptBytes,
|
|
@@ -272,4 +307,4 @@ export {
|
|
|
272
307
|
bufferToBase64,
|
|
273
308
|
base64ToBuffer
|
|
274
309
|
};
|
|
275
|
-
//# sourceMappingURL=chunk-
|
|
310
|
+
//# sourceMappingURL=chunk-37VGJM3T.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/crypto.ts"],"sourcesContent":["/**\n * Cryptographic primitives — thin wrappers around the Web Crypto API.\n *\n * ## Design principle\n *\n * **Zero npm crypto dependencies.** Every operation uses `globalThis.crypto.subtle`,\n * which is available natively in Node.js ≥ 18, all modern browsers, and\n * Deno/Bun. This avoids supply-chain risk from third-party crypto packages and\n * ensures the library stays auditable.\n *\n * ## Algorithms\n *\n * | Use case | Algorithm | Parameters |\n * |----------|-----------|------------|\n * | Key derivation | PBKDF2-SHA256 | 600,000 iterations, 32-byte salt |\n * | Record encryption | AES-256-GCM | 12-byte random IV per operation |\n * | DEK wrapping | AES-KW (RFC 3394) | 256-bit KEK |\n * | Binary encrypt | AES-256-GCM | same as record encryption |\n * | Integrity | HMAC-SHA256 | for presence channels |\n * | Content hash | SHA-256 | for ledger and bundle integrity |\n *\n * ## Key lifecycle\n *\n * ```\n * passphrase + salt\n * └─► deriveKey() → KEK (CryptoKey, extractable: false)\n * └─► wrapKey() → wrapped DEK bytes [stored in keyring]\n * └─► unwrapKey() → DEK (CryptoKey) [memory only during session]\n * └─► encrypt() / decrypt() → ciphertext / plaintext\n * ```\n *\n * IVs are generated fresh by {@link generateIV} on every encrypt call.\n * Reusing an IV with the same key would break GCM's authentication guarantee —\n * this function should be the only place IVs are produced.\n *\n * @module\n */\n\nimport { DecryptionError, InvalidKeyError, TamperedError } from './errors.js'\n\nconst PBKDF2_ITERATIONS = 600_000\nconst SALT_BYTES = 32\nconst IV_BYTES = 12\nconst KEY_BITS = 256\n\nconst subtle = globalThis.crypto.subtle\n\n// ─── Key Derivation ────────────────────────────────────────────────────\n\n/** Derive a KEK from a passphrase and salt using PBKDF2-SHA256. */\nexport async function deriveKey(\n passphrase: string,\n salt: Uint8Array,\n): Promise<CryptoKey> {\n const keyMaterial = await subtle.importKey(\n 'raw',\n new TextEncoder().encode(passphrase),\n 'PBKDF2',\n false,\n ['deriveKey'],\n )\n\n return subtle.deriveKey(\n {\n name: 'PBKDF2',\n salt: salt as BufferSource,\n iterations: PBKDF2_ITERATIONS,\n hash: 'SHA-256',\n },\n keyMaterial,\n { name: 'AES-KW', length: KEY_BITS },\n false,\n ['wrapKey', 'unwrapKey'],\n )\n}\n\n// ─── DEK Generation ────────────────────────────────────────────────────\n\n/** Generate a random AES-256-GCM data encryption key. */\nexport async function generateDEK(): Promise<CryptoKey> {\n return subtle.generateKey(\n { name: 'AES-GCM', length: KEY_BITS },\n true, // extractable — needed for AES-KW wrapping\n ['encrypt', 'decrypt'],\n )\n}\n\n// ─── Key Wrapping ──────────────────────────────────────────────────────\n\n/** Wrap (encrypt) a DEK with a KEK using AES-KW. Returns base64 string. */\nexport async function wrapKey(dek: CryptoKey, kek: CryptoKey): Promise<string> {\n const wrapped = await subtle.wrapKey('raw', dek, kek, 'AES-KW')\n return bufferToBase64(wrapped)\n}\n\n/** Unwrap (decrypt) a DEK from base64 string using a KEK. */\nexport async function unwrapKey(\n wrappedBase64: string,\n kek: CryptoKey,\n): Promise<CryptoKey> {\n try {\n return await subtle.unwrapKey(\n 'raw',\n base64ToBuffer(wrappedBase64) as BufferSource,\n kek,\n 'AES-KW',\n { name: 'AES-GCM', length: KEY_BITS },\n true,\n ['encrypt', 'decrypt'],\n )\n } catch {\n throw new InvalidKeyError()\n }\n}\n\n// ─── Per-record CEK wrapping ───────────────────────────────────────────\n\n/**\n * Derive a **dedicated** AES-KW wrapping key from a collection/tier DEK via\n * HKDF-SHA256, used to wrap/unwrap a per-record CEK.\n *\n * The collection/tier DEKs are AES-256-**GCM** keys (usages\n * `encrypt`/`decrypt`) and cannot themselves act as an AES-KW KEK. We do NOT\n * re-import the raw DEK bytes directly as an AES-KW key — that would reuse one\n * key across two primitives (AES-GCM for `_det`/presence, AES-KW for CEK\n * wrapping), violating key separation. Instead we HKDF-derive a domain-\n * separated KW key (salt `noydb-cek-wrap`), exactly as `derivePresenceKey`\n * derives a separate presence key. The derivation is deterministic, so\n * wrapping the same CEK under the same DEK always yields identical ciphertext\n * — which is what lets an update reuse a record's stable CEK and produce a\n * byte-identical `_cek`. The KW key is independent of the DEK's GCM key.\n *\n * The DEK must be extractable (it is — `generateDEK`/`unwrapKey` mint\n * extractable keys).\n */\nasync function asKwKey(dek: CryptoKey): Promise<CryptoKey> {\n const rawDek = await subtle.exportKey('raw', dek)\n const hkdfKey = await subtle.importKey('raw', rawDek, 'HKDF', false, ['deriveBits'])\n const salt = new TextEncoder().encode('noydb-cek-wrap')\n const info = new TextEncoder().encode('v1')\n const bits = await subtle.deriveBits(\n { name: 'HKDF', hash: 'SHA-256', salt, info },\n hkdfKey,\n KEY_BITS,\n )\n return subtle.importKey('raw', bits, 'AES-KW', false, ['wrapKey', 'unwrapKey'])\n}\n\n/**\n * AES-KW-wrap a per-record CEK under a collection/tier DEK. Returns base64.\n * Deterministic over `(cek, dek)`.\n */\nexport async function wrapCek(cek: CryptoKey, dek: CryptoKey): Promise<string> {\n const kw = await asKwKey(dek)\n const wrapped = await subtle.wrapKey('raw', cek, kw, 'AES-KW')\n return bufferToBase64(wrapped)\n}\n\n/**\n * Unwrap a per-record CEK from base64 under a collection/tier DEK. Throws\n * `InvalidKeyError` if the wrapped bytes do not authenticate under the DEK.\n */\nexport async function unwrapCek(wrappedBase64: string, dek: CryptoKey): Promise<CryptoKey> {\n const kw = await asKwKey(dek)\n try {\n return await subtle.unwrapKey(\n 'raw',\n base64ToBuffer(wrappedBase64) as BufferSource,\n kw,\n 'AES-KW',\n { name: 'AES-GCM', length: KEY_BITS },\n true,\n ['encrypt', 'decrypt'],\n )\n } catch {\n throw new InvalidKeyError()\n }\n}\n\n// ─── Encrypt / Decrypt ─────────────────────────────────────────────────\n\nexport interface EncryptResult {\n iv: string // base64\n data: string // base64\n}\n\n/** Encrypt plaintext JSON string with AES-256-GCM. Fresh IV per call. */\nexport async function encrypt(\n plaintext: string,\n dek: CryptoKey,\n): Promise<EncryptResult> {\n const iv = generateIV()\n const encoded = new TextEncoder().encode(plaintext)\n\n const ciphertext = await subtle.encrypt(\n { name: 'AES-GCM', iv: iv as BufferSource },\n dek,\n encoded,\n )\n\n return {\n iv: bufferToBase64(iv),\n data: bufferToBase64(ciphertext),\n }\n}\n\n/** Decrypt AES-256-GCM ciphertext. Throws on wrong key or tampered data. */\nexport async function decrypt(\n ivBase64: string,\n dataBase64: string,\n dek: CryptoKey,\n): Promise<string> {\n const iv = base64ToBuffer(ivBase64)\n const ciphertext = base64ToBuffer(dataBase64)\n\n try {\n const plaintext = await subtle.decrypt(\n { name: 'AES-GCM', iv: iv as BufferSource },\n dek,\n ciphertext as BufferSource,\n )\n return new TextDecoder().decode(plaintext)\n } catch (err) {\n if (err instanceof Error && err.name === 'OperationError') {\n throw new TamperedError()\n }\n throw new DecryptionError(\n err instanceof Error ? err.message : 'Decryption failed',\n )\n }\n}\n\n// ─── Binary Encrypt / Decrypt ────────\n\n/**\n * Encrypt raw bytes with AES-256-GCM using a fresh random IV.\n * Used by the attachment store so binary blobs avoid double base64 encoding\n * (the existing `encrypt()` function calls `TextEncoder` on a string — here\n * we pass the `Uint8Array` directly to `subtle.encrypt`).\n */\nexport async function encryptBytes(\n data: Uint8Array,\n dek: CryptoKey,\n): Promise<EncryptResult> {\n const iv = generateIV()\n const ciphertext = await subtle.encrypt(\n { name: 'AES-GCM', iv: iv as BufferSource },\n dek,\n data as unknown as BufferSource,\n )\n return {\n iv: bufferToBase64(iv),\n data: bufferToBase64(ciphertext),\n }\n}\n\n/**\n * Decrypt AES-256-GCM ciphertext back to raw bytes.\n * Counterpart to `encryptBytes`. Throws `TamperedError` on auth-tag failure.\n */\nexport async function decryptBytes(\n ivBase64: string,\n dataBase64: string,\n dek: CryptoKey,\n): Promise<Uint8Array> {\n const iv = base64ToBuffer(ivBase64)\n const ciphertext = base64ToBuffer(dataBase64)\n try {\n const plaintext = await subtle.decrypt(\n { name: 'AES-GCM', iv: iv as BufferSource },\n dek,\n ciphertext as BufferSource,\n )\n return new Uint8Array(plaintext)\n } catch (err) {\n if (err instanceof Error && err.name === 'OperationError') {\n throw new TamperedError()\n }\n throw new DecryptionError(\n err instanceof Error ? err.message : 'Decryption failed',\n )\n }\n}\n\n/**\n * SHA-256 hex digest of raw bytes. Used to derive content-addressed\n * eTags for blob deduplication. Computed on plaintext bytes\n * before compression and encryption so the eTag identifies content, not\n * ciphertext, and survives re-encryption (key rotation, re-upload).\n */\nexport async function sha256Hex(data: Uint8Array): Promise<string> {\n const hash = await subtle.digest('SHA-256', data as unknown as BufferSource)\n return Array.from(new Uint8Array(hash))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('')\n}\n\n// ─── HMAC-SHA-256 ─────────────────────────────\n\n/**\n * Compute HMAC-SHA-256(key, data) and return hex string.\n *\n * Used to derive content-addressed eTags that are opaque to the store:\n * ```\n * eTag = hmacSha256Hex(blobDEK, plaintext)\n * ```\n *\n * Unlike a plain SHA-256, the HMAC is keyed by the vault-shared `_blob` DEK,\n * so an attacker with store access cannot pre-compute eTags for known files.\n * Deduplication still works within a vault (same key + same content = same eTag).\n */\nexport async function hmacSha256Hex(key: CryptoKey, data: Uint8Array): Promise<string> {\n // Export AES-GCM DEK raw bytes → import as HMAC key\n const rawKey = await subtle.exportKey('raw', key)\n const hmacKey = await subtle.importKey(\n 'raw',\n rawKey,\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n )\n const sig = await subtle.sign('HMAC', hmacKey, data as unknown as BufferSource)\n return Array.from(new Uint8Array(sig))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('')\n}\n\n// ─── AAD-aware Binary Encrypt / Decrypt ──\n\n/**\n * Encrypt raw bytes with AES-256-GCM using Additional Authenticated Data.\n *\n * The AAD binds each chunk to its parent blob and position, preventing\n * chunk reorder, substitution, and truncation attacks:\n * ```\n * AAD = UTF-8(\"{eTag}:{chunkIndex}:{chunkCount}\")\n * ```\n *\n * The AAD is NOT stored — the reader reconstructs it from `BlobObject`\n * metadata and passes it to `decryptBytesWithAAD`.\n */\nexport async function encryptBytesWithAAD(\n data: Uint8Array,\n dek: CryptoKey,\n aad: Uint8Array,\n): Promise<EncryptResult> {\n const iv = generateIV()\n const ciphertext = await subtle.encrypt(\n {\n name: 'AES-GCM',\n iv: iv as BufferSource,\n additionalData: aad as BufferSource,\n },\n dek,\n data as unknown as BufferSource,\n )\n return {\n iv: bufferToBase64(iv),\n data: bufferToBase64(ciphertext),\n }\n}\n\n/**\n * Decrypt AES-256-GCM ciphertext with AAD verification.\n *\n * If the AAD does not match the one used at encryption time (e.g. because\n * a chunk was reordered or substituted from another blob), the GCM auth\n * tag fails and this throws `TamperedError`.\n */\nexport async function decryptBytesWithAAD(\n ivBase64: string,\n dataBase64: string,\n dek: CryptoKey,\n aad: Uint8Array,\n): Promise<Uint8Array> {\n const iv = base64ToBuffer(ivBase64)\n const ciphertext = base64ToBuffer(dataBase64)\n try {\n const plaintext = await subtle.decrypt(\n {\n name: 'AES-GCM',\n iv: iv as BufferSource,\n additionalData: aad as BufferSource,\n },\n dek,\n ciphertext as BufferSource,\n )\n return new Uint8Array(plaintext)\n } catch (err) {\n if (err instanceof Error && err.name === 'OperationError') {\n throw new TamperedError()\n }\n throw new DecryptionError(\n err instanceof Error ? err.message : 'Decryption failed',\n )\n }\n}\n\n// ─── Presence Key Derivation ──────────────────────────────\n\n/**\n * Derive an AES-256-GCM presence key from a collection DEK using HKDF-SHA256.\n *\n * The presence key is domain-separated from the data DEK by the fixed salt\n * `'noydb-presence'` and the `info` = collection name. This means:\n * - The adapter never sees the presence key.\n * - Presence payloads rotate automatically when the collection DEK is rotated.\n * - Revoked users cannot derive the new presence key after a DEK rotation.\n *\n * @param dek The collection's AES-256-GCM DEK (extractable).\n * @param collectionName Used as the HKDF `info` parameter for domain separation.\n * @returns A non-extractable AES-256-GCM key suitable for presence payload encryption.\n */\nexport async function derivePresenceKey(dek: CryptoKey, collectionName: string): Promise<CryptoKey> {\n // Step 1: export DEK raw bytes\n const rawDek = await subtle.exportKey('raw', dek)\n\n // Step 2: import as HKDF key material\n const hkdfKey = await subtle.importKey(\n 'raw',\n rawDek,\n 'HKDF',\n false,\n ['deriveBits'],\n )\n\n // Step 3: derive 256 bits with salt='noydb-presence' and info=collectionName\n const salt = new TextEncoder().encode('noydb-presence')\n const info = new TextEncoder().encode(collectionName)\n const bits = await subtle.deriveBits(\n { name: 'HKDF', hash: 'SHA-256', salt, info },\n hkdfKey,\n KEY_BITS,\n )\n\n // Step 4: import derived bits as AES-GCM key\n return subtle.importKey(\n 'raw',\n bits,\n { name: 'AES-GCM', length: KEY_BITS },\n false,\n ['encrypt', 'decrypt'],\n )\n}\n\n// ─── Deterministic Encryption ────────────────────────────\n\n/**\n * Derive a deterministic 12-byte IV from `{ DEK, context, plaintext }`\n * via HKDF-SHA256. Given the same three inputs, the IV is identical, so\n * `encryptDeterministic` produces the same ciphertext on every call —\n * which is precisely what enables blind equality search on encrypted\n * fields.\n *\n * **The side channel this opens.** Two records whose field value is the\n * same produce the same ciphertext. An observer with store access can\n * therefore tell which records share a value — not *what* the value is,\n * but the equivalence class. This is the well-known trade-off of\n * deterministic encryption and is why the feature is strictly opt-in\n * per field, guarded by `acknowledgeDeterministicRisk: true` at\n * collection creation.\n *\n * The context string MUST include the collection name and field name,\n * so:\n * - The same plaintext in two different fields encrypts differently\n * (no cross-field equality leak).\n * - The same plaintext in two different collections (different DEKs)\n * encrypts differently by virtue of the key, even before HKDF\n * domain separation kicks in.\n */\nasync function deriveDeterministicIV(\n dek: CryptoKey,\n context: string,\n plaintext: string,\n): Promise<Uint8Array> {\n const rawDek = await subtle.exportKey('raw', dek)\n const hkdfKey = await subtle.importKey('raw', rawDek, 'HKDF', false, ['deriveBits'])\n const salt = new TextEncoder().encode('noydb-deterministic-v1')\n const info = new TextEncoder().encode(`${context}\\x00${plaintext}`)\n const bits = await subtle.deriveBits(\n { name: 'HKDF', hash: 'SHA-256', salt, info },\n hkdfKey,\n IV_BYTES * 8,\n )\n return new Uint8Array(bits)\n}\n\n/**\n * Encrypt a plaintext string with AES-256-GCM and a deterministic,\n * HKDF-derived IV.\n *\n * The same `{ dek, context, plaintext }` triple always produces the\n * same `{ iv, data }` — call this twice and you can string-compare the\n * ciphertexts to check equality of the inputs without decrypting them.\n *\n * @param context Domain-separation string — by convention\n * `'<collection>/<field>'`. Different contexts encrypt\n * the same plaintext to different ciphertexts, so\n * `email` in collection `users` does not collide with\n * `email` in collection `customers`.\n */\nexport async function encryptDeterministic(\n plaintext: string,\n dek: CryptoKey,\n context: string,\n): Promise<EncryptResult> {\n const iv = await deriveDeterministicIV(dek, context, plaintext)\n const encoded = new TextEncoder().encode(plaintext)\n const ciphertext = await subtle.encrypt(\n { name: 'AES-GCM', iv: iv as BufferSource },\n dek,\n encoded,\n )\n return {\n iv: bufferToBase64(iv),\n data: bufferToBase64(ciphertext),\n }\n}\n\n/**\n * Counterpart to {@link encryptDeterministic}. The IV is stored\n * alongside the ciphertext (exactly like the randomized path), so\n * decrypt uses the stored IV and verifies the GCM auth tag — a tampered\n * ciphertext throws `TamperedError` just like randomized AES-GCM.\n */\nexport async function decryptDeterministic(\n ivBase64: string,\n dataBase64: string,\n dek: CryptoKey,\n): Promise<string> {\n return decrypt(ivBase64, dataBase64, dek)\n}\n\n// ─── Random Generation ─────────────────────────────────────────────────\n\n/** Generate a random 12-byte IV for AES-GCM. */\nexport function generateIV(): Uint8Array {\n return globalThis.crypto.getRandomValues(new Uint8Array(IV_BYTES))\n}\n\n/** Generate a random 32-byte salt for PBKDF2. */\nexport function generateSalt(): Uint8Array {\n return globalThis.crypto.getRandomValues(new Uint8Array(SALT_BYTES))\n}\n\n// ─── Base64 Helpers ────────────────────────────────────────────────────\n\nexport function bufferToBase64(buffer: ArrayBuffer | Uint8Array): string {\n const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer)\n let binary = ''\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]!)\n }\n return btoa(binary)\n}\n\nexport function base64ToBuffer(base64: string): Uint8Array<ArrayBuffer> {\n const binary = atob(base64)\n const bytes = new Uint8Array(binary.length)\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i)\n }\n return bytes\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
evaluateClause,
|
|
3
3
|
readPath
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-U2XSUCDF.js";
|
|
5
5
|
import {
|
|
6
6
|
IndexRequiredError
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-OTWT6BAJ.js";
|
|
8
8
|
|
|
9
9
|
// src/indexing/persisted-indexes.ts
|
|
10
10
|
var IDX_PREFIX = "_idx/";
|
|
@@ -182,7 +182,7 @@ var PersistedCollectionIndex = class {
|
|
|
182
182
|
}
|
|
183
183
|
/**
|
|
184
184
|
* Sorted iteration — return every entry on `field` as an
|
|
185
|
-
* `OrderedEntry[]`, sorted by the ORIGINAL TYPED value (
|
|
185
|
+
* `OrderedEntry[]`, sorted by the ORIGINAL TYPED value (no more
|
|
186
186
|
* `'10' < '2'` surprises on numeric fields). Consumers paginate with
|
|
187
187
|
* a numeric offset. `OrderedEntry.value` is the typed value.
|
|
188
188
|
*/
|
|
@@ -427,4 +427,4 @@ export {
|
|
|
427
427
|
PersistedCollectionIndex,
|
|
428
428
|
LazyQuery
|
|
429
429
|
};
|
|
430
|
-
//# sourceMappingURL=chunk-
|
|
430
|
+
//# sourceMappingURL=chunk-3HNKR65T.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/indexing/persisted-indexes.ts","../src/indexing/lazy-builder.ts"],"sourcesContent":["/**\n * Persistent, encrypted secondary indexes for lazy-mode collections.\n *\n * Parallel to the in-memory `CollectionIndexes` used by eager mode (see\n * `packages/hub/src/query/indexes.ts`): same logical surface, but entries\n * are materialised as encrypted side-car records (`_idx/<field>/<recordId>`)\n * and bulk-loaded into an in-memory mirror on first query.\n *\n * This module only owns the id-namespace convention, the in-memory mirror,\n * and the typed errors. Write-path integration (PR 2 / ), query-planner\n * dispatch (PR 3 / , PR 4 / ), and the rebuild/reconcile utilities\n * (PR 5 / ) live in other files.\n *\n * See the design spec for the full architecture + threat model.\n */\n\n/**\n * Reserved id prefix for encrypted index side-car records.\n * Matches the existing `_keyring`, `_ledger_deltas/…`, `_meta/handle`\n * conventions inside a collection's id namespace.\n */\nexport const IDX_PREFIX = '_idx/' as const\n\n/**\n * Encode the side-car record id for a (field, recordId) pair.\n *\n * Format: `_idx/<field>/<recordId>` — no escaping. Field names may contain\n * dots (for dotted-path access consistent with eager-mode `readPath`);\n * record ids may contain slashes. The first two slash-separated segments\n * are `_idx` and the field; everything after the *second* slash is the\n * record id verbatim.\n */\nexport function encodeIdxId(field: string, recordId: string): string {\n return `${IDX_PREFIX}${field}/${recordId}`\n}\n\n/**\n * Decode a side-car id back into `{ field, recordId }`, or `null` if the\n * input is not a well-formed idx id. A well-formed id is:\n * - prefixed with `_idx/`\n * - contains a field segment (non-empty, no slashes)\n * - contains a record-id segment (non-empty, may contain slashes)\n */\nexport function decodeIdxId(id: string): { field: string; recordId: string } | null {\n if (!id.startsWith(IDX_PREFIX)) return null\n const rest = id.slice(IDX_PREFIX.length)\n const firstSlash = rest.indexOf('/')\n if (firstSlash <= 0) return null\n const field = rest.slice(0, firstSlash)\n const recordId = rest.slice(firstSlash + 1)\n if (recordId.length === 0) return null\n return { field, recordId }\n}\n\n/**\n * Fast-path predicate for discriminating side-car ids from regular record\n * ids and other reserved namespaces. Used by the hub to filter `list()`\n * results during bulk-load of the in-memory mirror.\n */\nexport function isIdxId(id: string): boolean {\n return decodeIdxId(id) !== null\n}\n\n/**\n * Sorted-value entry returned by `orderedBy()`. Mirrors the body shape\n * used by the write path — but `orderedBy` emits them already sorted by\n * `value` in the requested direction. Consumers (PR 4 / ) treat the\n * array as immutable and paginate via a numeric offset.\n *\n * **Note on `value`:** as of, this is the ORIGINAL TYPED\n * value (number, Date, boolean, etc.), not the stringified bucket key.\n * That's what lets range predicates and `orderedBy` compare numerically\n * instead of stumbling into `'10' < '2'` on `String(n)`.\n */\nexport interface OrderedEntry {\n readonly recordId: string\n readonly value: unknown\n}\n\n/**\n * Bulk-load row shape accepted by `ingest()`. The `value` field is the\n * decrypted index body's `value` field verbatim.\n */\nexport interface IngestRow {\n readonly recordId: string\n readonly value: unknown\n}\n\n/**\n * In-memory mirror of the persisted index side-car records for a single\n * collection. Populated by bulk-loading `_idx/<field>/*` ids on first\n * query and maintained incrementally by `Collection.put()` / `.delete()`\n * via `upsert()` / `remove()`.\n *\n * API surface is deliberately parallel to `CollectionIndexes` (eager mode)\n * so the query planner in PR 3/4 can dispatch to either polymorphically.\n *\n * Lifecycle:\n * - `declare(field)` — accept the field as indexable (idempotent)\n * - `ingest(field, rows[])` — bulk-load from decrypted index bodies\n * - `upsert(recordId, field, newValue, previousValue)` — incremental update\n * - `remove(recordId, field, value)` — incremental remove\n * - `lookupEqual(field, value)` / `lookupIn(field, values)` — equality reads\n * - `orderedBy(field, dir)` — sorted iteration for orderBy\n * - `clear()` — drop all buckets (invalidation / rotation)\n */\n/**\n * Per-field storage: the equality bucket map AND a parallel table of typed\n * values keyed by recordId. The typed table exists so range predicates\n * and `orderedBy` can compare on the original typed value rather\n * than the stringified bucket key — String(10) < String(2) is the classic\n * landmine `stringifyKey` introduces for numeric fields.\n */\ninterface PersistedFieldState {\n readonly buckets: Map<string, Set<string>>\n readonly values: Map<string, unknown>\n}\n\n/**\n * Structured index definition. Single-field indexes carry just a field\n * name; composite indexes carry the ordered list of fields and\n * the synthetic `key` (= fields joined by `COMPOSITE_DELIMITER`) used\n * as the bucket-map key and side-car envelope id segment.\n */\nexport type PersistedIndexDef =\n | { readonly kind: 'single'; readonly field: string; readonly key: string }\n | { readonly kind: 'composite'; readonly fields: readonly string[]; readonly key: string }\n\n/**\n * Delimiter used to synthesize a composite-index key from an ordered\n * field list. Intentionally a character that is extremely unusual in\n * JavaScript object keys (`|`) so collision with a literal field name\n * is vanishingly rare in practice. Composite declarations whose field\n * names contain `|` are rejected at declare-time with an explicit\n * error.\n */\nexport const COMPOSITE_DELIMITER = '|'\n\nexport function compositeKey(fields: readonly string[]): string {\n return fields.join(COMPOSITE_DELIMITER)\n}\n\nexport class PersistedCollectionIndex {\n private readonly indexes = new Map<string, PersistedFieldState>()\n private readonly defs = new Map<string, PersistedIndexDef>()\n\n /**\n * Declare a single-field index. Subsequent `upsert` / `ingest` calls\n * populate the in-memory mirror; calls before `declare` are no-ops\n * (tolerant bulk-load ordering). Idempotent.\n */\n declare(field: string): void {\n if (this.indexes.has(field)) return\n this.indexes.set(field, { buckets: new Map(), values: new Map() })\n this.defs.set(field, { kind: 'single', field, key: field })\n }\n\n /**\n * Declare a composite (multi-field) index. The synthetic\n * key is `fields.join('|')`; it doubles as the in-memory map key and\n * the `_idx/<key>/<recordId>` side-car field segment. Callers upsert\n * and lookup via the same `key` as single-field indexes, just with a\n * tuple value (JSON-stringified for bucketing).\n */\n declareComposite(fields: readonly string[]): void {\n if (fields.length === 0) {\n throw new Error('declareComposite: fields array must be non-empty')\n }\n for (const f of fields) {\n if (f.includes(COMPOSITE_DELIMITER)) {\n throw new Error(\n `declareComposite: field \"${f}\" contains the composite delimiter ` +\n `\"${COMPOSITE_DELIMITER}\" — pick a different field name or open an ` +\n `issue to add hash-based composite keys.`,\n )\n }\n }\n const key = compositeKey(fields)\n if (this.indexes.has(key)) return\n this.indexes.set(key, { buckets: new Map(), values: new Map() })\n this.defs.set(key, { kind: 'composite', fields: [...fields], key })\n }\n\n /**\n * Every declared index's structured definition. Collection walks this\n * when materialising side-cars on put/delete so it can extract a\n * single-field value or a composite tuple appropriately.\n */\n definitions(): PersistedIndexDef[] {\n return [...this.defs.values()]\n }\n\n /** True if `field` has been declared as indexable on this mirror. */\n has(field: string): boolean {\n return this.indexes.has(field)\n }\n\n /** All declared field names, in declaration order. */\n fields(): string[] {\n return [...this.indexes.keys()]\n }\n\n /**\n * Bulk-load the mirror from decrypted index bodies. Intended to be\n * called once per field after reading the collection's `_idx/<field>/*`\n * side-cars. Safe to call twice with the same rows — bucket Sets\n * deduplicate recordIds. If `field` is not declared, this is a no-op\n * (tolerates the case where bulk-load runs before `declare()` lands).\n */\n ingest(field: string, rows: readonly IngestRow[]): void {\n const state = this.indexes.get(field)\n if (!state) return\n for (const row of rows) {\n addToState(state, row.recordId, row.value)\n }\n }\n\n /**\n * Incrementally update a record's index entry for one field. Called by\n * `Collection.put()` after the main write succeeds. If\n * `previousValue` is non-null, the record is removed from the old\n * bucket first — this is the update path. Pass `null` for fresh adds.\n * No-op if the field is not declared.\n */\n upsert(recordId: string, field: string, newValue: unknown, previousValue: unknown): void {\n const state = this.indexes.get(field)\n if (!state) return\n if (previousValue !== null && previousValue !== undefined) {\n removeFromState(state, recordId, previousValue)\n }\n addToState(state, recordId, newValue)\n }\n\n /**\n * Remove a record from the index for one field. Called by\n * `Collection.delete()`. No-op if the field is not declared or\n * the record isn't in the bucket. Empty buckets are dropped to keep\n * the Map clean.\n */\n remove(recordId: string, field: string, value: unknown): void {\n const state = this.indexes.get(field)\n if (!state) return\n removeFromState(state, recordId, value)\n }\n\n /**\n * Drop all bucket data while preserving field declarations. Called on\n * invalidation (incoming sync changes, keyring rotation) — the next\n * query re-populates via `ingest`.\n */\n clear(): void {\n for (const state of this.indexes.values()) {\n state.buckets.clear()\n state.values.clear()\n }\n }\n\n /**\n * Equality lookup — return the set of record ids whose `field` matches\n * `value`. Returns `null` if the field is not declared (caller falls\n * back to scan or throws `IndexRequiredError`). Returns a shared empty\n * set if the field is declared but no record matches — that set MUST\n * NOT be mutated by the caller.\n */\n lookupEqual(field: string, value: unknown): ReadonlySet<string> | null {\n const state = this.indexes.get(field)\n if (!state) return null\n const key = stringifyKey(value)\n return state.buckets.get(key) ?? EMPTY_SET\n }\n\n /**\n * Set lookup — return the union of record ids whose `field` matches any\n * of `values`. Returns `null` if the field is not declared. Returns a\n * fresh (non-shared) Set — safe for the caller to mutate.\n */\n lookupIn(field: string, values: readonly unknown[]): ReadonlySet<string> | null {\n const state = this.indexes.get(field)\n if (!state) return null\n const out = new Set<string>()\n for (const value of values) {\n const bucket = state.buckets.get(stringifyKey(value))\n if (bucket) for (const id of bucket) out.add(id)\n }\n return out\n }\n\n /**\n * Range lookup. Return record ids whose indexed value\n * satisfies the predicate. Comparison happens on the ORIGINAL TYPED\n * value carried in `state.values` — so numeric `<` sorts numerically,\n * not lexicographically on `String(n)`. Returns `null` if the field\n * is not declared.\n *\n * Supported ops: `'<'`, `'<='`, `'>'`, `'>='`, `'between'`. For\n * `'between'`, `value` is `[lo, hi]` and both bounds are inclusive\n * (matches the eager-mode operator contract in `predicate.ts`).\n */\n lookupRange(\n field: string,\n op: '<' | '<=' | '>' | '>=' | 'between',\n value: unknown,\n ): ReadonlySet<string> | null {\n const state = this.indexes.get(field)\n if (!state) return null\n const out = new Set<string>()\n for (const [recordId, live] of state.values) {\n if (live === undefined || live === null) continue\n if (matchesRange(live, op, value)) out.add(recordId)\n }\n return out\n }\n\n /**\n * Sorted iteration — return every entry on `field` as an\n * `OrderedEntry[]`, sorted by the ORIGINAL TYPED value (no more\n * `'10' < '2'` surprises on numeric fields). Consumers paginate with\n * a numeric offset. `OrderedEntry.value` is the typed value.\n */\n orderedBy(field: string, dir: 'asc' | 'desc'): readonly OrderedEntry[] | null {\n const state = this.indexes.get(field)\n if (!state) return null\n const entries: OrderedEntry[] = []\n for (const [recordId, value] of state.values) {\n entries.push({ recordId, value })\n }\n entries.sort((a, b) => compareTyped(a.value, b.value))\n if (dir === 'desc') entries.reverse()\n return entries\n }\n}\n\nconst EMPTY_SET: ReadonlySet<string> = new Set()\n\n/**\n * Canonicalize a value into a bucket key. Deliberately identical to the\n * eager-mode `stringifyKey` in `query/indexes.ts` so semantics match. When\n * `query/indexes.ts` changes its coercion rules, update this in lockstep.\n *\n * null / undefined values are NOT indexed — callers who pass them to\n * `upsert` / `remove` short-circuit before reaching this function; the\n * sentinel here exists only to make `lookupEqual(field, null)` return\n * an empty bucket (rather than matching some arbitrary record).\n */\nfunction stringifyKey(value: unknown): string {\n if (value === null || value === undefined) return '\\0NULL\\0'\n if (typeof value === 'string') return value\n if (typeof value === 'number' || typeof value === 'boolean') return String(value)\n if (value instanceof Date) return value.toISOString()\n // composite index values are tuple arrays. JSON.stringify\n // gives a delimiter-safe, order-preserving canonical form so buckets\n // for `['c-A', '2026-Q1']` and `['c-A', '2026-Q2']` never collide.\n if (Array.isArray(value)) {\n const parts: string[] = []\n for (const el of value) parts.push(stringifyKey(el))\n return JSON.stringify(parts)\n }\n return '\\0OBJECT\\0'\n}\n\nfunction addToState(state: PersistedFieldState, recordId: string, value: unknown): void {\n if (value === null || value === undefined) return\n const key = stringifyKey(value)\n let bucket = state.buckets.get(key)\n if (!bucket) {\n bucket = new Set()\n state.buckets.set(key, bucket)\n }\n bucket.add(recordId)\n state.values.set(recordId, value)\n}\n\nfunction removeFromState(state: PersistedFieldState, recordId: string, value: unknown): void {\n if (value === null || value === undefined) return\n const key = stringifyKey(value)\n const bucket = state.buckets.get(key)\n if (bucket) {\n bucket.delete(recordId)\n if (bucket.size === 0) state.buckets.delete(key)\n }\n state.values.delete(recordId)\n}\n\n/**\n * Range-predicate comparator. Runs on the ORIGINAL TYPED value so numeric\n * fields sort numerically (not lexicographically on `String(n)`). ISO-8601\n * date strings already sort correctly lexicographically; Date instances\n * compare via `getTime()` before the string branch to keep the contract\n * honest regardless of which form survived serialization.\n */\nfunction matchesRange(\n live: unknown,\n op: '<' | '<=' | '>' | '>=' | 'between',\n bound: unknown,\n): boolean {\n if (op === 'between') {\n if (!Array.isArray(bound) || bound.length !== 2) return false\n return compareTyped(live, bound[0]) >= 0 && compareTyped(live, bound[1]) <= 0\n }\n const cmp = compareTyped(live, bound)\n switch (op) {\n case '<': return cmp < 0\n case '<=': return cmp <= 0\n case '>': return cmp > 0\n case '>=': return cmp >= 0\n }\n}\n\nfunction compareTyped(a: unknown, b: unknown): number {\n if (a === undefined || a === null) return b === undefined || b === null ? 0 : 1\n if (b === undefined || b === null) return -1\n if (typeof a === 'number' && typeof b === 'number') return a - b\n if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime()\n if (typeof a === 'string' && typeof b === 'string') return a < b ? -1 : a > b ? 1 : 0\n if (typeof a === 'boolean' && typeof b === 'boolean') {\n return a === b ? 0 : a ? 1 : -1\n }\n // Mixed/unsupported types: deliberately treat as equal so sort stays\n // stable. Matches the eager-mode `compareValues` contract in\n // builder.ts — we don't silently coerce arbitrary objects to strings\n // (which would be meaningless) nor throw (which would be hostile).\n return 0\n}\n","/**\n * Lazy-mode query builder.\n *\n * Companion to `Query<T>` in `builder.ts`, but built for collections in lazy\n * mode where `snapshot()` is unavailable — records live in the adapter and\n * are pulled on demand. Dispatches through `PersistedCollectionIndex` to\n * resolve a candidate record-id set, then decrypts only those records.\n *\n * Scope:\n * - `.where(field, '==' | 'in', value)` — dispatched through the index\n * - `.where(field, other-op, value)` — evaluated against the decrypted\n * candidate set (non-indexed ops still require the field to be indexed\n * — we need SOMETHING to scope the candidate set)\n * - `.orderBy(field, dir?)` — dispatched through `orderedBy` when no\n * `==`/`in` clause is present; otherwise applied as an in-memory sort\n * over the candidate set\n * - `.limit(n)` / `.offset(n)` — page slice after filtering\n * - `.toArray()` / `.first()` / `.count()` — terminals\n *\n * Every field referenced by a where or orderBy clause MUST be indexed;\n * otherwise `toArray()` throws `IndexRequiredError`. This is deliberate:\n * silent scan-fallback would hide the very performance cliff that lazy-mode\n * indexes exist to prevent (see `docs/architecture.md` §indexes).\n */\n\nimport type { Clause, FieldClause, Operator } from '../query/predicate.js'\nimport { evaluateClause, readPath } from '../query/predicate.js'\nimport type { PersistedCollectionIndex } from './persisted-indexes.js'\nimport { IndexRequiredError } from '../errors.js'\n\nexport interface LazyOrderBy {\n readonly field: string\n readonly direction: 'asc' | 'desc'\n}\n\n/**\n * Source abstraction the LazyQuery runs against. Collection implements it.\n * Kept minimal so the builder stays test-friendly.\n */\nexport interface LazyQuerySource<T> {\n readonly collectionName: string\n readonly persistedIndexes: PersistedCollectionIndex\n /** Ensure `_idx/<field>/*` side-cars have been bulk-loaded into the mirror. */\n ensurePersistedIndexesLoaded(): Promise<void>\n /** Decrypt one record by id, or return null if it's gone. */\n getRecord(id: string): Promise<T | null>\n}\n\ninterface LazyPlan {\n readonly clauses: readonly FieldClause[]\n readonly orderBy: readonly LazyOrderBy[]\n readonly limit: number | undefined\n readonly offset: number\n}\n\nconst EMPTY_PLAN: LazyPlan = {\n clauses: [],\n orderBy: [],\n limit: undefined,\n offset: 0,\n}\n\nexport class LazyQuery<T> {\n private readonly source: LazyQuerySource<T>\n private readonly plan: LazyPlan\n\n constructor(source: LazyQuerySource<T>, plan: LazyPlan = EMPTY_PLAN) {\n this.source = source\n this.plan = plan\n }\n\n where<V>(field: string, op: Operator, value: V): LazyQuery<T> {\n const clause: FieldClause = { type: 'field', field, op, value }\n return new LazyQuery<T>(this.source, {\n ...this.plan,\n clauses: [...this.plan.clauses, clause],\n })\n }\n\n orderBy(field: string, direction: 'asc' | 'desc' = 'asc'): LazyQuery<T> {\n return new LazyQuery<T>(this.source, {\n ...this.plan,\n orderBy: [...this.plan.orderBy, { field, direction }],\n })\n }\n\n limit(n: number): LazyQuery<T> {\n return new LazyQuery<T>(this.source, { ...this.plan, limit: n })\n }\n\n offset(n: number): LazyQuery<T> {\n return new LazyQuery<T>(this.source, { ...this.plan, offset: n })\n }\n\n async toArray(): Promise<T[]> {\n await this.source.ensurePersistedIndexesLoaded()\n\n const touchedFields = collectTouchedFields(this.plan)\n const missingFields = touchedFields.filter(f => !isFieldIndexed(f, this.source.persistedIndexes))\n if (missingFields.length > 0) {\n throw new IndexRequiredError({\n collection: this.source.collectionName,\n touchedFields,\n missingFields,\n })\n }\n\n const candidateIds = this.resolveCandidateIds()\n if (candidateIds === null) {\n // No usable driver — every touched field is indexed but no clause\n // pins the candidate set. This happens when a query only uses\n // operators other than `==`/`in` and no `orderBy` clause is\n // present — we refuse to enumerate the whole index, because that\n // defeats the purpose of lazy mode.\n throw new IndexRequiredError({\n collection: this.source.collectionName,\n touchedFields,\n missingFields: touchedFields,\n })\n }\n\n const records: T[] = []\n for (const id of candidateIds) {\n const record = await this.source.getRecord(id)\n if (record === null) continue\n if (!matchesAll(record, this.plan.clauses)) continue\n records.push(record)\n }\n\n const sorted = this.plan.orderBy.length > 0\n ? sortRecords(records, this.plan.orderBy)\n : records\n\n const offset = this.plan.offset > 0 ? this.plan.offset : 0\n const limited = this.plan.limit === undefined\n ? sorted.slice(offset)\n : sorted.slice(offset, offset + this.plan.limit)\n\n return limited\n }\n\n async first(): Promise<T | null> {\n const out = await this.limit(1).toArray()\n return out.length > 0 ? out[0]! : null\n }\n\n async count(): Promise<number> {\n const out = await this.toArray()\n return out.length\n }\n\n /**\n * Resolve the candidate record-id set to decrypt. Returns null when the\n * query has no usable driver — no `==`/`in` clause and no `orderBy`\n * clause that can scope the scan. Callers interpret null as\n * IndexRequiredError (see `toArray`).\n */\n private resolveCandidateIds(): readonly string[] | null {\n const idx = this.source.persistedIndexes\n\n // prefer a composite index when the query's `==`\n // clauses cover every field of one declared composite. The\n // composite mirror lookup is O(matches) vs single-field +\n // post-filter on the decrypted candidate set.\n const eqMap = new Map<string, unknown>()\n for (const clause of this.plan.clauses) {\n if (clause.op === '==') eqMap.set(clause.field, clause.value)\n }\n if (eqMap.size >= 2) {\n for (const def of idx.definitions()) {\n if (def.kind !== 'composite') continue\n if (def.fields.every(f => eqMap.has(f))) {\n const tuple = def.fields.map(f => eqMap.get(f))\n const ids = idx.lookupEqual(def.key, tuple)\n if (ids) return [...ids]\n }\n }\n }\n\n for (const clause of this.plan.clauses) {\n if (clause.op === '==') {\n const ids = idx.lookupEqual(clause.field, clause.value)\n if (ids) return [...ids]\n } else if (clause.op === 'in' && Array.isArray(clause.value)) {\n const ids = idx.lookupIn(clause.field, clause.value as readonly unknown[])\n if (ids) return [...ids]\n } else if (isRangeOp(clause.op)) {\n // range predicates on an indexed field dispatch\n // through `lookupRange`, which compares on the original typed\n // value (no numeric-lexicographic landmines).\n const ids = idx.lookupRange(clause.field, clause.op, clause.value)\n if (ids) return [...ids]\n }\n }\n\n // No equality/range driver — try to scope via orderBy.\n if (this.plan.orderBy.length > 0) {\n const primary = this.plan.orderBy[0]!\n const entries = idx.orderedBy(primary.field, primary.direction)\n if (entries) return entries.map(e => e.recordId)\n }\n\n return null\n }\n}\n\n/**\n * True if the given field name is covered by either a single-field\n * index or appears as a component of a declared composite index.\n * Composite coverage is sufficient for the missing-field check because\n * composite writes also maintain the in-memory mirror — the range /\n * orderBy / single-equality lookup paths fall through to decrypted\n * candidates that still get post-filtered by the composite clause.\n */\nfunction isFieldIndexed(field: string, idx: PersistedCollectionIndex): boolean {\n if (idx.has(field)) return true\n for (const def of idx.definitions()) {\n if (def.kind === 'composite' && def.fields.includes(field)) return true\n }\n return false\n}\n\nfunction isRangeOp(op: Operator): op is '<' | '<=' | '>' | '>=' | 'between' {\n return op === '<' || op === '<=' || op === '>' || op === '>=' || op === 'between'\n}\n\nfunction collectTouchedFields(plan: LazyPlan): string[] {\n const seen = new Set<string>()\n for (const c of plan.clauses) seen.add(c.field)\n for (const o of plan.orderBy) seen.add(o.field)\n return [...seen]\n}\n\nfunction matchesAll(record: unknown, clauses: readonly Clause[]): boolean {\n for (const c of clauses) {\n if (!evaluateClause(record, c)) return false\n }\n return true\n}\n\nfunction sortRecords<T>(records: T[], orderBy: readonly LazyOrderBy[]): T[] {\n return [...records].sort((a, b) => {\n for (const { field, direction } of orderBy) {\n const av = readPath(a, field)\n const bv = readPath(b, field)\n const cmp = compareValues(av, bv)\n if (cmp !== 0) return direction === 'asc' ? cmp : -cmp\n }\n return 0\n })\n}\n\nfunction compareValues(a: unknown, b: unknown): number {\n if (a === undefined || a === null) return b === undefined || b === null ? 0 : 1\n if (b === undefined || b === null) return -1\n if (typeof a === 'number' && typeof b === 'number') return a - b\n if (typeof a === 'string' && typeof b === 'string') return a < b ? -1 : a > b ? 1 : 0\n if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime()\n return 0\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
import {
|
|
2
2
|
wrapDbWithPredicates
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-KOAJ3TZM.js";
|
|
4
4
|
import {
|
|
5
5
|
canonicalGroupKey,
|
|
6
6
|
groupAndReduce
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-JYNH4FIM.js";
|
|
8
8
|
import {
|
|
9
|
+
applyI18nLocale
|
|
10
|
+
} from "./chunk-TNH5SLCD.js";
|
|
11
|
+
import {
|
|
12
|
+
LocaleNotSpecifiedError,
|
|
9
13
|
MaterializedViewTooLargeError
|
|
10
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-OTWT6BAJ.js";
|
|
11
15
|
|
|
12
16
|
// src/materialized-views/executor.ts
|
|
13
17
|
var DEFAULT_MAX_ROWS = 1e5;
|
|
14
|
-
async function materializeQueryResult(q, mvName) {
|
|
18
|
+
async function materializeQueryResult(q, mvName, i18nLocale, i18nFields) {
|
|
15
19
|
if (typeof q?.toArray === "function") {
|
|
16
20
|
return await q.toArray();
|
|
17
21
|
}
|
|
18
22
|
if (typeof q?.run === "function") {
|
|
19
|
-
const
|
|
23
|
+
const runOpts = i18nLocale !== void 0 ? { locale: i18nLocale, i18nFields } : void 0;
|
|
24
|
+
const result = await Promise.resolve(q.run(runOpts));
|
|
20
25
|
if (Array.isArray(result)) {
|
|
21
26
|
return result;
|
|
22
27
|
}
|
|
@@ -30,13 +35,44 @@ async function materializeUnionResult(spec, db) {
|
|
|
30
35
|
const unified = [];
|
|
31
36
|
for (const arm of spec.unionSources) {
|
|
32
37
|
const coll = db.collection(arm.collection);
|
|
33
|
-
|
|
38
|
+
let q = coll.query();
|
|
39
|
+
if (arm.join?.length) {
|
|
40
|
+
for (const leg of arm.join) {
|
|
41
|
+
q = q.join(leg.field, { as: leg.as, maxRows: leg.maxRows, strategy: leg.strategy });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const sourceRows = q.toArray();
|
|
34
45
|
for (const r of sourceRows) {
|
|
35
|
-
|
|
46
|
+
const mapped = arm.map(r);
|
|
47
|
+
if (mapped == null) continue;
|
|
48
|
+
unified.push(mapped);
|
|
36
49
|
}
|
|
37
50
|
}
|
|
38
51
|
if (!spec.groupBy) return unified;
|
|
39
52
|
const groupFields = typeof spec.groupBy === "string" ? [spec.groupBy] : spec.groupBy;
|
|
53
|
+
if (spec.i18nLocale !== void 0 && spec.i18nFields !== void 0) {
|
|
54
|
+
const groupI18n = {};
|
|
55
|
+
for (const f of groupFields) {
|
|
56
|
+
const d = spec.i18nFields[f];
|
|
57
|
+
if (d !== void 0) groupI18n[f] = d;
|
|
58
|
+
}
|
|
59
|
+
if (Object.keys(groupI18n).length > 0) {
|
|
60
|
+
for (let i = 0; i < unified.length; i++) {
|
|
61
|
+
unified[i] = applyI18nLocale(unified[i], groupI18n, spec.i18nLocale, void 0, "mv");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
for (const f of groupFields) {
|
|
66
|
+
for (const row of unified) {
|
|
67
|
+
const v = row[f];
|
|
68
|
+
if (v !== null && typeof v === "object") {
|
|
69
|
+
throw new LocaleNotSpecifiedError(
|
|
70
|
+
f,
|
|
71
|
+
`Materialized view "${spec.name}" groups by "${f}", whose value is a raw i18n locale map \u2014 an unstable object group key. Declare { i18nLocale, i18nFields } on the MV to resolve it at the 'mv' layer, or group by a dictKey/staticDict code (the stable key) and resolve the label at read time.`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
40
76
|
if (!spec.aggregate) {
|
|
41
77
|
const seen = /* @__PURE__ */ new Map();
|
|
42
78
|
for (const row of unified) {
|
|
@@ -45,7 +81,7 @@ async function materializeUnionResult(spec, db) {
|
|
|
45
81
|
}
|
|
46
82
|
return [...seen.values()];
|
|
47
83
|
}
|
|
48
|
-
return groupAndReduce(unified, groupFields, spec.aggregate);
|
|
84
|
+
return groupAndReduce(unified, groupFields, spec.aggregate, spec.moneyFields);
|
|
49
85
|
}
|
|
50
86
|
var MaterializedViewExecutor = {
|
|
51
87
|
async refresh(reg, accessor) {
|
|
@@ -61,7 +97,7 @@ var MaterializedViewExecutor = {
|
|
|
61
97
|
rows = await materializeUnionResult(spec, ctxForQuery);
|
|
62
98
|
} else {
|
|
63
99
|
const q = spec.query(ctxForQuery);
|
|
64
|
-
rows = await materializeQueryResult(q, spec.name);
|
|
100
|
+
rows = await materializeQueryResult(q, spec.name, spec.i18nLocale, spec.i18nFields);
|
|
65
101
|
}
|
|
66
102
|
if (rows.length > maxRows) {
|
|
67
103
|
throw new MaterializedViewTooLargeError(spec.name, rows.length, maxRows);
|
|
@@ -142,4 +178,4 @@ async function listOutputIds(outputColl) {
|
|
|
142
178
|
export {
|
|
143
179
|
MaterializedViewExecutor
|
|
144
180
|
};
|
|
145
|
-
//# sourceMappingURL=chunk-
|
|
181
|
+
//# sourceMappingURL=chunk-5YTXYPES.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/materialized-views/executor.ts"],"sourcesContent":["import type { Collection } from '../collection.js'\nimport type { TxContext } from '../tx/transaction.js'\nimport type { EncryptedEnvelope } from '../types.js'\nimport { MaterializedViewTooLargeError, LocaleNotSpecifiedError } from '../errors.js'\nimport type { MaterializedFromMeta, MVQueryContext, MaterializedViewStrategy } from './types.js'\nimport type { RegisteredMV } from './registry.js'\nimport { wrapDbWithPredicates } from './registry.js'\nimport { groupAndReduce } from '../aggregate/groupby.js'\nimport { canonicalGroupKey } from '../aggregate/canonical-key.js'\nimport { applyI18nLocale, type I18nTextDescriptor } from '../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 // #285 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 (#297).\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 // #285 §2/§4 — 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,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DerivationCapExceededError,
|
|
3
3
|
DerivationOutputShapeError
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-OTWT6BAJ.js";
|
|
5
5
|
|
|
6
6
|
// src/derivations/executor.ts
|
|
7
7
|
var DerivationExecutor = {
|
|
@@ -121,4 +121,4 @@ var DerivationExecutor = {
|
|
|
121
121
|
export {
|
|
122
122
|
DerivationExecutor
|
|
123
123
|
};
|
|
124
|
-
//# sourceMappingURL=chunk-
|
|
124
|
+
//# sourceMappingURL=chunk-6QAZ5O6X.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/derivations/executor.ts"],"sourcesContent":["import { DerivationCapExceededError, DerivationOutputShapeError } from '../errors.js'\nimport type { DerivationContext, DerivationStrategy, DerivedFromMeta } from './types.js'\n\nexport interface RunResult {\n outputs: Record<string, OutputResult>\n failed: boolean\n}\n\n/**\n * Per-output result of a strategy invocation. Discriminated by\n * `kind`:\n *\n * - `record` — the existing v1 shape: one value (or a \"skipped\"\n * marker if the output was optional and `derive` returned null).\n * - `array` — a list of `(key, value)` entries.\n * The caller diffs these against the previously-emitted key set\n * (loaded from the fanout sidecar) to compute deletes + upserts.\n */\nexport type OutputResult =\n | RecordOutputResult\n | ArrayOutputResult\n | FailedOutputResult\n\nexport interface RecordOutputResult {\n kind: 'record'\n value: Record<string, unknown>\n ok: true\n /**\n * `true` when an optional output returned `null` /\n * `undefined`. The caller deletes any previously-emitted output at\n * the same id (mirrors \"tombstone for derived data\"); a never-emitted\n * output is a silent no-op. `ok: true` because skipping is a\n * successful outcome, not a failure.\n */\n skipped?: boolean\n}\n\nexport interface ArrayOutputResult {\n kind: 'array'\n ok: true\n /** One `(key, value)` per derived row. Empty array means \"all prior outputs for this source go.\" */\n entries: ReadonlyArray<{ readonly key: string; readonly value: Record<string, unknown> }>\n}\n\nexport interface FailedOutputResult {\n kind: 'failed'\n ok: false\n error: Error\n /** Always empty on failure; present so consumers don't have to narrow. */\n value: Record<string, unknown>\n}\n\n/**\n * Stateless functions that execute a derivation strategy. Persistence\n * (encrypt + store.put) is the caller's job — typically\n * `DerivationRegistry.onSourceWrite` which iterates run() results and\n * writes each output via `Collection.put`.\n */\nexport const DerivationExecutor = {\n /**\n * Run `derive` once, validate output shape against the spec, stamp\n * `_derivedFrom` onto every output. Returns per-output success or\n * failure; throws only for shape mismatches (a contract violation).\n */\n async run<\n TSource extends Record<string, unknown>,\n TOutputs extends Record<string, Record<string, unknown>>,\n >(\n strategy: DerivationStrategy<TSource, TOutputs>,\n source: TSource & { id: string },\n sourceVersion: number,\n strategyHash: string,\n ctx: DerivationContext,\n ): Promise<RunResult> {\n const outputs: Record<string, OutputResult> = {}\n let derived: Partial<TOutputs>\n\n try {\n derived = await Promise.resolve(strategy.derive(source as TSource, ctx))\n } catch (err) {\n for (const key of Object.keys(strategy.outputs)) {\n outputs[key] = {\n kind: 'failed',\n value: {},\n ok: false,\n error: err instanceof Error ? err : new Error(String(err)),\n }\n }\n return { outputs, failed: true }\n }\n\n const meta: DerivedFromMeta = {\n source: strategy.source,\n sourceId: source.id,\n sourceVersion,\n derivedAt: new Date().toISOString(),\n strategyHash,\n }\n\n for (const key of Object.keys(strategy.outputs)) {\n const outSpec = strategy.outputs[key]\n if (!outSpec) continue\n const value = (derived as Record<string, unknown>)[key]\n\n // ── Array-shape branch ─────────────────────────────────────\n if (outSpec.shape === 'array') {\n if (value === undefined || value === null) {\n // Treat null/undefined as \"empty array\" — clears all prior\n // outputs for this (source, output) pair. The caller's\n // diff turns that into deletes.\n outputs[key] = { kind: 'array', ok: true, entries: [] }\n continue\n }\n if (!Array.isArray(value)) {\n throw new DerivationOutputShapeError(\n key,\n `shape 'array' expects an array, got ${typeof value}`,\n )\n }\n const maxFanout = outSpec.maxFanout ?? 64\n if (value.length > maxFanout) {\n throw new DerivationCapExceededError(key, value.length, maxFanout)\n }\n const entries: Array<{ key: string; value: Record<string, unknown> }> = []\n const seenKeys = new Set<string>()\n for (let i = 0; i < value.length; i++) {\n const row = value[i] as unknown\n if (row === null || typeof row !== 'object') {\n throw new DerivationOutputShapeError(\n key,\n `array member at index ${i} must be a non-null object (got ${row === null ? 'null' : typeof row})`,\n )\n }\n let derivedKey: string\n try {\n derivedKey = outSpec.key(row as Record<string, unknown>)\n } catch (err) {\n throw new DerivationOutputShapeError(\n key,\n `key extractor threw on array member at index ${i}: `\n + (err instanceof Error ? err.message : String(err)),\n )\n }\n if (typeof derivedKey !== 'string' || derivedKey.length === 0) {\n throw new DerivationOutputShapeError(\n key,\n `key extractor returned ${typeof derivedKey === 'string' ? 'empty string' : typeof derivedKey} at index ${i}; expected non-empty string`,\n )\n }\n if (seenKeys.has(derivedKey)) {\n throw new DerivationOutputShapeError(\n key,\n `duplicate key \"${derivedKey}\" in array output (index ${i}); each derived row must have a unique key within a single derive() invocation`,\n )\n }\n seenKeys.add(derivedKey)\n entries.push({\n key: derivedKey,\n value: { ...(row as Record<string, unknown>), _derivedFrom: meta },\n })\n }\n outputs[key] = { kind: 'array', ok: true, entries }\n continue\n }\n\n // ── Record-shape branch (existing v1 behavior) ─────────────\n if (value === undefined || value === null) {\n if (outSpec.optional === true) {\n // Optional output explicitly skipped. Mark for caller\n // so any prior-emitted output at this id can be deleted.\n outputs[key] = { kind: 'record', value: {}, ok: true, skipped: true }\n continue\n }\n throw new DerivationOutputShapeError(\n key,\n `expected object, got ${value === undefined ? 'undefined' : 'null'}`,\n )\n }\n if (typeof value !== 'object') {\n throw new DerivationOutputShapeError(\n key,\n `expected object, got ${typeof value}`,\n )\n }\n outputs[key] = {\n kind: 'record',\n value: { ...(value as Record<string, unknown>), _derivedFrom: meta },\n ok: true,\n }\n }\n return { outputs, failed: false }\n },\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MaterializedViewConfigError,
|
|
3
3
|
ValidationError
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-OTWT6BAJ.js";
|
|
5
5
|
|
|
6
6
|
// src/materialized-views/with-materialized-view.ts
|
|
7
7
|
function withMaterializedView(spec) {
|
|
@@ -22,9 +22,9 @@ function withMaterializedView(spec) {
|
|
|
22
22
|
throw new ValidationError("withMaterializedView: query must be a function returning a Query<T>");
|
|
23
23
|
}
|
|
24
24
|
if (spec.unionSources) {
|
|
25
|
-
if (spec.unionSources.length <
|
|
25
|
+
if (spec.unionSources.length < 1) {
|
|
26
26
|
throw new MaterializedViewConfigError(
|
|
27
|
-
"unionSources requires at least
|
|
27
|
+
"unionSources requires at least 1 source collection"
|
|
28
28
|
);
|
|
29
29
|
}
|
|
30
30
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -56,12 +56,27 @@ function withMaterializedView(spec) {
|
|
|
56
56
|
`withMaterializedView "${spec.name}": UNION strategy with aggregate requires groupBy \u2014 use groupBy to declare the bucketing keys, or remove aggregate for a pure dedup MV`
|
|
57
57
|
);
|
|
58
58
|
}
|
|
59
|
+
if (spec.moneyFields && !spec.aggregate) {
|
|
60
|
+
throw new MaterializedViewConfigError(
|
|
61
|
+
`withMaterializedView "${spec.name}": moneyFields requires aggregate \u2014 moneyFields rewrites sum/min/max reducers over money output fields, so it is meaningless without an aggregate spec`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (spec.unionSources.some((s) => s.join && s.join.length > 0) && (!spec.sources || spec.sources.length === 0)) {
|
|
65
|
+
throw new MaterializedViewConfigError(
|
|
66
|
+
`withMaterializedView "${spec.name}": a unionSources arm declares join(s) but no \`sources\` are listed \u2014 declare sources: [...] with the right-side (join-target) collection names so writes to them trigger MV refresh`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
59
69
|
if (spec.predicates) {
|
|
60
70
|
throw new MaterializedViewConfigError(
|
|
61
71
|
`withMaterializedView "${spec.name}": predicates are not supported on UNION strategies \u2014 UNION mode does not use a Query<T> chain, so .wherePredicate() cannot fire. Use the query() form, or open an issue if per-arm predicates are needed`
|
|
62
72
|
);
|
|
63
73
|
}
|
|
64
74
|
}
|
|
75
|
+
if (spec.i18nLocale !== void 0 && spec.i18nFields === void 0) {
|
|
76
|
+
throw new MaterializedViewConfigError(
|
|
77
|
+
`withMaterializedView "${spec.name}": i18nLocale requires i18nFields \u2014 declare the i18nText descriptors of the group-key fields so they can be resolved at the mv layer before bucketing.`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
65
80
|
if (typeof spec.rowKey !== "function") {
|
|
66
81
|
throw new ValidationError("withMaterializedView: rowKey is required (no default; see spec \xA7 Type surface)");
|
|
67
82
|
}
|
|
@@ -79,4 +94,4 @@ function withMaterializedView(spec) {
|
|
|
79
94
|
export {
|
|
80
95
|
withMaterializedView
|
|
81
96
|
};
|
|
82
|
-
//# sourceMappingURL=chunk-
|
|
97
|
+
//# sourceMappingURL=chunk-6QE4DUYC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/materialized-views/with-materialized-view.ts"],"sourcesContent":["import { MaterializedViewConfigError, ValidationError } from '../errors.js'\nimport type { MaterializedViewStrategy, MaterializedViewStrategyHandle } from './types.js'\n\n/**\n * Register a materialized view: a declared query whose result is\n * persisted as a queryable collection and kept fresh as sources\n * change. Writes go through the standard `Collection.put` pipeline;\n * refresh-driven deletes route through `Collection._internalDelete` so\n * user `onDelete` guards on the output collection aren't tripped by\n * housekeeping.\n *\n * Two registration modes:\n * - **single-source** — declare `query: (db) => Query<TRow>`; the\n * dependency analyzer derives source collections from the plan.\n * - **UNION** — declare `unionSources: [{ collection, map }, ...]`\n * plus optional `groupBy` + `aggregate`; the executor reads each\n * arm, maps to the unified row shape, concatenates, then groups\n * and aggregates.\n *\n * The two modes are mutually exclusive — exactly one of `query` /\n * `unionSources` must be set at registration time.\n *\n * See docs/superpowers/specs/2026-05-20-dim14-mv-v2-design.md (single-source v2)\n * and docs/superpowers/specs/2026-05-21-dim14-mv-multikey-and-union.md (UNION).\n */\nexport function withMaterializedView<TRow extends Record<string, unknown>>(\n spec: MaterializedViewStrategy<TRow>,\n): MaterializedViewStrategyHandle {\n if (!spec.name || spec.name.length === 0) {\n throw new ValidationError('withMaterializedView: name is required')\n }\n // Mutual exclusion: query and unionSources cannot coexist.\n if (spec.query && spec.unionSources) {\n throw new MaterializedViewConfigError(\n 'query and unionSources are mutually exclusive — pick one',\n )\n }\n // Strategy must declare one of the two.\n if (!spec.query && !spec.unionSources) {\n throw new MaterializedViewConfigError(\n 'strategy must declare either query or unionSources',\n )\n }\n if (spec.query !== undefined && typeof spec.query !== 'function') {\n throw new ValidationError('withMaterializedView: query must be a function returning a Query<T>')\n }\n // UNION-form invariants.\n if (spec.unionSources) {\n // A single arm is a deliberate shape (#331): map→group→aggregate over\n // ONE collection with a COMPUTED bucket key (e.g. month sliced from a\n // date field) — something the query form's stored-field groupBy cannot\n // express. The executor and dependency analyzer are arm-count-agnostic.\n if (spec.unionSources.length < 1) {\n throw new MaterializedViewConfigError(\n 'unionSources requires at least 1 source collection',\n )\n }\n const seen = new Set<string>()\n for (const s of spec.unionSources) {\n if (typeof s?.collection !== 'string' || s.collection.length === 0) {\n throw new MaterializedViewConfigError(\n 'each unionSources entry must declare a non-empty `collection` string',\n )\n }\n if (typeof s.map !== 'function') {\n throw new MaterializedViewConfigError(\n `unionSources entry for \"${s.collection}\" is missing a \\`map\\` function`,\n )\n }\n if (seen.has(s.collection)) {\n throw new MaterializedViewConfigError(\n `unionSources must reference distinct collections (duplicate: \"${s.collection}\")`,\n )\n }\n seen.add(s.collection)\n }\n if (Array.isArray(spec.groupBy) && spec.groupBy.length === 0) {\n throw new MaterializedViewConfigError(\n `withMaterializedView \"${spec.name}\": groupBy must not be an empty array — omit it or provide at least one field name`,\n )\n }\n if (spec.aggregate && !spec.groupBy) {\n throw new MaterializedViewConfigError(\n `withMaterializedView \"${spec.name}\": UNION strategy with aggregate requires groupBy — `\n + `use groupBy to declare the bucketing keys, or remove aggregate for a pure dedup MV`,\n )\n }\n // `moneyFields` only has meaning when there's an aggregate to\n // money-rewrite — it keys reducer outputs, so declaring it without\n // `aggregate` is a no-op config mistake.\n if (spec.moneyFields && !spec.aggregate) {\n throw new MaterializedViewConfigError(\n `withMaterializedView \"${spec.name}\": moneyFields requires aggregate — `\n + `moneyFields rewrites sum/min/max reducers over money output fields, `\n + `so it is meaningless without an aggregate spec`,\n )\n }\n // Per-arm joins resolve right-side collections that the union\n // dependency set (built from arm `collection`s alone) does NOT\n // include. The consumer must list those right-side collections in\n // `sources` so writes to them trigger MV refresh.\n if (spec.unionSources.some(s => s.join && s.join.length > 0) && (!spec.sources || spec.sources.length === 0)) {\n throw new MaterializedViewConfigError(\n `withMaterializedView \"${spec.name}\": a unionSources arm declares join(s) but `\n + `no \\`sources\\` are listed — declare sources: [...] with the right-side `\n + `(join-target) collection names so writes to them trigger MV refresh`,\n )\n }\n if (spec.predicates) {\n throw new MaterializedViewConfigError(\n `withMaterializedView \"${spec.name}\": predicates are not supported on UNION strategies — `\n + `UNION mode does not use a Query<T> chain, so .wherePredicate() cannot fire. `\n + `Use the query() form, or open an issue if per-arm predicates are needed`,\n )\n }\n }\n // #285: i18nLocale + i18nFields drive compute-time i18n resolution of group-key\n // i18nText fields before bucketing — UNION mode (resolved on the unified rows)\n // AND query mode (resolved in GroupedAggregation.run before groupAndReduce).\n // i18nLocale without i18nFields cannot resolve anything, so reject it early.\n if (spec.i18nLocale !== undefined && spec.i18nFields === undefined) {\n throw new MaterializedViewConfigError(\n `withMaterializedView \"${spec.name}\": i18nLocale requires i18nFields — `\n + `declare the i18nText descriptors of the group-key fields so they can be `\n + `resolved at the mv layer before bucketing.`,\n )\n }\n if (typeof spec.rowKey !== 'function') {\n throw new ValidationError('withMaterializedView: rowKey is required (no default; see spec § Type surface)')\n }\n if (spec.refresh !== 'eager' && spec.refresh !== 'lazy' && spec.refresh !== 'manual') {\n throw new ValidationError(\n `withMaterializedView: refresh must be 'eager' | 'lazy' | 'manual', got \"${String(spec.refresh)}\"`,\n )\n }\n return {\n __noydb_strategy: 'materialized-view',\n spec,\n }\n}\n"],"mappings":";;;;;;AAyBO,SAAS,qBACd,MACgC;AAChC,MAAI,CAAC,KAAK,QAAQ,KAAK,KAAK,WAAW,GAAG;AACxC,UAAM,IAAI,gBAAgB,wCAAwC;AAAA,EACpE;AAEA,MAAI,KAAK,SAAS,KAAK,cAAc;AACnC,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,MAAI,CAAC,KAAK,SAAS,CAAC,KAAK,cAAc;AACrC,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,MAAI,KAAK,UAAU,UAAa,OAAO,KAAK,UAAU,YAAY;AAChE,UAAM,IAAI,gBAAgB,qEAAqE;AAAA,EACjG;AAEA,MAAI,KAAK,cAAc;AAKrB,QAAI,KAAK,aAAa,SAAS,GAAG;AAChC,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,UAAM,OAAO,oBAAI,IAAY;AAC7B,eAAW,KAAK,KAAK,cAAc;AACjC,UAAI,OAAO,GAAG,eAAe,YAAY,EAAE,WAAW,WAAW,GAAG;AAClE,cAAM,IAAI;AAAA,UACR;AAAA,QACF;AAAA,MACF;AACA,UAAI,OAAO,EAAE,QAAQ,YAAY;AAC/B,cAAM,IAAI;AAAA,UACR,2BAA2B,EAAE,UAAU;AAAA,QACzC;AAAA,MACF;AACA,UAAI,KAAK,IAAI,EAAE,UAAU,GAAG;AAC1B,cAAM,IAAI;AAAA,UACR,iEAAiE,EAAE,UAAU;AAAA,QAC/E;AAAA,MACF;AACA,WAAK,IAAI,EAAE,UAAU;AAAA,IACvB;AACA,QAAI,MAAM,QAAQ,KAAK,OAAO,KAAK,KAAK,QAAQ,WAAW,GAAG;AAC5D,YAAM,IAAI;AAAA,QACR,yBAAyB,KAAK,IAAI;AAAA,MACpC;AAAA,IACF;AACA,QAAI,KAAK,aAAa,CAAC,KAAK,SAAS;AACnC,YAAM,IAAI;AAAA,QACR,yBAAyB,KAAK,IAAI;AAAA,MAEpC;AAAA,IACF;AAIA,QAAI,KAAK,eAAe,CAAC,KAAK,WAAW;AACvC,YAAM,IAAI;AAAA,QACR,yBAAyB,KAAK,IAAI;AAAA,MAGpC;AAAA,IACF;AAKA,QAAI,KAAK,aAAa,KAAK,OAAK,EAAE,QAAQ,EAAE,KAAK,SAAS,CAAC,MAAM,CAAC,KAAK,WAAW,KAAK,QAAQ,WAAW,IAAI;AAC5G,YAAM,IAAI;AAAA,QACR,yBAAyB,KAAK,IAAI;AAAA,MAGpC;AAAA,IACF;AACA,QAAI,KAAK,YAAY;AACnB,YAAM,IAAI;AAAA,QACR,yBAAyB,KAAK,IAAI;AAAA,MAGpC;AAAA,IACF;AAAA,EACF;AAKA,MAAI,KAAK,eAAe,UAAa,KAAK,eAAe,QAAW;AAClE,UAAM,IAAI;AAAA,MACR,yBAAyB,KAAK,IAAI;AAAA,IAGpC;AAAA,EACF;AACA,MAAI,OAAO,KAAK,WAAW,YAAY;AACrC,UAAM,IAAI,gBAAgB,mFAAgF;AAAA,EAC5G;AACA,MAAI,KAAK,YAAY,WAAW,KAAK,YAAY,UAAU,KAAK,YAAY,UAAU;AACpF,UAAM,IAAI;AAAA,MACR,2EAA2E,OAAO,KAAK,OAAO,CAAC;AAAA,IACjG;AAAA,EACF;AACA,SAAO;AAAA,IACL,kBAAkB;AAAA,IAClB;AAAA,EACF;AACF;","names":[]}
|
|
@@ -4,13 +4,13 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
base64ToBuffer,
|
|
6
6
|
bufferToBase64
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-37VGJM3T.js";
|
|
8
8
|
import {
|
|
9
9
|
SessionExpiredError,
|
|
10
10
|
SessionNotFoundError,
|
|
11
11
|
SessionPolicyError,
|
|
12
12
|
ValidationError
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-OTWT6BAJ.js";
|
|
14
14
|
|
|
15
15
|
// src/session/session.ts
|
|
16
16
|
var subtle = globalThis.crypto.subtle;
|
|
@@ -364,4 +364,4 @@ export {
|
|
|
364
364
|
clearDevUnlock,
|
|
365
365
|
isDevUnlockActive
|
|
366
366
|
};
|
|
367
|
-
//# sourceMappingURL=chunk-
|
|
367
|
+
//# sourceMappingURL=chunk-7MRT7EPB.js.map
|