@noy-db/hub 0.3.0-pre.2 → 0.3.0-pre.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter/index.d.ts +2 -2
- package/dist/adapter/index.js +1 -1
- package/dist/aggregate/index.d.ts +3 -3
- package/dist/aggregate/index.js +4 -4
- package/dist/api-JOXUNSKP.js +20 -0
- package/dist/as/index.d.ts +4 -4
- package/dist/as/index.js +12 -6
- package/dist/at/index.d.ts +2 -2
- package/dist/attestation/index.d.ts +3 -3
- package/dist/attestation/index.js +19 -13
- package/dist/attestation/index.js.map +1 -1
- package/dist/{backup-KMJQ66MF.js → backup-MOB27UUN.js} +12 -6
- package/dist/{backup-KMJQ66MF.js.map → backup-MOB27UUN.js.map} +1 -1
- package/dist/blobs/index.d.ts +4 -4
- package/dist/blobs/index.js +11 -5
- package/dist/blobs/index.js.map +1 -1
- package/dist/bundle/index.d.ts +5 -5
- package/dist/bundle/index.js +21 -15
- package/dist/bundle/index.js.map +1 -1
- package/dist/{bundle-PwBGkhpe.d.ts → bundle-Bs13EZtX.d.ts} +1 -1
- package/dist/by/index.js +2 -2
- package/dist/cargo/index.d.ts +4 -4
- package/dist/cargo/index.js +23 -17
- package/dist/{chunk-SMXWYP24.js → chunk-3QRQOBS7.js} +3 -3
- package/dist/{chunk-QHJW5EXU.js → chunk-3Y4QLJ6K.js} +2 -2
- package/dist/{chunk-NF5JWERG.js → chunk-43ISXURO.js} +2 -2
- package/dist/{chunk-5LUY7UTV.js → chunk-4K3MQ5S3.js} +2 -2
- package/dist/{chunk-QZISFCVG.js → chunk-5CY35H55.js} +11 -9
- package/dist/{chunk-QZISFCVG.js.map → chunk-5CY35H55.js.map} +1 -1
- package/dist/chunk-5EWYUR5P.js +37 -0
- package/dist/chunk-5EWYUR5P.js.map +1 -0
- package/dist/{chunk-Y22T3KQE.js → chunk-5MRE3C4Q.js} +5 -5
- package/dist/{chunk-M6OST55S.js → chunk-5W7AOFWA.js} +2 -2
- package/dist/{chunk-AXRXJTE2.js → chunk-6HNKCKFZ.js} +7 -7
- package/dist/{chunk-UAG5KWVA.js → chunk-6RXPSMKR.js} +3 -3
- package/dist/{chunk-TJYQIGAP.js → chunk-7J7JRYXW.js} +9 -5
- package/dist/chunk-7J7JRYXW.js.map +1 -0
- package/dist/{chunk-UDRIWL7A.js → chunk-A5DYVMDR.js} +3 -3
- package/dist/chunk-AF5ZRTZ4.js +108 -0
- package/dist/chunk-AF5ZRTZ4.js.map +1 -0
- package/dist/{chunk-I3TIKTJO.js → chunk-BKGIWGCX.js} +2 -2
- package/dist/{chunk-IO6GRPT6.js → chunk-BMQOH7MM.js} +2 -2
- package/dist/{chunk-26LGBE4T.js → chunk-CPXPHQGX.js} +6 -4
- package/dist/{chunk-26LGBE4T.js.map → chunk-CPXPHQGX.js.map} +1 -1
- package/dist/{chunk-FISN7WE4.js → chunk-EWR7HL3K.js} +4 -4
- package/dist/{chunk-AQTBSL7R.js → chunk-FH52CDEW.js} +5 -5
- package/dist/chunk-G4MGYGSS.js +130 -0
- package/dist/chunk-G4MGYGSS.js.map +1 -0
- package/dist/{chunk-UV5MFVO3.js → chunk-G7RDYYTE.js} +2 -2
- package/dist/{chunk-UIU2THRG.js → chunk-GBTUANXF.js} +16 -297
- package/dist/chunk-GBTUANXF.js.map +1 -0
- package/dist/{chunk-TEILUWOI.js → chunk-GOUNIHFA.js} +10 -10
- package/dist/{chunk-LXQT4WMP.js → chunk-GQOT6QJR.js} +8 -6
- package/dist/{chunk-LXQT4WMP.js.map → chunk-GQOT6QJR.js.map} +1 -1
- package/dist/{chunk-RUEKROOS.js → chunk-GXGK22QU.js} +2 -2
- package/dist/{chunk-IELYAOGG.js → chunk-GZZL5R73.js} +4 -4
- package/dist/{chunk-JNUWZ6D3.js → chunk-H5XRIJX3.js} +7 -5
- package/dist/{chunk-JNUWZ6D3.js.map → chunk-H5XRIJX3.js.map} +1 -1
- package/dist/{chunk-CWPPNPOH.js → chunk-H7RYPVMJ.js} +2 -2
- package/dist/{chunk-5N6CZTCY.js → chunk-HBKDR7R3.js} +3 -3
- package/dist/{chunk-KXGNJJXB.js → chunk-HLUKZ36Q.js} +9 -9
- package/dist/{chunk-LMTH2RM6.js → chunk-HMXLN43G.js} +2 -2
- package/dist/{chunk-BG3SPSR4.js → chunk-HY6ZGGEC.js} +2 -2
- package/dist/{chunk-UPJNJGGA.js → chunk-K5P46KB3.js} +27 -10
- package/dist/chunk-K5P46KB3.js.map +1 -0
- package/dist/chunk-KYY7L72M.js +106 -0
- package/dist/chunk-KYY7L72M.js.map +1 -0
- package/dist/{chunk-62QYRORB.js → chunk-LIP6JWYK.js} +3 -3
- package/dist/{chunk-5TDJ56JE.js → chunk-LN22XH4B.js} +1 -1
- package/dist/chunk-LN22XH4B.js.map +1 -0
- package/dist/chunk-LP7BEXCT.js +32 -0
- package/dist/chunk-LP7BEXCT.js.map +1 -0
- package/dist/{chunk-IGUYVGGI.js → chunk-LPRPVCNY.js} +5 -5
- package/dist/{chunk-AIWULCUO.js → chunk-M66NAZA4.js} +5 -3
- package/dist/{chunk-AIWULCUO.js.map → chunk-M66NAZA4.js.map} +1 -1
- package/dist/{chunk-KVOXU4T5.js → chunk-N5YVZHCB.js} +2 -2
- package/dist/{chunk-76722EBH.js → chunk-NB47TXGP.js} +14 -12
- package/dist/{chunk-76722EBH.js.map → chunk-NB47TXGP.js.map} +1 -1
- package/dist/chunk-NO272T7Q.js +194 -0
- package/dist/chunk-NO272T7Q.js.map +1 -0
- package/dist/{chunk-SRB2XEWB.js → chunk-O2DB4C3M.js} +8 -6
- package/dist/{chunk-SRB2XEWB.js.map → chunk-O2DB4C3M.js.map} +1 -1
- package/dist/{chunk-IUEN57TV.js → chunk-OFBFAVLI.js} +6 -6
- package/dist/{chunk-MTMJVJ5W.js → chunk-OPTFANOX.js} +5 -3
- package/dist/chunk-OPTFANOX.js.map +1 -0
- package/dist/{chunk-5O3AOPDT.js → chunk-OVO2RZAE.js} +212 -439
- package/dist/chunk-OVO2RZAE.js.map +1 -0
- package/dist/{chunk-GYGWYIOZ.js → chunk-P27Z66EB.js} +7 -5
- package/dist/{chunk-GYGWYIOZ.js.map → chunk-P27Z66EB.js.map} +1 -1
- package/dist/{chunk-HCIPRAGS.js → chunk-P2LDXRGP.js} +2 -2
- package/dist/chunk-P3NGV5RV.js +41 -0
- package/dist/chunk-P3NGV5RV.js.map +1 -0
- package/dist/{chunk-5R3ERCDH.js → chunk-P3V7JTB6.js} +25 -9
- package/dist/{chunk-5R3ERCDH.js.map → chunk-P3V7JTB6.js.map} +1 -1
- package/dist/{chunk-LV7M27WM.js → chunk-P5WXDYMD.js} +8 -197
- package/dist/chunk-P5WXDYMD.js.map +1 -0
- package/dist/chunk-PGFY7IRW.js +95 -0
- package/dist/chunk-PGFY7IRW.js.map +1 -0
- package/dist/chunk-QKVILR7N.js +25 -0
- package/dist/chunk-QKVILR7N.js.map +1 -0
- package/dist/{chunk-V7RWQRG7.js → chunk-QNTEDPWP.js} +3 -3
- package/dist/{chunk-Q7SS3IME.js → chunk-RDHAGBEB.js} +3 -3
- package/dist/{chunk-EIZUR2XW.js → chunk-RTS23VIJ.js} +2 -2
- package/dist/{chunk-LFLXAY2W.js → chunk-SEQ2JI3Y.js} +9 -7
- package/dist/{chunk-LFLXAY2W.js.map → chunk-SEQ2JI3Y.js.map} +1 -1
- package/dist/{chunk-AZAOEIKW.js → chunk-SW56GSYC.js} +2 -2
- package/dist/{chunk-TA66UMI4.js → chunk-T2EGAXPM.js} +2 -2
- package/dist/{chunk-SM3AVUHC.js → chunk-T45LAT2Y.js} +2 -2
- package/dist/{chunk-SKF73JOP.js → chunk-T65UZXR6.js} +3 -3
- package/dist/{chunk-ITNUCOXM.js → chunk-TB5RNNJ4.js} +7 -5
- package/dist/{chunk-ITNUCOXM.js.map → chunk-TB5RNNJ4.js.map} +1 -1
- package/dist/chunk-TCIUO62Z.js +29 -0
- package/dist/chunk-TCIUO62Z.js.map +1 -0
- package/dist/chunk-THNJ3IKU.js +17 -0
- package/dist/chunk-THNJ3IKU.js.map +1 -0
- package/dist/chunk-TLJOJBZD.js +404 -0
- package/dist/chunk-TLJOJBZD.js.map +1 -0
- package/dist/chunk-TQ3REHVF.js +95 -0
- package/dist/chunk-TQ3REHVF.js.map +1 -0
- package/dist/{chunk-DGDI2D4M.js → chunk-TVRQ3RYH.js} +28 -7
- package/dist/chunk-TVRQ3RYH.js.map +1 -0
- package/dist/{chunk-PSMV73A5.js → chunk-TYGW5TOR.js} +7 -7
- package/dist/{chunk-ZYUC53LT.js → chunk-U23JUUPX.js} +58 -2
- package/dist/{chunk-ZYUC53LT.js.map → chunk-U23JUUPX.js.map} +1 -1
- package/dist/{chunk-3YFXJ3ZP.js → chunk-U24XHXNK.js} +2 -2
- package/dist/chunk-ULIHDPKL.js +94 -0
- package/dist/chunk-ULIHDPKL.js.map +1 -0
- package/dist/chunk-UOEWDCUX.js +69 -0
- package/dist/chunk-UOEWDCUX.js.map +1 -0
- package/dist/{chunk-WWGT2MDV.js → chunk-VAWY44JW.js} +8 -8
- package/dist/{chunk-WWGT2MDV.js.map → chunk-VAWY44JW.js.map} +1 -1
- package/dist/chunk-VFRNWE5P.js +239 -0
- package/dist/chunk-VFRNWE5P.js.map +1 -0
- package/dist/{chunk-IPB7FTQX.js → chunk-VMJ5URU7.js} +10 -8
- package/dist/chunk-VMJ5URU7.js.map +1 -0
- package/dist/{chunk-VFQGRQIH.js → chunk-VPCMJ5Z4.js} +7 -5
- package/dist/{chunk-VFQGRQIH.js.map → chunk-VPCMJ5Z4.js.map} +1 -1
- package/dist/{chunk-VCTFO5CO.js → chunk-VRPBEOWU.js} +2 -2
- package/dist/{chunk-ZCGAHGUS.js → chunk-VWGCJUTV.js} +2 -2
- package/dist/{chunk-XXYIOFUB.js → chunk-XJR3COJO.js} +5 -5
- package/dist/{chunk-PKHL44ER.js → chunk-XYYW64LB.js} +2 -2
- package/dist/{chunk-IAGBDSCS.js → chunk-YFF2RP3X.js} +2 -2
- package/dist/{chunk-MD3Y6J3L.js → chunk-Z3FZC5GV.js} +7 -5
- package/dist/{chunk-MD3Y6J3L.js.map → chunk-Z3FZC5GV.js.map} +1 -1
- package/dist/{chunk-DFEMTNPJ.js → chunk-Z5PIUYNB.js} +10 -8
- package/dist/{chunk-DFEMTNPJ.js.map → chunk-Z5PIUYNB.js.map} +1 -1
- package/dist/{chunk-WYSO2NIH.js → chunk-Z7KI4WHR.js} +2 -2
- package/dist/chunk-ZKF6HH7V.js +120 -0
- package/dist/chunk-ZKF6HH7V.js.map +1 -0
- package/dist/{chunk-I2NOIW44.js → chunk-ZKYMKF2S.js} +8 -6
- package/dist/{chunk-I2NOIW44.js.map → chunk-ZKYMKF2S.js.map} +1 -1
- package/dist/{chunk-PSHOXR6C.js → chunk-ZPHDGUZZ.js} +737 -1082
- package/dist/chunk-ZPHDGUZZ.js.map +1 -0
- package/dist/{chunk-CMGR4ZEW.js → chunk-ZROMNP7Q.js} +2 -2
- package/dist/classified/index.d.ts +17 -0
- package/dist/classified/index.js +38 -0
- package/dist/collection-facade-MKEBZDWW.js +39 -0
- package/dist/computed-BMMEFRFJ.js +11 -0
- package/dist/config-drift-KGM7ZRW4.js +24 -0
- package/dist/config-drift-KGM7ZRW4.js.map +1 -0
- package/dist/consent/index.d.ts +3 -3
- package/dist/consent/index.js +10 -4
- package/dist/consent/index.js.map +1 -1
- package/dist/crdt/index.d.ts +3 -3
- package/dist/delegation-BQNIEHZE.js +25 -0
- package/dist/derivations/index.d.ts +4 -4
- package/dist/derivations/index.js +12 -6
- package/dist/describe/index.d.ts +1 -1
- package/dist/{describe-Ccd1hS7a.d.ts → describe-C6iHNlqJ.d.ts} +19 -0
- package/dist/{dev-unlock-h0K-UZTk.d.ts → dev-unlock-Cqd5Xv5J.d.ts} +1 -1
- package/dist/{enclave-UL5LW66V.js → enclave-YN6223OG.js} +47 -18
- package/dist/executor-CBTNU5VZ.js +9 -0
- package/dist/executor-DJHNSTIR.js +9 -0
- package/dist/executor-FGISLQIN.js +21 -0
- package/dist/export-accessible-P7SLRLK3.js +23 -0
- package/dist/extract-partition-RNVSFHAB.js +36 -0
- package/dist/{fanout-sidecar-GF3DJ6KR.js → fanout-sidecar-WS22Q5WH.js} +17 -14
- package/dist/fanout-sidecar-WS22Q5WH.js.map +1 -0
- package/dist/fence-watcher-DHQ775JB.js +69 -0
- package/dist/fence-watcher-DHQ775JB.js.map +1 -0
- package/dist/find-RNBG7LBY.js +11 -0
- package/dist/forget/index.js +10 -4
- package/dist/forget/index.js.map +1 -1
- package/dist/guards/index.d.ts +4 -4
- package/dist/guards/index.js +3 -3
- package/dist/{hash-CdSr06sm.d.ts → hash-D8Q5MJLA.d.ts} +7 -2
- package/dist/history/index.d.ts +4 -4
- package/dist/history/index.js +12 -6
- package/dist/history/index.js.map +1 -1
- package/dist/i18n/index.d.ts +3 -3
- package/dist/i18n/index.js +14 -8
- package/dist/i18n/index.js.map +1 -1
- package/dist/in/index.d.ts +2 -2
- package/dist/{index-_6At2_9_.d.ts → index-D4GQe1Rx.d.ts} +1058 -49
- package/dist/{index-CGLLRtFc.d.ts → index-DRxk8q_7.d.ts} +3 -3
- package/dist/index.d.ts +60 -15
- package/dist/index.js +1081 -121
- package/dist/index.js.map +1 -1
- package/dist/indexing/index.d.ts +3 -3
- package/dist/indexing/index.js +4 -4
- package/dist/issue-W5QG53B5.js +19 -0
- package/dist/json-schema-I22JCYCG.js +44 -0
- package/dist/json-schema-I22JCYCG.js.map +1 -0
- package/dist/kernel/index.d.ts +3 -3
- package/dist/kernel/index.js +13 -7
- package/dist/lazy/index.d.ts +21 -0
- package/dist/lazy/index.js +12 -0
- package/dist/{ledger-RYI35CDS.js → ledger-KZWBKAG5.js} +12 -6
- package/dist/liberate-GMGCHIND.js +24 -0
- package/dist/link-set-UQEIYQAM.js +31 -0
- package/dist/materialized-views/index.d.ts +4 -4
- package/dist/materialized-views/index.js +16 -10
- package/dist/{mime-magic-B4RoFj3B.d.ts → mime-magic-DmwT7OzZ.d.ts} +1 -1
- package/dist/noydb-KS2O2MRI.js +60 -0
- package/dist/on/index.d.ts +2 -2
- package/dist/on/index.js +10 -4
- package/dist/overlay-views/index.d.ts +4 -4
- package/dist/overlay-views/index.js +4 -4
- package/dist/periods/index.d.ts +3 -3
- package/dist/periods/index.js +13 -7
- package/dist/pod/index.d.ts +3 -3
- package/dist/pod/index.js +12 -6
- package/dist/{policy-IXK4FVXP.js → policy-YIKUH4AD.js} +5 -5
- package/dist/portability/index.d.ts +3 -3
- package/dist/portability/index.js +8 -8
- package/dist/{public-envelope-JHDQCPSX.js → public-envelope-MRN5YYZZ.js} +4 -4
- package/dist/query/index.d.ts +2 -2
- package/dist/query/index.js +6 -6
- package/dist/register-K6DDSIXN.js +21 -0
- package/dist/registry-IMNMHWTM.js +17 -0
- package/dist/registry-K6VUQXKV.js +19 -0
- package/dist/registry-ZVO3T4M5.js +9 -0
- package/dist/registry-ZVO3T4M5.js.map +1 -0
- package/dist/request-withdrawal-EHALV66Y.js +31 -0
- package/dist/request-withdrawal-EHALV66Y.js.map +1 -0
- package/dist/reveal-TBLTFWQ2.js +38 -0
- package/dist/reveal-TBLTFWQ2.js.map +1 -0
- package/dist/revoke-MHAUAESM.js +24 -0
- package/dist/revoke-MHAUAESM.js.map +1 -0
- package/dist/sealed-record/index.d.ts +3 -3
- package/dist/sealed-record/index.js +13 -7
- package/dist/sealed-record/index.js.map +1 -1
- package/dist/session/index.d.ts +4 -4
- package/dist/session/index.js +10 -4
- package/dist/session/index.js.map +1 -1
- package/dist/shadow/index.d.ts +3 -3
- package/dist/shadow/index.js +2 -2
- package/dist/signer-PQ74YXRY.js +25 -0
- package/dist/signer-PQ74YXRY.js.map +1 -0
- package/dist/snapshots/index.d.ts +3 -3
- package/dist/snapshots/index.js +11 -5
- package/dist/snapshots/index.js.map +1 -1
- package/dist/{stale-3JWHNDIY.js → stale-7Q62KVI3.js} +2 -2
- package/dist/stale-7Q62KVI3.js.map +1 -0
- package/dist/storage-5E6YB2JY.js +21 -0
- package/dist/storage-5E6YB2JY.js.map +1 -0
- package/dist/{store-coordination-provider-3I7XWZZK.js → store-coordination-provider-JJCEMTAE.js} +3 -3
- package/dist/sync/index.d.ts +2 -2
- package/dist/sync/index.js +10 -4
- package/dist/sync/index.js.map +1 -1
- package/dist/team/index.d.ts +18 -4
- package/dist/team/index.js +24 -11
- package/dist/tiers/index.d.ts +2 -2
- package/dist/tiers/index.js +11 -5
- package/dist/to/index.d.ts +2 -2
- package/dist/to/index.js +1 -1
- package/dist/{transition-guard-DqodPNKw.d.ts → transition-guard-C4hvR-Ac.d.ts} +1 -1
- package/dist/tx/index.d.ts +3 -3
- package/dist/tx/index.js +3 -3
- package/dist/ui/index.d.ts +1 -1
- package/dist/util/index.js +1 -1
- package/dist/validators-Ctgkj5qd.d.ts +101 -0
- package/dist/{vault-diff-BtpiBvic.d.ts → vault-diff-B1Ir6EGs.d.ts} +1 -1
- package/dist/verify-YB5LQHNA.js +139 -0
- package/dist/verify-YB5LQHNA.js.map +1 -0
- package/dist/walk-5C3GPKT2.js +241 -0
- package/dist/walk-5C3GPKT2.js.map +1 -0
- package/dist/with/index.d.ts +3 -3
- package/dist/with/index.js +12 -6
- package/dist/{with-materialized-view-DMmWtYfJ.d.ts → with-materialized-view-Bp65kKdQ.d.ts} +1 -1
- package/dist/{with-overlayed-view-D_IB2nkM.d.ts → with-overlayed-view-BRhdQNrK.d.ts} +1 -1
- package/dist/{with-rollup-em2wxoHP.d.ts → with-rollup-bQRPc3y1.d.ts} +1 -1
- package/dist/withdraw-accessible-JMX2TCPX.js +28 -0
- package/dist/withdraw-accessible-JMX2TCPX.js.map +1 -0
- package/package.json +11 -3
- package/dist/api-RW3KP7WU.js +0 -14
- package/dist/chunk-5O3AOPDT.js.map +0 -1
- package/dist/chunk-5TDJ56JE.js.map +0 -1
- package/dist/chunk-DGDI2D4M.js.map +0 -1
- package/dist/chunk-IPB7FTQX.js.map +0 -1
- package/dist/chunk-LV7M27WM.js.map +0 -1
- package/dist/chunk-M2YKN37S.js +0 -524
- package/dist/chunk-M2YKN37S.js.map +0 -1
- package/dist/chunk-MTMJVJ5W.js.map +0 -1
- package/dist/chunk-PSHOXR6C.js.map +0 -1
- package/dist/chunk-TJYQIGAP.js.map +0 -1
- package/dist/chunk-UIU2THRG.js.map +0 -1
- package/dist/chunk-UPJNJGGA.js.map +0 -1
- package/dist/collection-facade-GQAOGJP2.js +0 -33
- package/dist/delegation-UL5HKLER.js +0 -19
- package/dist/executor-2FWQRLMG.js +0 -15
- package/dist/executor-QZ7BXICW.js +0 -9
- package/dist/executor-Z5ZLJVTV.js +0 -9
- package/dist/export-accessible-KRV5W4GH.js +0 -17
- package/dist/extract-partition-GLKBOXFQ.js +0 -30
- package/dist/fanout-sidecar-GF3DJ6KR.js.map +0 -1
- package/dist/issue-Y6UVIR43.js +0 -13
- package/dist/liberate-CRPZEV7O.js +0 -18
- package/dist/noydb-LDEBLNHY.js +0 -50
- package/dist/registry-45RJNWGU.js +0 -9
- package/dist/registry-5GRV5VXI.js +0 -13
- package/dist/registry-WEUCHBO4.js +0 -11
- package/dist/request-withdrawal-GBPH2FDY.js +0 -25
- package/dist/revoke-TUFRGI6S.js +0 -18
- package/dist/signer-PNKTBVF7.js +0 -19
- package/dist/withdraw-accessible-FFS46EOD.js +0 -22
- /package/dist/{api-RW3KP7WU.js.map → api-JOXUNSKP.js.map} +0 -0
- /package/dist/{chunk-SMXWYP24.js.map → chunk-3QRQOBS7.js.map} +0 -0
- /package/dist/{chunk-QHJW5EXU.js.map → chunk-3Y4QLJ6K.js.map} +0 -0
- /package/dist/{chunk-NF5JWERG.js.map → chunk-43ISXURO.js.map} +0 -0
- /package/dist/{chunk-5LUY7UTV.js.map → chunk-4K3MQ5S3.js.map} +0 -0
- /package/dist/{chunk-Y22T3KQE.js.map → chunk-5MRE3C4Q.js.map} +0 -0
- /package/dist/{chunk-M6OST55S.js.map → chunk-5W7AOFWA.js.map} +0 -0
- /package/dist/{chunk-AXRXJTE2.js.map → chunk-6HNKCKFZ.js.map} +0 -0
- /package/dist/{chunk-UAG5KWVA.js.map → chunk-6RXPSMKR.js.map} +0 -0
- /package/dist/{chunk-UDRIWL7A.js.map → chunk-A5DYVMDR.js.map} +0 -0
- /package/dist/{chunk-I3TIKTJO.js.map → chunk-BKGIWGCX.js.map} +0 -0
- /package/dist/{chunk-IO6GRPT6.js.map → chunk-BMQOH7MM.js.map} +0 -0
- /package/dist/{chunk-FISN7WE4.js.map → chunk-EWR7HL3K.js.map} +0 -0
- /package/dist/{chunk-AQTBSL7R.js.map → chunk-FH52CDEW.js.map} +0 -0
- /package/dist/{chunk-UV5MFVO3.js.map → chunk-G7RDYYTE.js.map} +0 -0
- /package/dist/{chunk-TEILUWOI.js.map → chunk-GOUNIHFA.js.map} +0 -0
- /package/dist/{chunk-RUEKROOS.js.map → chunk-GXGK22QU.js.map} +0 -0
- /package/dist/{chunk-IELYAOGG.js.map → chunk-GZZL5R73.js.map} +0 -0
- /package/dist/{chunk-CWPPNPOH.js.map → chunk-H7RYPVMJ.js.map} +0 -0
- /package/dist/{chunk-5N6CZTCY.js.map → chunk-HBKDR7R3.js.map} +0 -0
- /package/dist/{chunk-KXGNJJXB.js.map → chunk-HLUKZ36Q.js.map} +0 -0
- /package/dist/{chunk-LMTH2RM6.js.map → chunk-HMXLN43G.js.map} +0 -0
- /package/dist/{chunk-BG3SPSR4.js.map → chunk-HY6ZGGEC.js.map} +0 -0
- /package/dist/{chunk-62QYRORB.js.map → chunk-LIP6JWYK.js.map} +0 -0
- /package/dist/{chunk-IGUYVGGI.js.map → chunk-LPRPVCNY.js.map} +0 -0
- /package/dist/{chunk-KVOXU4T5.js.map → chunk-N5YVZHCB.js.map} +0 -0
- /package/dist/{chunk-IUEN57TV.js.map → chunk-OFBFAVLI.js.map} +0 -0
- /package/dist/{chunk-HCIPRAGS.js.map → chunk-P2LDXRGP.js.map} +0 -0
- /package/dist/{chunk-V7RWQRG7.js.map → chunk-QNTEDPWP.js.map} +0 -0
- /package/dist/{chunk-Q7SS3IME.js.map → chunk-RDHAGBEB.js.map} +0 -0
- /package/dist/{chunk-EIZUR2XW.js.map → chunk-RTS23VIJ.js.map} +0 -0
- /package/dist/{chunk-AZAOEIKW.js.map → chunk-SW56GSYC.js.map} +0 -0
- /package/dist/{chunk-TA66UMI4.js.map → chunk-T2EGAXPM.js.map} +0 -0
- /package/dist/{chunk-SM3AVUHC.js.map → chunk-T45LAT2Y.js.map} +0 -0
- /package/dist/{chunk-SKF73JOP.js.map → chunk-T65UZXR6.js.map} +0 -0
- /package/dist/{chunk-PSMV73A5.js.map → chunk-TYGW5TOR.js.map} +0 -0
- /package/dist/{chunk-3YFXJ3ZP.js.map → chunk-U24XHXNK.js.map} +0 -0
- /package/dist/{chunk-VCTFO5CO.js.map → chunk-VRPBEOWU.js.map} +0 -0
- /package/dist/{chunk-ZCGAHGUS.js.map → chunk-VWGCJUTV.js.map} +0 -0
- /package/dist/{chunk-XXYIOFUB.js.map → chunk-XJR3COJO.js.map} +0 -0
- /package/dist/{chunk-PKHL44ER.js.map → chunk-XYYW64LB.js.map} +0 -0
- /package/dist/{chunk-IAGBDSCS.js.map → chunk-YFF2RP3X.js.map} +0 -0
- /package/dist/{chunk-WYSO2NIH.js.map → chunk-Z7KI4WHR.js.map} +0 -0
- /package/dist/{chunk-CMGR4ZEW.js.map → chunk-ZROMNP7Q.js.map} +0 -0
- /package/dist/{collection-facade-GQAOGJP2.js.map → classified/index.js.map} +0 -0
- /package/dist/{delegation-UL5HKLER.js.map → collection-facade-MKEBZDWW.js.map} +0 -0
- /package/dist/{enclave-UL5LW66V.js.map → computed-BMMEFRFJ.js.map} +0 -0
- /package/dist/{executor-2FWQRLMG.js.map → delegation-BQNIEHZE.js.map} +0 -0
- /package/dist/{executor-QZ7BXICW.js.map → enclave-YN6223OG.js.map} +0 -0
- /package/dist/{executor-Z5ZLJVTV.js.map → executor-CBTNU5VZ.js.map} +0 -0
- /package/dist/{export-accessible-KRV5W4GH.js.map → executor-DJHNSTIR.js.map} +0 -0
- /package/dist/{extract-partition-GLKBOXFQ.js.map → executor-FGISLQIN.js.map} +0 -0
- /package/dist/{issue-Y6UVIR43.js.map → export-accessible-P7SLRLK3.js.map} +0 -0
- /package/dist/{ledger-RYI35CDS.js.map → extract-partition-RNVSFHAB.js.map} +0 -0
- /package/dist/{liberate-CRPZEV7O.js.map → find-RNBG7LBY.js.map} +0 -0
- /package/dist/{noydb-LDEBLNHY.js.map → issue-W5QG53B5.js.map} +0 -0
- /package/dist/{policy-IXK4FVXP.js.map → lazy/index.js.map} +0 -0
- /package/dist/{public-envelope-JHDQCPSX.js.map → ledger-KZWBKAG5.js.map} +0 -0
- /package/dist/{registry-45RJNWGU.js.map → liberate-GMGCHIND.js.map} +0 -0
- /package/dist/{registry-5GRV5VXI.js.map → link-set-UQEIYQAM.js.map} +0 -0
- /package/dist/{registry-WEUCHBO4.js.map → noydb-KS2O2MRI.js.map} +0 -0
- /package/dist/{request-withdrawal-GBPH2FDY.js.map → policy-YIKUH4AD.js.map} +0 -0
- /package/dist/{revoke-TUFRGI6S.js.map → public-envelope-MRN5YYZZ.js.map} +0 -0
- /package/dist/{signer-PNKTBVF7.js.map → register-K6DDSIXN.js.map} +0 -0
- /package/dist/{stale-3JWHNDIY.js.map → registry-IMNMHWTM.js.map} +0 -0
- /package/dist/{withdraw-accessible-FFS46EOD.js.map → registry-K6VUQXKV.js.map} +0 -0
- /package/dist/{store-coordination-provider-3I7XWZZK.js.map → store-coordination-provider-JJCEMTAE.js.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/with-cargo/walk-closure.ts","../src/with-cargo/extract-partition.ts"],"sourcesContent":["/**\n * Transitive-closure FK walker. Computes the set of\n * (collection, id) tuples reachable from seed predicates, so a\n * partition extraction ships a referentially-complete subset.\n *\n * Two-phase, plaintext, read-only (runs inside the unlocked vault\n * session — see foundation §13.4 / spec invariant 7):\n * 1. INBOUND expansion: from selected records, pull every record\n * that references them (children travel with parents), to a\n * fixed point.\n * 2. OUTBOUND completion: pull every parent the selected set\n * references (no dangling FKs), transitively, WITHOUT\n * re-expanding inbound from those parents (bounds the closure).\n *\n * The FK graph is auto-derived from the vault's existing RefRegistry\n * (the `ref('target')` declarations on collections) — no hand-written\n * edge list.\n *\n * @module\n */\nimport type { Vault } from '../kernel/vault.js'\nimport { PartitionExtractionError } from '../kernel/errors.js'\n\n/** Seed predicate per collection. Records that return true become roots. */\nexport interface WalkClosureOptions {\n readonly seeds: Record<\n string,\n (record: Record<string, unknown>) => boolean | Promise<boolean>\n >\n /** Max fixed-point iterations before throwing. Default 16. */\n readonly maxDepth?: number\n}\n\nexport interface ClosureResult {\n /** collection → set of record ids that travel together. */\n readonly closure: Map<string, Set<string>>\n readonly graph: {\n /** Fixed-point iterations the walk needed to converge. */\n readonly depth: number\n /** True if an edge pointed back to an already-selected node. */\n readonly cyclesDetected: boolean\n }\n}\n\nexport async function walkClosure(\n vault: Vault,\n opts: WalkClosureOptions,\n): Promise<ClosureResult> {\n const closure = new Map<string, Set<string>>()\n\n // Records carry a string `id` by construction (Collection.put(id: string)).\n // A non-string id during the walk means a malformed record — fail loud\n // rather than silently dropping it from the closure (which would leave a\n // dangling FK or a missing child in the extracted bundle).\n const requireStringId = (collection: string, record: Record<string, unknown>): string => {\n const id = record['id']\n if (typeof id !== 'string') {\n throw new PartitionExtractionError(\n `walkClosure: record in collection \"${collection}\" has a non-string ` +\n `id (${typeof id}); cannot include it in the partition closure.`,\n )\n }\n return id\n }\n\n const add = (collection: string, id: string): boolean => {\n let set = closure.get(collection)\n if (!set) {\n set = new Set<string>()\n closure.set(collection, set)\n }\n if (set.has(id)) return false\n set.add(id)\n return true\n }\n\n // Phase 0: evaluate seed predicates.\n for (const [collectionName, predicate] of Object.entries(opts.seeds)) {\n const coll = vault.collection<Record<string, unknown>>(collectionName)\n const records = await coll.list()\n for (const record of records) {\n if (await predicate(record)) {\n add(collectionName, requireStringId(collectionName, record))\n }\n }\n }\n\n const { refRegistry } = vault._introspectState()\n const maxDepth = opts.maxDepth ?? 16\n let cyclesDetected = false\n\n // `depth` counts PRODUCTIVE expansion generations (rounds that added at\n // least one new record), taken as the max over the two phases — i.e. the\n // FK hop-distance the closure needed, not the raw loop-iteration count.\n // The terminal draining pass that adds nothing does not count.\n let inboundDepth = 0\n let outboundDepth = 0\n\n // Phase 1 — INBOUND expansion. Worklist of newly-added (collection,id)\n // whose children we still need to pull.\n let frontier: Array<[string, string]> = []\n for (const [c, ids] of closure) for (const id of ids) frontier.push([c, id])\n\n while (frontier.length > 0) {\n const next: Array<[string, string]> = []\n for (const [collectionName, id] of frontier) {\n // Which collections reference THIS collection, and via which field?\n for (const inbound of refRegistry.getInbound(collectionName)) {\n const childColl = vault.collection<Record<string, unknown>>(inbound.collection)\n // TODO(perf): re-scans the full inbound collection on every frontier\n // element. O(frontier · inboundCollections · records) per depth. Fine\n // at consumer-firm scale (foundation §13.4); revisit with an index or\n // pagination if extraction over very large vaults gets slow.\n const childRecords = await childColl.list()\n for (const child of childRecords) {\n const fk = child[inbound.field]\n // Only scalar FK values can match an id; skip null/objects\n // (mirrors checkIntegrity's scalar guard, vault.ts).\n if (typeof fk !== 'string' && typeof fk !== 'number') continue\n if (String(fk) !== id) continue\n const childId = requireStringId(inbound.collection, child)\n if (add(inbound.collection, childId)) {\n next.push([inbound.collection, childId])\n } else {\n cyclesDetected = true\n }\n }\n }\n }\n if (next.length > 0 && ++inboundDepth > maxDepth) {\n throw new PartitionExtractionError(\n `walkClosure exceeded maxDepth=${maxDepth}; the FK graph may be ` +\n `unexpectedly deep or cyclic. Raise maxDepth or narrow the seeds.`,\n )\n }\n frontier = next\n }\n\n // Phase 2 — OUTBOUND completion. Pull referenced parents so no FK\n // dangles. Transitive over outbound edges only; parents are NOT\n // inbound-expanded (that would drag in unrelated siblings).\n let outboundFrontier: Array<[string, string]> = []\n for (const [c, ids] of closure) for (const id of ids) outboundFrontier.push([c, id])\n\n while (outboundFrontier.length > 0) {\n const next: Array<[string, string]> = []\n for (const [collectionName, id] of outboundFrontier) {\n const outbound = refRegistry.getOutbound(collectionName)\n if (Object.keys(outbound).length === 0) continue\n const coll = vault.collection<Record<string, unknown>>(collectionName)\n const record = await coll.get(id)\n if (!record) continue\n for (const [field, descriptor] of Object.entries(outbound)) {\n const rawId = record[field]\n // Only scalar FK values reference a parent id; skip null/objects.\n if (typeof rawId !== 'string' && typeof rawId !== 'number') continue\n const parentId = String(rawId)\n // Reaching an already-selected parent here is normal DAG\n // convergence (a child referencing its in-scope parent), not a\n // cycle — so do NOT flag cyclesDetected in the outbound phase.\n if (add(descriptor.target, parentId)) {\n next.push([descriptor.target, parentId])\n }\n }\n }\n if (next.length > 0 && ++outboundDepth > maxDepth) {\n throw new PartitionExtractionError(\n `walkClosure exceeded maxDepth=${maxDepth} during outbound completion.`,\n )\n }\n outboundFrontier = next\n }\n\n const depth = Math.max(inboundDepth, outboundDepth)\n\n return { closure, graph: { depth, cyclesDetected } }\n}\n","/**\n * Partition extraction. Walks the FK closure, re-encrypts\n * the selected records under fresh per-collection DEKs, seals those DEKs\n * under a one-time transfer key, and serializes an unowned\n * `extracted-partition` bundle.\n *\n * @module\n */\nimport type { Vault } from '../kernel/vault.js'\nimport type { EncryptedEnvelope, BlobObject, SlotRecord, VersionRecord } from '../kernel/types.js'\nimport { NOYDB_BACKUP_VERSION } from '../kernel/types.js'\nimport {\n decrypt,\n encrypt,\n openEnvelopeJson,\n generateDEK,\n bufferToBase64,\n encryptBytesWithAAD,\n decryptBytesWithAAD,\n unwrapCek,\n wrapCek,\n type EnclaveKey,\n} from '../kernel/enclave/index.js'\nimport {\n BLOB_COLLECTION,\n BLOB_INDEX_COLLECTION,\n BLOB_CHUNKS_COLLECTION,\n BLOB_SLOTS_PREFIX,\n BLOB_VERSIONS_PREFIX,\n} from '../with-shape/blobs/blob-set.js'\nimport { PartitionExtractionError } from '../kernel/errors.js'\nimport { walkClosure, type WalkClosureOptions } from './walk-closure.js'\nimport { generateULID } from '../with-pod/ulid.js'\nimport { SCHEMAS_COLLECTION } from '../with-shape/persisted-schemas/storage.js'\nimport { NOYDB_FORMAT_VERSION } from '../kernel/types.js'\nimport { LEDGER_COLLECTION } from '../with-commit/history/ledger/constants.js'\nimport { canonicalJson, hashEntry } from '../with-commit/history/ledger/entry.js'\nimport type { LedgerEntry } from '../with-commit/history/ledger/entry.js'\nimport { envelopePayloadHash } from '../with-commit/history/ledger/hash.js'\nimport {\n assembleBundleContainer,\n buildExtractedPartitionWrapper,\n type TransferSealPayload,\n} from '../with-pod/bundle.js'\n\n/** Re-keyed collections snapshot + the fresh DEKs used. */\nexport interface ReKeyResult {\n readonly collections: Record<string, Record<string, EncryptedEnvelope>>\n readonly deks: Map<string, EnclaveKey>\n}\n\n/**\n * Re-encrypt every record in `closure` under a fresh per-collection DEK.\n * Reads raw source envelopes, decrypts under the source DEK, re-encrypts\n * under the new DEK. Plaintext-pipeline: requires an unlocked vault.\n */\nexport async function reKeyClosure(\n vault: Vault,\n closure: Map<string, Set<string>>,\n fieldProjection?: Record<string, readonly string[]>,\n): Promise<ReKeyResult> {\n const { name: vaultName, adapter, getDEK } = vault._introspectState()\n const collections: Record<string, Record<string, EncryptedEnvelope>> = {}\n const deks = new Map<string, EnclaveKey>()\n\n for (const [collectionName, ids] of closure) {\n const srcDek = await getDEK(collectionName)\n const destDek = await generateDEK()\n deks.set(collectionName, destDek)\n const out: Record<string, EncryptedEnvelope> = {}\n // FR-7 structural field projection: when this collection has a projection,\n // narrow the plaintext body to `id` (always) + the listed fields BEFORE\n // re-encryption, so excluded fields never travel in the bundle. Applied\n // identically in both re-key branches; only the body changes — the `_cek`\n // re-wrap order is untouched. Absent/empty projection → byte-identical to\n // the un-projected path (`proj` is undefined and `project` is a no-op).\n const projList = fieldProjection?.[collectionName]\n const proj = projList ? new Set(projList) : undefined\n const project = (plaintext: string): string => {\n if (!proj) return plaintext\n const rec = JSON.parse(plaintext) as Record<string, unknown>\n const kept: Record<string, unknown> = {}\n if ('id' in rec) kept['id'] = rec['id'] // id ALWAYS preserved\n for (const f of proj) if (f in rec) kept[f] = rec[f]\n return JSON.stringify(kept)\n }\n\n for (const id of ids) {\n const env = await adapter.get(vaultName, collectionName, id)\n if (!env) continue\n if (env._cek !== undefined) {\n // Per-record CEK: a naive `{ ...env }` spread would carry a\n // SOURCE-DEK-wrapped CEK into a bundle re-keyed under a different\n // destination DEK — silently undecryptable for the recipient.\n // Re-wrap: unwrap the CEK under the source DEK, re-encrypt the body\n // under the SAME CEK (decision 2 — CEK reused on re-key, preserving\n // the history-chain identity), then wrap the CEK under the fresh\n // destination DEK. The recipient gains access transitively once they\n // re-wrap the collection DEK under their KEK on adopt.\n const cek = await unwrapCek(env._cek, srcDek)\n const plaintext = await decrypt(env._iv, env._data, cek)\n const { iv, data } = await encrypt(project(plaintext), cek)\n const wrapped = await wrapCek(cek, destDek)\n out[id] = { ...env, _iv: iv, _data: data, _cek: wrapped }\n continue\n }\n const plaintext = await openEnvelopeJson(env, srcDek)\n const { iv, data } = await encrypt(project(plaintext), destDek)\n out[id] = { ...env, _iv: iv, _data: data }\n }\n collections[collectionName] = out\n }\n\n return { collections, deks }\n}\n\n/**\n * Re-key the persisted JSON Schemas (`_schemas/<collection>`) for the\n * closure collections under the destination DEKs. Returns a\n * `{ collection: envelope }` map for the carried collections that actually\n * have a schema; collections without one are omitted.\n */\nexport async function reKeySchemas(\n vault: Vault,\n closure: Map<string, Set<string>>,\n destDeks: Map<string, EnclaveKey>,\n fieldProjection?: Record<string, readonly string[]>,\n): Promise<Record<string, EncryptedEnvelope>> {\n const { name: vaultName, adapter, getDEK } = vault._introspectState()\n const out: Record<string, EncryptedEnvelope> = {}\n\n for (const collectionName of closure.keys()) {\n // FR-7: skip a projected collection's schema — the narrowed shape no\n // longer matches the stored JSON Schema, so carrying it would assert a\n // contract the projected records violate (missing required fields).\n if (fieldProjection?.[collectionName]) continue\n const env = await adapter.get(vaultName, SCHEMAS_COLLECTION, collectionName)\n if (!env) continue // collection has no persisted schema — skip\n const destDek = destDeks.get(collectionName)\n if (!destDek) continue\n const srcDek = await getDEK(collectionName)\n const plaintext = await openEnvelopeJson(env, srcDek)\n const { iv, data } = await encrypt(plaintext, destDek)\n out[collectionName] = { ...env, _iv: iv, _data: data }\n }\n return out\n}\n\nconst paddedIndex = (n: number): string => String(n).padStart(10, '0')\n\nexport interface ReKeyLedgerResult {\n /** { paddedIndex: re-encrypted entry envelope } for backup._internal._ledger. */\n readonly entries: Record<string, EncryptedEnvelope>\n /** Recomputed ledgerHead for the carried chain (index -1 when empty). */\n readonly head: { hash: string; index: number; ts: string }\n}\n\n/**\n * Build the carried `_ledger` chain for an extracted partition.\n * Filters source entries to the closure, RE-CHAINS them (fresh index + prevHash),\n * and re-encrypts under `ledgerDek`. The `payloadHash` is recomputed against the\n * re-keyed envelope ONLY for the latest `put` per (collection,id) — the entry\n * `verifyBackupIntegrity` cross-checks; earlier puts + deletes keep their source\n * `payloadHash` verbatim (recomputing an intermediate put would assert a false\n * hash for an older version). Amendments + out-of-closure entries are dropped;\n * `_ledger_deltas`/`_history` are deferred to slice 2.\n */\nexport async function reKeyLedger(\n vault: Vault,\n closure: Map<string, Set<string>>,\n reKeyedCollections: Record<string, Record<string, EncryptedEnvelope>>,\n ledgerDek: EnclaveKey,\n): Promise<ReKeyLedgerResult> {\n const { name: vaultName, adapter, getDEK } = vault._introspectState()\n const srcLedgerDek = await getDEK(LEDGER_COLLECTION)\n\n // 1. Load + decrypt source entries in index order.\n const ids = (await adapter.list(vaultName, LEDGER_COLLECTION)).sort()\n const srcEntries: LedgerEntry[] = []\n for (const id of ids) {\n const env = await adapter.get(vaultName, LEDGER_COLLECTION, id)\n if (!env) continue\n srcEntries.push(JSON.parse(await openEnvelopeJson(env, srcLedgerDek)) as LedgerEntry)\n }\n\n // 2. Keep closure put/delete entries (drop amendments + out-of-closure).\n const kept = srcEntries.filter(\n (e) => (e.op === 'put' || e.op === 'delete') && (closure.get(e.collection)?.has(e.id) ?? false),\n )\n\n // 3a. Reverse pass: index of the LATEST put per (collection,id).\n const latestPutIndex = new Map<string, number>()\n for (let i = kept.length - 1; i >= 0; i--) {\n const e = kept[i]!\n if (e.op !== 'put') continue\n const key = `${e.collection}/${e.id}`\n if (!latestPutIndex.has(key)) latestPutIndex.set(key, i)\n }\n\n // 3b. Forward re-chain + re-encrypt.\n const entries: Record<string, EncryptedEnvelope> = {}\n let prevHash = ''\n let last: LedgerEntry | undefined\n for (let i = 0; i < kept.length; i++) {\n const src = kept[i]!\n const key = `${src.collection}/${src.id}`\n const isLatestPut = src.op === 'put' && latestPutIndex.get(key) === i\n const reKeyedEnv = reKeyedCollections[src.collection]?.[src.id]\n const payloadHash = isLatestPut && reKeyedEnv\n ? await envelopePayloadHash(reKeyedEnv)\n : src.payloadHash\n const entry: LedgerEntry = {\n index: i,\n prevHash,\n op: src.op,\n collection: src.collection,\n id: src.id,\n version: src.version,\n ts: src.ts,\n actor: src.actor,\n payloadHash,\n ...(src.reason !== undefined ? { reason: src.reason } : {}),\n }\n const { iv, data } = await encrypt(canonicalJson(entry), ledgerDek)\n entries[paddedIndex(i)] = {\n _noydb: NOYDB_FORMAT_VERSION, _v: i + 1, _ts: entry.ts, _iv: iv, _data: data, _by: entry.actor,\n }\n prevHash = await hashEntry(entry)\n last = entry\n }\n\n return {\n entries,\n head: last ? { hash: prevHash, index: last.index, ts: last.ts } : { hash: '', index: -1, ts: '' },\n }\n}\n\n/** Build the AAD binding for chunk integrity: \"{eTag}:{chunkIndex}:{chunkCount}\".\n * Mirrors `chunkAAD` in blob-set.ts — the eTag is preserved verbatim across\n * the transfer (see `reKeyBlobs`), so the same AAD that bound a chunk in the\n * source keeps binding it in the bundle. */\nfunction chunkAAD(eTag: string, chunkIndex: number, chunkCount: number): Uint8Array {\n return new TextEncoder().encode(`${eTag}:${chunkIndex}:${chunkCount}`)\n}\n\n/** Carried blob internals + the fresh transfer `_blob` DEK (present only when\n * the closure references at least one chunk-based blob). */\nexport interface ReKeyBlobsResult {\n /** `_blob_slots_<C>` / `_blob_versions_<C>` / `_blob_index` / `_blob_chunks`\n * envelopes for the bundle's `_internal` map. */\n readonly internal: Record<string, Record<string, EncryptedEnvelope>>\n /** Fresh transfer `_blob` DEK — seal it so owner-creation wraps it under the\n * recipient KEK. Undefined when no blob travels (source keyring untouched). */\n readonly blobDek?: EnclaveKey\n}\n\n/**\n * Carry the FK-closed slice's blobs — HARDENED key handling (no master-key leak).\n *\n * The source vault's shared `_blob` DEK decrypts (or unwraps the content CEK of)\n * EVERY blob in the source. Sealing it into the transfer would hand the recipient\n * a key to blobs far outside their slice. Instead we mint a **fresh transfer\n * `_blob` DEK** and arrange every carried blob into per-blob-CEK mode under it:\n *\n * - **erasable blob** (`_cek` present): unwrap the per-blob content CEK under the\n * SOURCE `_blob` DEK, re-wrap it under the FRESH transfer DEK. Chunks travel\n * **verbatim** (still ciphertext under that same content CEK — passthrough).\n * - **legacy blob** (no `_cek`, chunks under the shared `_blob` DEK): promote it\n * IN-BUNDLE — mint a fresh content CEK, decrypt each chunk under the source\n * `_blob` DEK and re-encrypt under the content CEK (same AAD, so the eTag-bound\n * integrity holds), then wrap the content CEK under the transfer DEK. The\n * SOURCE is never mutated (non-destructive), and the bundle never holds\n * plaintext blob bytes (zero-knowledge preserved).\n *\n * **eTag identity is preserved** (not re-HMAC'd). eTags are HMAC-keyed off the\n * `_blob` DEK, but they are stored as OPAQUE keys (`_blob_index/<eTag>`,\n * `_blob_chunks/<eTag>_<i>`) and never recomputed on read — only on a `put()`\n * for dedup. Keeping them verbatim keeps every slot/version/index/chunk key and\n * the chunk AAD coherent with zero chunk-key churn. The only consequence: a\n * future `put()` of identical bytes in the adopted vault computes a different\n * eTag (HMAC under the fresh DEK) and will NOT dedup against the carried blob —\n * an accepted, documented trade for hardened key isolation.\n *\n * Slots/versions are re-keyed under their parent collection's destination DEK\n * (honoring `fieldProjection` — projected-out blob fields' slots never travel);\n * `BlobObject.refCount` is recomputed from carried references only.\n *\n * `external` slots reference an unencrypted shared-bucket object that is not in\n * the bundle — their slot metadata travels (so the catalog entry survives) but\n * the bytes do not (a documented v1 limitation; their eTag is `''`).\n */\nexport async function reKeyBlobs(\n vault: Vault,\n closure: Map<string, Set<string>>,\n destDeks: Map<string, EnclaveKey>,\n fieldProjection?: Record<string, readonly string[]>,\n): Promise<ReKeyBlobsResult> {\n const { name: vaultName, adapter, getDEK } = vault._introspectState()\n const internal: Record<string, Record<string, EncryptedEnvelope>> = {}\n\n // travel set: eTag → number of carried references (slots + versions) within\n // the closure. Recomputed refCount, NOT the source's (which may count\n // out-of-slice referrers, leaving the adopted blob un-GC-able).\n const carriedRefs = new Map<string, number>()\n const addRef = (eTag: string): void => {\n if (!eTag) return // external slot — no chunk-based blob to carry\n carriedRefs.set(eTag, (carriedRefs.get(eTag) ?? 0) + 1)\n }\n\n const place = (collection: string, id: string, env: EncryptedEnvelope): void => {\n let bucket = internal[collection]\n if (!bucket) { bucket = {}; internal[collection] = bucket }\n bucket[id] = env\n }\n\n // ── Slots + versions (parent-collection-DEK keyed) ─────────────────\n for (const [collectionName, ids] of closure) {\n const destDek = destDeks.get(collectionName)\n if (!destDek) continue\n const srcDek = await getDEK(collectionName)\n const projList = fieldProjection?.[collectionName]\n const proj = projList ? new Set(projList) : undefined\n\n // Slots: one envelope per record, a `{ slotName: SlotRecord }` map.\n const slotsCollection = `${BLOB_SLOTS_PREFIX}${collectionName}`\n for (const id of ids) {\n const env = await adapter.get(vaultName, slotsCollection, id)\n if (!env) continue\n const slots = JSON.parse(await openEnvelopeJson(env, srcDek)) as Record<string, SlotRecord>\n // FR-7: drop projected-out blob fields' slots (slot names are blob field\n // names) — their eTags then never enter the travel set.\n const kept: Record<string, SlotRecord> = {}\n for (const [slotName, slot] of Object.entries(slots)) {\n if (proj && !proj.has(slotName)) continue\n kept[slotName] = slot\n addRef(slot.eTag)\n }\n if (Object.keys(kept).length === 0) continue\n const { iv, data } = await encrypt(JSON.stringify(kept), destDek)\n place(slotsCollection, id, { ...env, _iv: iv, _data: data })\n }\n\n // Versions: key = `${recordId}::${slotName}::${label}`; each an independent\n // refCount hold on its eTag.\n const versionsCollection = `${BLOB_VERSIONS_PREFIX}${collectionName}`\n const versionKeys = await adapter.list(vaultName, versionsCollection)\n for (const key of versionKeys) {\n const [recordId, slotName] = key.split('::')\n if (recordId === undefined || !ids.has(recordId)) continue\n if (proj && slotName !== undefined && !proj.has(slotName)) continue\n const env = await adapter.get(vaultName, versionsCollection, key)\n if (!env) continue\n const record = JSON.parse(await openEnvelopeJson(env, srcDek)) as VersionRecord\n addRef(record.eTag)\n const { iv, data } = await encrypt(JSON.stringify(record), destDek)\n place(versionsCollection, key, { ...env, _iv: iv, _data: data })\n }\n }\n\n // No chunk-based blob in the closure → carry nothing, mint nothing. Guarded\n // so `getDEK('_blob')` does not auto-mint + persist a phantom DEK on the\n // source keyring (mirrors the carryLedger non-destructive guard).\n if (carriedRefs.size === 0) return { internal }\n\n // ── Index + chunks (re-keyed under a FRESH transfer `_blob` DEK) ────\n const srcBlobDek = await getDEK(BLOB_COLLECTION)\n const transferBlobDek = await generateDEK()\n\n for (const [eTag, refCount] of carriedRefs) {\n const idxEnv = await adapter.get(vaultName, BLOB_INDEX_COLLECTION, eTag)\n if (!idxEnv) continue // dangling slot reference — nothing to carry\n const blob = JSON.parse(await openEnvelopeJson(idxEnv, srcBlobDek)) as BlobObject\n\n // Resolve the per-blob content CEK; passthrough vs. in-bundle promotion.\n let contentCek: EnclaveKey\n let chunksPassthrough: boolean\n if (blob._cek !== undefined) {\n contentCek = await unwrapCek(blob._cek, srcBlobDek)\n chunksPassthrough = true\n } else {\n // Legacy: promote to per-blob-CEK for the bundle (source untouched).\n contentCek = await generateDEK()\n chunksPassthrough = false\n }\n\n // Chunks: verbatim for an already-erasable blob; decrypt-then-re-encrypt\n // under the fresh content CEK for a legacy blob (same eTag-bound AAD).\n for (let i = 0; i < blob.chunkCount; i++) {\n const chunkId = `${eTag}_${i}`\n const chunkEnv = await adapter.get(vaultName, BLOB_CHUNKS_COLLECTION, chunkId)\n if (!chunkEnv) {\n throw new PartitionExtractionError(\n `reKeyBlobs: blob chunk ${i}/${blob.chunkCount} missing for eTag \"${eTag}\"; `\n + `cannot carry an incomplete blob into the partition.`,\n )\n }\n if (chunksPassthrough) {\n place(BLOB_CHUNKS_COLLECTION, chunkId, chunkEnv)\n } else {\n const aad = chunkAAD(eTag, i, blob.chunkCount)\n const plain = await decryptBytesWithAAD(chunkEnv._iv, chunkEnv._data, srcBlobDek, aad)\n const { iv, data } = await encryptBytesWithAAD(plain, contentCek, aad)\n place(BLOB_CHUNKS_COLLECTION, chunkId, { ...chunkEnv, _iv: iv, _data: data })\n }\n }\n\n // Index: per-blob-CEK mode under the transfer DEK, refCount corrected.\n // Drop any transient `_cekPending` (never set on a settled blob — possible\n // only if the source is mid-migration; we set a fresh settled `_cek`).\n const { _cekPending, ...rest } = blob\n void _cekPending\n const carried: BlobObject = { ...rest, refCount, _cek: await wrapCek(contentCek, transferBlobDek) }\n const { iv, data } = await encrypt(JSON.stringify(carried), transferBlobDek)\n place(BLOB_INDEX_COLLECTION, eTag, { ...idxEnv, _iv: iv, _data: data })\n }\n\n return { internal, blobDek: transferBlobDek }\n}\n\n/** A minted transfer key (raw 32 bytes) + the seal carrying the DEK set. */\nexport interface SealResult {\n readonly seal: TransferSealPayload\n readonly transferKey: Uint8Array\n}\n\n/**\n * Mint a random 32-byte transfer key, export each DEK to raw bytes, and\n * AES-256-GCM-seal the `{ collection: base64(rawDEK) }` map under the\n * transfer key. The transfer key is returned to the caller out-of-band;\n * only the sealed bytes travel in the bundle. Layout: iv(12) ‖ ct ‖ tag.\n */\nexport async function sealDeks(deks: Map<string, EnclaveKey>): Promise<SealResult> {\n const dekMap: Record<string, string> = {}\n for (const [collection, dek] of deks) {\n const raw = await crypto.subtle.exportKey('raw', dek)\n dekMap[collection] = bufferToBase64(raw)\n }\n\n const transferKey = crypto.getRandomValues(new Uint8Array(32))\n const key = await crypto.subtle.importKey('raw', transferKey, 'AES-GCM', false, ['encrypt'])\n const iv = crypto.getRandomValues(new Uint8Array(12))\n const plaintext = new TextEncoder().encode(JSON.stringify(dekMap))\n const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plaintext)\n\n const combined = new Uint8Array(iv.byteLength + ct.byteLength)\n combined.set(iv, 0)\n combined.set(new Uint8Array(ct), iv.byteLength)\n\n const sealId = bufferToBase64(crypto.getRandomValues(new Uint8Array(12)))\n return {\n seal: { v: 1, alg: 'aes-256-gcm-pre-shared', sealId, payload: bufferToBase64(combined) },\n transferKey,\n }\n}\n\nexport interface ExtractPartitionResult {\n readonly bundleBytes: Uint8Array\n /** Raw 32-byte transfer key — deliver out-of-band; required to adopt. */\n readonly transferKey: Uint8Array\n readonly sealId: string\n}\n\n/**\n * Extract a re-keyed, transfer-sealed partition. Owner-only\n * (invariant 5): producing a standalone re-keyed vault is an\n * ownership operation. Non-destructive on the source.\n */\nexport type ExtractPartitionOptions = WalkClosureOptions & {\n readonly compression?: 'auto' | 'brotli' | 'gzip' | 'none'\n readonly carrySchemas?: boolean\n readonly carryLedger?: boolean\n /**\n * FR-7 structural field projection: per-collection allow-list of fields\n * to keep. Non-listed fields are dropped from each record BEFORE\n * re-encryption (so they never travel in the bundle); `id` is always\n * preserved. A projected collection's persisted schema is NOT carried.\n * Absent/empty → un-projected behavior (byte-identical to today).\n */\n readonly fieldProjection?: Record<string, readonly string[]>\n}\n\n/**\n * Public extract-partition entry — gated behind `cargoStrategy: withCargo()`\n * (S4). Routes through the source vault's cargo strategy so an un-opted-in\n * caller hits `NO_CARGO`'s throw; `withCargo()` dynamically imports and runs\n * {@link extractPartitionCore} (the crypto engine, kept out of the floor).\n */\nexport async function extractPartition(\n vault: Vault,\n opts: ExtractPartitionOptions,\n): Promise<ExtractPartitionResult> {\n return vault.cargoStrategy.extractPartition(vault, opts)\n}\n\n/**\n * The extract-partition crypto engine — reached only when `withCargo()` is\n * opted in (via the lazy `with-cargo/active.ts` chunk). Body unchanged from the\n * hardened implementation; only the public gate wraps it.\n */\nexport async function extractPartitionCore(\n vault: Vault,\n opts: ExtractPartitionOptions,\n): Promise<ExtractPartitionResult> {\n // FR-6: extract-and-sever is the inalienability-floor half — owner-only. A\n // custodian operates fully but must NEVER produce a standalone re-keyed\n // partition (that would let it sever a copy out from under the sealed owner).\n // Explicit assertion so the security boundary is auditable at this site.\n if (vault.role === 'custodian') {\n throw new PartitionExtractionError(\n 'extractPartition is owner-only; a custodian cannot extract-and-sever '\n + '(FR-6: producing a re-keyed standalone partition is an ownership operation; use the Deed owner).',\n )\n }\n if (vault.role !== 'owner') {\n throw new PartitionExtractionError(\n `extractPartition requires the 'owner' role on the source vault; caller is '${vault.role}'. `\n + `Producing a re-keyed standalone partition is an ownership operation.`,\n )\n }\n\n // Persisted-schema writes (collection({ persistJsonSchema: true })) are fire-\n // and-forget queued onto vault._pendingSchemaWrites — a caller that does\n // `collection() → put() → extractPartition({ carrySchemas: true })` in quick\n // succession can hit a window where _schemas/<col> is not yet on disk and\n // reKeySchemas silently drops the row. Drain BEFORE reKeySchemas reads.\n if (opts.carrySchemas) await vault._drainPendingSchemaWrites()\n\n const { closure } = await walkClosure(vault, opts)\n const { collections, deks } = await reKeyClosure(vault, closure, opts.fieldProjection)\n\n // carryLedger: mint a fresh _ledger DEK, build the carried chain, and\n // SEAL the ledger DEK alongside the data DEKs so owner-creation wraps it into the\n // recipient keyring (lets them decrypt + verify the chain). Must run BEFORE\n // sealDeks.\n let ledgerHead: { hash: string; index: number; ts: string } | undefined\n let ledgerEntries: Record<string, EncryptedEnvelope> | undefined\n if (opts.carryLedger && vault._getLedgerOrNull() !== null) {\n // Skip when the source vault has no history strategy: reKeyLedger's first\n // `getDEK(LEDGER_COLLECTION)` would auto-mint and persist a phantom\n // _ledger DEK on the source keyring (contradicting \"non-destructive on\n // the source\"), and there's nothing to carry anyway. Mirrors the same\n // null-guard the source audit-append uses below.\n const ledgerDek = await generateDEK()\n const built = await reKeyLedger(vault, closure, collections, ledgerDek)\n if (built.head.index >= 0) {\n ledgerEntries = built.entries\n ledgerHead = built.head\n deks.set(LEDGER_COLLECTION, ledgerDek)\n }\n }\n\n // Carry the slice's blobs — re-keyed under a FRESH transfer `_blob` DEK\n // (HARDENED: never carries the source's shared blob DEK). Runs BEFORE\n // sealDeks so the fresh DEK is sealed alongside the data DEKs; non-destructive\n // on the source (mints + reads `_blob` only when the closure has blobs).\n const blobs = await reKeyBlobs(vault, closure, deks, opts.fieldProjection)\n if (blobs.blobDek) deks.set(BLOB_COLLECTION, blobs.blobDek)\n\n // Build _internal (schemas + ledger + blobs). reKeySchemas reads data-\n // collection DEKs only, so it is unaffected by the _ledger DEK added above.\n const internalSchemas = opts.carrySchemas ? await reKeySchemas(vault, closure, deks, opts.fieldProjection) : {}\n const internal: Record<string, Record<string, EncryptedEnvelope>> = {}\n if (Object.keys(internalSchemas).length > 0) internal[SCHEMAS_COLLECTION] = internalSchemas\n if (ledgerEntries) internal[LEDGER_COLLECTION] = ledgerEntries\n for (const [collection, records] of Object.entries(blobs.internal)) internal[collection] = records\n const hasInternal = Object.keys(internal).length > 0\n\n const { seal, transferKey } = await sealDeks(deks)\n\n // Source-side audit: record that a partition\n // was handed over. Non-destructive — an audit append, no record touched.\n // No-op when the source vault has no history strategy. append() fills\n // index/prevHash/ts and (since actor is '') the ledger's configured actor.\n await vault._getLedgerOrNull()?.append({\n op: 'lifecycle',\n collection: '',\n id: '',\n version: 0,\n actor: '',\n payloadHash: '',\n reason: `partition-handed-over:${seal.sealId}`,\n })\n\n // Build the dump JSON: unowned (empty keyrings), empty ledger (default),\n // re-keyed collections only.\n const { name: vaultName } = vault._introspectState()\n const backup = {\n _noydb_backup: NOYDB_BACKUP_VERSION,\n _compartment: vaultName,\n _exported_at: new Date().toISOString(),\n _exported_by: '', // unowned — no source user travels\n keyrings: {},\n collections,\n ...(hasInternal ? { _internal: internal } : {}),\n ...(ledgerHead ? { ledgerHead: { hash: ledgerHead.hash, index: ledgerHead.index, ts: ledgerHead.ts } } : {}),\n }\n const bodyJsonStr = JSON.stringify(buildExtractedPartitionWrapper(JSON.stringify(backup), seal))\n\n // An extracted partition is a NEW vault, not a re-export of the source —\n // mint a fresh handle rather than reusing the source's stable ULID\n // (which would collide if a recipient imports both source + partition).\n const handle = generateULID()\n const bundleBytes = await assembleBundleContainer({\n handle,\n bodyJsonStr,\n compression: opts.compression,\n headerExtras: {\n bundleKind: 'extracted-partition',\n transferSeal: { v: seal.v, alg: seal.alg, sealId: seal.sealId }, // indicator only\n },\n })\n\n return { bundleBytes, transferKey, sealId: seal.sealId }\n}\n"],"mappings":"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","names":["plaintext","iv","data"]}
|
|
1
|
+
{"version":3,"sources":["../src/with-cargo/walk-closure.ts","../src/with-cargo/extract-partition.ts"],"sourcesContent":["/**\n * Transitive-closure FK walker. Computes the set of\n * (collection, id) tuples reachable from seed predicates, so a\n * partition extraction ships a referentially-complete subset.\n *\n * Two-phase, plaintext, read-only (runs inside the unlocked vault\n * session — see foundation §13.4 / spec invariant 7):\n * 1. INBOUND expansion: from selected records, pull every record\n * that references them (children travel with parents), to a\n * fixed point.\n * 2. OUTBOUND completion: pull every parent the selected set\n * references (no dangling FKs), transitively, WITHOUT\n * re-expanding inbound from those parents (bounds the closure).\n *\n * The FK graph is auto-derived from the vault's existing RefRegistry\n * (the `ref('target')` declarations on collections) — no hand-written\n * edge list.\n *\n * @module\n */\nimport type { Vault } from '../kernel/vault.js'\nimport { PartitionExtractionError } from '../kernel/errors.js'\n\n/** Seed predicate per collection. Records that return true become roots. */\nexport interface WalkClosureOptions {\n readonly seeds: Record<\n string,\n (record: Record<string, unknown>) => boolean | Promise<boolean>\n >\n /** Max fixed-point iterations before throwing. Default 16. */\n readonly maxDepth?: number\n}\n\nexport interface ClosureResult {\n /** collection → set of record ids that travel together. */\n readonly closure: Map<string, Set<string>>\n readonly graph: {\n /** Fixed-point iterations the walk needed to converge. */\n readonly depth: number\n /** True if an edge pointed back to an already-selected node. */\n readonly cyclesDetected: boolean\n }\n}\n\nexport async function walkClosure(\n vault: Vault,\n opts: WalkClosureOptions,\n): Promise<ClosureResult> {\n const closure = new Map<string, Set<string>>()\n\n // Records carry a string `id` by construction (Collection.put(id: string)).\n // A non-string id during the walk means a malformed record — fail loud\n // rather than silently dropping it from the closure (which would leave a\n // dangling FK or a missing child in the extracted bundle).\n const requireStringId = (collection: string, record: Record<string, unknown>): string => {\n const id = record['id']\n if (typeof id !== 'string') {\n throw new PartitionExtractionError(\n `walkClosure: record in collection \"${collection}\" has a non-string ` +\n `id (${typeof id}); cannot include it in the partition closure.`,\n )\n }\n return id\n }\n\n const add = (collection: string, id: string): boolean => {\n let set = closure.get(collection)\n if (!set) {\n set = new Set<string>()\n closure.set(collection, set)\n }\n if (set.has(id)) return false\n set.add(id)\n return true\n }\n\n // Phase 0: evaluate seed predicates.\n for (const [collectionName, predicate] of Object.entries(opts.seeds)) {\n const coll = vault.collection<Record<string, unknown>>(collectionName)\n const records = await coll.list()\n for (const record of records) {\n if (await predicate(record)) {\n add(collectionName, requireStringId(collectionName, record))\n }\n }\n }\n\n const { refRegistry } = vault._introspectState()\n const maxDepth = opts.maxDepth ?? 16\n let cyclesDetected = false\n\n // `depth` counts PRODUCTIVE expansion generations (rounds that added at\n // least one new record), taken as the max over the two phases — i.e. the\n // FK hop-distance the closure needed, not the raw loop-iteration count.\n // The terminal draining pass that adds nothing does not count.\n let inboundDepth = 0\n let outboundDepth = 0\n\n // Phase 1 — INBOUND expansion. Worklist of newly-added (collection,id)\n // whose children we still need to pull.\n let frontier: Array<[string, string]> = []\n for (const [c, ids] of closure) for (const id of ids) frontier.push([c, id])\n\n while (frontier.length > 0) {\n const next: Array<[string, string]> = []\n for (const [collectionName, id] of frontier) {\n // Which collections reference THIS collection, and via which field?\n for (const inbound of refRegistry.getInbound(collectionName)) {\n const childColl = vault.collection<Record<string, unknown>>(inbound.collection)\n // TODO(perf): re-scans the full inbound collection on every frontier\n // element. O(frontier · inboundCollections · records) per depth. Fine\n // at consumer-firm scale (foundation §13.4); revisit with an index or\n // pagination if extraction over very large vaults gets slow.\n const childRecords = await childColl.list()\n for (const child of childRecords) {\n const fk = child[inbound.field]\n // Only scalar FK values can match an id; skip null/objects\n // (mirrors checkIntegrity's scalar guard, vault.ts).\n if (typeof fk !== 'string' && typeof fk !== 'number') continue\n if (String(fk) !== id) continue\n const childId = requireStringId(inbound.collection, child)\n if (add(inbound.collection, childId)) {\n next.push([inbound.collection, childId])\n } else {\n cyclesDetected = true\n }\n }\n }\n }\n if (next.length > 0 && ++inboundDepth > maxDepth) {\n throw new PartitionExtractionError(\n `walkClosure exceeded maxDepth=${maxDepth}; the FK graph may be ` +\n `unexpectedly deep or cyclic. Raise maxDepth or narrow the seeds.`,\n )\n }\n frontier = next\n }\n\n // Phase 2 — OUTBOUND completion. Pull referenced parents so no FK\n // dangles. Transitive over outbound edges only; parents are NOT\n // inbound-expanded (that would drag in unrelated siblings).\n let outboundFrontier: Array<[string, string]> = []\n for (const [c, ids] of closure) for (const id of ids) outboundFrontier.push([c, id])\n\n while (outboundFrontier.length > 0) {\n const next: Array<[string, string]> = []\n for (const [collectionName, id] of outboundFrontier) {\n const outbound = refRegistry.getOutbound(collectionName)\n if (Object.keys(outbound).length === 0) continue\n const coll = vault.collection<Record<string, unknown>>(collectionName)\n const record = await coll.get(id)\n if (!record) continue\n for (const [field, descriptor] of Object.entries(outbound)) {\n const rawId = record[field]\n // Only scalar FK values reference a parent id; skip null/objects.\n if (typeof rawId !== 'string' && typeof rawId !== 'number') continue\n const parentId = String(rawId)\n // Reaching an already-selected parent here is normal DAG\n // convergence (a child referencing its in-scope parent), not a\n // cycle — so do NOT flag cyclesDetected in the outbound phase.\n if (add(descriptor.target, parentId)) {\n next.push([descriptor.target, parentId])\n }\n }\n }\n if (next.length > 0 && ++outboundDepth > maxDepth) {\n throw new PartitionExtractionError(\n `walkClosure exceeded maxDepth=${maxDepth} during outbound completion.`,\n )\n }\n outboundFrontier = next\n }\n\n const depth = Math.max(inboundDepth, outboundDepth)\n\n return { closure, graph: { depth, cyclesDetected } }\n}\n","/**\n * Partition extraction. Walks the FK closure, re-encrypts\n * the selected records under fresh per-collection DEKs, seals those DEKs\n * under a one-time transfer key, and serializes an unowned\n * `extracted-partition` bundle.\n *\n * @module\n */\nimport type { Vault } from '../kernel/vault.js'\nimport type { EncryptedEnvelope, BlobObject, SlotRecord, VersionRecord } from '../kernel/types.js'\nimport { NOYDB_BACKUP_VERSION } from '../kernel/types.js'\nimport {\n decrypt,\n encrypt,\n openEnvelopeJson,\n generateDEK,\n bufferToBase64,\n encryptBytesWithAAD,\n decryptBytesWithAAD,\n unwrapCek,\n wrapCek,\n type EnclaveKey,\n} from '../kernel/enclave/index.js'\nimport {\n BLOB_COLLECTION,\n BLOB_INDEX_COLLECTION,\n BLOB_CHUNKS_COLLECTION,\n BLOB_SLOTS_PREFIX,\n BLOB_VERSIONS_PREFIX,\n} from '../with-shape/blobs/blob-set.js'\nimport { PartitionExtractionError } from '../kernel/errors.js'\nimport { walkClosure, type WalkClosureOptions } from './walk-closure.js'\nimport { generateULID } from '../with-pod/ulid.js'\nimport { SCHEMAS_COLLECTION } from '../with-shape/persisted-schemas/storage.js'\nimport { NOYDB_FORMAT_VERSION } from '../kernel/types.js'\nimport { LEDGER_COLLECTION } from '../with-commit/history/ledger/constants.js'\nimport { canonicalJson, hashEntry } from '../with-commit/history/ledger/entry.js'\nimport type { LedgerEntry } from '../with-commit/history/ledger/entry.js'\nimport { envelopePayloadHash } from '../with-commit/history/ledger/hash.js'\nimport {\n assembleBundleContainer,\n buildExtractedPartitionWrapper,\n type TransferSealPayload,\n} from '../with-pod/bundle.js'\n\n/** Re-keyed collections snapshot + the fresh DEKs used. */\nexport interface ReKeyResult {\n readonly collections: Record<string, Record<string, EncryptedEnvelope>>\n readonly deks: Map<string, EnclaveKey>\n}\n\n/**\n * Re-encrypt every record in `closure` under a fresh per-collection DEK.\n * Reads raw source envelopes, decrypts under the source DEK, re-encrypts\n * under the new DEK. Plaintext-pipeline: requires an unlocked vault.\n */\nexport async function reKeyClosure(\n vault: Vault,\n closure: Map<string, Set<string>>,\n fieldProjection?: Record<string, readonly string[]>,\n): Promise<ReKeyResult> {\n const { name: vaultName, adapter, getDEK } = vault._introspectState()\n const collections: Record<string, Record<string, EncryptedEnvelope>> = {}\n const deks = new Map<string, EnclaveKey>()\n\n for (const [collectionName, ids] of closure) {\n const srcDek = await getDEK(collectionName)\n const destDek = await generateDEK()\n deks.set(collectionName, destDek)\n const out: Record<string, EncryptedEnvelope> = {}\n // FR-7 structural field projection: when this collection has a projection,\n // narrow the plaintext body to `id` (always) + the listed fields BEFORE\n // re-encryption, so excluded fields never travel in the bundle. Applied\n // identically in both re-key branches; only the body changes — the `_cek`\n // re-wrap order is untouched. Absent/empty projection → byte-identical to\n // the un-projected path (`proj` is undefined and `project` is a no-op).\n const projList = fieldProjection?.[collectionName]\n const proj = projList ? new Set(projList) : undefined\n const project = (plaintext: string): string => {\n if (!proj) return plaintext\n const rec = JSON.parse(plaintext) as Record<string, unknown>\n const kept: Record<string, unknown> = {}\n if ('id' in rec) kept['id'] = rec['id'] // id ALWAYS preserved\n for (const f of proj) if (f in rec) kept[f] = rec[f]\n return JSON.stringify(kept)\n }\n\n for (const id of ids) {\n const env = await adapter.get(vaultName, collectionName, id)\n if (!env) continue\n if (env._cek !== undefined) {\n // Per-record CEK: a naive `{ ...env }` spread would carry a\n // SOURCE-DEK-wrapped CEK into a bundle re-keyed under a different\n // destination DEK — silently undecryptable for the recipient.\n // Re-wrap: unwrap the CEK under the source DEK, re-encrypt the body\n // under the SAME CEK (decision 2 — CEK reused on re-key, preserving\n // the history-chain identity), then wrap the CEK under the fresh\n // destination DEK. The recipient gains access transitively once they\n // re-wrap the collection DEK under their KEK on adopt.\n const cek = await unwrapCek(env._cek, srcDek)\n const plaintext = await decrypt(env._iv, env._data, cek)\n const { iv, data } = await encrypt(project(plaintext), cek)\n const wrapped = await wrapCek(cek, destDek)\n out[id] = { ...env, _iv: iv, _data: data, _cek: wrapped }\n continue\n }\n const plaintext = await openEnvelopeJson(env, srcDek)\n const { iv, data } = await encrypt(project(plaintext), destDek)\n out[id] = { ...env, _iv: iv, _data: data }\n }\n collections[collectionName] = out\n }\n\n return { collections, deks }\n}\n\n/**\n * Re-key the persisted JSON Schemas (`_schemas/<collection>`) for the\n * closure collections under the destination DEKs. Returns a\n * `{ collection: envelope }` map for the carried collections that actually\n * have a schema; collections without one are omitted.\n */\nexport async function reKeySchemas(\n vault: Vault,\n closure: Map<string, Set<string>>,\n destDeks: Map<string, EnclaveKey>,\n fieldProjection?: Record<string, readonly string[]>,\n): Promise<Record<string, EncryptedEnvelope>> {\n const { name: vaultName, adapter, getDEK } = vault._introspectState()\n const out: Record<string, EncryptedEnvelope> = {}\n\n for (const collectionName of closure.keys()) {\n // FR-7: skip a projected collection's schema — the narrowed shape no\n // longer matches the stored JSON Schema, so carrying it would assert a\n // contract the projected records violate (missing required fields).\n if (fieldProjection?.[collectionName]) continue\n const env = await adapter.get(vaultName, SCHEMAS_COLLECTION, collectionName)\n if (!env) continue // collection has no persisted schema — skip\n const destDek = destDeks.get(collectionName)\n if (!destDek) continue\n const srcDek = await getDEK(collectionName)\n const plaintext = await openEnvelopeJson(env, srcDek)\n const { iv, data } = await encrypt(plaintext, destDek)\n out[collectionName] = { ...env, _iv: iv, _data: data }\n }\n return out\n}\n\nconst paddedIndex = (n: number): string => String(n).padStart(10, '0')\n\nexport interface ReKeyLedgerResult {\n /** { paddedIndex: re-encrypted entry envelope } for backup._internal._ledger. */\n readonly entries: Record<string, EncryptedEnvelope>\n /** Recomputed ledgerHead for the carried chain (index -1 when empty). */\n readonly head: { hash: string; index: number; ts: string }\n}\n\n/**\n * Build the carried `_ledger` chain for an extracted partition.\n * Filters source entries to the closure, RE-CHAINS them (fresh index + prevHash),\n * and re-encrypts under `ledgerDek`. The `payloadHash` is recomputed against the\n * re-keyed envelope ONLY for the latest `put` per (collection,id) — the entry\n * `verifyBackupIntegrity` cross-checks; earlier puts + deletes keep their source\n * `payloadHash` verbatim (recomputing an intermediate put would assert a false\n * hash for an older version). Amendments + out-of-closure entries are dropped;\n * `_ledger_deltas`/`_history` are deferred to slice 2.\n */\nexport async function reKeyLedger(\n vault: Vault,\n closure: Map<string, Set<string>>,\n reKeyedCollections: Record<string, Record<string, EncryptedEnvelope>>,\n ledgerDek: EnclaveKey,\n): Promise<ReKeyLedgerResult> {\n const { name: vaultName, adapter, getDEK } = vault._introspectState()\n const srcLedgerDek = await getDEK(LEDGER_COLLECTION)\n\n // 1. Load + decrypt source entries in index order.\n const ids = (await adapter.list(vaultName, LEDGER_COLLECTION)).sort()\n const srcEntries: LedgerEntry[] = []\n for (const id of ids) {\n const env = await adapter.get(vaultName, LEDGER_COLLECTION, id)\n if (!env) continue\n srcEntries.push(JSON.parse(await openEnvelopeJson(env, srcLedgerDek)) as LedgerEntry)\n }\n\n // 2. Keep closure put/delete entries (drop amendments + out-of-closure).\n const kept = srcEntries.filter(\n (e) => (e.op === 'put' || e.op === 'delete') && (closure.get(e.collection)?.has(e.id) ?? false),\n )\n\n // 3a. Reverse pass: index of the LATEST put per (collection,id).\n const latestPutIndex = new Map<string, number>()\n for (let i = kept.length - 1; i >= 0; i--) {\n const e = kept[i]!\n if (e.op !== 'put') continue\n const key = `${e.collection}/${e.id}`\n if (!latestPutIndex.has(key)) latestPutIndex.set(key, i)\n }\n\n // 3b. Forward re-chain + re-encrypt.\n const entries: Record<string, EncryptedEnvelope> = {}\n let prevHash = ''\n let last: LedgerEntry | undefined\n for (let i = 0; i < kept.length; i++) {\n const src = kept[i]!\n const key = `${src.collection}/${src.id}`\n const isLatestPut = src.op === 'put' && latestPutIndex.get(key) === i\n const reKeyedEnv = reKeyedCollections[src.collection]?.[src.id]\n const payloadHash = isLatestPut && reKeyedEnv\n ? await envelopePayloadHash(reKeyedEnv)\n : src.payloadHash\n const entry: LedgerEntry = {\n index: i,\n prevHash,\n op: src.op,\n collection: src.collection,\n id: src.id,\n version: src.version,\n ts: src.ts,\n actor: src.actor,\n payloadHash,\n ...(src.reason !== undefined ? { reason: src.reason } : {}),\n }\n const { iv, data } = await encrypt(canonicalJson(entry), ledgerDek)\n entries[paddedIndex(i)] = {\n _noydb: NOYDB_FORMAT_VERSION, _v: i + 1, _ts: entry.ts, _iv: iv, _data: data, _by: entry.actor,\n }\n prevHash = await hashEntry(entry)\n last = entry\n }\n\n return {\n entries,\n head: last ? { hash: prevHash, index: last.index, ts: last.ts } : { hash: '', index: -1, ts: '' },\n }\n}\n\n/** Build the AAD binding for chunk integrity: \"{eTag}:{chunkIndex}:{chunkCount}\".\n * Mirrors `chunkAAD` in blob-set.ts — the eTag is preserved verbatim across\n * the transfer (see `reKeyBlobs`), so the same AAD that bound a chunk in the\n * source keeps binding it in the bundle. */\nfunction chunkAAD(eTag: string, chunkIndex: number, chunkCount: number): Uint8Array {\n return new TextEncoder().encode(`${eTag}:${chunkIndex}:${chunkCount}`)\n}\n\n/** Carried blob internals + the fresh transfer `_blob` DEK (present only when\n * the closure references at least one chunk-based blob). */\nexport interface ReKeyBlobsResult {\n /** `_blob_slots_<C>` / `_blob_versions_<C>` / `_blob_index` / `_blob_chunks`\n * envelopes for the bundle's `_internal` map. */\n readonly internal: Record<string, Record<string, EncryptedEnvelope>>\n /** Fresh transfer `_blob` DEK — seal it so owner-creation wraps it under the\n * recipient KEK. Undefined when no blob travels (source keyring untouched). */\n readonly blobDek?: EnclaveKey\n}\n\n/**\n * Carry the FK-closed slice's blobs — HARDENED key handling (no master-key leak).\n *\n * The source vault's shared `_blob` DEK decrypts (or unwraps the content CEK of)\n * EVERY blob in the source. Sealing it into the transfer would hand the recipient\n * a key to blobs far outside their slice. Instead we mint a **fresh transfer\n * `_blob` DEK** and arrange every carried blob into per-blob-CEK mode under it:\n *\n * - **erasable blob** (`_cek` present): unwrap the per-blob content CEK under the\n * SOURCE `_blob` DEK, re-wrap it under the FRESH transfer DEK. Chunks travel\n * **verbatim** (still ciphertext under that same content CEK — passthrough).\n * - **legacy blob** (no `_cek`, chunks under the shared `_blob` DEK): promote it\n * IN-BUNDLE — mint a fresh content CEK, decrypt each chunk under the source\n * `_blob` DEK and re-encrypt under the content CEK (same AAD, so the eTag-bound\n * integrity holds), then wrap the content CEK under the transfer DEK. The\n * SOURCE is never mutated (non-destructive), and the bundle never holds\n * plaintext blob bytes (zero-knowledge preserved).\n *\n * **eTag identity is preserved** (not re-HMAC'd). eTags are HMAC-keyed off the\n * `_blob` DEK, but they are stored as OPAQUE keys (`_blob_index/<eTag>`,\n * `_blob_chunks/<eTag>_<i>`) and never recomputed on read — only on a `put()`\n * for dedup. Keeping them verbatim keeps every slot/version/index/chunk key and\n * the chunk AAD coherent with zero chunk-key churn. The only consequence: a\n * future `put()` of identical bytes in the adopted vault computes a different\n * eTag (HMAC under the fresh DEK) and will NOT dedup against the carried blob —\n * an accepted, documented trade for hardened key isolation.\n *\n * Slots/versions are re-keyed under their parent collection's destination DEK\n * (honoring `fieldProjection` — projected-out blob fields' slots never travel);\n * `BlobObject.refCount` is recomputed from carried references only.\n *\n * `external` slots reference an unencrypted shared-bucket object that is not in\n * the bundle — their slot metadata travels (so the catalog entry survives) but\n * the bytes do not (a documented v1 limitation; their eTag is `''`).\n */\nexport async function reKeyBlobs(\n vault: Vault,\n closure: Map<string, Set<string>>,\n destDeks: Map<string, EnclaveKey>,\n fieldProjection?: Record<string, readonly string[]>,\n): Promise<ReKeyBlobsResult> {\n const { name: vaultName, adapter, getDEK } = vault._introspectState()\n const internal: Record<string, Record<string, EncryptedEnvelope>> = {}\n\n // travel set: eTag → number of carried references (slots + versions) within\n // the closure. Recomputed refCount, NOT the source's (which may count\n // out-of-slice referrers, leaving the adopted blob un-GC-able).\n const carriedRefs = new Map<string, number>()\n const addRef = (eTag: string): void => {\n if (!eTag) return // external slot — no chunk-based blob to carry\n carriedRefs.set(eTag, (carriedRefs.get(eTag) ?? 0) + 1)\n }\n\n const place = (collection: string, id: string, env: EncryptedEnvelope): void => {\n let bucket = internal[collection]\n if (!bucket) { bucket = {}; internal[collection] = bucket }\n bucket[id] = env\n }\n\n // ── Slots + versions (parent-collection-DEK keyed) ─────────────────\n for (const [collectionName, ids] of closure) {\n const destDek = destDeks.get(collectionName)\n if (!destDek) continue\n const srcDek = await getDEK(collectionName)\n const projList = fieldProjection?.[collectionName]\n const proj = projList ? new Set(projList) : undefined\n\n // Slots: one envelope per record, a `{ slotName: SlotRecord }` map.\n const slotsCollection = `${BLOB_SLOTS_PREFIX}${collectionName}`\n for (const id of ids) {\n const env = await adapter.get(vaultName, slotsCollection, id)\n if (!env) continue\n const slots = JSON.parse(await openEnvelopeJson(env, srcDek)) as Record<string, SlotRecord>\n // FR-7: drop projected-out blob fields' slots (slot names are blob field\n // names) — their eTags then never enter the travel set.\n const kept: Record<string, SlotRecord> = {}\n for (const [slotName, slot] of Object.entries(slots)) {\n if (proj && !proj.has(slotName)) continue\n kept[slotName] = slot\n addRef(slot.eTag)\n }\n if (Object.keys(kept).length === 0) continue\n const { iv, data } = await encrypt(JSON.stringify(kept), destDek)\n place(slotsCollection, id, { ...env, _iv: iv, _data: data })\n }\n\n // Versions: key = `${recordId}::${slotName}::${label}`; each an independent\n // refCount hold on its eTag.\n const versionsCollection = `${BLOB_VERSIONS_PREFIX}${collectionName}`\n const versionKeys = await adapter.list(vaultName, versionsCollection)\n for (const key of versionKeys) {\n const [recordId, slotName] = key.split('::')\n if (recordId === undefined || !ids.has(recordId)) continue\n if (proj && slotName !== undefined && !proj.has(slotName)) continue\n const env = await adapter.get(vaultName, versionsCollection, key)\n if (!env) continue\n const record = JSON.parse(await openEnvelopeJson(env, srcDek)) as VersionRecord\n addRef(record.eTag)\n const { iv, data } = await encrypt(JSON.stringify(record), destDek)\n place(versionsCollection, key, { ...env, _iv: iv, _data: data })\n }\n }\n\n // No chunk-based blob in the closure → carry nothing, mint nothing. Guarded\n // so `getDEK('_blob')` does not auto-mint + persist a phantom DEK on the\n // source keyring (mirrors the carryLedger non-destructive guard).\n if (carriedRefs.size === 0) return { internal }\n\n // ── Index + chunks (re-keyed under a FRESH transfer `_blob` DEK) ────\n const srcBlobDek = await getDEK(BLOB_COLLECTION)\n const transferBlobDek = await generateDEK()\n\n for (const [eTag, refCount] of carriedRefs) {\n const idxEnv = await adapter.get(vaultName, BLOB_INDEX_COLLECTION, eTag)\n if (!idxEnv) continue // dangling slot reference — nothing to carry\n const blob = JSON.parse(await openEnvelopeJson(idxEnv, srcBlobDek)) as BlobObject\n\n // Resolve the per-blob content CEK; passthrough vs. in-bundle promotion.\n let contentCek: EnclaveKey\n let chunksPassthrough: boolean\n if (blob._cek !== undefined) {\n contentCek = await unwrapCek(blob._cek, srcBlobDek)\n chunksPassthrough = true\n } else {\n // Legacy: promote to per-blob-CEK for the bundle (source untouched).\n contentCek = await generateDEK()\n chunksPassthrough = false\n }\n\n // Chunks: verbatim for an already-erasable blob; decrypt-then-re-encrypt\n // under the fresh content CEK for a legacy blob (same eTag-bound AAD).\n for (let i = 0; i < blob.chunkCount; i++) {\n const chunkId = `${eTag}_${i}`\n const chunkEnv = await adapter.get(vaultName, BLOB_CHUNKS_COLLECTION, chunkId)\n if (!chunkEnv) {\n throw new PartitionExtractionError(\n `reKeyBlobs: blob chunk ${i}/${blob.chunkCount} missing for eTag \"${eTag}\"; `\n + `cannot carry an incomplete blob into the partition.`,\n )\n }\n if (chunksPassthrough) {\n place(BLOB_CHUNKS_COLLECTION, chunkId, chunkEnv)\n } else {\n const aad = chunkAAD(eTag, i, blob.chunkCount)\n const plain = await decryptBytesWithAAD(chunkEnv._iv, chunkEnv._data, srcBlobDek, aad)\n const { iv, data } = await encryptBytesWithAAD(plain, contentCek, aad)\n place(BLOB_CHUNKS_COLLECTION, chunkId, { ...chunkEnv, _iv: iv, _data: data })\n }\n }\n\n // Index: per-blob-CEK mode under the transfer DEK, refCount corrected.\n // Drop any transient `_cekPending` (never set on a settled blob — possible\n // only if the source is mid-migration; we set a fresh settled `_cek`).\n const { _cekPending, ...rest } = blob\n void _cekPending\n const carried: BlobObject = { ...rest, refCount, _cek: await wrapCek(contentCek, transferBlobDek) }\n const { iv, data } = await encrypt(JSON.stringify(carried), transferBlobDek)\n place(BLOB_INDEX_COLLECTION, eTag, { ...idxEnv, _iv: iv, _data: data })\n }\n\n return { internal, blobDek: transferBlobDek }\n}\n\n/** A minted transfer key (raw 32 bytes) + the seal carrying the DEK set. */\nexport interface SealResult {\n readonly seal: TransferSealPayload\n readonly transferKey: Uint8Array\n}\n\n/**\n * Mint a random 32-byte transfer key, export each DEK to raw bytes, and\n * AES-256-GCM-seal the `{ collection: base64(rawDEK) }` map under the\n * transfer key. The transfer key is returned to the caller out-of-band;\n * only the sealed bytes travel in the bundle. Layout: iv(12) ‖ ct ‖ tag.\n */\nexport async function sealDeks(deks: Map<string, EnclaveKey>): Promise<SealResult> {\n const dekMap: Record<string, string> = {}\n for (const [collection, dek] of deks) {\n const raw = await crypto.subtle.exportKey('raw', dek)\n dekMap[collection] = bufferToBase64(raw)\n }\n\n const transferKey = crypto.getRandomValues(new Uint8Array(32))\n const key = await crypto.subtle.importKey('raw', transferKey, 'AES-GCM', false, ['encrypt'])\n const iv = crypto.getRandomValues(new Uint8Array(12))\n const plaintext = new TextEncoder().encode(JSON.stringify(dekMap))\n const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plaintext)\n\n const combined = new Uint8Array(iv.byteLength + ct.byteLength)\n combined.set(iv, 0)\n combined.set(new Uint8Array(ct), iv.byteLength)\n\n const sealId = bufferToBase64(crypto.getRandomValues(new Uint8Array(12)))\n return {\n seal: { v: 1, alg: 'aes-256-gcm-pre-shared', sealId, payload: bufferToBase64(combined) },\n transferKey,\n }\n}\n\nexport interface ExtractPartitionResult {\n readonly bundleBytes: Uint8Array\n /** Raw 32-byte transfer key — deliver out-of-band; required to adopt. */\n readonly transferKey: Uint8Array\n readonly sealId: string\n}\n\n/**\n * Extract a re-keyed, transfer-sealed partition. Owner-only\n * (invariant 5): producing a standalone re-keyed vault is an\n * ownership operation. Non-destructive on the source.\n */\nexport type ExtractPartitionOptions = WalkClosureOptions & {\n readonly compression?: 'auto' | 'brotli' | 'gzip' | 'none'\n readonly carrySchemas?: boolean\n readonly carryLedger?: boolean\n /**\n * FR-7 structural field projection: per-collection allow-list of fields\n * to keep. Non-listed fields are dropped from each record BEFORE\n * re-encryption (so they never travel in the bundle); `id` is always\n * preserved. A projected collection's persisted schema is NOT carried.\n * Absent/empty → un-projected behavior (byte-identical to today).\n */\n readonly fieldProjection?: Record<string, readonly string[]>\n}\n\n/**\n * Public extract-partition entry — gated behind `cargoStrategy: withCargo()`\n * (S4). Routes through the source vault's cargo strategy so an un-opted-in\n * caller hits `NO_CARGO`'s throw; `withCargo()` dynamically imports and runs\n * {@link extractPartitionCore} (the crypto engine, kept out of the floor).\n */\nexport async function extractPartition(\n vault: Vault,\n opts: ExtractPartitionOptions,\n): Promise<ExtractPartitionResult> {\n return vault.cargoStrategy.extractPartition(vault, opts)\n}\n\n/**\n * The extract-partition crypto engine — reached only when `withCargo()` is\n * opted in (via the lazy `with-cargo/active.ts` chunk). Body unchanged from the\n * hardened implementation; only the public gate wraps it.\n */\nexport async function extractPartitionCore(\n vault: Vault,\n opts: ExtractPartitionOptions,\n): Promise<ExtractPartitionResult> {\n // FR-6: extract-and-sever is the inalienability-floor half — owner-only. A\n // custodian operates fully but must NEVER produce a standalone re-keyed\n // partition (that would let it sever a copy out from under the sealed owner).\n // Explicit assertion so the security boundary is auditable at this site.\n if (vault.role === 'custodian') {\n throw new PartitionExtractionError(\n 'extractPartition is owner-only; a custodian cannot extract-and-sever '\n + '(FR-6: producing a re-keyed standalone partition is an ownership operation; use the Deed owner).',\n )\n }\n if (vault.role !== 'owner') {\n throw new PartitionExtractionError(\n `extractPartition requires the 'owner' role on the source vault; caller is '${vault.role}'. `\n + `Producing a re-keyed standalone partition is an ownership operation.`,\n )\n }\n\n // Persisted-schema writes (collection({ persistJsonSchema: true })) are fire-\n // and-forget queued onto vault._pendingSchemaWrites — a caller that does\n // `collection() → put() → extractPartition({ carrySchemas: true })` in quick\n // succession can hit a window where _schemas/<col> is not yet on disk and\n // reKeySchemas silently drops the row. Drain BEFORE reKeySchemas reads.\n if (opts.carrySchemas) await vault._drainPendingSchemaWrites()\n\n const { closure } = await walkClosure(vault, opts)\n const { collections, deks } = await reKeyClosure(vault, closure, opts.fieldProjection)\n\n // carryLedger: mint a fresh _ledger DEK, build the carried chain, and\n // SEAL the ledger DEK alongside the data DEKs so owner-creation wraps it into the\n // recipient keyring (lets them decrypt + verify the chain). Must run BEFORE\n // sealDeks.\n let ledgerHead: { hash: string; index: number; ts: string } | undefined\n let ledgerEntries: Record<string, EncryptedEnvelope> | undefined\n if (opts.carryLedger && vault._getLedgerOrNull() !== null) {\n // Skip when the source vault has no history strategy: reKeyLedger's first\n // `getDEK(LEDGER_COLLECTION)` would auto-mint and persist a phantom\n // _ledger DEK on the source keyring (contradicting \"non-destructive on\n // the source\"), and there's nothing to carry anyway. Mirrors the same\n // null-guard the source audit-append uses below.\n const ledgerDek = await generateDEK()\n const built = await reKeyLedger(vault, closure, collections, ledgerDek)\n if (built.head.index >= 0) {\n ledgerEntries = built.entries\n ledgerHead = built.head\n deks.set(LEDGER_COLLECTION, ledgerDek)\n }\n }\n\n // Carry the slice's blobs — re-keyed under a FRESH transfer `_blob` DEK\n // (HARDENED: never carries the source's shared blob DEK). Runs BEFORE\n // sealDeks so the fresh DEK is sealed alongside the data DEKs; non-destructive\n // on the source (mints + reads `_blob` only when the closure has blobs).\n const blobs = await reKeyBlobs(vault, closure, deks, opts.fieldProjection)\n if (blobs.blobDek) deks.set(BLOB_COLLECTION, blobs.blobDek)\n\n // Build _internal (schemas + ledger + blobs). reKeySchemas reads data-\n // collection DEKs only, so it is unaffected by the _ledger DEK added above.\n const internalSchemas = opts.carrySchemas ? await reKeySchemas(vault, closure, deks, opts.fieldProjection) : {}\n const internal: Record<string, Record<string, EncryptedEnvelope>> = {}\n if (Object.keys(internalSchemas).length > 0) internal[SCHEMAS_COLLECTION] = internalSchemas\n if (ledgerEntries) internal[LEDGER_COLLECTION] = ledgerEntries\n for (const [collection, records] of Object.entries(blobs.internal)) internal[collection] = records\n const hasInternal = Object.keys(internal).length > 0\n\n const { seal, transferKey } = await sealDeks(deks)\n\n // Source-side audit: record that a partition\n // was handed over. Non-destructive — an audit append, no record touched.\n // No-op when the source vault has no history strategy. append() fills\n // index/prevHash/ts and (since actor is '') the ledger's configured actor.\n await vault._getLedgerOrNull()?.append({\n op: 'lifecycle',\n collection: '',\n id: '',\n version: 0,\n actor: '',\n payloadHash: '',\n reason: `partition-handed-over:${seal.sealId}`,\n })\n\n // Build the dump JSON: unowned (empty keyrings), empty ledger (default),\n // re-keyed collections only.\n const { name: vaultName } = vault._introspectState()\n const backup = {\n _noydb_backup: NOYDB_BACKUP_VERSION,\n _compartment: vaultName,\n _exported_at: new Date().toISOString(),\n _exported_by: '', // unowned — no source user travels\n keyrings: {},\n collections,\n ...(hasInternal ? { _internal: internal } : {}),\n ...(ledgerHead ? { ledgerHead: { hash: ledgerHead.hash, index: ledgerHead.index, ts: ledgerHead.ts } } : {}),\n }\n const bodyJsonStr = JSON.stringify(buildExtractedPartitionWrapper(JSON.stringify(backup), seal))\n\n // An extracted partition is a NEW vault, not a re-export of the source —\n // mint a fresh handle rather than reusing the source's stable ULID\n // (which would collide if a recipient imports both source + partition).\n const handle = generateULID()\n const bundleBytes = await assembleBundleContainer({\n handle,\n bodyJsonStr,\n compression: opts.compression,\n headerExtras: {\n bundleKind: 'extracted-partition',\n transferSeal: { v: seal.v, alg: seal.alg, sealId: seal.sealId }, // indicator only\n },\n })\n\n return { bundleBytes, transferKey, sealId: seal.sealId }\n}\n"],"mappings":"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","names":["plaintext","iv","data"]}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// src/kernel/cache/lru.ts
|
|
2
|
+
var Lru = class {
|
|
3
|
+
entries = /* @__PURE__ */ new Map();
|
|
4
|
+
maxRecords;
|
|
5
|
+
maxBytes;
|
|
6
|
+
currentBytes = 0;
|
|
7
|
+
hits = 0;
|
|
8
|
+
misses = 0;
|
|
9
|
+
evictions = 0;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
if (options.maxRecords === void 0 && options.maxBytes === void 0) {
|
|
12
|
+
throw new Error("Lru: must specify maxRecords, maxBytes, or both");
|
|
13
|
+
}
|
|
14
|
+
this.maxRecords = options.maxRecords;
|
|
15
|
+
this.maxBytes = options.maxBytes;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Look up a key. Hits promote the entry to most-recently-used; misses
|
|
19
|
+
* return undefined. Both update the running stats counters.
|
|
20
|
+
*/
|
|
21
|
+
get(key) {
|
|
22
|
+
const entry = this.entries.get(key);
|
|
23
|
+
if (!entry) {
|
|
24
|
+
this.misses++;
|
|
25
|
+
return void 0;
|
|
26
|
+
}
|
|
27
|
+
this.entries.delete(key);
|
|
28
|
+
this.entries.set(key, entry);
|
|
29
|
+
this.hits++;
|
|
30
|
+
return entry.value;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Insert or update a key. If the key already exists, its size is
|
|
34
|
+
* accounted for and the entry is promoted to MRU. After insertion,
|
|
35
|
+
* eviction runs to maintain both budgets.
|
|
36
|
+
*/
|
|
37
|
+
set(key, value, size) {
|
|
38
|
+
const existing = this.entries.get(key);
|
|
39
|
+
if (existing) {
|
|
40
|
+
this.currentBytes -= existing.size;
|
|
41
|
+
this.entries.delete(key);
|
|
42
|
+
}
|
|
43
|
+
this.entries.set(key, { value, size });
|
|
44
|
+
this.currentBytes += size;
|
|
45
|
+
this.evictUntilUnderBudget();
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Remove a key without affecting hit/miss stats. Used by `Collection.delete()`.
|
|
49
|
+
* Returns true if the key was present.
|
|
50
|
+
*/
|
|
51
|
+
remove(key) {
|
|
52
|
+
const existing = this.entries.get(key);
|
|
53
|
+
if (!existing) return false;
|
|
54
|
+
this.currentBytes -= existing.size;
|
|
55
|
+
this.entries.delete(key);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
/** True if the cache currently holds an entry for the given key. */
|
|
59
|
+
has(key) {
|
|
60
|
+
return this.entries.has(key);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Drop every entry. Stats counters survive — call `resetStats()` if you
|
|
64
|
+
* want a clean slate. Used by `Collection.invalidate()` on key rotation.
|
|
65
|
+
*/
|
|
66
|
+
clear() {
|
|
67
|
+
this.entries.clear();
|
|
68
|
+
this.currentBytes = 0;
|
|
69
|
+
}
|
|
70
|
+
/** Reset hit/miss/eviction counters to zero. Does NOT touch entries. */
|
|
71
|
+
resetStats() {
|
|
72
|
+
this.hits = 0;
|
|
73
|
+
this.misses = 0;
|
|
74
|
+
this.evictions = 0;
|
|
75
|
+
}
|
|
76
|
+
/** Snapshot of current cache statistics. Cheap — no copying. */
|
|
77
|
+
stats() {
|
|
78
|
+
return {
|
|
79
|
+
hits: this.hits,
|
|
80
|
+
misses: this.misses,
|
|
81
|
+
evictions: this.evictions,
|
|
82
|
+
size: this.entries.size,
|
|
83
|
+
bytes: this.currentBytes
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Iterate over all currently-cached values. Order is least-recently-used
|
|
88
|
+
* first. Used by tests and devtools — production callers should use
|
|
89
|
+
* `Collection.scan()` instead.
|
|
90
|
+
*/
|
|
91
|
+
*values() {
|
|
92
|
+
for (const entry of this.entries.values()) yield entry.value;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Walk the cache from the LRU end and drop entries until both budgets
|
|
96
|
+
* are satisfied. Called after every `set()`. Single pass — entries are
|
|
97
|
+
* never re-promoted during eviction.
|
|
98
|
+
*/
|
|
99
|
+
evictUntilUnderBudget() {
|
|
100
|
+
while (this.overBudget()) {
|
|
101
|
+
const oldest = this.entries.keys().next();
|
|
102
|
+
if (oldest.done) return;
|
|
103
|
+
const key = oldest.value;
|
|
104
|
+
const entry = this.entries.get(key);
|
|
105
|
+
if (entry) this.currentBytes -= entry.size;
|
|
106
|
+
this.entries.delete(key);
|
|
107
|
+
this.evictions++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
overBudget() {
|
|
111
|
+
if (this.maxRecords !== void 0 && this.entries.size > this.maxRecords) return true;
|
|
112
|
+
if (this.maxBytes !== void 0 && this.currentBytes > this.maxBytes) return true;
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// src/kernel/cache/policy.ts
|
|
118
|
+
var UNITS = {
|
|
119
|
+
"": 1,
|
|
120
|
+
"B": 1,
|
|
121
|
+
"KB": 1024,
|
|
122
|
+
"MB": 1024 * 1024,
|
|
123
|
+
"GB": 1024 * 1024 * 1024
|
|
124
|
+
// 'TB' deliberately not supported — if you need it, you're not using NOYDB.
|
|
125
|
+
};
|
|
126
|
+
function parseBytes(input) {
|
|
127
|
+
if (typeof input === "number") {
|
|
128
|
+
if (!Number.isFinite(input) || input <= 0) {
|
|
129
|
+
throw new Error(`parseBytes: numeric input must be a positive finite number, got ${String(input)}`);
|
|
130
|
+
}
|
|
131
|
+
return Math.floor(input);
|
|
132
|
+
}
|
|
133
|
+
const trimmed = input.trim();
|
|
134
|
+
if (trimmed === "") {
|
|
135
|
+
throw new Error("parseBytes: empty string is not a valid byte budget");
|
|
136
|
+
}
|
|
137
|
+
const match = /^([0-9]+(?:\.[0-9]+)?)\s*([A-Za-z]*)$/.exec(trimmed);
|
|
138
|
+
if (!match) {
|
|
139
|
+
throw new Error(`parseBytes: invalid byte budget "${input}". Expected format: "1024", "50KB", "50MB", "1GB"`);
|
|
140
|
+
}
|
|
141
|
+
const value = parseFloat(match[1]);
|
|
142
|
+
const unit = (match[2] ?? "").toUpperCase();
|
|
143
|
+
if (!(unit in UNITS)) {
|
|
144
|
+
throw new Error(`parseBytes: unknown unit "${match[2]}" in "${input}". Supported: B, KB, MB, GB`);
|
|
145
|
+
}
|
|
146
|
+
const bytes = Math.floor(value * UNITS[unit]);
|
|
147
|
+
if (bytes <= 0) {
|
|
148
|
+
throw new Error(`parseBytes: byte budget must be > 0, got ${bytes} from "${input}"`);
|
|
149
|
+
}
|
|
150
|
+
return bytes;
|
|
151
|
+
}
|
|
152
|
+
function estimateRecordBytes(record) {
|
|
153
|
+
try {
|
|
154
|
+
return JSON.stringify(record).length;
|
|
155
|
+
} catch {
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/port/with/lazy-strategy.ts
|
|
161
|
+
function buildLazyCache(collection, cache) {
|
|
162
|
+
if (!cache || cache.maxRecords === void 0 && cache.maxBytes === void 0) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Collection "${collection}": lazy mode (prefetch: false) requires a cache option with maxRecords and/or maxBytes. An unbounded lazy cache defeats the purpose.`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
const lruOptions = {};
|
|
168
|
+
if (cache.maxRecords !== void 0) lruOptions.maxRecords = cache.maxRecords;
|
|
169
|
+
if (cache.maxBytes !== void 0) lruOptions.maxBytes = parseBytes(cache.maxBytes);
|
|
170
|
+
return new Lru(lruOptions);
|
|
171
|
+
}
|
|
172
|
+
var implicitLazyWarned = false;
|
|
173
|
+
var IMPLICIT_LAZY = {
|
|
174
|
+
createCache(collection, cache) {
|
|
175
|
+
if (!implicitLazyWarned) {
|
|
176
|
+
implicitLazyWarned = true;
|
|
177
|
+
if (!(typeof process !== "undefined" && process.env["NODE_ENV"] === "test")) {
|
|
178
|
+
console.warn(
|
|
179
|
+
`[noy-db] Collection "${collection}": lazy mode (prefetch: false) without the lazy service is deprecated \u2014 pass \`lazyStrategy: withLazy()\` (from "@noy-db/hub/lazy") to createNoydb(). The implicit path will be removed at 1.0.`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return buildLazyCache(collection, cache);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export {
|
|
188
|
+
Lru,
|
|
189
|
+
parseBytes,
|
|
190
|
+
estimateRecordBytes,
|
|
191
|
+
buildLazyCache,
|
|
192
|
+
IMPLICIT_LAZY
|
|
193
|
+
};
|
|
194
|
+
//# sourceMappingURL=chunk-NO272T7Q.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/kernel/cache/lru.ts","../src/kernel/cache/policy.ts","../src/port/with/lazy-strategy.ts"],"sourcesContent":["/**\n * Generic LRU cache for `Collection`'s lazy hydration mode.\n *\n * Backed by a JavaScript `Map`, which preserves insertion order. Promotion\n * is implemented as `delete()` + `set()` — O(1) on `Map` since both\n * operations are constant-time. Eviction walks the iterator from the front\n * (least recently used) until both budgets are satisfied.\n *\n * ships in-memory only. The cache is never persisted; on collection\n * close every entry is dropped. Persisting cache state is a follow-up\n * once the access patterns from real consumers tell us whether it would\n * pay back the complexity.\n */\n\nexport interface LruEntry<V> {\n /** The cached value. */\n readonly value: V\n /**\n * Approximate decrypted byte size of the entry. Used by the byte-budget\n * eviction path. Callers compute this once at insert time and pass it\n * in — recomputing on every access would dominate the per-record cost.\n */\n readonly size: number\n}\n\nexport interface LruOptions {\n /** Maximum number of entries before eviction. Required if `maxBytes` is unset. */\n maxRecords?: number\n /** Maximum total bytes before eviction. Computed from per-entry `size`. */\n maxBytes?: number\n}\n\nexport interface LruStats {\n /** Total cache hits since construction (or `resetStats()`). */\n hits: number\n /** Total cache misses since construction (or `resetStats()`). */\n misses: number\n /** Total entries evicted since construction (or `resetStats()`). */\n evictions: number\n /** Current number of cached entries. */\n size: number\n /** Current sum of cached entry sizes (in bytes, approximate). */\n bytes: number\n}\n\n/**\n * O(1) LRU cache. Both `get()` and `set()` promote the touched entry to\n * the most-recently-used end. Eviction happens after every insert and\n * walks the front of the Map iterator dropping entries until both\n * budgets are satisfied.\n */\nexport class Lru<K, V> {\n private readonly entries = new Map<K, LruEntry<V>>()\n private readonly maxRecords: number | undefined\n private readonly maxBytes: number | undefined\n private currentBytes = 0\n private hits = 0\n private misses = 0\n private evictions = 0\n\n constructor(options: LruOptions) {\n if (options.maxRecords === undefined && options.maxBytes === undefined) {\n throw new Error('Lru: must specify maxRecords, maxBytes, or both')\n }\n this.maxRecords = options.maxRecords\n this.maxBytes = options.maxBytes\n }\n\n /**\n * Look up a key. Hits promote the entry to most-recently-used; misses\n * return undefined. Both update the running stats counters.\n */\n get(key: K): V | undefined {\n const entry = this.entries.get(key)\n if (!entry) {\n this.misses++\n return undefined\n }\n // Promote: re-insert moves the entry to the end of the iteration order.\n this.entries.delete(key)\n this.entries.set(key, entry)\n this.hits++\n return entry.value\n }\n\n /**\n * Insert or update a key. If the key already exists, its size is\n * accounted for and the entry is promoted to MRU. After insertion,\n * eviction runs to maintain both budgets.\n */\n set(key: K, value: V, size: number): void {\n const existing = this.entries.get(key)\n if (existing) {\n // Update path: subtract the old size before adding the new one.\n this.currentBytes -= existing.size\n this.entries.delete(key)\n }\n this.entries.set(key, { value, size })\n this.currentBytes += size\n this.evictUntilUnderBudget()\n }\n\n /**\n * Remove a key without affecting hit/miss stats. Used by `Collection.delete()`.\n * Returns true if the key was present.\n */\n remove(key: K): boolean {\n const existing = this.entries.get(key)\n if (!existing) return false\n this.currentBytes -= existing.size\n this.entries.delete(key)\n return true\n }\n\n /** True if the cache currently holds an entry for the given key. */\n has(key: K): boolean {\n return this.entries.has(key)\n }\n\n /**\n * Drop every entry. Stats counters survive — call `resetStats()` if you\n * want a clean slate. Used by `Collection.invalidate()` on key rotation.\n */\n clear(): void {\n this.entries.clear()\n this.currentBytes = 0\n }\n\n /** Reset hit/miss/eviction counters to zero. Does NOT touch entries. */\n resetStats(): void {\n this.hits = 0\n this.misses = 0\n this.evictions = 0\n }\n\n /** Snapshot of current cache statistics. Cheap — no copying. */\n stats(): LruStats {\n return {\n hits: this.hits,\n misses: this.misses,\n evictions: this.evictions,\n size: this.entries.size,\n bytes: this.currentBytes,\n }\n }\n\n /**\n * Iterate over all currently-cached values. Order is least-recently-used\n * first. Used by tests and devtools — production callers should use\n * `Collection.scan()` instead.\n */\n *values(): IterableIterator<V> {\n for (const entry of this.entries.values()) yield entry.value\n }\n\n /**\n * Walk the cache from the LRU end and drop entries until both budgets\n * are satisfied. Called after every `set()`. Single pass — entries are\n * never re-promoted during eviction.\n */\n private evictUntilUnderBudget(): void {\n while (this.overBudget()) {\n const oldest = this.entries.keys().next()\n if (oldest.done) return // empty cache; nothing more to evict\n const key = oldest.value\n const entry = this.entries.get(key)\n if (entry) this.currentBytes -= entry.size\n this.entries.delete(key)\n this.evictions++\n }\n }\n\n private overBudget(): boolean {\n if (this.maxRecords !== undefined && this.entries.size > this.maxRecords) return true\n if (this.maxBytes !== undefined && this.currentBytes > this.maxBytes) return true\n return false\n }\n}\n","/**\n * Cache policy helpers — parse human-friendly byte budgets into raw numbers.\n *\n * Accepted shapes (case-insensitive on suffix):\n * number — interpreted as raw bytes\n * '1024' — string of digits, raw bytes\n * '50KB' — kilobytes (×1024)\n * '50MB' — megabytes (×1024²)\n * '1GB' — gigabytes (×1024³)\n *\n * Decimals are accepted (`'1.5GB'` → 1610612736 bytes).\n *\n * Anything else throws — better to fail loud at construction time than\n * to silently treat a typo as 0 bytes (which would evict everything).\n */\n\nconst UNITS: Record<string, number> = {\n '': 1,\n 'B': 1,\n 'KB': 1024,\n 'MB': 1024 * 1024,\n 'GB': 1024 * 1024 * 1024,\n // 'TB' deliberately not supported — if you need it, you're not using NOYDB.\n}\n\n/** Parse a byte budget into a positive integer number of bytes. */\nexport function parseBytes(input: number | string): number {\n if (typeof input === 'number') {\n if (!Number.isFinite(input) || input <= 0) {\n throw new Error(`parseBytes: numeric input must be a positive finite number, got ${String(input)}`)\n }\n return Math.floor(input)\n }\n\n const trimmed = input.trim()\n if (trimmed === '') {\n throw new Error('parseBytes: empty string is not a valid byte budget')\n }\n\n // Accept either a bare number or a number followed by a unit suffix.\n // Regex: optional sign, digits with optional decimal, optional unit.\n const match = /^([0-9]+(?:\\.[0-9]+)?)\\s*([A-Za-z]*)$/.exec(trimmed)\n if (!match) {\n throw new Error(`parseBytes: invalid byte budget \"${input}\". Expected format: \"1024\", \"50KB\", \"50MB\", \"1GB\"`)\n }\n\n const value = parseFloat(match[1]!)\n const unit = (match[2] ?? '').toUpperCase()\n\n if (!(unit in UNITS)) {\n throw new Error(`parseBytes: unknown unit \"${match[2]}\" in \"${input}\". Supported: B, KB, MB, GB`)\n }\n\n const bytes = Math.floor(value * UNITS[unit]!)\n if (bytes <= 0) {\n throw new Error(`parseBytes: byte budget must be > 0, got ${bytes} from \"${input}\"`)\n }\n return bytes\n}\n\n/**\n * Estimate the in-memory byte size of a decrypted record.\n *\n * Uses `JSON.stringify().length` as a stand-in for actual heap usage.\n * It's a deliberate approximation: real V8 heap size includes pointer\n * overhead, hidden classes, and string interning that we can't measure\n * from JavaScript. The JSON length is a stable, monotonic proxy that\n * costs O(record size) per insert — fine when records are typically\n * < 1 KB and the cache eviction is the slow path anyway.\n *\n * Returns `0` (and the caller must treat it as 1 for accounting) if\n * stringification throws on circular references; this is documented\n * but in practice records always come from JSON-decoded envelopes.\n */\nexport function estimateRecordBytes(record: unknown): number {\n try {\n return JSON.stringify(record).length\n } catch {\n return 0\n }\n}\n","/**\n * Lazy-mode capability contract (#267 Track A tail — lazy-mode promoted out\n * of `routing` into its own `lazy` service). Lives on the `/with` port (the\n * one seam the kernel spine may import statically) so `Collection` can hold\n * the back-compat default without a spine→service static import.\n *\n * Lazy mode (`prefetch: false`) skips bulk hydration: reads go per-id\n * against the store and land in a bounded LRU working set. The strategy owns\n * constructing that LRU:\n *\n * - `withLazy()` (`with-store/lazy/active.ts`, subpath `@noy-db/hub/lazy`)\n * is the explicit catalog opt-in — silent, forward-stable.\n * - {@link IMPLICIT_LAZY} is the floor default reached when a collection\n * declares `prefetch: false` WITHOUT `lazyStrategy: withLazy()`. It keeps\n * the pre-#267 behavior byte-for-byte (back-compat delegation, pre-1.0)\n * but emits a one-time deprecation warn outside test env. It will become\n * a throwing stub at 1.0, letting the LRU leave the floor bundle\n * entirely once the per-record-CEK cache stops pinning `Lru` there.\n *\n * The on-demand read/write branches themselves stay kernel-resident (they\n * are `if (this.lazy)` forks on the hot get/put/scan paths); the service\n * seam owns cache construction + budget validation, which is where the\n * opt-in and the future tree-shake win live.\n * @internal\n */\nimport { Lru, parseBytes } from '../../kernel/cache/index.js'\nimport type { CacheOptions } from '../../kernel/collection.js'\n\nexport interface LazyStrategy {\n /**\n * Build the bounded LRU working-set cache for a lazy collection.\n * MUST throw when `cache` declares neither `maxRecords` nor `maxBytes` —\n * an unbounded lazy cache defeats the purpose.\n */\n createCache<V>(collection: string, cache: CacheOptions | undefined): Lru<string, V>\n}\n\n/**\n * Shared budget validation + LRU construction — the one implementation both\n * {@link IMPLICIT_LAZY} and `withLazy()` run, so the two paths cannot drift.\n * @internal\n */\nexport function buildLazyCache<V>(collection: string, cache: CacheOptions | undefined): Lru<string, V> {\n if (!cache || (cache.maxRecords === undefined && cache.maxBytes === undefined)) {\n throw new Error(\n `Collection \"${collection}\": lazy mode (prefetch: false) requires a cache option ` +\n `with maxRecords and/or maxBytes. An unbounded lazy cache defeats the purpose.`,\n )\n }\n const lruOptions: { maxRecords?: number; maxBytes?: number } = {}\n if (cache.maxRecords !== undefined) lruOptions.maxRecords = cache.maxRecords\n if (cache.maxBytes !== undefined) lruOptions.maxBytes = parseBytes(cache.maxBytes)\n return new Lru<string, V>(lruOptions)\n}\n\nlet implicitLazyWarned = false\n\n/**\n * Back-compat default (deprecated): `prefetch: false` without\n * `lazyStrategy: withLazy()`. Identical behavior to the explicit opt-in,\n * plus a one-time deprecation warn (suppressed under NODE_ENV=test,\n * mirroring the listPage fallback warn). @internal\n */\nexport const IMPLICIT_LAZY: LazyStrategy = {\n createCache<V>(collection: string, cache: CacheOptions | undefined): Lru<string, V> {\n if (!implicitLazyWarned) {\n implicitLazyWarned = true\n if (!(typeof process !== 'undefined' && process.env['NODE_ENV'] === 'test')) {\n console.warn(\n `[noy-db] Collection \"${collection}\": lazy mode (prefetch: false) without the lazy ` +\n `service is deprecated — pass \\`lazyStrategy: withLazy()\\` (from \"@noy-db/hub/lazy\") ` +\n `to createNoydb(). The implicit path will be removed at 1.0.`,\n )\n }\n }\n return buildLazyCache<V>(collection, cache)\n },\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
openEnvelopeJson
|
|
3
|
+
} from "./chunk-OVO2RZAE.js";
|
|
4
|
+
import {
|
|
5
|
+
NOYDB_FORMAT_VERSION
|
|
6
|
+
} from "./chunk-LN22XH4B.js";
|
|
1
7
|
import {
|
|
2
8
|
encrypt,
|
|
3
9
|
hmacSha256Hex,
|
|
4
|
-
openEnvelopeJson,
|
|
5
10
|
sha256Hex
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import {
|
|
8
|
-
NOYDB_FORMAT_VERSION
|
|
9
|
-
} from "./chunk-5TDJ56JE.js";
|
|
11
|
+
} from "./chunk-TLJOJBZD.js";
|
|
10
12
|
|
|
11
13
|
// src/with-audit/forget/strategy.ts
|
|
12
14
|
var NO_FORGET = { subjects: {} };
|
|
@@ -145,4 +147,4 @@ export {
|
|
|
145
147
|
coerceSubjectId,
|
|
146
148
|
readDottedPath
|
|
147
149
|
};
|
|
148
|
-
//# sourceMappingURL=chunk-
|
|
150
|
+
//# sourceMappingURL=chunk-O2DB4C3M.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/with-audit/forget/strategy.ts","../src/with-audit/forget/subject-index.ts"],"sourcesContent":["/**\n * `withForgetCascade` — declaration surface for GDPR right-to-erasure via\n * per-record CEK crypto-shred.\n *\n * This file holds only the *declaration* shape and the disabled sentinel.\n * The actual erasure machinery lives in:\n * - `subject-index.ts` — the encrypted `_subject_index` reserved collection\n * - `vault.ts` `forget()` — the per-record tombstone + ledger flow\n * - `collection.ts` `_writeTombstone` — the envelope rewrite\n *\n * A `ForgetStrategy` declares which collections carry erasable subject data\n * and the (dotted-path) field on each record that names the data subject.\n * Declaring a collection here ALSO forces `perRecordKeys: true` for it (a\n * shred can only erase a record whose body is keyed off a per-record CEK),\n * so adopters opt into the CEK foundation transitively.\n *\n * @module\n */\nimport type { LedgerEntry } from '../../with-commit/history/ledger/entry.js'\n\n/**\n * User-supplied declaration passed to {@link withForgetCascade}. Maps a\n * collection name to the record field (dotted path supported, e.g.\n * `'billing.buyerId'`) that identifies the data subject for erasure.\n *\n * ```ts\n * withForgetCascade({ subjects: { invoices: 'buyerId', contacts: 'id' } })\n * ```\n */\nexport interface SubjectDeclaration {\n readonly subjects: Record<string, string>\n}\n\n/**\n * Resolved forget strategy threaded through Noydb → every Vault. Carries\n * the same `subjects` map the user declared. `NO_FORGET` (empty map) is the\n * off-by-default sentinel; `vault.forget()` throws\n * `ForgetStrategyNotConfiguredError` when the map is empty.\n */\nexport interface ForgetStrategy {\n /** Collection → subject-field (dotted path). Empty under `NO_FORGET`. */\n readonly subjects: Readonly<Record<string, string>>\n}\n\n/**\n * Disabled sentinel — no collections declare a subject field. `vault.forget()`\n * refuses with `ForgetStrategyNotConfiguredError`; no write hooks register; no\n * collection is forced into `perRecordKeys`. Non-adopters pay nothing.\n */\nexport const NO_FORGET: ForgetStrategy = { subjects: {} }\n\n// The `withForgetCascade` factory lives in `active.ts` (canonical\n// strategy.ts/active.ts/index.ts split); this file holds only the\n// declaration shape + disabled sentinel.\n\n/**\n * The outcome of a `vault.forget(subjectId)` call.\n *\n * `unmigratedRecords` lists `collection:id` pairs that were tombstoned but\n * whose body had NOT been migrated to a per-record CEK at shred time (legacy\n * body still under the shared collection DEK). Those records are tombstoned\n * (live envelope + history stripped) but their pre-shred ciphertext, if it\n * leaked into a backup before migration, remains decryptable under the\n * collection DEK — so erasure-completeness is NOT guaranteed for them. Run\n * the per-record-CEK migration pass, then re-forget, to close the gap.\n *\n * Blob attachments: a shredded record's **erasable** blobs (on a\n * `perRecordKeys` collection) are crypto-shredded inline — `blobsShredded`\n * counts those taken to refCount 0 (BlobObject deleted → chunks permanently\n * undecryptable), `blobsRetainedShared` counts those still referenced by\n * another record (shared content legitimately persists for its other owner).\n * `blobResidueCollections` now lists only collections with blobs that could\n * NOT be crypto-shredded: **legacy** blobs (no per-blob `_cek`, chunks under\n * the shared `_blob` DEK — migrate them), or a session without the blob\n * service loaded. An all-erasable subject yields an empty residue list.\n */\nexport interface ForgetResult {\n /** The subject id passed to `forget()`. Echoed for caller convenience. */\n readonly subject: string\n /** Count of live records rewritten to a tombstone. */\n readonly recordsShredded: number\n /** Count of `_history` envelopes tombstoned across all shredded records. */\n readonly historyVersionsShredded: number\n /** Distinct collections that had at least one record shredded. */\n readonly collections: readonly string[]\n /** `collection:id` pairs shredded while still un-migrated (see type docs). */\n readonly unmigratedRecords: readonly string[]\n /** Count of erasable blobs crypto-shredded (refCount → 0, BlobObject deleted). */\n readonly blobsShredded: number\n /** Count of erasable blobs retained because still referenced elsewhere (shared). */\n readonly blobsRetainedShared: number\n /** Collections with blobs that could NOT be crypto-shredded — legacy (no `_cek`) or blobs disabled (see type docs). */\n readonly blobResidueCollections: readonly string[]\n /**\n * Count of persisted `_idx/<field>/<recordId>` index side-cars hard-deleted\n * across the shredded records. These live under the retained\n * collection DEK, so crypto-shred alone would leave the indexed field VALUES\n * readable — `forget()` must delete them.\n */\n readonly indexPostingsPurged: number\n /**\n * `collection:id:field` entries whose persisted `_idx` side-car could NOT be\n * deleted — index residue that still leaks the indexed value under the\n * retained collection DEK. Non-empty means erasure is INCOMPLETE: retry, or\n * purge the side-car out of band.\n */\n readonly indexResidue: readonly string[]\n /**\n * Count of `_sealed[field]` slots dropped from the live store across the\n * shredded records. For slots written under `sensitive` +\n * `perRecordKeys` (the current path), the key derives off the per-record CEK,\n * so tombstoning the record — which drops `_cek` and `_sealed` — also\n * crypto-shreds the value. A legacy slot (written before per-record CEKs) keys\n * off the collection DEK instead, so dropping it removes it from the live store but a\n * pre-forget backup remains recoverable by a DEK holder (same caveat `_data`\n * carries); migrate by re-`put`ting before forgetting for full crypto-shred.\n */\n readonly sealedFieldsShredded: number\n /** Count of `_sealed_cek` host-delivery envelopes deleted (#H-1). A record sealed to an\n * at-* host via sealRecordToHost persists its raw CEK there; forget() must destroy them. */\n readonly sealedCekEnvelopesPurged: number\n /** `collection:id` whose `_sealed_cek` purge failed (residue — the host-recoverable CEK may survive). */\n readonly sealedCekResidue: readonly string[]\n /** `collection:id:field` sealed slots that were DEK-derived (legacy, written before per-record CEKs) and thus NOT crypto-shredded\n * by dropping `_cek` — the collection DEK is retained, so synced/backup copies stay decryptable (#M-1). */\n readonly sealedResidue: readonly string[]\n /** The single `op:'forget'` ledger entry appended for this erasure. */\n readonly ledgerEntry: LedgerEntry\n}\n","/**\n * The encrypted subject index.\n *\n * GDPR crypto-shred needs to answer \"which records belong to data subject\n * X?\" portably (the index must travel with the vault/bundle) WITHOUT leaking\n * subject-equivalence to the store. The rejected alternative — an unencrypted\n * subject tag in envelope metadata — would let anyone with store access see\n * which records share a subject. Instead we keep a reserved `_subject_index`\n * collection, encrypted under its OWN DEK (`getDEK('_subject_index')`):\n *\n * - record id = `HMAC-SHA256(indexDEK, subjectId)` (M-2). A bare\n * `sha256Hex(subjectId)` would be offline-computable: an attacker with\n * store access and a candidate list (emails / customer ids are low-entropy)\n * could dictionary the hash to confirm \"subject X is present here.\" Keying\n * the id with the vault-only index DEK removes that capability — without the\n * DEK the id cannot be derived. (Legacy entries written before M-2 used the\n * bare sha256 id; the read/remove paths dual-look-up both forms.)\n * - record body = AES-GCM(JSON `{ r: [{ collection, id }], p }`) under the\n * index DEK, where `p` pads the plaintext to a bucketed length so the\n * ciphertext `_data` length does not leak the approximate record count.\n * Legacy bodies were a bare `[{ collection, id }]` array; reads accept both.\n *\n * ## Concurrency (RISK #3 — known v1 limitation)\n *\n * `addSubjectRef` / `removeSubjectRef` are read-modify-write with no CAS. The\n * design assumes a SINGLE WRITER (the noy-db single-process write model). Two\n * concurrent writers racing on the SAME subject can lose an entry (last-write\n * wins on the ref list). This is documented, not fixed in v1:\n * `rebuildSubjectIndex` performs a full scan to recover a correct index from\n * the canonical records, so a lost ref is recoverable. A CAS-backed index is\n * deferred to a later slice.\n *\n * @module\n */\nimport { encrypt, openEnvelopeJson, hmacSha256Hex, sha256Hex, type EnclaveKey } from '../../kernel/enclave/index.js'\nimport type { NoydbStore, EncryptedEnvelope } from '../../kernel/types.js'\nimport { NOYDB_FORMAT_VERSION } from '../../kernel/types.js'\n\n/** Reserved collection holding the encrypted subject → records index. */\nexport const SUBJECT_INDEX_COLLECTION = '_subject_index'\n\n/**\n * Bucket (bytes) the encrypted ref-list plaintext is padded up to, so the\n * ciphertext `_data` length leaks only `count` rounded up to a bucket — not the\n * exact record count. 256 keeps small subjects (the common case) indistinguishable.\n */\nconst REF_LIST_BUCKET = 256\n\n/** A single record reference held in a subject's index entry. */\nexport interface SubjectRef {\n readonly collection: string\n readonly id: string\n}\n\ntype GetDEK = (collectionName: string) => Promise<EnclaveKey>\n\n/** SHA-256 hex of a UTF-8 string. The LEGACY (pre-M-2) subject-index key. */\nasync function sha256HexString(input: string): Promise<string> {\n return sha256Hex(new TextEncoder().encode(input))\n}\n\n/**\n * The subject-index record id(s) to consult for a subject, most-current first.\n *\n * - Encrypted vault: the PRIMARY id is `HMAC-SHA256(indexDEK, subjectId)` (M-2)\n * — not offline-computable. The LEGACY `sha256Hex(subjectId)` id is also\n * returned so reads/removes still find entries written before M-2 (dual-lookup).\n * - Plaintext/debug vault: no DEK to key with, so the only id is the legacy\n * sha256 form (unchanged behaviour — plaintext mode is not zero-knowledge anyway).\n */\nasync function subjectKeys(getDEK: GetDEK, encrypted: boolean, subjectId: string): Promise<string[]> {\n const legacy = await sha256HexString(subjectId)\n if (!encrypted) return [legacy]\n const dek = await getDEK(SUBJECT_INDEX_COLLECTION)\n const keyed = await hmacSha256Hex(dek, new TextEncoder().encode(subjectId))\n return keyed === legacy ? [keyed] : [keyed, legacy]\n}\n\n/** The id new writes land under (keyed when encrypted, else legacy sha256). */\nasync function primarySubjectKey(getDEK: GetDEK, encrypted: boolean, subjectId: string): Promise<string> {\n return (await subjectKeys(getDEK, encrypted, subjectId))[0]!\n}\n\n/** Parse a stored ref-list body: new padded `{ r, p }` wrapper OR legacy bare array. */\nfunction parseRefs(json: string): SubjectRef[] {\n const parsed = JSON.parse(json) as SubjectRef[] | { r: SubjectRef[] }\n return Array.isArray(parsed) ? parsed : parsed.r\n}\n\n/** Serialize + pad the ref list to a bucket boundary (encrypted vaults only). */\nfunction serializeRefs(refs: SubjectRef[]): string {\n const base = JSON.stringify({ r: refs, p: '' })\n const pad = Math.ceil(base.length / REF_LIST_BUCKET) * REF_LIST_BUCKET - base.length\n return JSON.stringify({ r: refs, p: ' '.repeat(pad) })\n}\n\n/** Read + decrypt the ref list at a SINGLE index key. Returns `[]` when absent. */\nasync function readRefs(\n adapter: NoydbStore,\n vault: string,\n getDEK: GetDEK,\n encrypted: boolean,\n key: string,\n): Promise<SubjectRef[]> {\n const env = await adapter.get(vault, SUBJECT_INDEX_COLLECTION, key)\n if (!env || !env._data) return []\n if (!encrypted) return parseRefs(env._data)\n const dek = await getDEK(SUBJECT_INDEX_COLLECTION)\n const json = await openEnvelopeJson(env, dek)\n return parseRefs(json)\n}\n\n/** Encrypt + write a ref list for a subject under its derived key. */\nasync function writeRefs(\n adapter: NoydbStore,\n vault: string,\n getDEK: GetDEK,\n encrypted: boolean,\n key: string,\n refs: SubjectRef[],\n): Promise<void> {\n let env: EncryptedEnvelope\n if (!encrypted) {\n // Plaintext/debug vault: keep the legacy bare-array form (no padding needed).\n env = { _noydb: NOYDB_FORMAT_VERSION, _v: 1, _ts: new Date().toISOString(), _iv: '', _data: JSON.stringify(refs) }\n } else {\n const dek = await getDEK(SUBJECT_INDEX_COLLECTION)\n const { iv, data } = await encrypt(serializeRefs(refs), dek)\n env = { _noydb: NOYDB_FORMAT_VERSION, _v: 1, _ts: new Date().toISOString(), _iv: iv, _data: data }\n }\n await adapter.put(vault, SUBJECT_INDEX_COLLECTION, key, env)\n}\n\n/**\n * Add a `{ collection, id }` ref to a subject's index entry (idempotent —\n * a duplicate ref is not appended). Read-modify-write; see the concurrency\n * note in the module docstring.\n */\nexport async function addSubjectRef(\n adapter: NoydbStore,\n vault: string,\n getDEK: GetDEK,\n encrypted: boolean,\n subjectId: string,\n ref: SubjectRef,\n): Promise<void> {\n const key = await primarySubjectKey(getDEK, encrypted, subjectId)\n const refs = await readRefs(adapter, vault, getDEK, encrypted, key)\n if (refs.some((r) => r.collection === ref.collection && r.id === ref.id)) return\n refs.push(ref)\n await writeRefs(adapter, vault, getDEK, encrypted, key, refs)\n}\n\n/**\n * Remove a `{ collection, id }` ref from a subject's index entry. When the\n * last ref is removed the (now empty) entry is deleted so the store holds no\n * residual key for an erased subject.\n */\nexport async function removeSubjectRef(\n adapter: NoydbStore,\n vault: string,\n getDEK: GetDEK,\n encrypted: boolean,\n subjectId: string,\n ref: SubjectRef,\n): Promise<void> {\n // Dual-lookup: drop the ref from BOTH the keyed (M-2) and the legacy sha256\n // entry, so a pre-M-2 subject is still fully erased.\n for (const key of await subjectKeys(getDEK, encrypted, subjectId)) {\n const refs = await readRefs(adapter, vault, getDEK, encrypted, key)\n const next = refs.filter((r) => !(r.collection === ref.collection && r.id === ref.id))\n if (next.length === refs.length) continue\n if (next.length === 0) {\n await adapter.delete(vault, SUBJECT_INDEX_COLLECTION, key)\n } else {\n await writeRefs(adapter, vault, getDEK, encrypted, key, next)\n }\n }\n}\n\n/**\n * Look up every record ref for a subject. Returns `[]` when none exist. Unions\n * the keyed (M-2) and legacy sha256 entries (dual-lookup), deduplicated, so a\n * subject indexed before M-2 is still fully found.\n */\nexport async function lookupSubject(\n adapter: NoydbStore,\n vault: string,\n getDEK: GetDEK,\n encrypted: boolean,\n subjectId: string,\n): Promise<SubjectRef[]> {\n const keys = await subjectKeys(getDEK, encrypted, subjectId)\n const seen = new Set<string>()\n const out: SubjectRef[] = []\n for (const key of keys) {\n for (const ref of await readRefs(adapter, vault, getDEK, encrypted, key)) {\n const dedup = `${ref.collection}\\u0000${ref.id}`\n if (seen.has(dedup)) continue\n seen.add(dedup)\n out.push(ref)\n }\n }\n return out\n}\n\n/**\n * Rebuild the entire subject index from the canonical records (the recovery\n * path for the documented read-modify-write race). Scans each declared\n * collection, reads `record[subjectField]` (dotted path), and rewrites the\n * index from scratch. Tombstoned (already-shredded) records contribute no\n * ref — their body is gone, so they cannot be re-indexed.\n *\n * `decodeRecord` decrypts an envelope to a plain object (or returns null for\n * a tombstone / unreadable record); supplied by the caller so this module\n * stays free of Collection internals.\n */\nexport async function rebuildSubjectIndex(\n adapter: NoydbStore,\n vault: string,\n getDEK: GetDEK,\n encrypted: boolean,\n subjects: Readonly<Record<string, string>>,\n decodeRecord: (collection: string, id: string, env: EncryptedEnvelope) => Promise<Record<string, unknown> | null>,\n): Promise<number> {\n // Drop every existing index entry first so removed refs don't linger.\n const existing = await adapter.list(vault, SUBJECT_INDEX_COLLECTION)\n for (const k of existing) {\n await adapter.delete(vault, SUBJECT_INDEX_COLLECTION, k)\n }\n\n // subjectId → refs, accumulated across all declared collections.\n const bySubject = new Map<string, SubjectRef[]>()\n for (const [collection, field] of Object.entries(subjects)) {\n const ids = await adapter.list(vault, collection)\n for (const id of ids) {\n if (id.startsWith('_')) continue\n const env = await adapter.get(vault, collection, id)\n if (!env || !env._data) continue // missing or tombstone\n const record = await decodeRecord(collection, id, env)\n if (record === null) continue\n const subjectValue = readDottedPath(record, field)\n if (subjectValue === undefined || subjectValue === null) continue\n const subjectId = coerceSubjectId(subjectValue)\n const list = bySubject.get(subjectId) ?? []\n list.push({ collection, id })\n bySubject.set(subjectId, list)\n }\n }\n\n let entries = 0\n for (const [subjectId, refs] of bySubject) {\n const key = await primarySubjectKey(getDEK, encrypted, subjectId)\n await writeRefs(adapter, vault, getDEK, encrypted, key, refs)\n entries++\n }\n return entries\n}\n\n/**\n * Coerce a read subject-field value to a stable string id. Primitives use\n * their natural string form; objects/arrays are JSON-stringified so structural\n * subjects still get a deterministic key (avoids the `[object Object]` trap).\n */\nexport function coerceSubjectId(value: unknown): string {\n if (typeof value === 'string') return value\n if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {\n return String(value)\n }\n return JSON.stringify(value)\n}\n\n/** Read a (possibly dotted) field path from a plain record. */\nexport function readDottedPath(record: Record<string, unknown>, field: string): unknown {\n if (!field.includes('.')) return record[field]\n let cursor: unknown = record\n for (const segment of field.split('.')) {\n if (cursor === null || cursor === undefined) return undefined\n cursor = (cursor as Record<string, unknown>)[segment]\n }\n return cursor\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/with-audit/forget/strategy.ts","../src/with-audit/forget/subject-index.ts"],"sourcesContent":["/**\n * `withForgetCascade` — declaration surface for GDPR right-to-erasure via\n * per-record CEK crypto-shred.\n *\n * This file holds only the *declaration* shape and the disabled sentinel.\n * The actual erasure machinery lives in:\n * - `subject-index.ts` — the encrypted `_subject_index` reserved collection\n * - `vault.ts` `forget()` — the per-record tombstone + ledger flow\n * - `collection.ts` `_writeTombstone` — the envelope rewrite\n *\n * A `ForgetStrategy` declares which collections carry erasable subject data\n * and the (dotted-path) field on each record that names the data subject.\n * Declaring a collection here ALSO forces `perRecordKeys: true` for it (a\n * shred can only erase a record whose body is keyed off a per-record CEK),\n * so adopters opt into the CEK foundation transitively.\n *\n * @module\n */\nimport type { LedgerEntry } from '../../with-commit/history/ledger/entry.js'\n\n/**\n * User-supplied declaration passed to {@link withForgetCascade}. Maps a\n * collection name to the record field (dotted path supported, e.g.\n * `'billing.buyerId'`) that identifies the data subject for erasure.\n *\n * ```ts\n * withForgetCascade({ subjects: { invoices: 'buyerId', contacts: 'id' } })\n * ```\n */\nexport interface SubjectDeclaration {\n readonly subjects: Record<string, string>\n}\n\n/**\n * Resolved forget strategy threaded through Noydb → every Vault. Carries\n * the same `subjects` map the user declared. `NO_FORGET` (empty map) is the\n * off-by-default sentinel; `vault.forget()` throws\n * `ForgetStrategyNotConfiguredError` when the map is empty.\n */\nexport interface ForgetStrategy {\n /** Collection → subject-field (dotted path). Empty under `NO_FORGET`. */\n readonly subjects: Readonly<Record<string, string>>\n}\n\n/**\n * Disabled sentinel — no collections declare a subject field. `vault.forget()`\n * refuses with `ForgetStrategyNotConfiguredError`; no write hooks register; no\n * collection is forced into `perRecordKeys`. Non-adopters pay nothing.\n */\nexport const NO_FORGET: ForgetStrategy = { subjects: {} }\n\n// The `withForgetCascade` factory lives in `active.ts` (canonical\n// strategy.ts/active.ts/index.ts split); this file holds only the\n// declaration shape + disabled sentinel.\n\n/**\n * The outcome of a `vault.forget(subjectId)` call.\n *\n * `unmigratedRecords` lists `collection:id` pairs that were tombstoned but\n * whose body had NOT been migrated to a per-record CEK at shred time (legacy\n * body still under the shared collection DEK). Those records are tombstoned\n * (live envelope + history stripped) but their pre-shred ciphertext, if it\n * leaked into a backup before migration, remains decryptable under the\n * collection DEK — so erasure-completeness is NOT guaranteed for them. Run\n * the per-record-CEK migration pass, then re-forget, to close the gap.\n *\n * Blob attachments: a shredded record's **erasable** blobs (on a\n * `perRecordKeys` collection) are crypto-shredded inline — `blobsShredded`\n * counts those taken to refCount 0 (BlobObject deleted → chunks permanently\n * undecryptable), `blobsRetainedShared` counts those still referenced by\n * another record (shared content legitimately persists for its other owner).\n * `blobResidueCollections` now lists only collections with blobs that could\n * NOT be crypto-shredded: **legacy** blobs (no per-blob `_cek`, chunks under\n * the shared `_blob` DEK — migrate them), or a session without the blob\n * service loaded. An all-erasable subject yields an empty residue list.\n */\nexport interface ForgetResult {\n /** The subject id passed to `forget()`. Echoed for caller convenience. */\n readonly subject: string\n /** Count of live records rewritten to a tombstone. */\n readonly recordsShredded: number\n /** Count of `_history` envelopes tombstoned across all shredded records. */\n readonly historyVersionsShredded: number\n /** Distinct collections that had at least one record shredded. */\n readonly collections: readonly string[]\n /** `collection:id` pairs shredded while still un-migrated (see type docs). */\n readonly unmigratedRecords: readonly string[]\n /** Count of erasable blobs crypto-shredded (refCount → 0, BlobObject deleted). */\n readonly blobsShredded: number\n /** Count of erasable blobs retained because still referenced elsewhere (shared). */\n readonly blobsRetainedShared: number\n /** Collections with blobs that could NOT be crypto-shredded — legacy (no `_cek`) or blobs disabled (see type docs). */\n readonly blobResidueCollections: readonly string[]\n /**\n * Count of persisted `_idx/<field>/<recordId>` index side-cars hard-deleted\n * across the shredded records. These live under the retained\n * collection DEK, so crypto-shred alone would leave the indexed field VALUES\n * readable — `forget()` must delete them.\n */\n readonly indexPostingsPurged: number\n /**\n * `collection:id:field` entries whose persisted `_idx` side-car could NOT be\n * deleted — index residue that still leaks the indexed value under the\n * retained collection DEK. Non-empty means erasure is INCOMPLETE: retry, or\n * purge the side-car out of band.\n */\n readonly indexResidue: readonly string[]\n /**\n * Count of `_sealed[field]` slots dropped from the live store across the\n * shredded records. For slots written under `sensitive` +\n * `perRecordKeys` (the current path), the key derives off the per-record CEK,\n * so tombstoning the record — which drops `_cek` and `_sealed` — also\n * crypto-shreds the value. A legacy slot (written before per-record CEKs) keys\n * off the collection DEK instead, so dropping it removes it from the live store but a\n * pre-forget backup remains recoverable by a DEK holder (same caveat `_data`\n * carries); migrate by re-`put`ting before forgetting for full crypto-shred.\n */\n readonly sealedFieldsShredded: number\n /** Count of `_sealed_cek` host-delivery envelopes deleted (#H-1). A record sealed to an\n * at-* host via sealRecordToHost persists its raw CEK there; forget() must destroy them. */\n readonly sealedCekEnvelopesPurged: number\n /** `collection:id` whose `_sealed_cek` purge failed (residue — the host-recoverable CEK may survive). */\n readonly sealedCekResidue: readonly string[]\n /** `collection:id:field` sealed slots that were DEK-derived (legacy, written before per-record CEKs) and thus NOT crypto-shredded\n * by dropping `_cek` — the collection DEK is retained, so synced/backup copies stay decryptable (#M-1). */\n readonly sealedResidue: readonly string[]\n /** The single `op:'forget'` ledger entry appended for this erasure. */\n readonly ledgerEntry: LedgerEntry\n}\n","/**\n * The encrypted subject index.\n *\n * GDPR crypto-shred needs to answer \"which records belong to data subject\n * X?\" portably (the index must travel with the vault/bundle) WITHOUT leaking\n * subject-equivalence to the store. The rejected alternative — an unencrypted\n * subject tag in envelope metadata — would let anyone with store access see\n * which records share a subject. Instead we keep a reserved `_subject_index`\n * collection, encrypted under its OWN DEK (`getDEK('_subject_index')`):\n *\n * - record id = `HMAC-SHA256(indexDEK, subjectId)` (M-2). A bare\n * `sha256Hex(subjectId)` would be offline-computable: an attacker with\n * store access and a candidate list (emails / customer ids are low-entropy)\n * could dictionary the hash to confirm \"subject X is present here.\" Keying\n * the id with the vault-only index DEK removes that capability — without the\n * DEK the id cannot be derived. (Legacy entries written before M-2 used the\n * bare sha256 id; the read/remove paths dual-look-up both forms.)\n * - record body = AES-GCM(JSON `{ r: [{ collection, id }], p }`) under the\n * index DEK, where `p` pads the plaintext to a bucketed length so the\n * ciphertext `_data` length does not leak the approximate record count.\n * Legacy bodies were a bare `[{ collection, id }]` array; reads accept both.\n *\n * ## Concurrency (RISK #3 — known v1 limitation)\n *\n * `addSubjectRef` / `removeSubjectRef` are read-modify-write with no CAS. The\n * design assumes a SINGLE WRITER (the noy-db single-process write model). Two\n * concurrent writers racing on the SAME subject can lose an entry (last-write\n * wins on the ref list). This is documented, not fixed in v1:\n * `rebuildSubjectIndex` performs a full scan to recover a correct index from\n * the canonical records, so a lost ref is recoverable. A CAS-backed index is\n * deferred to a later slice.\n *\n * @module\n */\nimport { encrypt, openEnvelopeJson, hmacSha256Hex, sha256Hex, type EnclaveKey } from '../../kernel/enclave/index.js'\nimport type { NoydbStore, EncryptedEnvelope } from '../../kernel/types.js'\nimport { NOYDB_FORMAT_VERSION } from '../../kernel/types.js'\n\n/** Reserved collection holding the encrypted subject → records index. */\nexport const SUBJECT_INDEX_COLLECTION = '_subject_index'\n\n/**\n * Bucket (bytes) the encrypted ref-list plaintext is padded up to, so the\n * ciphertext `_data` length leaks only `count` rounded up to a bucket — not the\n * exact record count. 256 keeps small subjects (the common case) indistinguishable.\n */\nconst REF_LIST_BUCKET = 256\n\n/** A single record reference held in a subject's index entry. */\nexport interface SubjectRef {\n readonly collection: string\n readonly id: string\n}\n\ntype GetDEK = (collectionName: string) => Promise<EnclaveKey>\n\n/** SHA-256 hex of a UTF-8 string. The LEGACY (pre-M-2) subject-index key. */\nasync function sha256HexString(input: string): Promise<string> {\n return sha256Hex(new TextEncoder().encode(input))\n}\n\n/**\n * The subject-index record id(s) to consult for a subject, most-current first.\n *\n * - Encrypted vault: the PRIMARY id is `HMAC-SHA256(indexDEK, subjectId)` (M-2)\n * — not offline-computable. The LEGACY `sha256Hex(subjectId)` id is also\n * returned so reads/removes still find entries written before M-2 (dual-lookup).\n * - Plaintext/debug vault: no DEK to key with, so the only id is the legacy\n * sha256 form (unchanged behaviour — plaintext mode is not zero-knowledge anyway).\n */\nasync function subjectKeys(getDEK: GetDEK, encrypted: boolean, subjectId: string): Promise<string[]> {\n const legacy = await sha256HexString(subjectId)\n if (!encrypted) return [legacy]\n const dek = await getDEK(SUBJECT_INDEX_COLLECTION)\n const keyed = await hmacSha256Hex(dek, new TextEncoder().encode(subjectId))\n return keyed === legacy ? [keyed] : [keyed, legacy]\n}\n\n/** The id new writes land under (keyed when encrypted, else legacy sha256). */\nasync function primarySubjectKey(getDEK: GetDEK, encrypted: boolean, subjectId: string): Promise<string> {\n return (await subjectKeys(getDEK, encrypted, subjectId))[0]!\n}\n\n/** Parse a stored ref-list body: new padded `{ r, p }` wrapper OR legacy bare array. */\nfunction parseRefs(json: string): SubjectRef[] {\n const parsed = JSON.parse(json) as SubjectRef[] | { r: SubjectRef[] }\n return Array.isArray(parsed) ? parsed : parsed.r\n}\n\n/** Serialize + pad the ref list to a bucket boundary (encrypted vaults only). */\nfunction serializeRefs(refs: SubjectRef[]): string {\n const base = JSON.stringify({ r: refs, p: '' })\n const pad = Math.ceil(base.length / REF_LIST_BUCKET) * REF_LIST_BUCKET - base.length\n return JSON.stringify({ r: refs, p: ' '.repeat(pad) })\n}\n\n/** Read + decrypt the ref list at a SINGLE index key. Returns `[]` when absent. */\nasync function readRefs(\n adapter: NoydbStore,\n vault: string,\n getDEK: GetDEK,\n encrypted: boolean,\n key: string,\n): Promise<SubjectRef[]> {\n const env = await adapter.get(vault, SUBJECT_INDEX_COLLECTION, key)\n if (!env || !env._data) return []\n if (!encrypted) return parseRefs(env._data)\n const dek = await getDEK(SUBJECT_INDEX_COLLECTION)\n const json = await openEnvelopeJson(env, dek)\n return parseRefs(json)\n}\n\n/** Encrypt + write a ref list for a subject under its derived key. */\nasync function writeRefs(\n adapter: NoydbStore,\n vault: string,\n getDEK: GetDEK,\n encrypted: boolean,\n key: string,\n refs: SubjectRef[],\n): Promise<void> {\n let env: EncryptedEnvelope\n if (!encrypted) {\n // Plaintext/debug vault: keep the legacy bare-array form (no padding needed).\n env = { _noydb: NOYDB_FORMAT_VERSION, _v: 1, _ts: new Date().toISOString(), _iv: '', _data: JSON.stringify(refs) }\n } else {\n const dek = await getDEK(SUBJECT_INDEX_COLLECTION)\n const { iv, data } = await encrypt(serializeRefs(refs), dek)\n env = { _noydb: NOYDB_FORMAT_VERSION, _v: 1, _ts: new Date().toISOString(), _iv: iv, _data: data }\n }\n await adapter.put(vault, SUBJECT_INDEX_COLLECTION, key, env)\n}\n\n/**\n * Add a `{ collection, id }` ref to a subject's index entry (idempotent —\n * a duplicate ref is not appended). Read-modify-write; see the concurrency\n * note in the module docstring.\n */\nexport async function addSubjectRef(\n adapter: NoydbStore,\n vault: string,\n getDEK: GetDEK,\n encrypted: boolean,\n subjectId: string,\n ref: SubjectRef,\n): Promise<void> {\n const key = await primarySubjectKey(getDEK, encrypted, subjectId)\n const refs = await readRefs(adapter, vault, getDEK, encrypted, key)\n if (refs.some((r) => r.collection === ref.collection && r.id === ref.id)) return\n refs.push(ref)\n await writeRefs(adapter, vault, getDEK, encrypted, key, refs)\n}\n\n/**\n * Remove a `{ collection, id }` ref from a subject's index entry. When the\n * last ref is removed the (now empty) entry is deleted so the store holds no\n * residual key for an erased subject.\n */\nexport async function removeSubjectRef(\n adapter: NoydbStore,\n vault: string,\n getDEK: GetDEK,\n encrypted: boolean,\n subjectId: string,\n ref: SubjectRef,\n): Promise<void> {\n // Dual-lookup: drop the ref from BOTH the keyed (M-2) and the legacy sha256\n // entry, so a pre-M-2 subject is still fully erased.\n for (const key of await subjectKeys(getDEK, encrypted, subjectId)) {\n const refs = await readRefs(adapter, vault, getDEK, encrypted, key)\n const next = refs.filter((r) => !(r.collection === ref.collection && r.id === ref.id))\n if (next.length === refs.length) continue\n if (next.length === 0) {\n await adapter.delete(vault, SUBJECT_INDEX_COLLECTION, key)\n } else {\n await writeRefs(adapter, vault, getDEK, encrypted, key, next)\n }\n }\n}\n\n/**\n * Look up every record ref for a subject. Returns `[]` when none exist. Unions\n * the keyed (M-2) and legacy sha256 entries (dual-lookup), deduplicated, so a\n * subject indexed before M-2 is still fully found.\n */\nexport async function lookupSubject(\n adapter: NoydbStore,\n vault: string,\n getDEK: GetDEK,\n encrypted: boolean,\n subjectId: string,\n): Promise<SubjectRef[]> {\n const keys = await subjectKeys(getDEK, encrypted, subjectId)\n const seen = new Set<string>()\n const out: SubjectRef[] = []\n for (const key of keys) {\n for (const ref of await readRefs(adapter, vault, getDEK, encrypted, key)) {\n const dedup = `${ref.collection}\\u0000${ref.id}`\n if (seen.has(dedup)) continue\n seen.add(dedup)\n out.push(ref)\n }\n }\n return out\n}\n\n/**\n * Rebuild the entire subject index from the canonical records (the recovery\n * path for the documented read-modify-write race). Scans each declared\n * collection, reads `record[subjectField]` (dotted path), and rewrites the\n * index from scratch. Tombstoned (already-shredded) records contribute no\n * ref — their body is gone, so they cannot be re-indexed.\n *\n * `decodeRecord` decrypts an envelope to a plain object (or returns null for\n * a tombstone / unreadable record); supplied by the caller so this module\n * stays free of Collection internals.\n */\nexport async function rebuildSubjectIndex(\n adapter: NoydbStore,\n vault: string,\n getDEK: GetDEK,\n encrypted: boolean,\n subjects: Readonly<Record<string, string>>,\n decodeRecord: (collection: string, id: string, env: EncryptedEnvelope) => Promise<Record<string, unknown> | null>,\n): Promise<number> {\n // Drop every existing index entry first so removed refs don't linger.\n const existing = await adapter.list(vault, SUBJECT_INDEX_COLLECTION)\n for (const k of existing) {\n await adapter.delete(vault, SUBJECT_INDEX_COLLECTION, k)\n }\n\n // subjectId → refs, accumulated across all declared collections.\n const bySubject = new Map<string, SubjectRef[]>()\n for (const [collection, field] of Object.entries(subjects)) {\n const ids = await adapter.list(vault, collection)\n for (const id of ids) {\n if (id.startsWith('_')) continue\n const env = await adapter.get(vault, collection, id)\n if (!env || !env._data) continue // missing or tombstone\n const record = await decodeRecord(collection, id, env)\n if (record === null) continue\n const subjectValue = readDottedPath(record, field)\n if (subjectValue === undefined || subjectValue === null) continue\n const subjectId = coerceSubjectId(subjectValue)\n const list = bySubject.get(subjectId) ?? []\n list.push({ collection, id })\n bySubject.set(subjectId, list)\n }\n }\n\n let entries = 0\n for (const [subjectId, refs] of bySubject) {\n const key = await primarySubjectKey(getDEK, encrypted, subjectId)\n await writeRefs(adapter, vault, getDEK, encrypted, key, refs)\n entries++\n }\n return entries\n}\n\n/**\n * Coerce a read subject-field value to a stable string id. Primitives use\n * their natural string form; objects/arrays are JSON-stringified so structural\n * subjects still get a deterministic key (avoids the `[object Object]` trap).\n */\nexport function coerceSubjectId(value: unknown): string {\n if (typeof value === 'string') return value\n if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {\n return String(value)\n }\n return JSON.stringify(value)\n}\n\n/** Read a (possibly dotted) field path from a plain record. */\nexport function readDottedPath(record: Record<string, unknown>, field: string): unknown {\n if (!field.includes('.')) return record[field]\n let cursor: unknown = record\n for (const segment of field.split('.')) {\n if (cursor === null || cursor === undefined) return undefined\n cursor = (cursor as Record<string, unknown>)[segment]\n }\n return cursor\n}\n"],"mappings":"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","names":[]}
|