@noy-db/hub 0.2.0-pre.16 → 0.2.0-pre.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aggregate/index.cjs.map +1 -1
- package/dist/aggregate/index.d.cts +3 -2
- package/dist/aggregate/index.d.ts +3 -2
- package/dist/aggregate/index.js +4 -4
- package/dist/attestation/index.cjs.map +1 -1
- package/dist/attestation/index.d.cts +5 -3
- package/dist/attestation/index.d.ts +5 -3
- package/dist/attestation/index.js +6 -6
- package/dist/blobs/index.cjs +226 -11
- package/dist/blobs/index.cjs.map +1 -1
- package/dist/blobs/index.d.cts +6 -4
- package/dist/blobs/index.d.ts +6 -4
- package/dist/blobs/index.js +6 -5
- package/dist/blobs/index.js.map +1 -1
- package/dist/bundle/index.cjs +1087 -95
- package/dist/bundle/index.cjs.map +1 -1
- package/dist/bundle/index.d.cts +7 -5
- package/dist/bundle/index.d.ts +7 -5
- package/dist/bundle/index.js +21 -10
- package/dist/bundle/index.js.map +1 -1
- package/dist/{chunk-G4SCICH5.js → chunk-2FU2FTXD.js} +2 -2
- package/dist/{chunk-JD3OZAI4.js → chunk-3G3W65EQ.js} +2 -2
- package/dist/{chunk-XWH4MXIU.js → chunk-5AXTH4QZ.js} +2 -2
- package/dist/{chunk-4TBBMHVC.js → chunk-5LIROIDM.js} +2 -2
- package/dist/{chunk-L2BNJ6HM.js → chunk-7H2GEJ3O.js} +3 -3
- package/dist/{chunk-GNI5STXQ.js → chunk-AEIKD3PP.js} +52 -38
- package/dist/chunk-AEIKD3PP.js.map +1 -0
- package/dist/{chunk-QSUK7YWK.js → chunk-BH3X5L6A.js} +4 -4
- package/dist/{chunk-BQ65SS5A.js → chunk-BJSLBUJ7.js} +2 -2
- package/dist/{chunk-FFXM3ZIF.js → chunk-BL5GYANC.js} +3 -3
- package/dist/{chunk-6H2ZUNR7.js → chunk-BSZOCSDZ.js} +4 -4
- package/dist/{chunk-ZNQYHJXX.js → chunk-C3HYQPV4.js} +2 -2
- package/dist/{chunk-E2CDVKMH.js → chunk-CD2AVTEM.js} +5 -5
- package/dist/{chunk-667MB6AH.js → chunk-D77ZQSQQ.js} +769 -131
- package/dist/chunk-D77ZQSQQ.js.map +1 -0
- package/dist/{chunk-BR3AMFGS.js → chunk-DWEBTE2W.js} +5 -5
- package/dist/{chunk-Z4DO7YSI.js → chunk-DYYYUW5D.js} +2 -2
- package/dist/{chunk-SCJPI4Z5.js → chunk-E77UKJYL.js} +5 -5
- package/dist/{chunk-OMAMZKKD.js → chunk-F4G63NTZ.js} +2 -2
- package/dist/{chunk-TKIY625R.js → chunk-FEJDVE3Z.js} +2 -2
- package/dist/{chunk-7Z7KSVA5.js → chunk-GP3SDSH2.js} +2 -2
- package/dist/{chunk-IQLVUT37.js → chunk-H2MRGONI.js} +2 -2
- package/dist/{chunk-DUREQF5W.js → chunk-HGVSHKZW.js} +8 -5
- package/dist/chunk-HGVSHKZW.js.map +1 -0
- package/dist/chunk-I5IUYN7B.js +125 -0
- package/dist/chunk-I5IUYN7B.js.map +1 -0
- package/dist/{chunk-CJORTUJ2.js → chunk-J7RWBXFY.js} +2 -2
- package/dist/{chunk-AAVWKNZW.js → chunk-JDWE6JMX.js} +2 -2
- package/dist/{chunk-XL35NSEN.js → chunk-KCEHMDZF.js} +3 -3
- package/dist/{chunk-TS26M2SB.js → chunk-M476FOQ7.js} +2 -2
- package/dist/{chunk-F4OJZIWQ.js → chunk-NBBMMJ2H.js} +4 -4
- package/dist/{chunk-CZI2A4MQ.js → chunk-NYSYPFXJ.js} +3 -3
- package/dist/{chunk-OQSRJG6A.js → chunk-PDULVIBY.js} +2 -2
- package/dist/{chunk-Z6FNBOTC.js → chunk-PDVP3C2I.js} +1 -1
- package/dist/{chunk-Z6FNBOTC.js.map → chunk-PDVP3C2I.js.map} +1 -1
- package/dist/{chunk-DLZ2ONOD.js → chunk-QHM6XEAH.js} +6 -6
- package/dist/{chunk-HBXJ37ZY.js → chunk-QO6RGLLD.js} +4 -4
- package/dist/{chunk-7BQ4QWYX.js → chunk-ROPJVUG3.js} +23 -6
- package/dist/chunk-ROPJVUG3.js.map +1 -0
- package/dist/{chunk-42FEUPZQ.js → chunk-ROVO6NPJ.js} +2 -2
- package/dist/{chunk-6RR3MNMG.js → chunk-SHX5QBCI.js} +3 -3
- package/dist/{chunk-F3BPIPLS.js → chunk-SISBMAPO.js} +1 -1
- package/dist/chunk-SISBMAPO.js.map +1 -0
- package/dist/{chunk-3YWP3WBP.js → chunk-SNMJ7SB3.js} +5 -5
- package/dist/{chunk-IXBIFDEW.js → chunk-TIDXB5DF.js} +4 -4
- package/dist/chunk-U5QCMH3W.js +151 -0
- package/dist/chunk-U5QCMH3W.js.map +1 -0
- package/dist/{chunk-YULZKK4F.js → chunk-UNTGHX5A.js} +37 -2
- package/dist/chunk-UNTGHX5A.js.map +1 -0
- package/dist/{chunk-FWPKCXTN.js → chunk-WIAOUFFB.js} +2 -2
- package/dist/{chunk-KABJXG2F.js → chunk-WV7WV6JO.js} +195 -17
- package/dist/chunk-WV7WV6JO.js.map +1 -0
- package/dist/{chunk-X73VS74Y.js → chunk-XJV6OB4D.js} +2 -2
- package/dist/{chunk-VLMPU56Q.js → chunk-XMHUK5PN.js} +2 -2
- package/dist/{chunk-BI6ETQPF.js → chunk-XMVHEWF6.js} +4 -4
- package/dist/{chunk-HOR4R722.js → chunk-XPIHJ34I.js} +30 -4
- package/dist/chunk-XPIHJ34I.js.map +1 -0
- package/dist/{chunk-OB2ZJQ2D.js → chunk-YYVZYTWW.js} +3 -3
- package/dist/{chunk-535SSHBS.js → chunk-ZEGSDPB7.js} +81 -2
- package/dist/chunk-ZEGSDPB7.js.map +1 -0
- package/dist/{chunk-QVIEAYTP.js → chunk-ZNGPEV5J.js} +3 -3
- package/dist/consent/index.cjs.map +1 -1
- package/dist/consent/index.d.cts +6 -4
- package/dist/consent/index.d.ts +6 -4
- package/dist/consent/index.js +3 -3
- package/dist/{crypto-QXQOHMHF.js → crypto-7BN2HDWG.js} +7 -3
- package/dist/{delegation-NIQ43IPU.js → delegation-MGH5SODX.js} +5 -5
- package/dist/derivations/index.cjs.map +1 -1
- package/dist/derivations/index.d.cts +7 -5
- package/dist/derivations/index.d.ts +7 -5
- package/dist/derivations/index.js +4 -4
- package/dist/{dev-unlock-DR3upLd1.d.ts → dev-unlock-CI1ijTML.d.ts} +1 -1
- package/dist/{dev-unlock-8XzcD2Z4.d.cts → dev-unlock-iXbYFAWl.d.cts} +1 -1
- package/dist/{strategy-BtW8fAjz.d.ts → errors-Dz64FA65.d.cts} +98 -727
- package/dist/{strategy-BtW8fAjz.d.cts → errors-Dz64FA65.d.ts} +98 -727
- package/dist/executor-3W63Y44O.js +11 -0
- package/dist/executor-CFFWPWBJ.js +8 -0
- package/dist/executor-VDQQOR4F.js +8 -0
- package/dist/{fanout-sidecar-67CMI3UT.js → fanout-sidecar-FIJJ46YG.js} +2 -2
- package/dist/forget/index.cjs +43 -0
- package/dist/forget/index.cjs.map +1 -0
- package/dist/forget/index.d.cts +1 -0
- package/dist/forget/index.d.ts +1 -0
- package/dist/forget/index.js +14 -0
- package/dist/guards/index.cjs.map +1 -1
- package/dist/guards/index.d.cts +7 -5
- package/dist/guards/index.d.ts +7 -5
- package/dist/guards/index.js +6 -6
- package/dist/{hash-CDjye9KV.d.ts → hash-blk7Bkes.d.ts} +1 -1
- package/dist/{hash-DuQ88_5W.d.cts → hash-tEcM5fnv.d.cts} +1 -1
- package/dist/history/index.cjs +27 -4
- package/dist/history/index.cjs.map +1 -1
- package/dist/history/index.d.cts +7 -5
- package/dist/history/index.d.ts +7 -5
- package/dist/history/index.js +9 -7
- package/dist/history/index.js.map +1 -1
- package/dist/i18n/index.cjs +53 -0
- package/dist/i18n/index.cjs.map +1 -1
- package/dist/i18n/index.d.cts +6 -4
- package/dist/i18n/index.d.ts +6 -4
- package/dist/i18n/index.js +16 -8
- package/dist/i18n/index.js.map +1 -1
- package/dist/{immutable-guard-Dov3WvwF.d.ts → immutable-guard-B5M95nbq.d.ts} +1 -1
- package/dist/{immutable-guard-CRPvu24K.d.cts → immutable-guard-qN3zF8o1.d.cts} +1 -1
- package/dist/index-C-SSRIxP.d.cts +348 -0
- package/dist/index-C-SSRIxP.d.ts +348 -0
- package/dist/{index-nP99bXLg.d.ts → index-DpU6KWof.d.ts} +9 -1
- package/dist/{index-C8Bk3-VF.d.cts → index-u-kWzSrL.d.cts} +9 -1
- package/dist/index.cjs +7271 -6079
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -12
- package/dist/index.d.ts +15 -12
- package/dist/index.js +130 -106
- package/dist/index.js.map +1 -1
- package/dist/indexing/index.cjs.map +1 -1
- package/dist/indexing/index.js +4 -4
- package/dist/issue-TTMGHQ2J.js +12 -0
- package/dist/{ledger-A3LL253R.js → ledger-LFVLHE5H.js} +6 -6
- package/dist/materialized-views/index.cjs.map +1 -1
- package/dist/materialized-views/index.d.cts +7 -5
- package/dist/materialized-views/index.d.ts +7 -5
- package/dist/materialized-views/index.js +12 -12
- package/dist/noydb-36S6GQNC.js +37 -0
- package/dist/overlay-views/index.cjs.map +1 -1
- package/dist/overlay-views/index.d.cts +7 -5
- package/dist/overlay-views/index.d.ts +7 -5
- package/dist/overlay-views/index.js +4 -4
- package/dist/periods/index.cjs.map +1 -1
- package/dist/periods/index.d.cts +6 -4
- package/dist/periods/index.d.ts +6 -4
- package/dist/periods/index.js +6 -6
- package/dist/{public-envelope-YP2UWMLG.js → public-envelope-RXZNP3V6.js} +4 -4
- package/dist/query/index.cjs +4 -1
- package/dist/query/index.cjs.map +1 -1
- package/dist/query/index.d.cts +3 -2
- package/dist/query/index.d.ts +3 -2
- package/dist/query/index.js +6 -6
- package/dist/registry-3YFLZ7WD.js +8 -0
- package/dist/{registry-UTA4CLQS.js → registry-SECUWSGY.js} +3 -3
- package/dist/registry-TGZISEWC.js +8 -0
- package/dist/{revoke-HNMQZSCL.js → revoke-B54H2S2W.js} +6 -6
- package/dist/sealed-record/index.cjs +139 -0
- package/dist/sealed-record/index.cjs.map +1 -0
- package/dist/sealed-record/index.d.cts +123 -0
- package/dist/sealed-record/index.d.ts +123 -0
- package/dist/sealed-record/index.js +42 -0
- package/dist/sealed-record/index.js.map +1 -0
- package/dist/session/index.cjs.map +1 -1
- package/dist/session/index.d.cts +7 -5
- package/dist/session/index.d.ts +7 -5
- package/dist/session/index.js +3 -3
- package/dist/shadow/index.cjs.map +1 -1
- package/dist/shadow/index.d.cts +6 -4
- package/dist/shadow/index.d.ts +6 -4
- package/dist/shadow/index.js +2 -2
- package/dist/{signer-DCMNKXSF.js → signer-YSXZT574.js} +5 -5
- package/dist/snapshots/index.cjs.map +1 -1
- package/dist/snapshots/index.d.cts +6 -4
- package/dist/snapshots/index.d.ts +6 -4
- package/dist/snapshots/index.js +4 -4
- package/dist/{stale-W5PQTRYH.js → stale-TOA36SRK.js} +2 -2
- package/dist/stale-TOA36SRK.js.map +1 -0
- package/dist/{state-vault-TMXZRTY5.js → state-vault-W2OEABNO.js} +3 -3
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +6 -4
- package/dist/store/index.d.ts +6 -4
- package/dist/store/index.js +2 -2
- package/dist/strategy-4M9jo172.d.ts +739 -0
- package/dist/strategy-CLC1j79g.d.cts +739 -0
- package/dist/sync/index.cjs.map +1 -1
- package/dist/sync/index.d.cts +5 -3
- package/dist/sync/index.d.ts +5 -3
- package/dist/sync/index.js +4 -4
- package/dist/team/index.cjs.map +1 -1
- package/dist/team/index.d.cts +6 -4
- package/dist/team/index.d.ts +6 -4
- package/dist/team/index.js +8 -8
- package/dist/tx/index.cjs.map +1 -1
- package/dist/tx/index.d.cts +6 -4
- package/dist/tx/index.d.ts +6 -4
- package/dist/tx/index.js +3 -3
- package/dist/{types-DrmBTscX.d.ts → types-CljIHm_J.d.ts} +789 -500
- package/dist/{types-Bze6vkwm.d.cts → types-CrSpRDuG.d.cts} +789 -500
- package/dist/{ulid-DbBVrNSt.d.ts → ulid-CWfL2Vfv.d.ts} +1 -1
- package/dist/{ulid-DfZlAh0u.d.cts → ulid-CrI7PPbA.d.cts} +1 -1
- package/dist/util/index.cjs.map +1 -1
- package/dist/util/index.js +1 -1
- package/dist/{vault-group-DX2HFQMX.js → vault-group-DHAHFX2A.js} +4 -4
- package/dist/{with-derivation-_lySGdlm.d.ts → with-derivation-BZ2y4bzF.d.ts} +1 -1
- package/dist/{with-derivation-CCqAchD5.d.cts → with-derivation-Bozs8DmD.d.cts} +1 -1
- package/dist/{with-materialized-view-QT1Tp7NO.d.ts → with-materialized-view-B892zYZV.d.ts} +1 -1
- package/dist/{with-materialized-view--4PsvMDu.d.cts → with-materialized-view-NzF71cG_.d.cts} +1 -1
- package/dist/{with-overlayed-view-BEXfpzSb.d.ts → with-overlayed-view-CR6m7CHe.d.ts} +1 -1
- package/dist/{with-overlayed-view-DlH5qmeB.d.cts → with-overlayed-view-UI8qSGL4.d.cts} +1 -1
- package/package.json +23 -3
- package/dist/chunk-535SSHBS.js.map +0 -1
- package/dist/chunk-667MB6AH.js.map +0 -1
- package/dist/chunk-7BQ4QWYX.js.map +0 -1
- package/dist/chunk-DUREQF5W.js.map +0 -1
- package/dist/chunk-F3BPIPLS.js.map +0 -1
- package/dist/chunk-GNI5STXQ.js.map +0 -1
- package/dist/chunk-HOR4R722.js.map +0 -1
- package/dist/chunk-KABJXG2F.js.map +0 -1
- package/dist/chunk-YULZKK4F.js.map +0 -1
- package/dist/executor-6ZDSDZ6V.js +0 -8
- package/dist/executor-AZLS3KBK.js +0 -11
- package/dist/executor-IDZDAFNH.js +0 -8
- package/dist/issue-RZP3VI6O.js +0 -12
- package/dist/noydb-WCMY2ZOW.js +0 -35
- package/dist/registry-EB6SISTA.js +0 -8
- package/dist/registry-IUZQVVBB.js +0 -8
- /package/dist/{chunk-G4SCICH5.js.map → chunk-2FU2FTXD.js.map} +0 -0
- /package/dist/{chunk-JD3OZAI4.js.map → chunk-3G3W65EQ.js.map} +0 -0
- /package/dist/{chunk-XWH4MXIU.js.map → chunk-5AXTH4QZ.js.map} +0 -0
- /package/dist/{chunk-4TBBMHVC.js.map → chunk-5LIROIDM.js.map} +0 -0
- /package/dist/{chunk-L2BNJ6HM.js.map → chunk-7H2GEJ3O.js.map} +0 -0
- /package/dist/{chunk-QSUK7YWK.js.map → chunk-BH3X5L6A.js.map} +0 -0
- /package/dist/{chunk-BQ65SS5A.js.map → chunk-BJSLBUJ7.js.map} +0 -0
- /package/dist/{chunk-FFXM3ZIF.js.map → chunk-BL5GYANC.js.map} +0 -0
- /package/dist/{chunk-6H2ZUNR7.js.map → chunk-BSZOCSDZ.js.map} +0 -0
- /package/dist/{chunk-ZNQYHJXX.js.map → chunk-C3HYQPV4.js.map} +0 -0
- /package/dist/{chunk-E2CDVKMH.js.map → chunk-CD2AVTEM.js.map} +0 -0
- /package/dist/{chunk-BR3AMFGS.js.map → chunk-DWEBTE2W.js.map} +0 -0
- /package/dist/{chunk-Z4DO7YSI.js.map → chunk-DYYYUW5D.js.map} +0 -0
- /package/dist/{chunk-SCJPI4Z5.js.map → chunk-E77UKJYL.js.map} +0 -0
- /package/dist/{chunk-OMAMZKKD.js.map → chunk-F4G63NTZ.js.map} +0 -0
- /package/dist/{chunk-TKIY625R.js.map → chunk-FEJDVE3Z.js.map} +0 -0
- /package/dist/{chunk-7Z7KSVA5.js.map → chunk-GP3SDSH2.js.map} +0 -0
- /package/dist/{chunk-IQLVUT37.js.map → chunk-H2MRGONI.js.map} +0 -0
- /package/dist/{chunk-CJORTUJ2.js.map → chunk-J7RWBXFY.js.map} +0 -0
- /package/dist/{chunk-AAVWKNZW.js.map → chunk-JDWE6JMX.js.map} +0 -0
- /package/dist/{chunk-XL35NSEN.js.map → chunk-KCEHMDZF.js.map} +0 -0
- /package/dist/{chunk-TS26M2SB.js.map → chunk-M476FOQ7.js.map} +0 -0
- /package/dist/{chunk-F4OJZIWQ.js.map → chunk-NBBMMJ2H.js.map} +0 -0
- /package/dist/{chunk-CZI2A4MQ.js.map → chunk-NYSYPFXJ.js.map} +0 -0
- /package/dist/{chunk-OQSRJG6A.js.map → chunk-PDULVIBY.js.map} +0 -0
- /package/dist/{chunk-DLZ2ONOD.js.map → chunk-QHM6XEAH.js.map} +0 -0
- /package/dist/{chunk-HBXJ37ZY.js.map → chunk-QO6RGLLD.js.map} +0 -0
- /package/dist/{chunk-42FEUPZQ.js.map → chunk-ROVO6NPJ.js.map} +0 -0
- /package/dist/{chunk-6RR3MNMG.js.map → chunk-SHX5QBCI.js.map} +0 -0
- /package/dist/{chunk-3YWP3WBP.js.map → chunk-SNMJ7SB3.js.map} +0 -0
- /package/dist/{chunk-IXBIFDEW.js.map → chunk-TIDXB5DF.js.map} +0 -0
- /package/dist/{chunk-FWPKCXTN.js.map → chunk-WIAOUFFB.js.map} +0 -0
- /package/dist/{chunk-X73VS74Y.js.map → chunk-XJV6OB4D.js.map} +0 -0
- /package/dist/{chunk-VLMPU56Q.js.map → chunk-XMHUK5PN.js.map} +0 -0
- /package/dist/{chunk-BI6ETQPF.js.map → chunk-XMVHEWF6.js.map} +0 -0
- /package/dist/{chunk-OB2ZJQ2D.js.map → chunk-YYVZYTWW.js.map} +0 -0
- /package/dist/{chunk-QVIEAYTP.js.map → chunk-ZNGPEV5J.js.map} +0 -0
- /package/dist/{crypto-QXQOHMHF.js.map → crypto-7BN2HDWG.js.map} +0 -0
- /package/dist/{delegation-NIQ43IPU.js.map → delegation-MGH5SODX.js.map} +0 -0
- /package/dist/{executor-6ZDSDZ6V.js.map → executor-3W63Y44O.js.map} +0 -0
- /package/dist/{executor-AZLS3KBK.js.map → executor-CFFWPWBJ.js.map} +0 -0
- /package/dist/{executor-IDZDAFNH.js.map → executor-VDQQOR4F.js.map} +0 -0
- /package/dist/{fanout-sidecar-67CMI3UT.js.map → fanout-sidecar-FIJJ46YG.js.map} +0 -0
- /package/dist/{issue-RZP3VI6O.js.map → forget/index.js.map} +0 -0
- /package/dist/{ledger-A3LL253R.js.map → issue-TTMGHQ2J.js.map} +0 -0
- /package/dist/{noydb-WCMY2ZOW.js.map → ledger-LFVLHE5H.js.map} +0 -0
- /package/dist/{public-envelope-YP2UWMLG.js.map → noydb-36S6GQNC.js.map} +0 -0
- /package/dist/{registry-EB6SISTA.js.map → public-envelope-RXZNP3V6.js.map} +0 -0
- /package/dist/{registry-IUZQVVBB.js.map → registry-3YFLZ7WD.js.map} +0 -0
- /package/dist/{registry-UTA4CLQS.js.map → registry-SECUWSGY.js.map} +0 -0
- /package/dist/{revoke-HNMQZSCL.js.map → registry-TGZISEWC.js.map} +0 -0
- /package/dist/{signer-DCMNKXSF.js.map → revoke-B54H2S2W.js.map} +0 -0
- /package/dist/{stale-W5PQTRYH.js.map → signer-YSXZT574.js.map} +0 -0
- /package/dist/{state-vault-TMXZRTY5.js.map → state-vault-W2OEABNO.js.map} +0 -0
- /package/dist/{vault-group-DX2HFQMX.js.map → vault-group-DHAHFX2A.js.map} +0 -0
package/dist/bundle/index.cjs
CHANGED
|
@@ -31,7 +31,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
31
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
32
|
|
|
33
33
|
// src/errors.ts
|
|
34
|
-
var NoydbError, DecryptionError, TamperedError, InvalidKeyError, KeyringCorruptError, NoAccessError, ReadOnlyError, PermissionDeniedError, ExportCapabilityError, KeyringExpiredError, ImportCapabilityError, StoreCapabilityError, PrivilegeEscalationError, ReservedVaultNameError, FieldFrozenError, InvariantError, AmendmentForbiddenError, TierNotGrantedError, ElevationExpiredError, AlreadyElevatedError, TierDemoteDeniedError, DelegationTargetMissingError, ConflictError, LedgerContentionError, SequenceContentionError, SequenceOfflineError, NumberingUncertaintyError, BundleVersionConflictError, ValidationError, SchemaValidationError, SchemaUpdateError, SchemaFenceError, MigrationRequiredError, QuiesceTimeoutError, GroupCardinalityError, IndexRequiredError, UniqueConstraintError, UnsupportedIndexOptionError, IndexWriteFailureError, BundleIntegrityError, BundleSealMismatchError, ReservedCollectionNameError, LocaleNotSpecifiedError, TranslatorNotConfiguredError, BackupLedgerError, BackupCorruptedError, PartitionExtractionError, TransferSealError, AdoptionStateError, AttestationError, JoinTooLargeError, CrossJoinTooLargeError, CrossJoinSourceUnknownError, DanglingReferenceError, DerivationCycleError, DerivationOutputShapeError, DerivationCapExceededError, MaterializedViewCycleError, MaterializedViewSourceUnknownError, MaterializedViewTooLargeError, OverlayBaseIsVirtualError, OverlayCollectionUnavailableError, OverlayNameCollisionError, OverlayIdMismatchError, UnknownShardError, ShardProvisioningError, CrossShardJoinError, VaultTemplateNotFoundError;
|
|
34
|
+
var NoydbError, DecryptionError, TamperedError, InvalidKeyError, KeyringCorruptError, NoAccessError, ReadOnlyError, PermissionDeniedError, ExportCapabilityError, KeyringExpiredError, ImportCapabilityError, StoreCapabilityError, PrivilegeEscalationError, ReservedVaultNameError, FieldFrozenError, InvariantError, AmendmentForbiddenError, TierNotGrantedError, ElevationExpiredError, AlreadyElevatedError, TierDemoteDeniedError, DelegationTargetMissingError, ConflictError, LedgerContentionError, SequenceContentionError, SequenceOfflineError, NumberingUncertaintyError, BundleVersionConflictError, ValidationError, SchemaValidationError, SchemaUpdateError, SchemaFenceError, MigrationRequiredError, QuiesceTimeoutError, GroupCardinalityError, IndexRequiredError, UniqueConstraintError, UnsupportedIndexOptionError, IndexWriteFailureError, BundleIntegrityError, BundleSealMismatchError, ReservedCollectionNameError, LocaleNotSpecifiedError, StaticDictReadonlyError, UnknownDictCodeError, TranslatorNotConfiguredError, BackupLedgerError, BackupCorruptedError, PartitionExtractionError, TransferSealError, AdoptionStateError, AttestationError, JoinTooLargeError, CrossJoinTooLargeError, CrossJoinSourceUnknownError, DanglingReferenceError, DerivationCycleError, DerivationOutputShapeError, DerivationCapExceededError, MaterializedViewCycleError, MaterializedViewSourceUnknownError, MaterializedViewTooLargeError, OverlayBaseIsVirtualError, OverlayCollectionUnavailableError, OverlayNameCollisionError, OverlayIdMismatchError, UnknownShardError, ShardProvisioningError, CrossShardJoinError, VaultTemplateNotFoundError, ForgetStrategyNotConfiguredError, RecordCekNotFoundError;
|
|
35
35
|
var init_errors = __esm({
|
|
36
36
|
"src/errors.ts"() {
|
|
37
37
|
"use strict";
|
|
@@ -485,6 +485,36 @@ Resolutions:
|
|
|
485
485
|
this.field = field;
|
|
486
486
|
}
|
|
487
487
|
};
|
|
488
|
+
StaticDictReadonlyError = class extends NoydbError {
|
|
489
|
+
/** The static dictionary name that was the target of the mutation. */
|
|
490
|
+
dictionaryName;
|
|
491
|
+
constructor(dictionaryName) {
|
|
492
|
+
super(
|
|
493
|
+
"STATIC_DICT_READONLY",
|
|
494
|
+
`Dictionary "${dictionaryName}" is a staticDict \u2014 its labels are code constants with no mutation surface. put/putAll/rename/delete are not supported; change the label in the staticDict() table and redeploy.`
|
|
495
|
+
);
|
|
496
|
+
this.name = "StaticDictReadonlyError";
|
|
497
|
+
this.dictionaryName = dictionaryName;
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
UnknownDictCodeError = class extends NoydbError {
|
|
501
|
+
/** The static dictionary name. */
|
|
502
|
+
dictionaryName;
|
|
503
|
+
/** The field that carried the unknown code. */
|
|
504
|
+
field;
|
|
505
|
+
/** The offending code value. */
|
|
506
|
+
code;
|
|
507
|
+
constructor(dictionaryName, field, code) {
|
|
508
|
+
super(
|
|
509
|
+
"UNKNOWN_DICT_CODE",
|
|
510
|
+
`Field "${field}": code "${code}" is not a known key of staticDict "${dictionaryName}". Use a declared code, or pass { validateCodes: false } on the descriptor to allow open codes.`
|
|
511
|
+
);
|
|
512
|
+
this.name = "UnknownDictCodeError";
|
|
513
|
+
this.dictionaryName = dictionaryName;
|
|
514
|
+
this.field = field;
|
|
515
|
+
this.code = code;
|
|
516
|
+
}
|
|
517
|
+
};
|
|
488
518
|
TranslatorNotConfiguredError = class extends NoydbError {
|
|
489
519
|
/** The field that requested auto-translation. */
|
|
490
520
|
field;
|
|
@@ -763,6 +793,25 @@ Resolutions:
|
|
|
763
793
|
this.templateName = templateName;
|
|
764
794
|
}
|
|
765
795
|
};
|
|
796
|
+
ForgetStrategyNotConfiguredError = class extends NoydbError {
|
|
797
|
+
constructor(message = 'vault.forget() requires a forget strategy. Pass `forgetStrategy: withForgetCascade({ subjects: { <collection>: <subjectField> } })` from "@noy-db/hub/forget" to createNoydb().') {
|
|
798
|
+
super("FORGET_NOT_CONFIGURED", message);
|
|
799
|
+
this.name = "ForgetStrategyNotConfiguredError";
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
RecordCekNotFoundError = class extends NoydbError {
|
|
803
|
+
collection;
|
|
804
|
+
id;
|
|
805
|
+
constructor(collection, id) {
|
|
806
|
+
super(
|
|
807
|
+
"RECORD_CEK_NOT_FOUND",
|
|
808
|
+
`No per-record CEK for ${collection}/${id}. The record is missing, or its collection was not opened with { perRecordKeys: true } \u2014 only per-record-key records carry a sealable CEK.`
|
|
809
|
+
);
|
|
810
|
+
this.name = "RecordCekNotFoundError";
|
|
811
|
+
this.collection = collection;
|
|
812
|
+
this.id = id;
|
|
813
|
+
}
|
|
814
|
+
};
|
|
766
815
|
}
|
|
767
816
|
});
|
|
768
817
|
|
|
@@ -1016,6 +1065,39 @@ async function unwrapKey(wrappedBase64, kek) {
|
|
|
1016
1065
|
throw new InvalidKeyError();
|
|
1017
1066
|
}
|
|
1018
1067
|
}
|
|
1068
|
+
async function asKwKey(dek) {
|
|
1069
|
+
const rawDek = await subtle.exportKey("raw", dek);
|
|
1070
|
+
const hkdfKey = await subtle.importKey("raw", rawDek, "HKDF", false, ["deriveBits"]);
|
|
1071
|
+
const salt = new TextEncoder().encode("noydb-cek-wrap");
|
|
1072
|
+
const info = new TextEncoder().encode("v1");
|
|
1073
|
+
const bits = await subtle.deriveBits(
|
|
1074
|
+
{ name: "HKDF", hash: "SHA-256", salt, info },
|
|
1075
|
+
hkdfKey,
|
|
1076
|
+
KEY_BITS
|
|
1077
|
+
);
|
|
1078
|
+
return subtle.importKey("raw", bits, "AES-KW", false, ["wrapKey", "unwrapKey"]);
|
|
1079
|
+
}
|
|
1080
|
+
async function wrapCek(cek, dek) {
|
|
1081
|
+
const kw = await asKwKey(dek);
|
|
1082
|
+
const wrapped = await subtle.wrapKey("raw", cek, kw, "AES-KW");
|
|
1083
|
+
return bufferToBase64(wrapped);
|
|
1084
|
+
}
|
|
1085
|
+
async function unwrapCek(wrappedBase64, dek) {
|
|
1086
|
+
const kw = await asKwKey(dek);
|
|
1087
|
+
try {
|
|
1088
|
+
return await subtle.unwrapKey(
|
|
1089
|
+
"raw",
|
|
1090
|
+
base64ToBuffer(wrappedBase64),
|
|
1091
|
+
kw,
|
|
1092
|
+
"AES-KW",
|
|
1093
|
+
{ name: "AES-GCM", length: KEY_BITS },
|
|
1094
|
+
true,
|
|
1095
|
+
["encrypt", "decrypt"]
|
|
1096
|
+
);
|
|
1097
|
+
} catch {
|
|
1098
|
+
throw new InvalidKeyError();
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1019
1101
|
async function encrypt(plaintext, dek) {
|
|
1020
1102
|
const iv = generateIV();
|
|
1021
1103
|
const encoded = new TextEncoder().encode(plaintext);
|
|
@@ -1112,6 +1194,163 @@ var init_crypto = __esm({
|
|
|
1112
1194
|
}
|
|
1113
1195
|
});
|
|
1114
1196
|
|
|
1197
|
+
// src/record-keys/tombstone.ts
|
|
1198
|
+
function isTombstone(envelope, encrypted) {
|
|
1199
|
+
if (!encrypted) return false;
|
|
1200
|
+
return !envelope._data && envelope._cek === void 0;
|
|
1201
|
+
}
|
|
1202
|
+
function buildTombstone(version, actor) {
|
|
1203
|
+
return {
|
|
1204
|
+
_noydb: NOYDB_FORMAT_VERSION,
|
|
1205
|
+
_v: version,
|
|
1206
|
+
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1207
|
+
_iv: "",
|
|
1208
|
+
_data: "",
|
|
1209
|
+
...actor ? { _by: actor } : {}
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
var init_tombstone = __esm({
|
|
1213
|
+
"src/record-keys/tombstone.ts"() {
|
|
1214
|
+
"use strict";
|
|
1215
|
+
init_types();
|
|
1216
|
+
}
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
// src/record-keys/lifecycle.ts
|
|
1220
|
+
async function resolveStableCek(deps, id) {
|
|
1221
|
+
const cached = deps.cache?.get(id);
|
|
1222
|
+
if (cached) return cached;
|
|
1223
|
+
const live = await deps.getLive(id);
|
|
1224
|
+
if (live?._cek !== void 0) {
|
|
1225
|
+
const cek = await unwrapCek(live._cek, await deps.getDEK());
|
|
1226
|
+
deps.cache?.set(id, cek, 1);
|
|
1227
|
+
return cek;
|
|
1228
|
+
}
|
|
1229
|
+
const fresh = await generateDEK();
|
|
1230
|
+
deps.cache?.set(id, fresh, 1);
|
|
1231
|
+
return fresh;
|
|
1232
|
+
}
|
|
1233
|
+
async function rewrapBodyToDek(envelope, fromDek, toDek) {
|
|
1234
|
+
if (envelope._cek !== void 0) {
|
|
1235
|
+
const cek = await unwrapCek(envelope._cek, fromDek);
|
|
1236
|
+
const plaintext2 = await decrypt(envelope._iv, envelope._data, cek);
|
|
1237
|
+
const { iv: iv2, data: data2 } = await encrypt(plaintext2, cek);
|
|
1238
|
+
return { _iv: iv2, _data: data2, _cek: await wrapCek(cek, toDek), cek };
|
|
1239
|
+
}
|
|
1240
|
+
const plaintext = await decrypt(envelope._iv, envelope._data, fromDek);
|
|
1241
|
+
const { iv, data } = await encrypt(plaintext, toDek);
|
|
1242
|
+
return { _iv: iv, _data: data, cek: null };
|
|
1243
|
+
}
|
|
1244
|
+
var init_lifecycle = __esm({
|
|
1245
|
+
"src/record-keys/lifecycle.ts"() {
|
|
1246
|
+
"use strict";
|
|
1247
|
+
init_crypto();
|
|
1248
|
+
}
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
// src/record-keys/sealing.ts
|
|
1252
|
+
async function sealRecordToHost(ctx, collection, id, hostSealer, opts) {
|
|
1253
|
+
if (collection.includes("/")) throw new ValidationError(`sealRecordToHost: collection "${collection}" must not contain "/"`);
|
|
1254
|
+
if (id.includes("/")) throw new ValidationError(`sealRecordToHost: id "${id}" must not contain "/"`);
|
|
1255
|
+
const live = await ctx.adapter.get(ctx.vault, collection, id);
|
|
1256
|
+
if (!live || live._cek === void 0) {
|
|
1257
|
+
throw new RecordCekNotFoundError(collection, id);
|
|
1258
|
+
}
|
|
1259
|
+
const dek = await ctx.getDEK(collection);
|
|
1260
|
+
const cek = await unwrapCek(live._cek, dek);
|
|
1261
|
+
const rawCek = await subtle2.exportKey("raw", cek);
|
|
1262
|
+
const cekB64 = bufferToBase64(rawCek);
|
|
1263
|
+
const hint = await hostSealer.publishRecipientHint();
|
|
1264
|
+
if (hint.pid.includes("/")) throw new ValidationError(`sealRecordToHost: recipient pid "${hint.pid}" must not contain "/"`);
|
|
1265
|
+
const binding = {
|
|
1266
|
+
collection,
|
|
1267
|
+
id,
|
|
1268
|
+
cek: cekB64,
|
|
1269
|
+
expiresAt: opts.expiresAt
|
|
1270
|
+
};
|
|
1271
|
+
const sealed = await hostSealer.sealForRecipient(
|
|
1272
|
+
new TextEncoder().encode(JSON.stringify(binding)),
|
|
1273
|
+
hint
|
|
1274
|
+
);
|
|
1275
|
+
const delivery = {
|
|
1276
|
+
v: 1,
|
|
1277
|
+
_noydb_sealed_cek: 1,
|
|
1278
|
+
pid: hint.pid,
|
|
1279
|
+
payload: bufferToBase64(sealed),
|
|
1280
|
+
expiresAt: opts.expiresAt
|
|
1281
|
+
};
|
|
1282
|
+
const envelopeKey = `${collection}/${id}/${hint.pid}`;
|
|
1283
|
+
const prior = await ctx.adapter.get(ctx.vault, SEALED_CEK_NS, envelopeKey);
|
|
1284
|
+
const env = {
|
|
1285
|
+
_noydb: NOYDB_FORMAT_VERSION,
|
|
1286
|
+
_v: (prior?._v ?? 0) + 1,
|
|
1287
|
+
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1288
|
+
// AES-GCM bypassed — the sealing layer is the security boundary, exactly
|
|
1289
|
+
// like the managed-passphrase `_meta/sealed-passphrase` envelope.
|
|
1290
|
+
_iv: "",
|
|
1291
|
+
_data: JSON.stringify(delivery),
|
|
1292
|
+
...ctx.actor ? { _by: ctx.actor } : {}
|
|
1293
|
+
};
|
|
1294
|
+
await ctx.adapter.put(ctx.vault, SEALED_CEK_NS, envelopeKey, env);
|
|
1295
|
+
return { pid: hint.pid, envelopeKey };
|
|
1296
|
+
}
|
|
1297
|
+
async function revokeSealedRecord(ctx, collection, id, pid) {
|
|
1298
|
+
await ctx.adapter.delete(ctx.vault, SEALED_CEK_NS, `${collection}/${id}/${pid}`);
|
|
1299
|
+
}
|
|
1300
|
+
async function rotateRecordCek(ctx, collection, id) {
|
|
1301
|
+
const live = await ctx.adapter.get(ctx.vault, collection, id);
|
|
1302
|
+
if (!live || live._cek === void 0) {
|
|
1303
|
+
throw new RecordCekNotFoundError(collection, id);
|
|
1304
|
+
}
|
|
1305
|
+
const dek = await ctx.getDEK(collection);
|
|
1306
|
+
const oldCek = await unwrapCek(live._cek, dek);
|
|
1307
|
+
const json = await decrypt(live._iv, live._data, oldCek);
|
|
1308
|
+
const newCek = await generateDEK();
|
|
1309
|
+
const { iv, data } = await encrypt(json, newCek);
|
|
1310
|
+
const env = {
|
|
1311
|
+
_noydb: NOYDB_FORMAT_VERSION,
|
|
1312
|
+
_v: live._v + 1,
|
|
1313
|
+
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1314
|
+
_iv: iv,
|
|
1315
|
+
_data: data,
|
|
1316
|
+
_cek: await wrapCek(newCek, dek),
|
|
1317
|
+
...ctx.actor ? { _by: ctx.actor } : {},
|
|
1318
|
+
...live._tier !== void 0 ? { _tier: live._tier } : {},
|
|
1319
|
+
...live._det !== void 0 ? { _det: live._det } : {}
|
|
1320
|
+
};
|
|
1321
|
+
await ctx.adapter.put(ctx.vault, collection, id, env);
|
|
1322
|
+
await ctx.invalidateRecordCaches(collection, id);
|
|
1323
|
+
const prefix = `${collection}/${id}/`;
|
|
1324
|
+
const keys = await ctx.adapter.list(ctx.vault, SEALED_CEK_NS);
|
|
1325
|
+
for (const key of keys) {
|
|
1326
|
+
if (key.startsWith(prefix)) {
|
|
1327
|
+
await ctx.adapter.delete(ctx.vault, SEALED_CEK_NS, key);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
var subtle2, SEALED_CEK_NS;
|
|
1332
|
+
var init_sealing = __esm({
|
|
1333
|
+
"src/record-keys/sealing.ts"() {
|
|
1334
|
+
"use strict";
|
|
1335
|
+
init_crypto();
|
|
1336
|
+
init_types();
|
|
1337
|
+
init_errors();
|
|
1338
|
+
subtle2 = globalThis.crypto.subtle;
|
|
1339
|
+
SEALED_CEK_NS = "_sealed_cek";
|
|
1340
|
+
}
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
// src/record-keys/index.ts
|
|
1344
|
+
var init_record_keys = __esm({
|
|
1345
|
+
"src/record-keys/index.ts"() {
|
|
1346
|
+
"use strict";
|
|
1347
|
+
init_crypto();
|
|
1348
|
+
init_tombstone();
|
|
1349
|
+
init_lifecycle();
|
|
1350
|
+
init_sealing();
|
|
1351
|
+
}
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1115
1354
|
// src/persisted-schemas/storage.ts
|
|
1116
1355
|
async function loadPersistedSchema(store, vault, collection, dek) {
|
|
1117
1356
|
const envelope = await store.get(vault, SCHEMAS_COLLECTION, collection);
|
|
@@ -2815,11 +3054,11 @@ async function mintWrappedDeksBlob(deks, credential) {
|
|
|
2815
3054
|
const wrappingKey = await deriveWrappingKey(credential, salt);
|
|
2816
3055
|
const exported = {};
|
|
2817
3056
|
for (const [coll, dek] of deks) {
|
|
2818
|
-
const raw = await
|
|
3057
|
+
const raw = await subtle3.exportKey("raw", dek);
|
|
2819
3058
|
exported[coll] = bytesToBase643(new Uint8Array(raw));
|
|
2820
3059
|
}
|
|
2821
3060
|
const plaintext = new TextEncoder().encode(JSON.stringify({ deks: exported }));
|
|
2822
|
-
const ciphertext = await
|
|
3061
|
+
const ciphertext = await subtle3.encrypt(
|
|
2823
3062
|
{ name: "AES-GCM", iv },
|
|
2824
3063
|
wrappingKey,
|
|
2825
3064
|
plaintext
|
|
@@ -2832,7 +3071,7 @@ async function mintWrappedDeksBlob(deks, credential) {
|
|
|
2832
3071
|
}
|
|
2833
3072
|
async function unwrapDeksFromBlob(blob, credential) {
|
|
2834
3073
|
const wrappingKey = await deriveWrappingKey(credential, base64ToBytes3(blob.salt));
|
|
2835
|
-
const plaintext = await
|
|
3074
|
+
const plaintext = await subtle3.decrypt(
|
|
2836
3075
|
{ name: "AES-GCM", iv: base64ToBytes3(blob.iv) },
|
|
2837
3076
|
wrappingKey,
|
|
2838
3077
|
base64ToBytes3(blob.wrappedDeks)
|
|
@@ -2841,7 +3080,7 @@ async function unwrapDeksFromBlob(blob, credential) {
|
|
|
2841
3080
|
const deks = /* @__PURE__ */ new Map();
|
|
2842
3081
|
for (const [coll, b64] of Object.entries(parsed.deks)) {
|
|
2843
3082
|
const raw = base64ToBytes3(b64);
|
|
2844
|
-
const key = await
|
|
3083
|
+
const key = await subtle3.importKey(
|
|
2845
3084
|
"raw",
|
|
2846
3085
|
raw,
|
|
2847
3086
|
{ name: "AES-GCM", length: 256 },
|
|
@@ -2853,14 +3092,14 @@ async function unwrapDeksFromBlob(blob, credential) {
|
|
|
2853
3092
|
return deks;
|
|
2854
3093
|
}
|
|
2855
3094
|
async function deriveWrappingKey(credential, salt) {
|
|
2856
|
-
const ikm = await
|
|
3095
|
+
const ikm = await subtle3.importKey(
|
|
2857
3096
|
"raw",
|
|
2858
3097
|
new TextEncoder().encode(credential),
|
|
2859
3098
|
"PBKDF2",
|
|
2860
3099
|
false,
|
|
2861
3100
|
["deriveKey"]
|
|
2862
3101
|
);
|
|
2863
|
-
return
|
|
3102
|
+
return subtle3.deriveKey(
|
|
2864
3103
|
{
|
|
2865
3104
|
name: "PBKDF2",
|
|
2866
3105
|
salt,
|
|
@@ -2884,14 +3123,14 @@ function base64ToBytes3(b64) {
|
|
|
2884
3123
|
for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
|
|
2885
3124
|
return out;
|
|
2886
3125
|
}
|
|
2887
|
-
var PBKDF2_ITERATIONS2, SALT_BYTES2, IV_BYTES2,
|
|
3126
|
+
var PBKDF2_ITERATIONS2, SALT_BYTES2, IV_BYTES2, subtle3;
|
|
2888
3127
|
var init_wrapped_deks = __esm({
|
|
2889
3128
|
"src/team/wrapped-deks.ts"() {
|
|
2890
3129
|
"use strict";
|
|
2891
3130
|
PBKDF2_ITERATIONS2 = 6e5;
|
|
2892
3131
|
SALT_BYTES2 = 32;
|
|
2893
3132
|
IV_BYTES2 = 12;
|
|
2894
|
-
|
|
3133
|
+
subtle3 = globalThis.crypto.subtle;
|
|
2895
3134
|
}
|
|
2896
3135
|
});
|
|
2897
3136
|
|
|
@@ -3696,6 +3935,21 @@ var init_core = __esm({
|
|
|
3696
3935
|
}
|
|
3697
3936
|
});
|
|
3698
3937
|
|
|
3938
|
+
// src/i18n/dictionary.ts
|
|
3939
|
+
function isDictCollectionName(name) {
|
|
3940
|
+
return name.startsWith(DICT_COLLECTION_PREFIX);
|
|
3941
|
+
}
|
|
3942
|
+
function isStaticDictDescriptor(x) {
|
|
3943
|
+
return typeof x === "object" && x !== null && x._noydbStaticDict === true;
|
|
3944
|
+
}
|
|
3945
|
+
var DICT_COLLECTION_PREFIX;
|
|
3946
|
+
var init_dictionary = __esm({
|
|
3947
|
+
"src/i18n/dictionary.ts"() {
|
|
3948
|
+
"use strict";
|
|
3949
|
+
DICT_COLLECTION_PREFIX = "_dict_";
|
|
3950
|
+
}
|
|
3951
|
+
});
|
|
3952
|
+
|
|
3699
3953
|
// src/money/fixed-point.ts
|
|
3700
3954
|
function expandExponent(s) {
|
|
3701
3955
|
const m = /^([+-]?)(\d+)(?:\.(\d+))?[eE]([+-]?\d+)$/.exec(s);
|
|
@@ -4315,6 +4569,9 @@ var init_strategy3 = __esm({
|
|
|
4315
4569
|
async clearHistory() {
|
|
4316
4570
|
return 0;
|
|
4317
4571
|
},
|
|
4572
|
+
async tombstoneHistory() {
|
|
4573
|
+
return 0;
|
|
4574
|
+
},
|
|
4318
4575
|
async envelopePayloadHash() {
|
|
4319
4576
|
return "";
|
|
4320
4577
|
},
|
|
@@ -5279,6 +5536,7 @@ function buildDictLabelResolver(joinCtx, field) {
|
|
|
5279
5536
|
const dictSource = joinCtx.resolveDictSource(field);
|
|
5280
5537
|
if (!dictSource) return void 0;
|
|
5281
5538
|
const snapshot = dictSource.snapshot();
|
|
5539
|
+
const displayLocale = dictSource.displayLocale;
|
|
5282
5540
|
const dictMap = /* @__PURE__ */ new Map();
|
|
5283
5541
|
for (const entry of snapshot) {
|
|
5284
5542
|
const k = entry["key"];
|
|
@@ -5288,9 +5546,11 @@ function buildDictLabelResolver(joinCtx, field) {
|
|
|
5288
5546
|
}
|
|
5289
5547
|
}
|
|
5290
5548
|
return async (key, locale, fallback) => {
|
|
5549
|
+
const effLocale = locale || displayLocale;
|
|
5550
|
+
if (!effLocale) return void 0;
|
|
5291
5551
|
const labels = dictMap.get(key);
|
|
5292
5552
|
if (!labels) return void 0;
|
|
5293
|
-
if (labels[
|
|
5553
|
+
if (labels[effLocale] !== void 0) return labels[effLocale];
|
|
5294
5554
|
const chain = Array.isArray(fallback) ? fallback : fallback ? [fallback] : [];
|
|
5295
5555
|
for (const fb of chain) {
|
|
5296
5556
|
if (fb === "any") {
|
|
@@ -8064,12 +8324,14 @@ var init_collection = __esm({
|
|
|
8064
8324
|
init_types();
|
|
8065
8325
|
init_strategy();
|
|
8066
8326
|
init_core();
|
|
8327
|
+
init_dictionary();
|
|
8067
8328
|
init_normalize();
|
|
8068
8329
|
init_paths();
|
|
8069
8330
|
init_computed();
|
|
8070
8331
|
init_strategy2();
|
|
8071
8332
|
init_policy();
|
|
8072
8333
|
init_crypto();
|
|
8334
|
+
init_record_keys();
|
|
8073
8335
|
init_errors();
|
|
8074
8336
|
init_tiers();
|
|
8075
8337
|
init_keyring();
|
|
@@ -8267,6 +8529,25 @@ var init_collection = __esm({
|
|
|
8267
8529
|
* is inactive for this collection; a frozen `Set` otherwise.
|
|
8268
8530
|
*/
|
|
8269
8531
|
deterministicFields;
|
|
8532
|
+
/**
|
|
8533
|
+
* Per-record CEK opt-in (`perRecordKeys: true`). When set, writes mint /
|
|
8534
|
+
* reuse a per-record content-encryption key and stamp `_cek` on the
|
|
8535
|
+
* envelope (see {@link EncryptedEnvelope._cek}). OFF by default — a
|
|
8536
|
+
* non-adopting collection takes the byte-identical legacy path. The READ
|
|
8537
|
+
* path does not consult this flag: `_cek` presence on the envelope is the
|
|
8538
|
+
* format discriminant, so a mixed vault (and a recipient that never set the
|
|
8539
|
+
* flag) still decrypts CEK records.
|
|
8540
|
+
*/
|
|
8541
|
+
perRecordCek;
|
|
8542
|
+
/**
|
|
8543
|
+
* Session-scoped `(id) → CEK` cache for this collection. Lets updates
|
|
8544
|
+
* reuse a record's stable CEK and lets repeated reads skip the AES-KW
|
|
8545
|
+
* unwrap. Bounded by LRU; never persisted. Dropped when the owning
|
|
8546
|
+
* collection instance is discarded — `vault.load()` clears the
|
|
8547
|
+
* collectionCache, so a keyring refresh drops every CEK alongside the
|
|
8548
|
+
* DEK cache. `null` unless `perRecordCek` is set.
|
|
8549
|
+
*/
|
|
8550
|
+
cekCache;
|
|
8270
8551
|
/**
|
|
8271
8552
|
* declared tiers for this collection. `null` when
|
|
8272
8553
|
* tier-aware methods are disabled. Tier 0 is implicit and never
|
|
@@ -8413,19 +8694,24 @@ var init_collection = __esm({
|
|
|
8413
8694
|
} else {
|
|
8414
8695
|
this.deterministicFields = null;
|
|
8415
8696
|
}
|
|
8697
|
+
this.perRecordCek = opts.perRecordKeys === true;
|
|
8698
|
+
this.cekCache = this.perRecordCek ? new Lru({ maxRecords: 4096 }) : null;
|
|
8416
8699
|
if (opts.crdt && opts.onRegisterConflictResolver) {
|
|
8417
8700
|
const crdtMode = opts.crdt;
|
|
8418
|
-
const crdtResolver = async (
|
|
8701
|
+
const crdtResolver = async (id, local, remote) => {
|
|
8419
8702
|
if (crdtMode === "yjs") {
|
|
8420
8703
|
return local._v >= remote._v ? local : remote;
|
|
8421
8704
|
}
|
|
8422
|
-
const localJson = await this.decryptJsonString(local);
|
|
8423
|
-
const remoteJson = await this.decryptJsonString(remote);
|
|
8705
|
+
const localJson = await this.decryptJsonString(local, id);
|
|
8706
|
+
const remoteJson = await this.decryptJsonString(remote, id);
|
|
8707
|
+
if (localJson === null) return local;
|
|
8708
|
+
if (remoteJson === null) return remote;
|
|
8424
8709
|
const localState = JSON.parse(localJson);
|
|
8425
8710
|
const remoteState = JSON.parse(remoteJson);
|
|
8426
8711
|
const merged = this.crdtStrategy.mergeCrdtStates(localState, remoteState);
|
|
8427
8712
|
const mergedVersion = Math.max(local._v, remote._v) + 1;
|
|
8428
|
-
|
|
8713
|
+
const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
|
|
8714
|
+
return this.encryptJsonString(JSON.stringify(merged), mergedVersion, cek);
|
|
8429
8715
|
};
|
|
8430
8716
|
opts.onRegisterConflictResolver(this.name, crdtResolver);
|
|
8431
8717
|
}
|
|
@@ -8465,12 +8751,15 @@ var init_collection = __esm({
|
|
|
8465
8751
|
});
|
|
8466
8752
|
} else {
|
|
8467
8753
|
const mergeFn = policy;
|
|
8468
|
-
resolver = async (
|
|
8469
|
-
const localRecord = await this.decryptRecord(local, { skipValidation: true });
|
|
8470
|
-
const remoteRecord = await this.decryptRecord(remote, { skipValidation: true });
|
|
8754
|
+
resolver = async (id, local, remote) => {
|
|
8755
|
+
const localRecord = await this.decryptRecord(local, { skipValidation: true, id });
|
|
8756
|
+
const remoteRecord = await this.decryptRecord(remote, { skipValidation: true, id });
|
|
8757
|
+
if (localRecord === null) return local;
|
|
8758
|
+
if (remoteRecord === null) return remote;
|
|
8471
8759
|
const merged = mergeFn(localRecord, remoteRecord);
|
|
8472
8760
|
const mergedVersion = Math.max(local._v, remote._v) + 1;
|
|
8473
|
-
|
|
8761
|
+
const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
|
|
8762
|
+
return this.encryptRecord(merged, mergedVersion, cek);
|
|
8474
8763
|
};
|
|
8475
8764
|
}
|
|
8476
8765
|
opts.onRegisterConflictResolver(collectionName, resolver);
|
|
@@ -8562,7 +8851,9 @@ var init_collection = __esm({
|
|
|
8562
8851
|
} else {
|
|
8563
8852
|
const envelope = await this.adapter.get(this.vault, this.name, id);
|
|
8564
8853
|
if (!envelope) return null;
|
|
8565
|
-
|
|
8854
|
+
if (isTombstone(envelope, this.encrypted)) return null;
|
|
8855
|
+
record = await this.decryptRecord(envelope, { id });
|
|
8856
|
+
if (record === null) return null;
|
|
8566
8857
|
this.lru.set(id, { record, version: envelope._v }, estimateRecordBytes(record));
|
|
8567
8858
|
}
|
|
8568
8859
|
} else {
|
|
@@ -8589,6 +8880,7 @@ var init_collection = __esm({
|
|
|
8589
8880
|
const envelope = await this.adapter.get(this.vault, this.name, id);
|
|
8590
8881
|
if (!envelope) return null;
|
|
8591
8882
|
const json = await this.decryptJsonString(envelope);
|
|
8883
|
+
if (json === null) return null;
|
|
8592
8884
|
return JSON.parse(json);
|
|
8593
8885
|
}
|
|
8594
8886
|
/**
|
|
@@ -8677,7 +8969,7 @@ var init_collection = __esm({
|
|
|
8677
8969
|
if (cached2) return { record: cached2.record, version: cached2.version };
|
|
8678
8970
|
const env = await this.adapter.get(this.vault, this.name, id);
|
|
8679
8971
|
if (!env) return { record: null, version: 0 };
|
|
8680
|
-
return { record: await this.decryptRecord(env, { skipValidation: true }), version: env._v };
|
|
8972
|
+
return { record: await this.decryptRecord(env, { skipValidation: true }) ?? null, version: env._v };
|
|
8681
8973
|
}
|
|
8682
8974
|
await this.ensureHydrated();
|
|
8683
8975
|
const cached = this.cache.get(id);
|
|
@@ -8790,9 +9082,11 @@ var init_collection = __esm({
|
|
|
8790
9082
|
let existingState;
|
|
8791
9083
|
if (existingEnvelope) {
|
|
8792
9084
|
const prevJson = await this.decryptJsonString(existingEnvelope);
|
|
8793
|
-
|
|
8794
|
-
|
|
8795
|
-
|
|
9085
|
+
if (prevJson !== null) {
|
|
9086
|
+
const prevParsed = JSON.parse(prevJson);
|
|
9087
|
+
if (prevParsed !== null && typeof prevParsed === "object" && "_crdt" in prevParsed) {
|
|
9088
|
+
existingState = prevParsed;
|
|
9089
|
+
}
|
|
8796
9090
|
}
|
|
8797
9091
|
}
|
|
8798
9092
|
crdtState = this.crdtStrategy.buildLwwMapState(record, existingState, now);
|
|
@@ -8800,9 +9094,11 @@ var init_collection = __esm({
|
|
|
8800
9094
|
let existingState;
|
|
8801
9095
|
if (existingEnvelope) {
|
|
8802
9096
|
const prevJson = await this.decryptJsonString(existingEnvelope);
|
|
8803
|
-
|
|
8804
|
-
|
|
8805
|
-
|
|
9097
|
+
if (prevJson !== null) {
|
|
9098
|
+
const prevParsed = JSON.parse(prevJson);
|
|
9099
|
+
if (prevParsed !== null && typeof prevParsed === "object" && "_crdt" in prevParsed) {
|
|
9100
|
+
existingState = prevParsed;
|
|
9101
|
+
}
|
|
8806
9102
|
}
|
|
8807
9103
|
}
|
|
8808
9104
|
const arr = Array.isArray(record) ? record : [record];
|
|
@@ -8811,12 +9107,14 @@ var init_collection = __esm({
|
|
|
8811
9107
|
crdtState = { _crdt: "yjs", update: record };
|
|
8812
9108
|
}
|
|
8813
9109
|
const version2 = existingVersion + 1;
|
|
8814
|
-
const
|
|
9110
|
+
const cek2 = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
|
|
9111
|
+
const envelope2 = await this.encryptJsonString(JSON.stringify(crdtState), version2, cek2);
|
|
8815
9112
|
await this.adapter.put(this.vault, this.name, id, envelope2);
|
|
8816
9113
|
const resolvedRecord = this.crdtStrategy.resolveCrdtSnapshot(crdtState);
|
|
8817
|
-
const
|
|
9114
|
+
const existingResolvedRecord = existingEnvelope ? await this.decryptRecord(existingEnvelope, { skipValidation: true }) : null;
|
|
9115
|
+
const existingResolved = existingResolvedRecord !== null ? { record: existingResolvedRecord, version: existingVersion } : void 0;
|
|
8818
9116
|
if (existingResolved && this.historyConfig.enabled !== false) {
|
|
8819
|
-
const histEnvelope = await this.encryptRecord(existingResolved.record, existingResolved.version);
|
|
9117
|
+
const histEnvelope = await this.encryptRecord(existingResolved.record, existingResolved.version, cek2);
|
|
8820
9118
|
await this.historyStrategy.saveHistory(this.adapter, this.vault, this.name, id, histEnvelope);
|
|
8821
9119
|
this.emitter.emit("history:save", { vault: this.vault, collection: this.name, id, version: existingResolved.version });
|
|
8822
9120
|
if (this.historyConfig.maxVersions) {
|
|
@@ -8862,7 +9160,9 @@ var init_collection = __esm({
|
|
|
8862
9160
|
const previousEnvelope = await this.adapter.get(this.vault, this.name, id);
|
|
8863
9161
|
if (previousEnvelope) {
|
|
8864
9162
|
const previousRecord = await this.decryptRecord(previousEnvelope);
|
|
8865
|
-
|
|
9163
|
+
if (previousRecord !== null) {
|
|
9164
|
+
existing = { record: previousRecord, version: previousEnvelope._v };
|
|
9165
|
+
}
|
|
8866
9166
|
}
|
|
8867
9167
|
}
|
|
8868
9168
|
} else {
|
|
@@ -8871,8 +9171,9 @@ var init_collection = __esm({
|
|
|
8871
9171
|
}
|
|
8872
9172
|
const version = existing ? existing.version + 1 : 1;
|
|
8873
9173
|
this.uniqueConstraints?.check(id, record);
|
|
9174
|
+
const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
|
|
8874
9175
|
if (existing && this.historyConfig.enabled !== false) {
|
|
8875
|
-
const historyEnvelope = await this.encryptRecord(existing.record, existing.version);
|
|
9176
|
+
const historyEnvelope = await this.encryptRecord(existing.record, existing.version, cek);
|
|
8876
9177
|
await this.historyStrategy.saveHistory(this.adapter, this.vault, this.name, id, historyEnvelope);
|
|
8877
9178
|
this.emitter.emit("history:save", {
|
|
8878
9179
|
vault: this.vault,
|
|
@@ -8886,7 +9187,7 @@ var init_collection = __esm({
|
|
|
8886
9187
|
});
|
|
8887
9188
|
}
|
|
8888
9189
|
}
|
|
8889
|
-
const envelope = await this.encryptRecord(record, version);
|
|
9190
|
+
const envelope = await this.encryptRecord(record, version, cek);
|
|
8890
9191
|
await this.adapter.put(this.vault, this.name, id, envelope);
|
|
8891
9192
|
if (this.ledger) {
|
|
8892
9193
|
const appendInput = {
|
|
@@ -9122,11 +9423,14 @@ var init_collection = __esm({
|
|
|
9122
9423
|
let count = 0;
|
|
9123
9424
|
for (const id of ids) {
|
|
9124
9425
|
const env = await this.adapter.get(this.vault, this.name, id);
|
|
9125
|
-
if (!env) continue;
|
|
9126
|
-
const
|
|
9426
|
+
if (!env || isTombstone(env, this.encrypted)) continue;
|
|
9427
|
+
const decoded = await this.decryptRecord(env, { skipValidation: true, id });
|
|
9428
|
+
if (decoded === null) continue;
|
|
9429
|
+
const record = decoded;
|
|
9127
9430
|
const next = transform(record);
|
|
9128
9431
|
const nextVersion = (env._v ?? 0) + 1;
|
|
9129
|
-
const
|
|
9432
|
+
const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
|
|
9433
|
+
const newEnv = await this.encryptRecord(next, nextVersion, cek);
|
|
9130
9434
|
await this.adapter.put(this.vault, this.name, id, newEnv);
|
|
9131
9435
|
await this._invalidateCacheEntry(id);
|
|
9132
9436
|
if (this.ledger) {
|
|
@@ -9238,14 +9542,17 @@ var init_collection = __esm({
|
|
|
9238
9542
|
const previousEnvelope2 = await this.adapter.get(this.vault, this.name, id);
|
|
9239
9543
|
if (previousEnvelope2) {
|
|
9240
9544
|
const previousRecord = await this.decryptRecord(previousEnvelope2);
|
|
9241
|
-
|
|
9545
|
+
if (previousRecord !== null) {
|
|
9546
|
+
existing = { record: previousRecord, version: previousEnvelope2._v };
|
|
9547
|
+
}
|
|
9242
9548
|
}
|
|
9243
9549
|
}
|
|
9244
9550
|
} else {
|
|
9245
9551
|
existing = this.cache.get(id);
|
|
9246
9552
|
}
|
|
9247
9553
|
if (existing && this.historyConfig.enabled !== false) {
|
|
9248
|
-
const
|
|
9554
|
+
const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
|
|
9555
|
+
const historyEnvelope = await this.encryptRecord(existing.record, existing.version, cek);
|
|
9249
9556
|
await this.historyStrategy.saveHistory(this.adapter, this.vault, this.name, id, historyEnvelope);
|
|
9250
9557
|
}
|
|
9251
9558
|
const previousEnvelope = await this.adapter.get(this.vault, this.name, id);
|
|
@@ -9286,6 +9593,50 @@ var init_collection = __esm({
|
|
|
9286
9593
|
await this.dispatchArrayDerivationsOnDelete(id);
|
|
9287
9594
|
}
|
|
9288
9595
|
}
|
|
9596
|
+
/**
|
|
9597
|
+
* @internal — GDPR crypto-shred a LIVE record to a tombstone (#304).
|
|
9598
|
+
*
|
|
9599
|
+
* Rewrites the on-disk envelope to `{ _noydb, _v, _ts, _by, _iv:'', _data:'' }`,
|
|
9600
|
+
* dropping `_iv`/`_data`/`_cek`/`_det`. The wrapped per-record CEK is gone, so
|
|
9601
|
+
* the body — and (via {@link tombstoneHistory}) every history version under
|
|
9602
|
+
* the same CEK — is permanently undecryptable; the collection DEK and every
|
|
9603
|
+
* other record are untouched. `_det` is stripped too, so `findByDet` no
|
|
9604
|
+
* longer matches the shredded record (avoiding a post-shred TamperedError).
|
|
9605
|
+
*
|
|
9606
|
+
* Unlike `delete()`/`_internalDelete`, this:
|
|
9607
|
+
* - does NOT fire onDelete guards / MV / derivation dispatch (a shred is an
|
|
9608
|
+
* erasure, not a domain delete — re-running those would be wrong),
|
|
9609
|
+
* - does NOT append a per-record ledger entry (`vault.forget()` appends a
|
|
9610
|
+
* single `op:'forget'` summary for the whole subject),
|
|
9611
|
+
* - keeps the record KEY present (it's an overwrite, not an adapter delete)
|
|
9612
|
+
* so the version counter + "record existed" survive for audit.
|
|
9613
|
+
*
|
|
9614
|
+
* Idempotent: returns `null` when the record is absent or already a tombstone.
|
|
9615
|
+
* Otherwise returns `{ previousVersion }`. Invalidates the eager cache, the
|
|
9616
|
+
* lazy LRU, and the per-record CEK cache for this id.
|
|
9617
|
+
*/
|
|
9618
|
+
/**
|
|
9619
|
+
* @internal — decrypt an envelope to a plain record for subject-index
|
|
9620
|
+
* rebuild (#304). Returns `null` for a tombstone or unreadable envelope.
|
|
9621
|
+
* Skips schema validation — the rebuild only reads the subject field.
|
|
9622
|
+
*/
|
|
9623
|
+
async _decodeEnvelope(envelope, id) {
|
|
9624
|
+
try {
|
|
9625
|
+
const rec = await this.decryptRecord(envelope, { skipValidation: true, id });
|
|
9626
|
+
return rec === null ? null : rec;
|
|
9627
|
+
} catch {
|
|
9628
|
+
return null;
|
|
9629
|
+
}
|
|
9630
|
+
}
|
|
9631
|
+
async _writeTombstone(id, actor) {
|
|
9632
|
+
const live = await this.adapter.get(this.vault, this.name, id);
|
|
9633
|
+
if (!live || isTombstone(live, this.encrypted)) return null;
|
|
9634
|
+
await this.adapter.put(this.vault, this.name, id, buildTombstone(live._v, actor));
|
|
9635
|
+
this.cache.delete(id);
|
|
9636
|
+
this.lru?.remove(id);
|
|
9637
|
+
this.cekCache?.remove(id);
|
|
9638
|
+
return { previousVersion: live._v };
|
|
9639
|
+
}
|
|
9289
9640
|
/**
|
|
9290
9641
|
* Cascade deletes of array-shape derived rows when a source row is
|
|
9291
9642
|
* deleted. Reads each registered strategy's fanout sidecar
|
|
@@ -9698,6 +10049,7 @@ var init_collection = __esm({
|
|
|
9698
10049
|
const entries = [];
|
|
9699
10050
|
for (const env of envelopes) {
|
|
9700
10051
|
const record = await this.decryptRecord(env, { skipValidation: true });
|
|
10052
|
+
if (record === null) continue;
|
|
9701
10053
|
entries.push({
|
|
9702
10054
|
version: env._v,
|
|
9703
10055
|
timestamp: env._ts,
|
|
@@ -9834,6 +10186,7 @@ var init_collection = __esm({
|
|
|
9834
10186
|
const envelope = await this.adapter.get(this.vault, this.name, id);
|
|
9835
10187
|
if (envelope) {
|
|
9836
10188
|
const record = await this.decryptRecord(envelope);
|
|
10189
|
+
if (record === null) continue;
|
|
9837
10190
|
items.push(record);
|
|
9838
10191
|
if (!this.lazy && !this.cache.has(id)) {
|
|
9839
10192
|
this.cache.set(id, { record, version: envelope._v });
|
|
@@ -9910,6 +10263,7 @@ var init_collection = __esm({
|
|
|
9910
10263
|
const out = [];
|
|
9911
10264
|
for (const { id, envelope } of items) {
|
|
9912
10265
|
const record = await this.decryptRecord(envelope);
|
|
10266
|
+
if (record === null) continue;
|
|
9913
10267
|
out.push({ id, record, version: envelope._v });
|
|
9914
10268
|
}
|
|
9915
10269
|
return out;
|
|
@@ -9931,6 +10285,18 @@ var init_collection = __esm({
|
|
|
9931
10285
|
* the cache entry (record still present) or deletes it (record was
|
|
9932
10286
|
* gone before the tx and the revert deleted it again).
|
|
9933
10287
|
*/
|
|
10288
|
+
/**
|
|
10289
|
+
* @internal — evict ONLY the per-record CEK cache entry for `id`. Used by
|
|
10290
|
+
* `vault.rotateRecordCek()`: after a hard CEK rotation the cached unwrapped
|
|
10291
|
+
* CEK is stale (it would decrypt the pre-rotation body and fail GCM auth on
|
|
10292
|
+
* the post-rotation body). Eviction must be synchronous with the live-envelope
|
|
10293
|
+
* rewrite so no concurrent read observes the old CEK. Paired with
|
|
10294
|
+
* {@link _invalidateCacheEntry} (which refreshes the decrypted-record cache).
|
|
10295
|
+
* No-op when the collection is not `perRecordKeys`.
|
|
10296
|
+
*/
|
|
10297
|
+
_invalidateCekCacheEntry(id) {
|
|
10298
|
+
this.cekCache?.remove(id);
|
|
10299
|
+
}
|
|
9934
10300
|
async _invalidateCacheEntry(id) {
|
|
9935
10301
|
if (this.lazy && this.lru) {
|
|
9936
10302
|
this.lru.remove(id);
|
|
@@ -9948,6 +10314,14 @@ var init_collection = __esm({
|
|
|
9948
10314
|
return;
|
|
9949
10315
|
}
|
|
9950
10316
|
const record = await this.decryptRecord(envelope);
|
|
10317
|
+
if (record === null) {
|
|
10318
|
+
this.cache.delete(id);
|
|
10319
|
+
if (previous) {
|
|
10320
|
+
this.indexes?.remove(id, previous.record);
|
|
10321
|
+
this.uniqueConstraints?.remove(id, previous.record);
|
|
10322
|
+
}
|
|
10323
|
+
return;
|
|
10324
|
+
}
|
|
9951
10325
|
this.cache.set(id, { record, version: envelope._v });
|
|
9952
10326
|
this.indexes?.upsert(id, record, previous ? previous.record : null);
|
|
9953
10327
|
this.uniqueConstraints?.upsert(id, record, previous?.record);
|
|
@@ -9972,8 +10346,9 @@ var init_collection = __esm({
|
|
|
9972
10346
|
const ids = await this.adapter.list(this.vault, this.name);
|
|
9973
10347
|
for (const id of ids) {
|
|
9974
10348
|
const envelope = await this.adapter.get(this.vault, this.name, id);
|
|
9975
|
-
if (envelope) {
|
|
9976
|
-
const record = await this.decryptRecord(envelope);
|
|
10349
|
+
if (envelope && !isTombstone(envelope, this.encrypted)) {
|
|
10350
|
+
const record = await this.decryptRecord(envelope, { id });
|
|
10351
|
+
if (record === null) continue;
|
|
9977
10352
|
this.cache.set(id, { record, version: envelope._v });
|
|
9978
10353
|
}
|
|
9979
10354
|
}
|
|
@@ -9984,7 +10359,9 @@ var init_collection = __esm({
|
|
|
9984
10359
|
/** Hydrate from a pre-loaded snapshot (used by Vault). */
|
|
9985
10360
|
async hydrateFromSnapshot(records) {
|
|
9986
10361
|
for (const [id, envelope] of Object.entries(records)) {
|
|
9987
|
-
|
|
10362
|
+
if (isTombstone(envelope, this.encrypted)) continue;
|
|
10363
|
+
const record = await this.decryptRecord(envelope, { id });
|
|
10364
|
+
if (record === null) continue;
|
|
9988
10365
|
this.cache.set(id, { record, version: envelope._v });
|
|
9989
10366
|
}
|
|
9990
10367
|
this.hydrated = true;
|
|
@@ -10072,6 +10449,7 @@ var init_collection = __esm({
|
|
|
10072
10449
|
const envelope = await this.adapter.get(this.vault, this.name, recordId2);
|
|
10073
10450
|
if (!envelope) continue;
|
|
10074
10451
|
const record = await this.decryptRecord(envelope, { skipValidation: true });
|
|
10452
|
+
if (record === null) continue;
|
|
10075
10453
|
await this.maintainPersistedIndexesOnPut(recordId2, record, null, envelope._v);
|
|
10076
10454
|
}
|
|
10077
10455
|
this.persistedIndexesLoaded = true;
|
|
@@ -10122,8 +10500,13 @@ var init_collection = __esm({
|
|
|
10122
10500
|
const env = await this.adapter.get(this.vault, this.name, id);
|
|
10123
10501
|
if (!env) continue;
|
|
10124
10502
|
try {
|
|
10125
|
-
const
|
|
10126
|
-
|
|
10503
|
+
const sidecarJson = await this.decryptJsonString(env);
|
|
10504
|
+
if (sidecarJson === null) {
|
|
10505
|
+
sidecar.set(decoded.recordId, void 0);
|
|
10506
|
+
} else {
|
|
10507
|
+
const body = JSON.parse(sidecarJson);
|
|
10508
|
+
sidecar.set(decoded.recordId, body.value);
|
|
10509
|
+
}
|
|
10127
10510
|
} catch {
|
|
10128
10511
|
sidecar.set(decoded.recordId, void 0);
|
|
10129
10512
|
}
|
|
@@ -10137,6 +10520,7 @@ var init_collection = __esm({
|
|
|
10137
10520
|
const env = await this.adapter.get(this.vault, this.name, id);
|
|
10138
10521
|
if (!env) continue;
|
|
10139
10522
|
const record = await this.decryptRecord(env, { skipValidation: true });
|
|
10523
|
+
if (record === null) continue;
|
|
10140
10524
|
const live = readPersistedValue(record, field);
|
|
10141
10525
|
const stored = sidecar.get(id);
|
|
10142
10526
|
const hasSidecar = sidecarIds.has(id);
|
|
@@ -10219,7 +10603,8 @@ var init_collection = __esm({
|
|
|
10219
10603
|
recordId: id,
|
|
10220
10604
|
getDEK: this.getDEK,
|
|
10221
10605
|
encrypted: this.encrypted,
|
|
10222
|
-
userId: this.keyring.userId
|
|
10606
|
+
userId: this.keyring.userId,
|
|
10607
|
+
erasableBlobs: this.perRecordCek
|
|
10223
10608
|
});
|
|
10224
10609
|
}
|
|
10225
10610
|
/** Get all records as encrypted envelopes (for dump). */
|
|
@@ -10227,7 +10612,8 @@ var init_collection = __esm({
|
|
|
10227
10612
|
await this.ensureHydrated();
|
|
10228
10613
|
const result = {};
|
|
10229
10614
|
for (const [id, entry] of this.cache) {
|
|
10230
|
-
|
|
10615
|
+
const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
|
|
10616
|
+
result[id] = await this.encryptRecord(entry.record, entry.version, cek);
|
|
10231
10617
|
}
|
|
10232
10618
|
return result;
|
|
10233
10619
|
}
|
|
@@ -10255,8 +10641,11 @@ var init_collection = __esm({
|
|
|
10255
10641
|
if (hasMoney && this.moneyFields) {
|
|
10256
10642
|
result = decodeMoneyFields(result, this.moneyFields, typeof locale === "string" ? locale : void 0);
|
|
10257
10643
|
}
|
|
10258
|
-
|
|
10259
|
-
|
|
10644
|
+
const hasStaticDisplay = hasDict && this.dictKeyFields !== void 0 && Object.values(this.dictKeyFields).some(
|
|
10645
|
+
(d) => isStaticDictDescriptor(d) && d.displayLocale !== void 0
|
|
10646
|
+
);
|
|
10647
|
+
if (!locale && !hasStaticDisplay) return result;
|
|
10648
|
+
if (locale && hasI18n && this.i18nFields) {
|
|
10260
10649
|
result = this.i18nStrategy.applyI18nLocale(result, this.i18nFields, locale, localeOpts?.fallback);
|
|
10261
10650
|
}
|
|
10262
10651
|
if (hasDict && this.dictKeyFields && this.dictLabelResolver && locale !== "raw") {
|
|
@@ -10265,13 +10654,23 @@ var init_collection = __esm({
|
|
|
10265
10654
|
for (const [field, desc] of Object.entries(this.dictKeyFields)) {
|
|
10266
10655
|
const policy = desc.onMissing ? resolvePolicy(desc.onMissing, "read") : "null";
|
|
10267
10656
|
const fallback = policy === "substitute" ? localeOpts?.fallback ?? desc.substitute : localeOpts?.fallback;
|
|
10657
|
+
const effLocale = locale ?? (isStaticDictDescriptor(desc) ? desc.displayLocale : void 0);
|
|
10268
10658
|
const resolveKey = async (key) => {
|
|
10269
|
-
|
|
10659
|
+
if (!effLocale) {
|
|
10660
|
+
if (policy === "throw") {
|
|
10661
|
+
throw new LocaleNotSpecifiedError(
|
|
10662
|
+
field,
|
|
10663
|
+
`dictKey "${field}": no locale active to resolve key "${key}".`
|
|
10664
|
+
);
|
|
10665
|
+
}
|
|
10666
|
+
return null;
|
|
10667
|
+
}
|
|
10668
|
+
const label = await resolver(desc.name, key, effLocale, fallback);
|
|
10270
10669
|
if (label === void 0) {
|
|
10271
10670
|
if (policy === "throw") {
|
|
10272
10671
|
throw new LocaleNotSpecifiedError(
|
|
10273
10672
|
field,
|
|
10274
|
-
`dictKey "${field}": no label for key "${key}" in locale "${
|
|
10673
|
+
`dictKey "${field}": no label for key "${key}" in locale "${effLocale}".`
|
|
10275
10674
|
);
|
|
10276
10675
|
}
|
|
10277
10676
|
return null;
|
|
@@ -10428,6 +10827,7 @@ var init_collection = __esm({
|
|
|
10428
10827
|
if (!envelope) continue;
|
|
10429
10828
|
try {
|
|
10430
10829
|
const json = await this.decryptJsonString(envelope);
|
|
10830
|
+
if (json === null) continue;
|
|
10431
10831
|
const body = JSON.parse(json);
|
|
10432
10832
|
if (typeof body.recordId !== "string") continue;
|
|
10433
10833
|
const rows = byField.get(decoded.field) ?? [];
|
|
@@ -10537,7 +10937,31 @@ var init_collection = __esm({
|
|
|
10537
10937
|
};
|
|
10538
10938
|
return new LazyQuery(source);
|
|
10539
10939
|
}
|
|
10540
|
-
|
|
10940
|
+
/**
|
|
10941
|
+
* Resolve the stable CEK for a record on the WRITE path — see
|
|
10942
|
+
* {@link resolveStableCek}. Thin delegate that supplies the collection's
|
|
10943
|
+
* CEK cache, live-envelope reader, and DEK resolver.
|
|
10944
|
+
*/
|
|
10945
|
+
resolveRecordCek(id) {
|
|
10946
|
+
return resolveStableCek(
|
|
10947
|
+
{
|
|
10948
|
+
cache: this.cekCache,
|
|
10949
|
+
getLive: (rid) => this.adapter.get(this.vault, this.name, rid),
|
|
10950
|
+
getDEK: () => this.getDEK(this.name)
|
|
10951
|
+
},
|
|
10952
|
+
id
|
|
10953
|
+
);
|
|
10954
|
+
}
|
|
10955
|
+
/**
|
|
10956
|
+
* Encrypt a JSON body into an envelope.
|
|
10957
|
+
*
|
|
10958
|
+
* When `cek` is supplied (per-record CEK collections), the body is
|
|
10959
|
+
* encrypted under the CEK and the CEK is AES-KW-wrapped under the
|
|
10960
|
+
* collection DEK and stamped on `_cek`. When `cek` is omitted, the legacy
|
|
10961
|
+
* path encrypts the body directly under the collection DEK — byte-identical
|
|
10962
|
+
* to pre-CEK behaviour, so non-adopting collections pay nothing.
|
|
10963
|
+
*/
|
|
10964
|
+
async encryptJsonString(json, version, cek) {
|
|
10541
10965
|
const by = this.keyring.userId;
|
|
10542
10966
|
if (!this.encrypted) {
|
|
10543
10967
|
return {
|
|
@@ -10550,6 +10974,19 @@ var init_collection = __esm({
|
|
|
10550
10974
|
};
|
|
10551
10975
|
}
|
|
10552
10976
|
const dek = await this.getDEK(this.name);
|
|
10977
|
+
if (cek !== void 0) {
|
|
10978
|
+
const { iv: iv2, data: data2 } = await encrypt(json, cek);
|
|
10979
|
+
const wrapped = await wrapCek(cek, dek);
|
|
10980
|
+
return {
|
|
10981
|
+
_noydb: NOYDB_FORMAT_VERSION,
|
|
10982
|
+
_v: version,
|
|
10983
|
+
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10984
|
+
_iv: iv2,
|
|
10985
|
+
_data: data2,
|
|
10986
|
+
_by: by,
|
|
10987
|
+
_cek: wrapped
|
|
10988
|
+
};
|
|
10989
|
+
}
|
|
10553
10990
|
const { iv, data } = await encrypt(json, dek);
|
|
10554
10991
|
return {
|
|
10555
10992
|
_noydb: NOYDB_FORMAT_VERSION,
|
|
@@ -10560,8 +10997,8 @@ var init_collection = __esm({
|
|
|
10560
10997
|
_by: by
|
|
10561
10998
|
};
|
|
10562
10999
|
}
|
|
10563
|
-
async encryptRecord(record, version) {
|
|
10564
|
-
const base = await this.encryptJsonString(JSON.stringify(record), version);
|
|
11000
|
+
async encryptRecord(record, version, cek) {
|
|
11001
|
+
const base = await this.encryptJsonString(JSON.stringify(record), version, cek);
|
|
10565
11002
|
if (!this.deterministicFields || !this.encrypted) return base;
|
|
10566
11003
|
const dek = await this.getDEK(this.name);
|
|
10567
11004
|
const rec = record;
|
|
@@ -10639,7 +11076,8 @@ var init_collection = __esm({
|
|
|
10639
11076
|
const env = await this.adapter.get(this.vault, this.name, id);
|
|
10640
11077
|
if (!env || !env._det) continue;
|
|
10641
11078
|
if (env._det[field] === target) {
|
|
10642
|
-
|
|
11079
|
+
const rec = await this.decryptRecord(env);
|
|
11080
|
+
if (rec !== null) matches.push(rec);
|
|
10643
11081
|
}
|
|
10644
11082
|
}
|
|
10645
11083
|
return matches;
|
|
@@ -10740,7 +11178,14 @@ var init_collection = __esm({
|
|
|
10740
11178
|
return null;
|
|
10741
11179
|
}
|
|
10742
11180
|
const dek = await this.getDEK(key);
|
|
10743
|
-
|
|
11181
|
+
let plaintext;
|
|
11182
|
+
if (envelope._cek !== void 0) {
|
|
11183
|
+
const cek = await unwrapCek(envelope._cek, dek);
|
|
11184
|
+
this.cekCache?.set(id, cek, 1);
|
|
11185
|
+
plaintext = await decrypt(envelope._iv, envelope._data, cek);
|
|
11186
|
+
} else {
|
|
11187
|
+
plaintext = await decrypt(envelope._iv, envelope._data, dek);
|
|
11188
|
+
}
|
|
10744
11189
|
const record = JSON.parse(plaintext);
|
|
10745
11190
|
this.emitCrossTierEvent({
|
|
10746
11191
|
actor: this.keyring.userId,
|
|
@@ -10796,18 +11241,19 @@ var init_collection = __esm({
|
|
|
10796
11241
|
const toKey = dekKey(this.name, toTier);
|
|
10797
11242
|
const fromDek = await this.getDEK(fromKey);
|
|
10798
11243
|
const toDek = await this.getDEK(toKey);
|
|
10799
|
-
const plaintext = await decrypt(envelope._iv, envelope._data, fromDek);
|
|
10800
|
-
const { iv, data } = await encrypt(plaintext, toDek);
|
|
10801
11244
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11245
|
+
const body = await rewrapBodyToDek(envelope, fromDek, toDek);
|
|
11246
|
+
if (body.cek) this.cekCache?.set(id, body.cek, 1);
|
|
10802
11247
|
const next = {
|
|
10803
11248
|
_noydb: NOYDB_FORMAT_VERSION,
|
|
10804
11249
|
_v: envelope._v + 1,
|
|
10805
11250
|
_ts: now,
|
|
10806
|
-
_iv:
|
|
10807
|
-
_data:
|
|
11251
|
+
_iv: body._iv,
|
|
11252
|
+
_data: body._data,
|
|
10808
11253
|
_by: this.keyring.userId,
|
|
10809
11254
|
_tier: toTier,
|
|
10810
|
-
_elevatedBy: this.keyring.userId
|
|
11255
|
+
_elevatedBy: this.keyring.userId,
|
|
11256
|
+
...body._cek !== void 0 ? { _cek: body._cek } : {}
|
|
10811
11257
|
};
|
|
10812
11258
|
await this.adapter.put(this.vault, this.name, id, next);
|
|
10813
11259
|
this.emitCrossTierEvent({
|
|
@@ -10843,17 +11289,18 @@ var init_collection = __esm({
|
|
|
10843
11289
|
if (toTier > 0) this.assertDeclaredTier(toTier);
|
|
10844
11290
|
const fromDek = await this.getDEK(dekKey(this.name, fromTier));
|
|
10845
11291
|
const toDek = await this.getDEK(dekKey(this.name, toTier));
|
|
10846
|
-
const plaintext = await decrypt(envelope._iv, envelope._data, fromDek);
|
|
10847
|
-
const { iv, data } = await encrypt(plaintext, toDek);
|
|
10848
11292
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11293
|
+
const body = await rewrapBodyToDek(envelope, fromDek, toDek);
|
|
11294
|
+
if (body.cek) this.cekCache?.set(id, body.cek, 1);
|
|
10849
11295
|
const next = {
|
|
10850
11296
|
_noydb: NOYDB_FORMAT_VERSION,
|
|
10851
11297
|
_v: envelope._v + 1,
|
|
10852
11298
|
_ts: now,
|
|
10853
|
-
_iv:
|
|
10854
|
-
_data:
|
|
11299
|
+
_iv: body._iv,
|
|
11300
|
+
_data: body._data,
|
|
10855
11301
|
_by: this.keyring.userId,
|
|
10856
|
-
...toTier > 0 && { _tier: toTier }
|
|
11302
|
+
...toTier > 0 && { _tier: toTier },
|
|
11303
|
+
...body._cek !== void 0 ? { _cek: body._cek } : {}
|
|
10857
11304
|
};
|
|
10858
11305
|
await this.adapter.put(this.vault, this.name, id, next);
|
|
10859
11306
|
this.emitCrossTierEvent({
|
|
@@ -10875,10 +11322,30 @@ var init_collection = __esm({
|
|
|
10875
11322
|
} catch {
|
|
10876
11323
|
}
|
|
10877
11324
|
}
|
|
10878
|
-
/**
|
|
10879
|
-
|
|
11325
|
+
/**
|
|
11326
|
+
* Low-level: decrypt an envelope and return the raw JSON string.
|
|
11327
|
+
*
|
|
11328
|
+
* `_cek` presence is the format discriminant (NOT `this.perRecordCek`),
|
|
11329
|
+
* so a mixed vault — and a recipient that never opted into
|
|
11330
|
+
* `perRecordKeys` — decrypts both legacy and CEK records:
|
|
11331
|
+
* - `_cek` present → unwrap the CEK under the collection DEK, decrypt the
|
|
11332
|
+
* body under the CEK (cache the unwrapped CEK so repeated reads skip it).
|
|
11333
|
+
* - `_cek` absent → legacy path, body decrypts directly under the
|
|
11334
|
+
* collection DEK.
|
|
11335
|
+
*
|
|
11336
|
+
* The optional `id` lets reads populate the CEK cache; it is omitted by
|
|
11337
|
+
* callers (history, conflict merge) that have only the envelope.
|
|
11338
|
+
*/
|
|
11339
|
+
async decryptJsonString(envelope, id) {
|
|
11340
|
+
if (isTombstone(envelope, this.encrypted)) return null;
|
|
10880
11341
|
if (!this.encrypted) return envelope._data;
|
|
10881
11342
|
const dek = await this.getDEK(this.name);
|
|
11343
|
+
if (envelope._cek !== void 0) {
|
|
11344
|
+
const cached = id !== void 0 ? this.cekCache?.get(id) : void 0;
|
|
11345
|
+
const cek = cached ?? await unwrapCek(envelope._cek, dek);
|
|
11346
|
+
if (cached === void 0 && id !== void 0) this.cekCache?.set(id, cek, 1);
|
|
11347
|
+
return decrypt(envelope._iv, envelope._data, cek);
|
|
11348
|
+
}
|
|
10882
11349
|
return decrypt(envelope._iv, envelope._data, dek);
|
|
10883
11350
|
}
|
|
10884
11351
|
/**
|
|
@@ -10897,7 +11364,8 @@ var init_collection = __esm({
|
|
|
10897
11364
|
* false positive. Every non-history read leaves this flag `false`.
|
|
10898
11365
|
*/
|
|
10899
11366
|
async decryptRecord(envelope, opts = {}) {
|
|
10900
|
-
const json = await this.decryptJsonString(envelope);
|
|
11367
|
+
const json = await this.decryptJsonString(envelope, opts.id);
|
|
11368
|
+
if (json === null) return null;
|
|
10901
11369
|
let parsed = JSON.parse(json);
|
|
10902
11370
|
if (this.crdtMode && parsed !== null && typeof parsed === "object" && "_crdt" in parsed) {
|
|
10903
11371
|
parsed = this.crdtStrategy.resolveCrdtSnapshot(parsed);
|
|
@@ -11502,9 +11970,125 @@ var init_numbering = __esm({
|
|
|
11502
11970
|
}
|
|
11503
11971
|
});
|
|
11504
11972
|
|
|
11973
|
+
// src/forget/strategy.ts
|
|
11974
|
+
var NO_FORGET;
|
|
11975
|
+
var init_strategy7 = __esm({
|
|
11976
|
+
"src/forget/strategy.ts"() {
|
|
11977
|
+
"use strict";
|
|
11978
|
+
NO_FORGET = { subjects: {} };
|
|
11979
|
+
}
|
|
11980
|
+
});
|
|
11981
|
+
|
|
11982
|
+
// src/forget/subject-index.ts
|
|
11983
|
+
async function sha256HexString(input) {
|
|
11984
|
+
const bytes = new TextEncoder().encode(input);
|
|
11985
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
|
|
11986
|
+
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
11987
|
+
}
|
|
11988
|
+
async function subjectKey(subjectId) {
|
|
11989
|
+
return sha256HexString(subjectId);
|
|
11990
|
+
}
|
|
11991
|
+
async function readRefs(adapter, vault, getDEK, encrypted, key) {
|
|
11992
|
+
const env = await adapter.get(vault, SUBJECT_INDEX_COLLECTION, key);
|
|
11993
|
+
if (!env || !env._data) return [];
|
|
11994
|
+
if (!encrypted) return JSON.parse(env._data);
|
|
11995
|
+
const dek = await getDEK(SUBJECT_INDEX_COLLECTION);
|
|
11996
|
+
const json = await decrypt(env._iv, env._data, dek);
|
|
11997
|
+
return JSON.parse(json);
|
|
11998
|
+
}
|
|
11999
|
+
async function writeRefs(adapter, vault, getDEK, encrypted, key, refs) {
|
|
12000
|
+
const json = JSON.stringify(refs);
|
|
12001
|
+
let env;
|
|
12002
|
+
if (!encrypted) {
|
|
12003
|
+
env = { _noydb: NOYDB_FORMAT_VERSION, _v: 1, _ts: (/* @__PURE__ */ new Date()).toISOString(), _iv: "", _data: json };
|
|
12004
|
+
} else {
|
|
12005
|
+
const dek = await getDEK(SUBJECT_INDEX_COLLECTION);
|
|
12006
|
+
const { iv, data } = await encrypt(json, dek);
|
|
12007
|
+
env = { _noydb: NOYDB_FORMAT_VERSION, _v: 1, _ts: (/* @__PURE__ */ new Date()).toISOString(), _iv: iv, _data: data };
|
|
12008
|
+
}
|
|
12009
|
+
await adapter.put(vault, SUBJECT_INDEX_COLLECTION, key, env);
|
|
12010
|
+
}
|
|
12011
|
+
async function addSubjectRef(adapter, vault, getDEK, encrypted, subjectId, ref) {
|
|
12012
|
+
const key = await subjectKey(subjectId);
|
|
12013
|
+
const refs = await readRefs(adapter, vault, getDEK, encrypted, key);
|
|
12014
|
+
if (refs.some((r) => r.collection === ref.collection && r.id === ref.id)) return;
|
|
12015
|
+
refs.push(ref);
|
|
12016
|
+
await writeRefs(adapter, vault, getDEK, encrypted, key, refs);
|
|
12017
|
+
}
|
|
12018
|
+
async function removeSubjectRef(adapter, vault, getDEK, encrypted, subjectId, ref) {
|
|
12019
|
+
const key = await subjectKey(subjectId);
|
|
12020
|
+
const refs = await readRefs(adapter, vault, getDEK, encrypted, key);
|
|
12021
|
+
const next = refs.filter((r) => !(r.collection === ref.collection && r.id === ref.id));
|
|
12022
|
+
if (next.length === refs.length) return;
|
|
12023
|
+
if (next.length === 0) {
|
|
12024
|
+
await adapter.delete(vault, SUBJECT_INDEX_COLLECTION, key);
|
|
12025
|
+
return;
|
|
12026
|
+
}
|
|
12027
|
+
await writeRefs(adapter, vault, getDEK, encrypted, key, next);
|
|
12028
|
+
}
|
|
12029
|
+
async function lookupSubject(adapter, vault, getDEK, encrypted, subjectId) {
|
|
12030
|
+
const key = await subjectKey(subjectId);
|
|
12031
|
+
return readRefs(adapter, vault, getDEK, encrypted, key);
|
|
12032
|
+
}
|
|
12033
|
+
async function rebuildSubjectIndex(adapter, vault, getDEK, encrypted, subjects, decodeRecord) {
|
|
12034
|
+
const existing = await adapter.list(vault, SUBJECT_INDEX_COLLECTION);
|
|
12035
|
+
for (const k of existing) {
|
|
12036
|
+
await adapter.delete(vault, SUBJECT_INDEX_COLLECTION, k);
|
|
12037
|
+
}
|
|
12038
|
+
const bySubject = /* @__PURE__ */ new Map();
|
|
12039
|
+
for (const [collection, field] of Object.entries(subjects)) {
|
|
12040
|
+
const ids = await adapter.list(vault, collection);
|
|
12041
|
+
for (const id of ids) {
|
|
12042
|
+
if (id.startsWith("_")) continue;
|
|
12043
|
+
const env = await adapter.get(vault, collection, id);
|
|
12044
|
+
if (!env || !env._data) continue;
|
|
12045
|
+
const record = await decodeRecord(collection, id, env);
|
|
12046
|
+
if (record === null) continue;
|
|
12047
|
+
const subjectValue = readDottedPath(record, field);
|
|
12048
|
+
if (subjectValue === void 0 || subjectValue === null) continue;
|
|
12049
|
+
const subjectId = coerceSubjectId(subjectValue);
|
|
12050
|
+
const list = bySubject.get(subjectId) ?? [];
|
|
12051
|
+
list.push({ collection, id });
|
|
12052
|
+
bySubject.set(subjectId, list);
|
|
12053
|
+
}
|
|
12054
|
+
}
|
|
12055
|
+
let entries = 0;
|
|
12056
|
+
for (const [subjectId, refs] of bySubject) {
|
|
12057
|
+
const key = await subjectKey(subjectId);
|
|
12058
|
+
await writeRefs(adapter, vault, getDEK, encrypted, key, refs);
|
|
12059
|
+
entries++;
|
|
12060
|
+
}
|
|
12061
|
+
return entries;
|
|
12062
|
+
}
|
|
12063
|
+
function coerceSubjectId(value) {
|
|
12064
|
+
if (typeof value === "string") return value;
|
|
12065
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
12066
|
+
return String(value);
|
|
12067
|
+
}
|
|
12068
|
+
return JSON.stringify(value);
|
|
12069
|
+
}
|
|
12070
|
+
function readDottedPath(record, field) {
|
|
12071
|
+
if (!field.includes(".")) return record[field];
|
|
12072
|
+
let cursor = record;
|
|
12073
|
+
for (const segment of field.split(".")) {
|
|
12074
|
+
if (cursor === null || cursor === void 0) return void 0;
|
|
12075
|
+
cursor = cursor[segment];
|
|
12076
|
+
}
|
|
12077
|
+
return cursor;
|
|
12078
|
+
}
|
|
12079
|
+
var SUBJECT_INDEX_COLLECTION;
|
|
12080
|
+
var init_subject_index = __esm({
|
|
12081
|
+
"src/forget/subject-index.ts"() {
|
|
12082
|
+
"use strict";
|
|
12083
|
+
init_crypto();
|
|
12084
|
+
init_types();
|
|
12085
|
+
SUBJECT_INDEX_COLLECTION = "_subject_index";
|
|
12086
|
+
}
|
|
12087
|
+
});
|
|
12088
|
+
|
|
11505
12089
|
// src/shadow/strategy.ts
|
|
11506
12090
|
var NOT_ENABLED3, NO_SHADOW;
|
|
11507
|
-
var
|
|
12091
|
+
var init_strategy8 = __esm({
|
|
11508
12092
|
"src/shadow/strategy.ts"() {
|
|
11509
12093
|
"use strict";
|
|
11510
12094
|
NOT_ENABLED3 = new Error(
|
|
@@ -11520,7 +12104,7 @@ var init_strategy7 = __esm({
|
|
|
11520
12104
|
|
|
11521
12105
|
// src/consent/strategy.ts
|
|
11522
12106
|
var NO_CONSENT;
|
|
11523
|
-
var
|
|
12107
|
+
var init_strategy9 = __esm({
|
|
11524
12108
|
"src/consent/strategy.ts"() {
|
|
11525
12109
|
"use strict";
|
|
11526
12110
|
NO_CONSENT = {
|
|
@@ -11535,7 +12119,7 @@ var init_strategy8 = __esm({
|
|
|
11535
12119
|
|
|
11536
12120
|
// src/periods/strategy.ts
|
|
11537
12121
|
var NOT_ENABLED4, NO_PERIODS;
|
|
11538
|
-
var
|
|
12122
|
+
var init_strategy10 = __esm({
|
|
11539
12123
|
"src/periods/strategy.ts"() {
|
|
11540
12124
|
"use strict";
|
|
11541
12125
|
NOT_ENABLED4 = new Error(
|
|
@@ -11647,18 +12231,6 @@ var init_refs = __esm({
|
|
|
11647
12231
|
}
|
|
11648
12232
|
});
|
|
11649
12233
|
|
|
11650
|
-
// src/i18n/dictionary.ts
|
|
11651
|
-
function isDictCollectionName(name) {
|
|
11652
|
-
return name.startsWith(DICT_COLLECTION_PREFIX);
|
|
11653
|
-
}
|
|
11654
|
-
var DICT_COLLECTION_PREFIX;
|
|
11655
|
-
var init_dictionary = __esm({
|
|
11656
|
-
"src/i18n/dictionary.ts"() {
|
|
11657
|
-
"use strict";
|
|
11658
|
-
DICT_COLLECTION_PREFIX = "_dict_";
|
|
11659
|
-
}
|
|
11660
|
-
});
|
|
11661
|
-
|
|
11662
12234
|
// src/periods/periods.ts
|
|
11663
12235
|
var PERIODS_COLLECTION;
|
|
11664
12236
|
var init_periods = __esm({
|
|
@@ -13627,6 +14199,19 @@ var init_delegation = __esm({
|
|
|
13627
14199
|
});
|
|
13628
14200
|
|
|
13629
14201
|
// src/vault.ts
|
|
14202
|
+
function resolveLabelFromMap(labels, locale, fallback) {
|
|
14203
|
+
if (labels[locale] !== void 0) return labels[locale];
|
|
14204
|
+
const chain = Array.isArray(fallback) ? fallback : fallback ? [fallback] : [];
|
|
14205
|
+
for (const fb of chain) {
|
|
14206
|
+
if (fb === "any") {
|
|
14207
|
+
const any = Object.values(labels)[0];
|
|
14208
|
+
if (any !== void 0) return any;
|
|
14209
|
+
} else if (labels[fb] !== void 0) {
|
|
14210
|
+
return labels[fb];
|
|
14211
|
+
}
|
|
14212
|
+
}
|
|
14213
|
+
return void 0;
|
|
14214
|
+
}
|
|
13630
14215
|
var Vault, ELEVATION_AUDIT_COLLECTION, ElevatedHandle;
|
|
13631
14216
|
var init_vault = __esm({
|
|
13632
14217
|
"src/vault.ts"() {
|
|
@@ -13645,8 +14230,11 @@ var init_vault = __esm({
|
|
|
13645
14230
|
init_entry();
|
|
13646
14231
|
init_strategy3();
|
|
13647
14232
|
init_strategy7();
|
|
14233
|
+
init_subject_index();
|
|
14234
|
+
init_errors();
|
|
13648
14235
|
init_strategy8();
|
|
13649
14236
|
init_strategy9();
|
|
14237
|
+
init_strategy10();
|
|
13650
14238
|
init_refs();
|
|
13651
14239
|
init_dictionary();
|
|
13652
14240
|
init_core();
|
|
@@ -13655,6 +14243,7 @@ var init_vault = __esm({
|
|
|
13655
14243
|
init_errors();
|
|
13656
14244
|
init_periods2();
|
|
13657
14245
|
init_crypto();
|
|
14246
|
+
init_record_keys();
|
|
13658
14247
|
init_export_blobs();
|
|
13659
14248
|
init_blob_compaction();
|
|
13660
14249
|
init_magic_link_grant();
|
|
@@ -13710,6 +14299,7 @@ var init_vault = __esm({
|
|
|
13710
14299
|
periodsStrategy;
|
|
13711
14300
|
shadowStrategy;
|
|
13712
14301
|
historyStrategy;
|
|
14302
|
+
forgetStrategy;
|
|
13713
14303
|
i18nStrategy;
|
|
13714
14304
|
syncStrategy;
|
|
13715
14305
|
/**
|
|
@@ -13876,6 +14466,27 @@ var init_vault = __esm({
|
|
|
13876
14466
|
* Populated by `collection()` when the `dictKeyFields` option is passed.
|
|
13877
14467
|
*/
|
|
13878
14468
|
dictKeyFieldRegistry = /* @__PURE__ */ new Map();
|
|
14469
|
+
/**
|
|
14470
|
+
* Names of dictionaries backed by a `staticDict()` descriptor (#291).
|
|
14471
|
+
* A static dict skips the `dictKeyFieldRegistry` rename machinery, but the
|
|
14472
|
+
* vault must still *know* a name is static so `vault.dictionary(name)` can
|
|
14473
|
+
* refuse mutation (`StaticDictReadonlyError`). Populated at `collection()`
|
|
14474
|
+
* config time whenever a `StaticDictDescriptor` is seen.
|
|
14475
|
+
*/
|
|
14476
|
+
staticDictNames = /* @__PURE__ */ new Set();
|
|
14477
|
+
/**
|
|
14478
|
+
* Static-dict descriptors keyed by dictionary name (#291). Backs the
|
|
14479
|
+
* read-path label resolver (resolve from the in-memory table) and the
|
|
14480
|
+
* query-seam `resolveDictSource` snapshot. Last writer wins when the same
|
|
14481
|
+
* name is registered by multiple collections (identical-across-vaults by
|
|
14482
|
+
* construction, so the tables match).
|
|
14483
|
+
*/
|
|
14484
|
+
staticByName = /* @__PURE__ */ new Map();
|
|
14485
|
+
/**
|
|
14486
|
+
* Per-collection map of field name → StaticDictDescriptor (#291). Used by
|
|
14487
|
+
* `enforceStaticDictOnPut` to validate stored codes against `desc.keys`.
|
|
14488
|
+
*/
|
|
14489
|
+
staticDescriptorByField = /* @__PURE__ */ new Map();
|
|
13879
14490
|
/**
|
|
13880
14491
|
* Registry of i18nText fields declared across all collections. Keyed
|
|
13881
14492
|
* by collection name → field name → I18nTextDescriptor. Used by
|
|
@@ -13922,6 +14533,7 @@ var init_vault = __esm({
|
|
|
13922
14533
|
this.periodsStrategy = opts.periodsStrategy ?? NO_PERIODS;
|
|
13923
14534
|
this.shadowStrategy = opts.shadowStrategy ?? NO_SHADOW;
|
|
13924
14535
|
this.historyStrategy = opts.historyStrategy ?? NO_HISTORY;
|
|
14536
|
+
this.forgetStrategy = opts.forgetStrategy ?? NO_FORGET;
|
|
13925
14537
|
this.i18nStrategy = opts.i18nStrategy ?? NO_I18N;
|
|
13926
14538
|
this.syncStrategy = opts.syncStrategy ?? NO_SYNC;
|
|
13927
14539
|
void opts.guardStrategies;
|
|
@@ -14026,10 +14638,22 @@ var init_vault = __esm({
|
|
|
14026
14638
|
}
|
|
14027
14639
|
if (options?.dictKeyFields) {
|
|
14028
14640
|
const dictFieldMap = {};
|
|
14641
|
+
const staticFieldMap = {};
|
|
14029
14642
|
for (const [field, desc] of Object.entries(options.dictKeyFields)) {
|
|
14030
|
-
|
|
14643
|
+
if (isStaticDictDescriptor(desc)) {
|
|
14644
|
+
staticFieldMap[field] = desc;
|
|
14645
|
+
this.staticDictNames.add(desc.name);
|
|
14646
|
+
this.staticByName.set(desc.name, desc);
|
|
14647
|
+
} else {
|
|
14648
|
+
dictFieldMap[field] = desc.name;
|
|
14649
|
+
}
|
|
14650
|
+
}
|
|
14651
|
+
if (Object.keys(dictFieldMap).length > 0) {
|
|
14652
|
+
this.dictKeyFieldRegistry.set(collectionName, dictFieldMap);
|
|
14653
|
+
}
|
|
14654
|
+
if (Object.keys(staticFieldMap).length > 0) {
|
|
14655
|
+
this.staticDescriptorByField.set(collectionName, staticFieldMap);
|
|
14031
14656
|
}
|
|
14032
|
-
this.dictKeyFieldRegistry.set(collectionName, dictFieldMap);
|
|
14033
14657
|
}
|
|
14034
14658
|
if ((options?.schemaUpdate?.length ?? 0) > 0) {
|
|
14035
14659
|
this.#schemaUpdateNames.set(collectionName, (options.schemaUpdate ?? []).map((s) => s.name));
|
|
@@ -14131,6 +14755,17 @@ var init_vault = __esm({
|
|
|
14131
14755
|
if (options?.acknowledgeDeterministicRisk !== void 0) {
|
|
14132
14756
|
collOpts.acknowledgeDeterministicRisk = options.acknowledgeDeterministicRisk;
|
|
14133
14757
|
}
|
|
14758
|
+
if (options?.perRecordKeys !== void 0) {
|
|
14759
|
+
collOpts.perRecordKeys = options.perRecordKeys;
|
|
14760
|
+
}
|
|
14761
|
+
if (this.forgetStrategy.subjects[collectionName] !== void 0) {
|
|
14762
|
+
if (options?.perRecordKeys === false) {
|
|
14763
|
+
console.warn(
|
|
14764
|
+
`[noy-db] Collection "${collectionName}" is declared in withForgetCascade but opened with perRecordKeys: false. Forcing perRecordKeys: true \u2014 GDPR crypto-shred requires per-record CEKs.`
|
|
14765
|
+
);
|
|
14766
|
+
}
|
|
14767
|
+
collOpts.perRecordKeys = true;
|
|
14768
|
+
}
|
|
14134
14769
|
if (options?.tiers !== void 0) collOpts.tiers = options.tiers;
|
|
14135
14770
|
if (options?.tierMode !== void 0) collOpts.tierMode = options.tierMode;
|
|
14136
14771
|
collOpts.onCrossTierAccess = (event) => this.emitCrossTier(event);
|
|
@@ -14140,6 +14775,11 @@ var init_vault = __esm({
|
|
|
14140
14775
|
if (options?.computed !== void 0) collOpts.computed = options.computed;
|
|
14141
14776
|
if (options?.dictKeyFields !== void 0) {
|
|
14142
14777
|
collOpts.dictLabelResolver = async (dictName, key, locale, fallback) => {
|
|
14778
|
+
const stat = this.staticByName.get(dictName);
|
|
14779
|
+
if (stat) {
|
|
14780
|
+
const labels = stat.table[key];
|
|
14781
|
+
return labels ? resolveLabelFromMap(labels, locale, fallback) : void 0;
|
|
14782
|
+
}
|
|
14143
14783
|
const handle = this.dictionary(dictName);
|
|
14144
14784
|
return handle.resolveLabel(key, locale, fallback);
|
|
14145
14785
|
};
|
|
@@ -14148,6 +14788,7 @@ var init_vault = __esm({
|
|
|
14148
14788
|
if (options?.i18nFields !== void 0 || options?.dictKeyFields !== void 0) {
|
|
14149
14789
|
collOpts.i18nPutValidator = (record) => {
|
|
14150
14790
|
this.enforceI18nOnPut(collectionName, record);
|
|
14791
|
+
this.enforceStaticDictOnPut(collectionName, record);
|
|
14151
14792
|
};
|
|
14152
14793
|
}
|
|
14153
14794
|
if (options?.i18nFields !== void 0 && this.translateText) {
|
|
@@ -14287,6 +14928,34 @@ var init_vault = __esm({
|
|
|
14287
14928
|
}
|
|
14288
14929
|
}
|
|
14289
14930
|
}
|
|
14931
|
+
/**
|
|
14932
|
+
* Validate staticDict codes on a `put()` (#291). For each `staticDict()`
|
|
14933
|
+
* field, every stored code must be a declared key of the descriptor's
|
|
14934
|
+
* table, else `UnknownDictCodeError`. Opt out per descriptor with
|
|
14935
|
+
* `{ validateCodes: false }`. Supports scalar, dotted, and `[].`-wildcard
|
|
14936
|
+
* field paths via `getAtPath` (same path support as i18n validation).
|
|
14937
|
+
*/
|
|
14938
|
+
enforceStaticDictOnPut(collectionName, record) {
|
|
14939
|
+
const staticFields = this.staticDescriptorByField.get(collectionName);
|
|
14940
|
+
if (!staticFields || Object.keys(staticFields).length === 0) return;
|
|
14941
|
+
if (!record || typeof record !== "object") return;
|
|
14942
|
+
const obj = record;
|
|
14943
|
+
for (const [field, desc] of Object.entries(staticFields)) {
|
|
14944
|
+
if (desc.validateCodes === false) continue;
|
|
14945
|
+
const known = new Set(desc.keys);
|
|
14946
|
+
const values = getAtPath(obj, field);
|
|
14947
|
+
for (const value of values) {
|
|
14948
|
+
if (value === void 0 || value === null) continue;
|
|
14949
|
+
const codes = Array.isArray(value) ? value : [value];
|
|
14950
|
+
for (const code of codes) {
|
|
14951
|
+
if (typeof code !== "string") continue;
|
|
14952
|
+
if (!known.has(code)) {
|
|
14953
|
+
throw new UnknownDictCodeError(desc.name, field, code);
|
|
14954
|
+
}
|
|
14955
|
+
}
|
|
14956
|
+
}
|
|
14957
|
+
}
|
|
14958
|
+
}
|
|
14290
14959
|
/**
|
|
14291
14960
|
* Apply locale resolution to a record for the given collection.
|
|
14292
14961
|
*
|
|
@@ -14295,14 +14964,18 @@ var init_vault = __esm({
|
|
|
14295
14964
|
*/
|
|
14296
14965
|
async applyLocale(collectionName, record, localeOpts) {
|
|
14297
14966
|
const locale = localeOpts.locale ?? this.locale;
|
|
14298
|
-
|
|
14967
|
+
const staticFields = this.staticDescriptorByField.get(collectionName);
|
|
14968
|
+
const hasStaticDisplay = staticFields !== void 0 && Object.values(staticFields).some((d) => d.displayLocale !== void 0);
|
|
14969
|
+
if (!locale && !hasStaticDisplay) return record;
|
|
14299
14970
|
let result = record;
|
|
14300
|
-
|
|
14301
|
-
|
|
14302
|
-
|
|
14971
|
+
if (locale) {
|
|
14972
|
+
const i18nFields = this.i18nFieldRegistry.get(collectionName);
|
|
14973
|
+
if (i18nFields && Object.keys(i18nFields).length > 0) {
|
|
14974
|
+
result = this.i18nStrategy.applyI18nLocale(result, i18nFields, locale, localeOpts.fallback);
|
|
14975
|
+
}
|
|
14303
14976
|
}
|
|
14304
14977
|
const dictFields = this.dictKeyFieldRegistry.get(collectionName);
|
|
14305
|
-
if (dictFields && Object.keys(dictFields).length > 0 && locale !== "raw") {
|
|
14978
|
+
if (locale && dictFields && Object.keys(dictFields).length > 0 && locale !== "raw") {
|
|
14306
14979
|
const withLabels = { ...result };
|
|
14307
14980
|
for (const [field, dictName] of Object.entries(dictFields)) {
|
|
14308
14981
|
const key = result[field];
|
|
@@ -14315,6 +14988,22 @@ var init_vault = __esm({
|
|
|
14315
14988
|
}
|
|
14316
14989
|
result = withLabels;
|
|
14317
14990
|
}
|
|
14991
|
+
if (staticFields && Object.keys(staticFields).length > 0 && locale !== "raw") {
|
|
14992
|
+
const withLabels = { ...result };
|
|
14993
|
+
for (const [field, desc] of Object.entries(staticFields)) {
|
|
14994
|
+
const effLocale = locale ?? desc.displayLocale;
|
|
14995
|
+
if (!effLocale) continue;
|
|
14996
|
+
const key = result[field];
|
|
14997
|
+
if (typeof key !== "string") continue;
|
|
14998
|
+
const labels = desc.table[key];
|
|
14999
|
+
if (!labels) continue;
|
|
15000
|
+
const label = resolveLabelFromMap(labels, effLocale, localeOpts.fallback ?? desc.substitute);
|
|
15001
|
+
if (label !== void 0) {
|
|
15002
|
+
withLabels[`${field}Label`] = label;
|
|
15003
|
+
}
|
|
15004
|
+
}
|
|
15005
|
+
result = withLabels;
|
|
15006
|
+
}
|
|
14318
15007
|
return result;
|
|
14319
15008
|
}
|
|
14320
15009
|
/**
|
|
@@ -14336,6 +15025,9 @@ var init_vault = __esm({
|
|
|
14336
15025
|
* ```
|
|
14337
15026
|
*/
|
|
14338
15027
|
dictionary(name, options = {}) {
|
|
15028
|
+
if (this.staticDictNames.has(name)) {
|
|
15029
|
+
throw new StaticDictReadonlyError(name);
|
|
15030
|
+
}
|
|
14339
15031
|
let handle = this.dictionaryCache.get(name);
|
|
14340
15032
|
if (!handle) {
|
|
14341
15033
|
handle = this.i18nStrategy.buildDictionaryHandle({
|
|
@@ -14405,6 +15097,26 @@ var init_vault = __esm({
|
|
|
14405
15097
|
* Returns `null` when `field` is not a dictKey in `leftCollection`.
|
|
14406
15098
|
*/
|
|
14407
15099
|
resolveDictSource(leftCollection, field) {
|
|
15100
|
+
const staticFields = this.staticDescriptorByField.get(leftCollection);
|
|
15101
|
+
if (staticFields && field in staticFields) {
|
|
15102
|
+
const desc = staticFields[field];
|
|
15103
|
+
const rows = Object.entries(desc.table).map(
|
|
15104
|
+
([key, labels]) => ({ key, labels, ...labels })
|
|
15105
|
+
);
|
|
15106
|
+
const source = {
|
|
15107
|
+
snapshot() {
|
|
15108
|
+
return rows;
|
|
15109
|
+
},
|
|
15110
|
+
lookupById(id) {
|
|
15111
|
+
return rows.find((e) => e["key"] === id);
|
|
15112
|
+
}
|
|
15113
|
+
};
|
|
15114
|
+
if (desc.displayLocale !== void 0) {
|
|
15115
|
+
;
|
|
15116
|
+
source.displayLocale = desc.displayLocale;
|
|
15117
|
+
}
|
|
15118
|
+
return source;
|
|
15119
|
+
}
|
|
14408
15120
|
const dictFields = this.dictKeyFieldRegistry.get(leftCollection);
|
|
14409
15121
|
if (!dictFields || !(field in dictFields)) return null;
|
|
14410
15122
|
const dictName = dictFields[field];
|
|
@@ -15048,6 +15760,218 @@ var init_vault = __esm({
|
|
|
15048
15760
|
}
|
|
15049
15761
|
return this.ledgerStore;
|
|
15050
15762
|
}
|
|
15763
|
+
// ─── GDPR right-to-erasure (#304) ────────────────────────────────
|
|
15764
|
+
/** @internal — add a subject→record ref to the encrypted subject index. */
|
|
15765
|
+
async _addSubjectRef(subjectId, ref) {
|
|
15766
|
+
await addSubjectRef(this.adapter, this.name, this.getDEK, this.encrypted, subjectId, ref);
|
|
15767
|
+
}
|
|
15768
|
+
/** @internal — drop a subject→record ref from the encrypted subject index. */
|
|
15769
|
+
async _removeSubjectRef(subjectId, ref) {
|
|
15770
|
+
await removeSubjectRef(this.adapter, this.name, this.getDEK, this.encrypted, subjectId, ref);
|
|
15771
|
+
}
|
|
15772
|
+
/**
|
|
15773
|
+
* Rebuild the encrypted subject index from canonical records. The recovery
|
|
15774
|
+
* path for the documented read-modify-write race (RISK #3). Returns the
|
|
15775
|
+
* number of distinct subjects re-indexed.
|
|
15776
|
+
*/
|
|
15777
|
+
async rebuildSubjectIndex() {
|
|
15778
|
+
if (Object.keys(this.forgetStrategy.subjects).length === 0) {
|
|
15779
|
+
throw new ForgetStrategyNotConfiguredError();
|
|
15780
|
+
}
|
|
15781
|
+
return rebuildSubjectIndex(
|
|
15782
|
+
this.adapter,
|
|
15783
|
+
this.name,
|
|
15784
|
+
this.getDEK,
|
|
15785
|
+
this.encrypted,
|
|
15786
|
+
this.forgetStrategy.subjects,
|
|
15787
|
+
async (collectionName, id, env) => {
|
|
15788
|
+
const coll = this.collection(collectionName);
|
|
15789
|
+
return coll._decodeEnvelope(env, id);
|
|
15790
|
+
}
|
|
15791
|
+
);
|
|
15792
|
+
}
|
|
15793
|
+
/**
|
|
15794
|
+
* GDPR crypto-shred of a data subject (#304). Consults the encrypted subject
|
|
15795
|
+
* index and, per matching record:
|
|
15796
|
+
* - rewrites the LIVE envelope to a tombstone (drops `_iv`/`_data`/`_cek`/`_det`),
|
|
15797
|
+
* - tombstones every `_history` version of the record,
|
|
15798
|
+
* so the body and all prior versions become permanently undecryptable while
|
|
15799
|
+
* the collection DEK and every OTHER record stay intact. Then appends ONE
|
|
15800
|
+
* `op:'forget'` ledger entry whose `payloadHash` is `sha256Hex(subjectId)` —
|
|
15801
|
+
* the chain still `verify()`s, PROVING the subject existed and was erased
|
|
15802
|
+
* without retaining any plaintext.
|
|
15803
|
+
*
|
|
15804
|
+
* Reports — but does not silently swallow — two completeness gaps:
|
|
15805
|
+
* - `unmigratedRecords`: a record whose body was NOT yet migrated to a
|
|
15806
|
+
* per-record CEK (legacy body still under the shared collection DEK). It
|
|
15807
|
+
* is still tombstoned, but its pre-shred ciphertext (if leaked to a
|
|
15808
|
+
* backup before migration) stays decryptable. Migrate, then re-forget.
|
|
15809
|
+
* - `blobResidueCollections`: a shredded record still has blob attachments,
|
|
15810
|
+
* which are keyed off a separate `_blob` DEK and are out of scope here.
|
|
15811
|
+
*
|
|
15812
|
+
* @throws ForgetStrategyNotConfiguredError when no `withForgetCascade` was set.
|
|
15813
|
+
*/
|
|
15814
|
+
async forget(subjectId) {
|
|
15815
|
+
if (Object.keys(this.forgetStrategy.subjects).length === 0) {
|
|
15816
|
+
throw new ForgetStrategyNotConfiguredError();
|
|
15817
|
+
}
|
|
15818
|
+
const refs = await lookupSubject(this.adapter, this.name, this.getDEK, this.encrypted, subjectId);
|
|
15819
|
+
let recordsShredded = 0;
|
|
15820
|
+
let historyVersionsShredded = 0;
|
|
15821
|
+
const collections = /* @__PURE__ */ new Set();
|
|
15822
|
+
const unmigratedRecords = [];
|
|
15823
|
+
const blobResidueCollections = /* @__PURE__ */ new Set();
|
|
15824
|
+
let blobsShredded = 0;
|
|
15825
|
+
let blobsRetainedShared = 0;
|
|
15826
|
+
const blobsEnabled = this.blobStrategy !== void 0;
|
|
15827
|
+
const actor = this.keyring.userId;
|
|
15828
|
+
for (const ref of refs) {
|
|
15829
|
+
const coll = this.collection(ref.collection);
|
|
15830
|
+
const perRecordKeys = this.forgetStrategy.subjects[ref.collection] !== void 0;
|
|
15831
|
+
const live = await this.adapter.get(this.name, ref.collection, ref.id);
|
|
15832
|
+
if (perRecordKeys && live && live._data && live._cek === void 0) {
|
|
15833
|
+
unmigratedRecords.push(`${ref.collection}:${ref.id}`);
|
|
15834
|
+
}
|
|
15835
|
+
const shred = await coll._writeTombstone(ref.id, actor);
|
|
15836
|
+
if (shred !== null) {
|
|
15837
|
+
recordsShredded++;
|
|
15838
|
+
collections.add(ref.collection);
|
|
15839
|
+
}
|
|
15840
|
+
historyVersionsShredded += await this.historyStrategy.tombstoneHistory(
|
|
15841
|
+
this.adapter,
|
|
15842
|
+
this.name,
|
|
15843
|
+
ref.collection,
|
|
15844
|
+
ref.id,
|
|
15845
|
+
actor
|
|
15846
|
+
);
|
|
15847
|
+
if (blobsEnabled) {
|
|
15848
|
+
const r = await this.collection(ref.collection).blob(ref.id).shredAllForRecord();
|
|
15849
|
+
blobsShredded += r.shredded.length;
|
|
15850
|
+
blobsRetainedShared += r.retainedShared.length;
|
|
15851
|
+
if (r.residue.length > 0) blobResidueCollections.add(ref.collection);
|
|
15852
|
+
} else {
|
|
15853
|
+
try {
|
|
15854
|
+
const slotIds = await this.adapter.list(this.name, `_blob_slots_${ref.collection}`);
|
|
15855
|
+
if (slotIds.includes(ref.id)) blobResidueCollections.add(ref.collection);
|
|
15856
|
+
} catch {
|
|
15857
|
+
}
|
|
15858
|
+
}
|
|
15859
|
+
await this._removeSubjectRef(subjectId, ref);
|
|
15860
|
+
}
|
|
15861
|
+
const subjectHash = await sha256Hex3(subjectId);
|
|
15862
|
+
const ledger = this.getLedgerOrNull();
|
|
15863
|
+
if (!ledger) {
|
|
15864
|
+
throw new Error(
|
|
15865
|
+
'vault.forget() requires the history strategy for the erasure-proof ledger entry. Pass `historyStrategy: withHistory()` from "@noy-db/hub/history" to createNoydb().'
|
|
15866
|
+
);
|
|
15867
|
+
}
|
|
15868
|
+
const ledgerEntry = await ledger.append({
|
|
15869
|
+
op: "forget",
|
|
15870
|
+
collection: "",
|
|
15871
|
+
id: "",
|
|
15872
|
+
version: 0,
|
|
15873
|
+
actor,
|
|
15874
|
+
payloadHash: subjectHash,
|
|
15875
|
+
reason: JSON.stringify({
|
|
15876
|
+
recordsShredded,
|
|
15877
|
+
historyVersionsShredded,
|
|
15878
|
+
collections: [...collections],
|
|
15879
|
+
unmigratedCount: unmigratedRecords.length,
|
|
15880
|
+
blobsShredded,
|
|
15881
|
+
blobsRetainedShared,
|
|
15882
|
+
blobResidueCollections: [...blobResidueCollections]
|
|
15883
|
+
})
|
|
15884
|
+
});
|
|
15885
|
+
return {
|
|
15886
|
+
subject: subjectId,
|
|
15887
|
+
recordsShredded,
|
|
15888
|
+
historyVersionsShredded,
|
|
15889
|
+
collections: [...collections],
|
|
15890
|
+
unmigratedRecords,
|
|
15891
|
+
blobsShredded,
|
|
15892
|
+
blobsRetainedShared,
|
|
15893
|
+
blobResidueCollections: [...blobResidueCollections],
|
|
15894
|
+
ledgerEntry
|
|
15895
|
+
};
|
|
15896
|
+
}
|
|
15897
|
+
// ─── Record-scoped CEK sealing (#306 slices 2-3) ──────────────────────
|
|
15898
|
+
/**
|
|
15899
|
+
* Seal ONE record's content-encryption key (CEK) to an `at-*` host so that
|
|
15900
|
+
* host — and only that host — can decrypt exactly that record, with no
|
|
15901
|
+
* access to the vault DEK and no ability to read any other record.
|
|
15902
|
+
*
|
|
15903
|
+
* The grantor (this caller, who holds the collection DEK) reads the record's
|
|
15904
|
+
* live `_cek`, unwraps it under the collection DEK, exports the raw CEK
|
|
15905
|
+
* bytes, builds a {@link SealedCekBinding} `{collection, id, cek, expiresAt}`,
|
|
15906
|
+
* seals that binding for the recipient host via the host's published hint,
|
|
15907
|
+
* and persists a thin {@link SealedCekDeliveryEnvelope} at
|
|
15908
|
+
* `_sealed_cek/<collection>/<id>/<pid>`. The binding (not the delivery
|
|
15909
|
+
* envelope) is the security boundary: the host re-verifies `{collection, id}`
|
|
15910
|
+
* and `expiresAt` from inside the sealed payload.
|
|
15911
|
+
*
|
|
15912
|
+
* Only works on a `perRecordKeys` record — a legacy record has no `_cek` to
|
|
15913
|
+
* seal (its body is under the shared collection DEK, which is never exposed
|
|
15914
|
+
* by sealing) → {@link RecordCekNotFoundError}.
|
|
15915
|
+
*
|
|
15916
|
+
* @param collection Collection holding the record.
|
|
15917
|
+
* @param id Record id.
|
|
15918
|
+
* @param hostSealer The recipient host's {@link RecipientSealer}.
|
|
15919
|
+
* @param opts.expiresAt REQUIRED authoritative expiry (ISO 8601), sealed into
|
|
15920
|
+
* the binding the host verifies.
|
|
15921
|
+
* @returns `{ pid, envelopeKey }` — the host provider id and the
|
|
15922
|
+
* `<collection>/<id>/<pid>` key the delivery envelope was written under.
|
|
15923
|
+
*/
|
|
15924
|
+
async sealRecordToHost(collection, id, hostSealer, opts) {
|
|
15925
|
+
return sealRecordToHost(this.sealingContext(), collection, id, hostSealer, opts);
|
|
15926
|
+
}
|
|
15927
|
+
/**
|
|
15928
|
+
* Revoke a single sealed-CEK delivery envelope by deleting it from the store.
|
|
15929
|
+
* A soft revocation: it removes the host's copy of the sealed CEK, but a host
|
|
15930
|
+
* that already fetched the envelope keeps whatever it cached. For a hard
|
|
15931
|
+
* revocation that makes the live record undecryptable to every prior grant,
|
|
15932
|
+
* use {@link rotateRecordCek}.
|
|
15933
|
+
*/
|
|
15934
|
+
async revokeSealedRecord(collection, id, pid) {
|
|
15935
|
+
return revokeSealedRecord(this.sealingContext(), collection, id, pid);
|
|
15936
|
+
}
|
|
15937
|
+
/**
|
|
15938
|
+
* HARD-rotate a record's CEK: decrypt the live body under the old CEK,
|
|
15939
|
+
* re-encrypt it under a freshly-minted CEK, write the new live envelope, evict
|
|
15940
|
+
* the in-memory caches, and delete EVERY sealed-CEK delivery envelope for the
|
|
15941
|
+
* record. After this, any host holding a previously-sealed CEK can still
|
|
15942
|
+
* decrypt PRE-rotation history versions (they keep their old `_cek`) but NOT
|
|
15943
|
+
* the rotated live record (its body is under the new CEK → the old CEK fails
|
|
15944
|
+
* the AES-GCM auth tag → `TamperedError`). That asymmetry IS the revocation:
|
|
15945
|
+
* old grants lose the live record.
|
|
15946
|
+
*
|
|
15947
|
+
* Administrative path — bypasses `Collection.put` deliberately (no guards, no
|
|
15948
|
+
* history snapshot, no materialized-view refresh): rotation is a key-rotation
|
|
15949
|
+
* operation, not a business write, and must not version-bump history (which
|
|
15950
|
+
* would re-encrypt the prior version under the NEW CEK and defeat the point).
|
|
15951
|
+
*
|
|
15952
|
+
* @throws {@link RecordCekNotFoundError} if the record is missing or has no `_cek`.
|
|
15953
|
+
*/
|
|
15954
|
+
async rotateRecordCek(collection, id) {
|
|
15955
|
+
return rotateRecordCek(this.sealingContext(), collection, id);
|
|
15956
|
+
}
|
|
15957
|
+
/**
|
|
15958
|
+
* Build the {@link SealingContext} the record-keys grantor functions need:
|
|
15959
|
+
* the vault-bound adapter, DEK resolver, actor, and the dual-cache eviction
|
|
15960
|
+
* `rotateRecordCek` performs (per-record CEK cache + decrypted-record cache).
|
|
15961
|
+
*/
|
|
15962
|
+
sealingContext() {
|
|
15963
|
+
return {
|
|
15964
|
+
adapter: this.adapter,
|
|
15965
|
+
vault: this.name,
|
|
15966
|
+
getDEK: (collection) => this.getDEK(collection),
|
|
15967
|
+
actor: this.keyring.userId,
|
|
15968
|
+
invalidateRecordCaches: async (collection, id) => {
|
|
15969
|
+
const coll = this.collection(collection);
|
|
15970
|
+
coll._invalidateCekCacheEntry(id);
|
|
15971
|
+
await coll._invalidateCacheEntry(id);
|
|
15972
|
+
}
|
|
15973
|
+
};
|
|
15974
|
+
}
|
|
15051
15975
|
/**
|
|
15052
15976
|
* @internal — called by `Noydb.openVault` after construction.
|
|
15053
15977
|
* Dynamic-imports `GuardRegistry` + `ReadOnlyVaultFacade` and seeds
|
|
@@ -17150,7 +18074,7 @@ var init_unlock_state = __esm({
|
|
|
17150
18074
|
|
|
17151
18075
|
// src/snapshots/strategy.ts
|
|
17152
18076
|
var NOT_ENABLED5, NO_SNAPSHOTS;
|
|
17153
|
-
var
|
|
18077
|
+
var init_strategy11 = __esm({
|
|
17154
18078
|
"src/snapshots/strategy.ts"() {
|
|
17155
18079
|
"use strict";
|
|
17156
18080
|
NOT_ENABLED5 = new Error(
|
|
@@ -17291,7 +18215,7 @@ var init_scheduler = __esm({
|
|
|
17291
18215
|
|
|
17292
18216
|
// src/tx/strategy.ts
|
|
17293
18217
|
var NOT_ENABLED6, NO_TX;
|
|
17294
|
-
var
|
|
18218
|
+
var init_strategy12 = __esm({
|
|
17295
18219
|
"src/tx/strategy.ts"() {
|
|
17296
18220
|
"use strict";
|
|
17297
18221
|
NOT_ENABLED6 = new Error(
|
|
@@ -17327,7 +18251,7 @@ function notEnabled4(op) {
|
|
|
17327
18251
|
);
|
|
17328
18252
|
}
|
|
17329
18253
|
var NO_SESSION;
|
|
17330
|
-
var
|
|
18254
|
+
var init_strategy13 = __esm({
|
|
17331
18255
|
"src/session/strategy.ts"() {
|
|
17332
18256
|
"use strict";
|
|
17333
18257
|
NO_SESSION = {
|
|
@@ -18553,12 +19477,14 @@ var init_noydb = __esm({
|
|
|
18553
19477
|
init_authenticators();
|
|
18554
19478
|
init_unlock_state();
|
|
18555
19479
|
init_sync_strategy();
|
|
18556
|
-
|
|
19480
|
+
init_strategy11();
|
|
18557
19481
|
init_scheduler();
|
|
18558
19482
|
init_transaction();
|
|
18559
|
-
init_strategy11();
|
|
18560
|
-
init_sync_policy();
|
|
18561
19483
|
init_strategy12();
|
|
19484
|
+
init_strategy7();
|
|
19485
|
+
init_subject_index();
|
|
19486
|
+
init_sync_policy();
|
|
19487
|
+
init_strategy13();
|
|
18562
19488
|
init_policy3();
|
|
18563
19489
|
ROLE_RANK = {
|
|
18564
19490
|
client: 1,
|
|
@@ -18614,6 +19540,7 @@ var init_noydb = __esm({
|
|
|
18614
19540
|
policyEnforcers = /* @__PURE__ */ new Map();
|
|
18615
19541
|
vaultTemplates = /* @__PURE__ */ new Map();
|
|
18616
19542
|
txStrategy;
|
|
19543
|
+
forgetStrategy;
|
|
18617
19544
|
sessionStrategy;
|
|
18618
19545
|
syncStrategy;
|
|
18619
19546
|
snapshotStrategy;
|
|
@@ -18641,6 +19568,7 @@ var init_noydb = __esm({
|
|
|
18641
19568
|
constructor(options) {
|
|
18642
19569
|
this.options = options;
|
|
18643
19570
|
this.txStrategy = options.txStrategy ?? NO_TX;
|
|
19571
|
+
this.forgetStrategy = options.forgetStrategy ?? NO_FORGET;
|
|
18644
19572
|
this.sessionStrategy = options.sessionStrategy ?? NO_SESSION;
|
|
18645
19573
|
this.syncStrategy = options.syncStrategy ?? NO_SYNC;
|
|
18646
19574
|
this.snapshotStrategy = options.snapshotStrategy ?? NO_SNAPSHOTS;
|
|
@@ -18651,8 +19579,61 @@ var init_noydb = __esm({
|
|
|
18651
19579
|
}
|
|
18652
19580
|
this.#registerGuardGate();
|
|
18653
19581
|
this.#registerPeriodGate();
|
|
19582
|
+
this.#registerForgetHooks();
|
|
18654
19583
|
this.resetSessionTimer();
|
|
18655
19584
|
}
|
|
19585
|
+
/** @internal — resolved forget strategy (NO_FORGET when not configured). */
|
|
19586
|
+
get _forgetStrategy() {
|
|
19587
|
+
return this.forgetStrategy;
|
|
19588
|
+
}
|
|
19589
|
+
// #304 — GDPR subject-index maintenance. When `withForgetCascade` declares
|
|
19590
|
+
// any subject fields, keep the encrypted `_subject_index` in lock-step with
|
|
19591
|
+
// writes so `vault.forget(subjectId)` can find every record for a subject.
|
|
19592
|
+
//
|
|
19593
|
+
// Two consumers are required because they cover disjoint events:
|
|
19594
|
+
// - onAfterWrite fires on create/update (NOT delete) — add the new ref;
|
|
19595
|
+
// on an update that changed the subject value, drop the stale ref.
|
|
19596
|
+
// - the subsystemBus `afterDelete` observer fires on delete (onAfterWrite
|
|
19597
|
+
// does NOT) — drop the ref so a deleted record never lingers in the
|
|
19598
|
+
// index (RISK #2). Without it, forget() would try to shred a ghost.
|
|
19599
|
+
#registerForgetHooks() {
|
|
19600
|
+
const subjects = this.forgetStrategy.subjects;
|
|
19601
|
+
if (Object.keys(subjects).length === 0) return;
|
|
19602
|
+
const subjectFieldFor = (collection) => subjects[collection];
|
|
19603
|
+
this.writeHooks.onAfterWrite(async (event) => {
|
|
19604
|
+
const field = subjectFieldFor(event.collection);
|
|
19605
|
+
if (field === void 0) return;
|
|
19606
|
+
const vault = this.vaultCache.get(event.vault);
|
|
19607
|
+
if (!vault) return;
|
|
19608
|
+
if (event.after !== null && typeof event.after === "object") {
|
|
19609
|
+
const subjectValue = readDottedPath(event.after, field);
|
|
19610
|
+
if (subjectValue !== void 0 && subjectValue !== null) {
|
|
19611
|
+
await vault._addSubjectRef(coerceSubjectId(subjectValue), { collection: event.collection, id: event.docId });
|
|
19612
|
+
}
|
|
19613
|
+
}
|
|
19614
|
+
if (event.op === "update" && event.before !== null && typeof event.before === "object") {
|
|
19615
|
+
const beforeValue = readDottedPath(event.before, field);
|
|
19616
|
+
const afterValue = event.after !== null && typeof event.after === "object" ? readDottedPath(event.after, field) : void 0;
|
|
19617
|
+
const beforeId = beforeValue === void 0 || beforeValue === null ? void 0 : coerceSubjectId(beforeValue);
|
|
19618
|
+
const afterId = afterValue === void 0 || afterValue === null ? void 0 : coerceSubjectId(afterValue);
|
|
19619
|
+
if (beforeId !== void 0 && beforeId !== afterId) {
|
|
19620
|
+
await vault._removeSubjectRef(beforeId, { collection: event.collection, id: event.docId });
|
|
19621
|
+
}
|
|
19622
|
+
}
|
|
19623
|
+
});
|
|
19624
|
+
this.subsystemBus.register("afterDelete", async (event) => {
|
|
19625
|
+
const field = subjectFieldFor(event.collection);
|
|
19626
|
+
if (field === void 0) return;
|
|
19627
|
+
const vault = this.vaultCache.get(event.vault);
|
|
19628
|
+
if (!vault) return;
|
|
19629
|
+
if (event.before !== null && typeof event.before === "object") {
|
|
19630
|
+
const subjectValue = readDottedPath(event.before, field);
|
|
19631
|
+
if (subjectValue !== void 0 && subjectValue !== null) {
|
|
19632
|
+
await vault._removeSubjectRef(coerceSubjectId(subjectValue), { collection: event.collection, id: event.docId });
|
|
19633
|
+
}
|
|
19634
|
+
}
|
|
19635
|
+
});
|
|
19636
|
+
}
|
|
18656
19637
|
// Track A — guards migration. Registers record-lock / field-freeze / onDelete
|
|
18657
19638
|
// / amendment-collect as gate-bus handlers (only when guards are opted in, so
|
|
18658
19639
|
// the write path is zero-cost otherwise). Resolves the live vault's
|
|
@@ -18873,6 +19854,7 @@ var init_noydb = __esm({
|
|
|
18873
19854
|
...this.options.syncStrategy !== void 0 ? { syncStrategy: this.options.syncStrategy } : {},
|
|
18874
19855
|
...this.options.guardStrategies !== void 0 ? { guardStrategies: this.options.guardStrategies } : {},
|
|
18875
19856
|
...this.options.numbering !== void 0 ? { numberingConfigs: this.options.numbering } : {},
|
|
19857
|
+
forgetStrategy: this.forgetStrategy,
|
|
18876
19858
|
locale: opts?.locale,
|
|
18877
19859
|
// Thread the translator hook so Collection.put() can invoke it
|
|
18878
19860
|
plaintextTranslator: this.options.plaintextTranslator ? (text, from, to, field, collection) => this.invokeTranslator(text, from, to, field, collection) : void 0,
|
|
@@ -18927,7 +19909,8 @@ var init_noydb = __esm({
|
|
|
18927
19909
|
...this.options.i18nStrategy !== void 0 ? { i18nStrategy: this.options.i18nStrategy } : {},
|
|
18928
19910
|
...this.options.syncStrategy !== void 0 ? { syncStrategy: this.options.syncStrategy } : {},
|
|
18929
19911
|
...this.options.guardStrategies !== void 0 ? { guardStrategies: this.options.guardStrategies } : {},
|
|
18930
|
-
...this.options.numbering !== void 0 ? { numberingConfigs: this.options.numbering } : {}
|
|
19912
|
+
...this.options.numbering !== void 0 ? { numberingConfigs: this.options.numbering } : {},
|
|
19913
|
+
forgetStrategy: this.forgetStrategy
|
|
18931
19914
|
});
|
|
18932
19915
|
this.vaultCache.set(name, comp2);
|
|
18933
19916
|
return comp2;
|
|
@@ -21712,6 +22695,7 @@ async function describeExtraction(vault, opts) {
|
|
|
21712
22695
|
// src/bundle/extract-partition.ts
|
|
21713
22696
|
init_types();
|
|
21714
22697
|
init_crypto();
|
|
22698
|
+
init_record_keys();
|
|
21715
22699
|
init_errors();
|
|
21716
22700
|
init_ulid();
|
|
21717
22701
|
init_storage2();
|
|
@@ -21731,6 +22715,14 @@ async function reKeyClosure(vault, closure) {
|
|
|
21731
22715
|
for (const id of ids) {
|
|
21732
22716
|
const env = await adapter.get(vaultName, collectionName, id);
|
|
21733
22717
|
if (!env) continue;
|
|
22718
|
+
if (env._cek !== void 0) {
|
|
22719
|
+
const cek = await unwrapCek(env._cek, srcDek);
|
|
22720
|
+
const plaintext2 = await decrypt(env._iv, env._data, cek);
|
|
22721
|
+
const { iv: iv2, data: data2 } = await encrypt(plaintext2, cek);
|
|
22722
|
+
const wrapped = await wrapCek(cek, destDek);
|
|
22723
|
+
out[id] = { ...env, _iv: iv2, _data: data2, _cek: wrapped };
|
|
22724
|
+
continue;
|
|
22725
|
+
}
|
|
21734
22726
|
const plaintext = await decrypt(env._iv, env._data, srcDek);
|
|
21735
22727
|
const { iv, data } = await encrypt(plaintext, destDek);
|
|
21736
22728
|
out[id] = { ...env, _iv: iv, _data: data };
|