@noy-db/hub 0.6.0-pre.17 → 0.6.0-pre.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{api-AEIN3KAQ.js → api-FRFEJ6WR.js} +6 -6
- package/dist/attestation/index.js +12 -12
- package/dist/{backup-7X3PFTSI.js → backup-OLBRFYY7.js} +10 -10
- package/dist/{backup-7X3PFTSI.js.map → backup-OLBRFYY7.js.map} +1 -1
- package/dist/blobs/index.js +8 -8
- package/dist/broker/index.js +6 -6
- package/dist/cargo/index.js +26 -26
- package/dist/{chunk-V3OLO4TK.js → chunk-2ION5ZRH.js} +17 -14
- package/dist/chunk-2ION5ZRH.js.map +1 -0
- package/dist/{chunk-466KGOQB.js → chunk-2NE7ILO7.js} +2 -2
- package/dist/{chunk-GWI65NDQ.js → chunk-3KXIGMZ4.js} +2 -2
- package/dist/{chunk-C3YP5KKS.js → chunk-3ODOWQMV.js} +13 -14
- package/dist/chunk-3ODOWQMV.js.map +1 -0
- package/dist/{chunk-X2FXEEQT.js → chunk-3RZ7QLJ4.js} +5 -5
- package/dist/{chunk-5UGEGEIJ.js → chunk-3Y4OFWQJ.js} +1 -1
- package/dist/chunk-3Y4OFWQJ.js.map +1 -0
- package/dist/{chunk-FZ2SSI5R.js → chunk-6AL6O4CS.js} +2 -2
- package/dist/{chunk-67XA44HH.js → chunk-6D3CN2JR.js} +2 -2
- package/dist/{chunk-5VVBZ6E4.js → chunk-6QCXMCLZ.js} +3 -3
- package/dist/{chunk-4GR422R5.js → chunk-7EH322GJ.js} +14 -7
- package/dist/chunk-7EH322GJ.js.map +1 -0
- package/dist/{chunk-AMKCY454.js → chunk-7WD2G6K4.js} +7 -2
- package/dist/{chunk-AMKCY454.js.map → chunk-7WD2G6K4.js.map} +1 -1
- package/dist/{chunk-URVL4EKU.js → chunk-A5QQKWCK.js} +14 -23
- package/dist/chunk-A5QQKWCK.js.map +1 -0
- package/dist/{chunk-2RGQNFJB.js → chunk-AA2WWTS3.js} +9 -7
- package/dist/chunk-AA2WWTS3.js.map +1 -0
- package/dist/{chunk-UKKBKEUI.js → chunk-AA7LM2A3.js} +2 -2
- package/dist/{chunk-SJOK23XT.js → chunk-AIZNLHKN.js} +4 -4
- package/dist/chunk-AIZNLHKN.js.map +1 -0
- package/dist/{chunk-UNR6G3EJ.js → chunk-AMMYIT7F.js} +35 -31
- package/dist/chunk-AMMYIT7F.js.map +1 -0
- package/dist/{chunk-NWPR2V5W.js → chunk-BBU3OD3D.js} +2 -2
- package/dist/{chunk-EEBMKMA7.js → chunk-BVIGGV6D.js} +33 -45
- package/dist/chunk-BVIGGV6D.js.map +1 -0
- package/dist/{chunk-OXRPH6AR.js → chunk-CFTPERGL.js} +14 -11
- package/dist/chunk-CFTPERGL.js.map +1 -0
- package/dist/{chunk-AKFNLRA2.js → chunk-CGQUSSW7.js} +26 -142
- package/dist/chunk-CGQUSSW7.js.map +1 -0
- package/dist/{chunk-JHYISAID.js → chunk-DFFSCYIW.js} +9 -9
- package/dist/{chunk-BMDF4TLU.js → chunk-DKYEVHZH.js} +7 -10
- package/dist/chunk-DKYEVHZH.js.map +1 -0
- package/dist/{chunk-YVOW35HH.js → chunk-DL4GDOWL.js} +4 -6
- package/dist/chunk-DL4GDOWL.js.map +1 -0
- package/dist/{chunk-Z2OFE6IX.js → chunk-DWO55ZWE.js} +13 -22
- package/dist/chunk-DWO55ZWE.js.map +1 -0
- package/dist/{chunk-XTC5EKST.js → chunk-ER3BQCCE.js} +2 -2
- package/dist/{chunk-JU3JKOY5.js → chunk-EWAGI3EM.js} +2 -2
- package/dist/{chunk-OAWVM5L6.js → chunk-EZ3UHHND.js} +13 -15
- package/dist/chunk-EZ3UHHND.js.map +1 -0
- package/dist/{chunk-IEA3TZJE.js → chunk-FDIZHZ3C.js} +29 -47
- package/dist/chunk-FDIZHZ3C.js.map +1 -0
- package/dist/{chunk-7TZTQS4H.js → chunk-FOABWV7L.js} +45 -18
- package/dist/chunk-FOABWV7L.js.map +1 -0
- package/dist/{chunk-QKRJUPAC.js → chunk-FRGF56PD.js} +11 -14
- package/dist/chunk-FRGF56PD.js.map +1 -0
- package/dist/{chunk-N4GAJSRQ.js → chunk-FY33DKPH.js} +2 -2
- package/dist/{chunk-JV2CNEX2.js → chunk-GSRW5MTU.js} +220 -169
- package/dist/chunk-GSRW5MTU.js.map +1 -0
- package/dist/{chunk-C52CJDFO.js → chunk-HJRTFXAA.js} +7 -10
- package/dist/chunk-HJRTFXAA.js.map +1 -0
- package/dist/chunk-HK67PX4J.js +27 -0
- package/dist/chunk-HK67PX4J.js.map +1 -0
- package/dist/{chunk-6JRIYG2C.js → chunk-IN5EZWYQ.js} +13 -14
- package/dist/chunk-IN5EZWYQ.js.map +1 -0
- package/dist/{chunk-QJBXB4WU.js → chunk-ITXPKEDP.js} +23 -29
- package/dist/chunk-ITXPKEDP.js.map +1 -0
- package/dist/{chunk-EMFFWSAD.js → chunk-JYFJNSTW.js} +9 -9
- package/dist/chunk-JYFJNSTW.js.map +1 -0
- package/dist/{chunk-D6E5N2O3.js → chunk-KLVGF2D4.js} +11 -9
- package/dist/{chunk-D6E5N2O3.js.map → chunk-KLVGF2D4.js.map} +1 -1
- package/dist/{chunk-7LLKPODL.js → chunk-LPAGIMXJ.js} +2 -2
- package/dist/{chunk-3JQVA527.js → chunk-LU6NPJWN.js} +3 -3
- package/dist/{chunk-VUQZYVYX.js → chunk-NAAPSNCS.js} +90 -33
- package/dist/chunk-NAAPSNCS.js.map +1 -0
- package/dist/{chunk-DEP24EE3.js → chunk-NFPSC2HL.js} +3 -3
- package/dist/{chunk-JAHIL6WO.js → chunk-NRNTXBHT.js} +7 -10
- package/dist/chunk-NRNTXBHT.js.map +1 -0
- package/dist/{chunk-N7HOHWKF.js → chunk-O4QDBAB6.js} +7 -10
- package/dist/{chunk-N7HOHWKF.js.map → chunk-O4QDBAB6.js.map} +1 -1
- package/dist/{chunk-VQQUOEON.js → chunk-OCCNYGKG.js} +7 -7
- package/dist/{chunk-VEA35MQP.js → chunk-OZD3D2DZ.js} +10 -13
- package/dist/chunk-OZD3D2DZ.js.map +1 -0
- package/dist/{chunk-IRHOQFA7.js → chunk-PBPYQZDW.js} +2 -2
- package/dist/{chunk-FHFMWYGM.js → chunk-PBRIHUQ5.js} +5 -5
- package/dist/{chunk-FHFMWYGM.js.map → chunk-PBRIHUQ5.js.map} +1 -1
- package/dist/{chunk-LUFZPROB.js → chunk-Q67P7S2I.js} +14 -15
- package/dist/chunk-Q67P7S2I.js.map +1 -0
- package/dist/{chunk-ES36R3U2.js → chunk-RAYGDC4A.js} +50 -63
- package/dist/chunk-RAYGDC4A.js.map +1 -0
- package/dist/{chunk-RVTOCAHQ.js → chunk-SQSZRCYT.js} +1 -1
- package/dist/chunk-SQSZRCYT.js.map +1 -0
- package/dist/{chunk-JN64WH3U.js → chunk-T7IR4N2S.js} +1 -1
- package/dist/{chunk-JN64WH3U.js.map → chunk-T7IR4N2S.js.map} +1 -1
- package/dist/{chunk-SDEHNGWW.js → chunk-TNOQNU3R.js} +5 -4
- package/dist/{chunk-SDEHNGWW.js.map → chunk-TNOQNU3R.js.map} +1 -1
- package/dist/{chunk-P5ARVLNW.js → chunk-TXDOMADN.js} +9 -6
- package/dist/chunk-TXDOMADN.js.map +1 -0
- package/dist/{chunk-6VAM5THN.js → chunk-U6D5XTI5.js} +13 -13
- package/dist/{chunk-6VAM5THN.js.map → chunk-U6D5XTI5.js.map} +1 -1
- package/dist/{chunk-APUP2J5M.js → chunk-UE72AQDZ.js} +6 -6
- package/dist/chunk-UIYZ5SOU.js +190 -0
- package/dist/chunk-UIYZ5SOU.js.map +1 -0
- package/dist/{chunk-QC2HGYFX.js → chunk-VXVKEWYC.js} +3 -3
- package/dist/{chunk-BKYQNNO3.js → chunk-X3AXSYBL.js} +198 -132
- package/dist/chunk-X3AXSYBL.js.map +1 -0
- package/dist/{chunk-I7JF5TNL.js → chunk-ZDATYUYG.js} +2 -2
- package/dist/{chunk-WBHQX6ME.js → chunk-ZSOTZ2RZ.js} +12 -13
- package/dist/chunk-ZSOTZ2RZ.js.map +1 -0
- package/dist/{chunk-RGPB25DB.js → chunk-ZVAFJQZB.js} +25 -19
- package/dist/chunk-ZVAFJQZB.js.map +1 -0
- package/dist/classified/index.js +4 -4
- package/dist/{classified-marker-W2PUYC3K.js → classified-marker-GWQTNDSL.js} +3 -3
- package/dist/{collection-facade-CRXN72DU.js → collection-facade-56RTPO4G.js} +12 -12
- package/dist/consent/index.js +4 -4
- package/dist/cover/index.js +9 -2
- package/dist/custody/index.js +6 -6
- package/dist/{dead-filter-MVFQCLXY.js → dead-filter-XP7MPHCJ.js} +2 -2
- package/dist/{delegation-I7XXO5LL.js → delegation-Q3HEAVLA.js} +18 -19
- package/dist/delegation-Q3HEAVLA.js.map +1 -0
- package/dist/derivations/index.js +3 -3
- package/dist/{derive-NL7X2EFT.js → derive-SVDY36ER.js} +4 -4
- package/dist/directory/index.js +5 -5
- package/dist/{dispatch-K3MDLCXG.js → dispatch-MG5KNMWG.js} +5 -5
- package/dist/{dispatch-PY5RUHJF.js → dispatch-RVJQ4BOL.js} +11 -11
- package/dist/{enclave-77K7UDLS.js → enclave-AOZTWFZK.js} +12 -4
- package/dist/{executor-FNGVGDW3.js → executor-FRUVM36Q.js} +12 -12
- package/dist/{export-accessible-QWUYY2OY.js → export-accessible-HDDHZYPM.js} +7 -7
- package/dist/{extract-partition-CGCURVS4.js → extract-partition-TRADP234.js} +12 -12
- package/dist/{fanout-sidecar-JI3MUPQV.js → fanout-sidecar-NZA6J7QB.js} +13 -22
- package/dist/fanout-sidecar-NZA6J7QB.js.map +1 -0
- package/dist/forget/index.js +4 -4
- package/dist/history/index.js +18 -15
- package/dist/history/index.js.map +1 -1
- package/dist/i18n/index.js +9 -9
- package/dist/index.d.ts +16 -0
- package/dist/index.js +129 -127
- package/dist/index.js.map +1 -1
- package/dist/introspection/index.js +5 -5
- package/dist/{issue-7IPUNAFA.js → issue-GZQDW5TQ.js} +6 -6
- package/dist/kernel/enclave/index.d.ts +6 -5
- package/dist/kernel/enclave/record-aad.d.ts +79 -7
- package/dist/kernel/enclave/record-envelope.d.ts +54 -7
- package/dist/kernel/enclave/record-keys/envelope-body.d.ts +34 -2
- package/dist/kernel/enclave/record-keys/lifecycle.d.ts +22 -5
- package/dist/kernel/enclave/record-keys/record-codec.d.ts +47 -14
- package/dist/kernel/enclave/record-keys/rekey.d.ts +8 -2
- package/dist/kernel/enclave/record-keys/tombstone.d.ts +3 -3
- package/dist/kernel/merge-authority.d.ts +25 -0
- package/dist/kernel/tier-visibility.d.ts +15 -0
- package/dist/kernel/types.d.ts +8 -0
- package/dist/kernel/vault.d.ts +1 -1
- package/dist/kernel/via/index.d.ts +11 -2
- package/dist/{ledger-3RDSSMGD.js → ledger-KQAJPEMZ.js} +6 -6
- package/dist/liberate-Z2EXJY2S.js +24 -0
- package/dist/{link-set-VJIWWUZO.js → link-set-XKN5V7VM.js} +5 -5
- package/dist/{managed-secret-XGGJ3BC3.js → managed-secret-7WIIVMJ5.js} +11 -3
- package/dist/materialized-views/index.js +14 -14
- package/dist/money/index.js +3 -3
- package/dist/{noydb-CLQP36JR.js → noydb-NPW3C2EX.js} +39 -38
- package/dist/periods/index.js +6 -6
- package/dist/pod/index.js +42 -41
- package/dist/{pod-handle-7BVSWP5B.js → pod-handle-SKDKRPOR.js} +6 -6
- package/dist/{pod-handle-7BVSWP5B.js.map → pod-handle-SKDKRPOR.js.map} +1 -1
- package/dist/policy/index.js +10 -3
- package/dist/port/with/merge-authority.d.ts +52 -0
- package/dist/port/with/strategies.d.ts +2 -0
- package/dist/portability/index.js +6 -6
- package/dist/{post-register-W3T4U57K.js → post-register-CVS7CCJE.js} +4 -4
- package/dist/reduce/index.js +3 -3
- package/dist/{register-YUVC4G27.js → register-GJ6PW4KP.js} +10 -10
- package/dist/{registry-BPKJX562.js → registry-6LUJ5IJW.js} +4 -4
- package/dist/{registry-HRFRBFDV.js → registry-KI77RVK7.js} +4 -4
- package/dist/{request-withdrawal-CXTQLMJK.js → request-withdrawal-LICOUGXG.js} +9 -9
- package/dist/{reveal-G7W7N3AZ.js → reveal-PWM47FQN.js} +3 -3
- package/dist/{revoke-ZX3TZUI6.js → revoke-AWZKZLZB.js} +6 -6
- package/dist/sealed-record/index.js +14 -7
- package/dist/sealed-record/index.js.map +1 -1
- package/dist/search/index.js +3 -3
- package/dist/{seed-2FUULTQV.js → seed-LNTLU2A3.js} +14 -17
- package/dist/seed-LNTLU2A3.js.map +1 -0
- package/dist/sequence/index.js +4 -4
- package/dist/session/index.js +3 -3
- package/dist/{signer-FPUIX63Z.js → signer-JE6NEURY.js} +5 -5
- package/dist/snapshots/index.js +5 -5
- package/dist/{stale-42FPALK2.js → stale-ZWTKUVZX.js} +5 -5
- package/dist/{storage-6QODT6K5.js → storage-WCZOCA46.js} +5 -5
- package/dist/{store-coordination-provider-Y3VXBHPL.js → store-coordination-provider-7H62VO2C.js} +18 -12
- package/dist/store-coordination-provider-7H62VO2C.js.map +1 -0
- package/dist/sync/index.js +5 -5
- package/dist/team/index.js +10 -9
- package/dist/tiers/index.js +6 -5
- package/dist/vault-head/index.js +111 -0
- package/dist/vault-head/index.js.map +1 -0
- package/dist/{walk-5PGJI3ND.js → walk-HSS3NLQ3.js} +6 -6
- package/dist/with-audit/periods/periods.d.ts +1 -1
- package/dist/with-audit/periods/strategy.d.ts +1 -1
- package/dist/with-audit/sealed-record/index.d.ts +13 -0
- package/dist/with-audit/tiers/index.d.ts +17 -2
- package/dist/with-commit/history/history.d.ts +26 -0
- package/dist/with-commit/history/index.d.ts +2 -1
- package/dist/with-commit/history/ledger/store.d.ts +7 -1
- package/dist/with-commit/history/strategy.d.ts +11 -1
- package/dist/with-commit/vault-head/active.d.ts +2 -0
- package/dist/with-commit/vault-head/head.d.ts +28 -0
- package/dist/with-commit/vault-head/index.d.ts +10 -0
- package/dist/with-commit/vault-head/strategy.d.ts +127 -0
- package/dist/with-commit/vault-head/verify.d.ts +50 -0
- package/dist/with-party/directory/cover/storage.d.ts +0 -8
- package/dist/with-party/directory/storage.d.ts +0 -11
- package/dist/with-party/directory/visibility.d.ts +0 -21
- package/dist/with-party/policy/storage.d.ts +0 -10
- package/dist/with-party/team/tiers.d.ts +2 -1
- package/dist/with-sync/engine.d.ts +33 -5
- package/dist/with-sync/strategy.d.ts +3 -0
- package/dist/{withdraw-accessible-PFCQQGNP.js → withdraw-accessible-GHBA4DAA.js} +8 -8
- package/package.json +7 -3
- package/dist/chunk-2RGQNFJB.js.map +0 -1
- package/dist/chunk-4GR422R5.js.map +0 -1
- package/dist/chunk-5UGEGEIJ.js.map +0 -1
- package/dist/chunk-6JRIYG2C.js.map +0 -1
- package/dist/chunk-7TZTQS4H.js.map +0 -1
- package/dist/chunk-AFHQATY4.js +0 -47
- package/dist/chunk-AFHQATY4.js.map +0 -1
- package/dist/chunk-AKFNLRA2.js.map +0 -1
- package/dist/chunk-BKYQNNO3.js.map +0 -1
- package/dist/chunk-BMDF4TLU.js.map +0 -1
- package/dist/chunk-C3YP5KKS.js.map +0 -1
- package/dist/chunk-C52CJDFO.js.map +0 -1
- package/dist/chunk-EEBMKMA7.js.map +0 -1
- package/dist/chunk-EMFFWSAD.js.map +0 -1
- package/dist/chunk-ES36R3U2.js.map +0 -1
- package/dist/chunk-IEA3TZJE.js.map +0 -1
- package/dist/chunk-JAHIL6WO.js.map +0 -1
- package/dist/chunk-JV2CNEX2.js.map +0 -1
- package/dist/chunk-LUFZPROB.js.map +0 -1
- package/dist/chunk-OAWVM5L6.js.map +0 -1
- package/dist/chunk-OXRPH6AR.js.map +0 -1
- package/dist/chunk-P5ARVLNW.js.map +0 -1
- package/dist/chunk-QJBXB4WU.js.map +0 -1
- package/dist/chunk-QKRJUPAC.js.map +0 -1
- package/dist/chunk-RGPB25DB.js.map +0 -1
- package/dist/chunk-RVTOCAHQ.js.map +0 -1
- package/dist/chunk-SJOK23XT.js.map +0 -1
- package/dist/chunk-UNR6G3EJ.js.map +0 -1
- package/dist/chunk-URVL4EKU.js.map +0 -1
- package/dist/chunk-V3OLO4TK.js.map +0 -1
- package/dist/chunk-VEA35MQP.js.map +0 -1
- package/dist/chunk-VUQZYVYX.js.map +0 -1
- package/dist/chunk-WBHQX6ME.js.map +0 -1
- package/dist/chunk-YVOW35HH.js.map +0 -1
- package/dist/chunk-Z2OFE6IX.js.map +0 -1
- package/dist/delegation-I7XXO5LL.js.map +0 -1
- package/dist/fanout-sidecar-JI3MUPQV.js.map +0 -1
- package/dist/liberate-2DNJ7AJ7.js +0 -24
- package/dist/seed-2FUULTQV.js.map +0 -1
- package/dist/store-coordination-provider-Y3VXBHPL.js.map +0 -1
- /package/dist/{api-AEIN3KAQ.js.map → api-FRFEJ6WR.js.map} +0 -0
- /package/dist/{chunk-466KGOQB.js.map → chunk-2NE7ILO7.js.map} +0 -0
- /package/dist/{chunk-GWI65NDQ.js.map → chunk-3KXIGMZ4.js.map} +0 -0
- /package/dist/{chunk-X2FXEEQT.js.map → chunk-3RZ7QLJ4.js.map} +0 -0
- /package/dist/{chunk-FZ2SSI5R.js.map → chunk-6AL6O4CS.js.map} +0 -0
- /package/dist/{chunk-67XA44HH.js.map → chunk-6D3CN2JR.js.map} +0 -0
- /package/dist/{chunk-5VVBZ6E4.js.map → chunk-6QCXMCLZ.js.map} +0 -0
- /package/dist/{chunk-UKKBKEUI.js.map → chunk-AA7LM2A3.js.map} +0 -0
- /package/dist/{chunk-NWPR2V5W.js.map → chunk-BBU3OD3D.js.map} +0 -0
- /package/dist/{chunk-JHYISAID.js.map → chunk-DFFSCYIW.js.map} +0 -0
- /package/dist/{chunk-XTC5EKST.js.map → chunk-ER3BQCCE.js.map} +0 -0
- /package/dist/{chunk-JU3JKOY5.js.map → chunk-EWAGI3EM.js.map} +0 -0
- /package/dist/{chunk-N4GAJSRQ.js.map → chunk-FY33DKPH.js.map} +0 -0
- /package/dist/{chunk-7LLKPODL.js.map → chunk-LPAGIMXJ.js.map} +0 -0
- /package/dist/{chunk-3JQVA527.js.map → chunk-LU6NPJWN.js.map} +0 -0
- /package/dist/{chunk-DEP24EE3.js.map → chunk-NFPSC2HL.js.map} +0 -0
- /package/dist/{chunk-VQQUOEON.js.map → chunk-OCCNYGKG.js.map} +0 -0
- /package/dist/{chunk-IRHOQFA7.js.map → chunk-PBPYQZDW.js.map} +0 -0
- /package/dist/{chunk-APUP2J5M.js.map → chunk-UE72AQDZ.js.map} +0 -0
- /package/dist/{chunk-QC2HGYFX.js.map → chunk-VXVKEWYC.js.map} +0 -0
- /package/dist/{chunk-I7JF5TNL.js.map → chunk-ZDATYUYG.js.map} +0 -0
- /package/dist/{classified-marker-W2PUYC3K.js.map → classified-marker-GWQTNDSL.js.map} +0 -0
- /package/dist/{collection-facade-CRXN72DU.js.map → collection-facade-56RTPO4G.js.map} +0 -0
- /package/dist/{dead-filter-MVFQCLXY.js.map → dead-filter-XP7MPHCJ.js.map} +0 -0
- /package/dist/{derive-NL7X2EFT.js.map → derive-SVDY36ER.js.map} +0 -0
- /package/dist/{dispatch-K3MDLCXG.js.map → dispatch-MG5KNMWG.js.map} +0 -0
- /package/dist/{dispatch-PY5RUHJF.js.map → dispatch-RVJQ4BOL.js.map} +0 -0
- /package/dist/{enclave-77K7UDLS.js.map → enclave-AOZTWFZK.js.map} +0 -0
- /package/dist/{executor-FNGVGDW3.js.map → executor-FRUVM36Q.js.map} +0 -0
- /package/dist/{export-accessible-QWUYY2OY.js.map → export-accessible-HDDHZYPM.js.map} +0 -0
- /package/dist/{extract-partition-CGCURVS4.js.map → extract-partition-TRADP234.js.map} +0 -0
- /package/dist/{issue-7IPUNAFA.js.map → issue-GZQDW5TQ.js.map} +0 -0
- /package/dist/{ledger-3RDSSMGD.js.map → ledger-KQAJPEMZ.js.map} +0 -0
- /package/dist/{liberate-2DNJ7AJ7.js.map → liberate-Z2EXJY2S.js.map} +0 -0
- /package/dist/{link-set-VJIWWUZO.js.map → link-set-XKN5V7VM.js.map} +0 -0
- /package/dist/{managed-secret-XGGJ3BC3.js.map → managed-secret-7WIIVMJ5.js.map} +0 -0
- /package/dist/{noydb-CLQP36JR.js.map → noydb-NPW3C2EX.js.map} +0 -0
- /package/dist/{post-register-W3T4U57K.js.map → post-register-CVS7CCJE.js.map} +0 -0
- /package/dist/{register-YUVC4G27.js.map → register-GJ6PW4KP.js.map} +0 -0
- /package/dist/{registry-BPKJX562.js.map → registry-6LUJ5IJW.js.map} +0 -0
- /package/dist/{registry-HRFRBFDV.js.map → registry-KI77RVK7.js.map} +0 -0
- /package/dist/{request-withdrawal-CXTQLMJK.js.map → request-withdrawal-LICOUGXG.js.map} +0 -0
- /package/dist/{reveal-G7W7N3AZ.js.map → reveal-PWM47FQN.js.map} +0 -0
- /package/dist/{revoke-ZX3TZUI6.js.map → revoke-AWZKZLZB.js.map} +0 -0
- /package/dist/{signer-FPUIX63Z.js.map → signer-JE6NEURY.js.map} +0 -0
- /package/dist/{stale-42FPALK2.js.map → stale-ZWTKUVZX.js.map} +0 -0
- /package/dist/{storage-6QODT6K5.js.map → storage-WCZOCA46.js.map} +0 -0
- /package/dist/{walk-5PGJI3ND.js.map → walk-HSS3NLQ3.js.map} +0 -0
- /package/dist/{withdraw-accessible-PFCQQGNP.js.map → withdraw-accessible-GHBA4DAA.js.map} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/with-audit/portability/request-withdrawal.ts"],"sourcesContent":["/**\n * Two-party withdrawal ceremony.\n *\n * The conservative counterpart to `unilateralWithdrawal`: a non-owner —\n * including a read-only `client`/`viewer` who cannot self-serve a deletion —\n * files a durable REQUEST; an owner/admin reviews it and either APPROVES\n * (extract-and-dispose under firm authority) or REJECTS it. Every step is\n * audited in the tamper-evident ledger.\n *\n * requester: vault.user.requestWithdrawal({ scope, disposition?, legalBasis? })\n * owner: vault.user.listWithdrawalRequests()\n * vault.user.approveWithdrawal(requestId, { reKey }) → { bundle, snapshot? }\n * vault.user.rejectWithdrawal(requestId, { reason })\n *\n * Requests live in the reserved `_user_withdrawal_requests` namespace. The\n * record body is plaintext metadata (collection names + disposition + legal\n * basis — none secret in this trust model; the owner sees the data anyway) and\n * carries NO secret: the re-key secret is supplied by the approver at\n * approval time and conveyed to the requester out-of-band, so no secret is\n * stored at rest.\n */\nimport type { Vault } from '../../kernel/vault.js'\nimport type { EncryptedEnvelope } from '../../kernel/types.js'\nimport { NOYDB_FORMAT_VERSION } from '../../kernel/types.js'\nimport { NoydbError } from '../../kernel/errors.js'\nimport { resolveAccessibleCollections, buildAccessibleBundle } from './export-accessible.js'\nimport { freezeAndDeleteClosure, randomId, type FrozenSnapshotRef, type WithdrawResult } from './withdraw-accessible.js'\n\nexport const WITHDRAWAL_REQUESTS_COLLECTION = '_user_withdrawal_requests'\n\n/** Raised when a request is missing, already decided, or expired. */\nexport class WithdrawalRequestError extends NoydbError {\n constructor(message: string) {\n super('WITHDRAWAL_REQUEST', message)\n this.name = 'WithdrawalRequestError'\n }\n}\n\nexport type WithdrawalRequestStatus = 'pending' | 'approved' | 'rejected'\n\nexport interface WithdrawalRequest {\n readonly requestId: string\n readonly requester: string\n readonly collections: readonly string[]\n readonly disposition: 'delete' | 'freeze'\n readonly legalBasis?: string\n readonly status: WithdrawalRequestStatus\n readonly requestedAt: string\n readonly expiresAt?: string\n readonly decidedAt?: string\n readonly decidedBy?: string\n readonly rejectReason?: string\n readonly snapshotSha256?: string\n}\n\nexport interface RequestWithdrawalOptions {\n readonly scope?: { readonly collections?: readonly string[] }\n readonly disposition?: 'delete' | 'freeze'\n readonly legalBasis?: string\n /** Time-to-live in ms; after this the request can no longer be approved. */\n readonly expiresInMs?: number\n}\n\nexport interface RequestWithdrawalResult {\n readonly requestId: string\n readonly status: WithdrawalRequestStatus\n readonly expiresAt?: string\n}\n\nfunction writeRequest(vault: Vault, req: WithdrawalRequest, expectedVersion: number): Promise<void> {\n const { name: vaultName, adapter } = vault._introspectState()\n const body = JSON.stringify(req)\n const env: EncryptedEnvelope = {\n _noydb: NOYDB_FORMAT_VERSION, _v: expectedVersion + 1, _ts: req.decidedAt ?? req.requestedAt,\n _iv: '', _data: body, _by: req.decidedBy ?? req.requester,\n }\n return adapter.put(vaultName, WITHDRAWAL_REQUESTS_COLLECTION, req.requestId, env, expectedVersion)\n}\n\nasync function readRequest(vault: Vault, requestId: string): Promise<{ req: WithdrawalRequest; version: number }> {\n const { name: vaultName, adapter } = vault._introspectState()\n const env = await adapter.get(vaultName, WITHDRAWAL_REQUESTS_COLLECTION, requestId)\n if (!env) throw new WithdrawalRequestError(`withdrawal request \"${requestId}\" not found`)\n return { req: JSON.parse(env._data) as WithdrawalRequest, version: env._v }\n}\n\n/**\n * Requester side. Files a durable request for the caller's accessible scope.\n * Gated by `user-request-withdrawal` (checked in the UserApi wrapper).\n */\nexport async function requestWithdrawal(\n vault: Vault,\n opts: RequestWithdrawalOptions = {},\n): Promise<RequestWithdrawalResult> {\n const { keyring } = vault._introspectState()\n // Resolve the requester's accessible collections (read access defines \"theirs\";\n // the owner — who can delete anything — executes the disposition on approval).\n const collections = resolveAccessibleCollections(keyring, opts.scope, false)\n if (!collections || collections.length === 0) {\n throw new WithdrawalRequestError(\n 'requestWithdrawal needs a concrete scope.collections (the caller has all-collection access — name the collections to withdraw)',\n )\n }\n const requestId = `wr-${randomId()}`\n const requestedAt = new Date().toISOString()\n const req: WithdrawalRequest = {\n requestId, requester: keyring.userId, collections, disposition: opts.disposition ?? 'delete',\n status: 'pending', requestedAt,\n ...(opts.legalBasis ? { legalBasis: opts.legalBasis } : {}),\n ...(opts.expiresInMs ? { expiresAt: new Date(Date.parse(requestedAt) + opts.expiresInMs).toISOString() } : {}),\n }\n await writeRequest(vault, req, 0) // create-only\n await vault._getLedgerOrNull()?.append({\n op: 'lifecycle', collection: '', id: '', version: 0, actor: keyring.userId, payloadHash: '',\n reason: `user-withdrawal-request:${requestId}:${keyring.userId}`,\n })\n return { requestId, status: 'pending', ...(req.expiresAt ? { expiresAt: req.expiresAt } : {}) }\n}\n\n/** Owner side. List filed requests (optionally by status). */\nexport async function listWithdrawalRequests(\n vault: Vault,\n opts: { status?: WithdrawalRequestStatus } = {},\n): Promise<WithdrawalRequest[]> {\n const { name: vaultName, adapter } = vault._introspectState()\n const ids = await adapter.list(vaultName, WITHDRAWAL_REQUESTS_COLLECTION)\n const out: WithdrawalRequest[] = []\n for (const id of ids) {\n const env = await adapter.get(vaultName, WITHDRAWAL_REQUESTS_COLLECTION, id)\n if (!env) continue\n const req = JSON.parse(env._data) as WithdrawalRequest\n if (!opts.status || req.status === opts.status) out.push(req)\n }\n return out\n}\n\nfunction assertApprover(vault: Vault): string {\n const { keyring } = vault._introspectState()\n // FR-6: custodian is INTENTIONALLY excluded (SAFER default — review flag).\n // Approving a two-party withdrawal is a destructive extract-and-dispose\n // exercised under FIRM authority — a governance act adjacent to grant/revoke,\n // which a non-owning custodian cannot do. It stays owner/admin-only.\n if (keyring.role !== 'owner' && keyring.role !== 'admin') {\n throw new WithdrawalRequestError('approveWithdrawal / rejectWithdrawal require an owner or admin')\n }\n return keyring.userId\n}\n\nfunction assertPending(req: WithdrawalRequest): void {\n if (req.status !== 'pending') {\n throw new WithdrawalRequestError(`withdrawal request \"${req.requestId}\" is already ${req.status}`)\n }\n if (req.expiresAt && Date.now() > Date.parse(req.expiresAt)) {\n throw new WithdrawalRequestError(`withdrawal request \"${req.requestId}\" expired at ${req.expiresAt}`)\n }\n}\n\nexport interface ApproveWithdrawalOptions {\n /** Re-key the handed-back bundle to a secret the requester will use. */\n readonly reKey?: { readonly secret: string }\n}\n\n/**\n * Owner side. Extract the requester's recorded scope under firm authority,\n * dispose of the source per the request's disposition, mark the request\n * approved, and return the re-keyed bundle to hand back. Gated by\n * `approve-user-withdrawal` (checked in the UserApi wrapper) + owner/admin role.\n */\nexport async function approveWithdrawal(\n vault: Vault,\n requestId: string,\n opts: ApproveWithdrawalOptions = {},\n): Promise<WithdrawResult> {\n const approver = assertApprover(vault)\n const { req, version } = await readRequest(vault, requestId)\n assertPending(req)\n\n // 1. Produce the requester's re-keyed portable copy FIRST (owner authority\n // holds every DEK, so the recorded collections are all readable).\n const bundle = await buildAccessibleBundle(vault, [...req.collections], opts.reKey)\n\n // 2. + 3. dispose of the source (freeze snapshot, optional → delete-closure).\n const snapshot: FrozenSnapshotRef | undefined = await freezeAndDeleteClosure(vault, req.collections, {\n disposition: req.disposition, actorUserId: approver, withdrawalId: `wd-${requestId}`,\n })\n\n // 4. mark approved + audit (OCC against the version we read).\n const decidedAt = new Date().toISOString()\n await writeRequest(vault, {\n ...req, status: 'approved', decidedAt, decidedBy: approver,\n ...(snapshot ? { snapshotSha256: snapshot.sha256 } : {}),\n }, version)\n await vault._getLedgerOrNull()?.append({\n op: 'lifecycle', collection: '', id: '', version: 0, actor: approver, payloadHash: '',\n reason: `user-withdrawal-approved:${requestId}:${req.requester}:${req.disposition}`,\n })\n\n return snapshot ? { bundle, snapshot } : { bundle }\n}\n\nexport interface RejectWithdrawalOptions {\n readonly reason?: string\n}\n\n/** Owner side. Decline a pending request (no data is touched). */\nexport async function rejectWithdrawal(\n vault: Vault,\n requestId: string,\n opts: RejectWithdrawalOptions = {},\n): Promise<WithdrawalRequest> {\n const approver = assertApprover(vault)\n const { req, version } = await readRequest(vault, requestId)\n assertPending(req)\n const decidedAt = new Date().toISOString()\n const updated: WithdrawalRequest = {\n ...req, status: 'rejected', decidedAt, decidedBy: approver,\n ...(opts.reason ? { rejectReason: opts.reason } : {}),\n }\n await writeRequest(vault, updated, version)\n await vault._getLedgerOrNull()?.append({\n op: 'lifecycle', collection: '', id: '', version: 0, actor: approver, payloadHash: '',\n reason: `user-withdrawal-rejected:${requestId}:${req.requester}`,\n })\n return updated\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/with-audit/tiers/active.ts","../src/with-audit/tiers/strategy.ts","../src/with-audit/tiers/index.ts"],"sourcesContent":["/**\n * Enable the hierarchical-tiers capability.\n * Pass to `createNoydb({ tiersStrategy: withTiers() })` to make a collection's\n * `putAtTier` / `getAtTier` / `listAtTier` / `elevate` / `demote` methods live.\n * The tier read/write/re-key engine is dynamically imported here, so it stays\n * out of the floor bundle until opted in.\n */\nimport type { TiersStrategy } from './strategy.js'\n\nexport function withTiers(): TiersStrategy {\n return {\n async putAtTier(ctx, id, record, tier, opts) {\n const { putAtTier } = await import('./index.js')\n return putAtTier(ctx, id, record, tier, opts)\n },\n async getAtTier(ctx, id) {\n const { getAtTier } = await import('./index.js')\n return getAtTier(ctx, id)\n },\n async listAtTier(ctx) {\n const { listAtTier } = await import('./index.js')\n return listAtTier(ctx)\n },\n async elevate(ctx, id, toTier) {\n const { elevate } = await import('./index.js')\n return elevate(ctx, id, toTier)\n },\n async demote(ctx, id, toTier) {\n const { demote } = await import('./index.js')\n return demote(ctx, id, toTier)\n },\n }\n}\n","/**\n * Tiers capability strategy — the five on-demand collection-level tier\n * operations the `Collection` routes through. The active engine\n * ({@link withTiers}) dynamically imports the tier cores (keeping them out of\n * the floor bundle); {@link NO_TIERS} throws. Collection always builds its\n * per-call {@link TiersContext} and delegates here, so an un-opted-in caller\n * hits `NO_TIERS`'s throw.\n * @internal\n */\nimport type { GhostRecord } from '../../kernel/types.js'\nimport type { TiersContext, TierMoveResult } from './index.js'\nimport { TiersNotEnabledError } from '../../kernel/errors.js'\n\n// #779 — re-exported so kernel-spine callers (e.g. `Vault._elevatedPut`) can reference the\n// result shape via this grandfathered strategy.js seam instead of statically importing\n// tiers/index.js directly (the port-layering check only grandfathers this file).\nexport type { TierMoveResult }\n\nexport interface TiersStrategy {\n putAtTier<T>(\n ctx: TiersContext<T>,\n id: string,\n record: T,\n tier: number,\n opts?: { elevation?: { reason: string; fromTier: number }; source?: string; sourceTs?: string },\n ): Promise<TierMoveResult>\n getAtTier<T>(ctx: TiersContext<T>, id: string): Promise<T | GhostRecord | null>\n listAtTier<T>(ctx: TiersContext<T>): Promise<Array<{ id: string; tier: number; readable: boolean }>>\n elevate<T>(ctx: TiersContext<T>, id: string, toTier: number): Promise<TierMoveResult>\n demote<T>(ctx: TiersContext<T>, id: string, toTier: number): Promise<TierMoveResult>\n}\n\n/**\n * No-op stub — the floor default. Every capability method throws\n * {@link TiersNotEnabledError}; opt in with `tiersStrategy: withTiers()` in\n * createNoydb. @internal\n */\nexport const NO_TIERS: TiersStrategy = {\n async putAtTier() { throw new TiersNotEnabledError() },\n async getAtTier() { throw new TiersNotEnabledError() },\n async listAtTier() { throw new TiersNotEnabledError() },\n async elevate() { throw new TiersNotEnabledError() },\n async demote() { throw new TiersNotEnabledError() },\n}\n","/**\n * Hierarchical access — the collection-level tier operations\n * (`putAtTier` / `getAtTier` / `listAtTier` / `elevate` / `demote`).\n *\n * A collection opted into `{ tiers: [...] }` stamps `_tier: N` on each envelope\n * and encrypts its body under the tier-N DEK. These helpers are the read/write\n * surface for that scheme: write at a tier, read with tier-aware visibility\n * (invisibility vs. ghost), and move a record up (`elevate`) or down (`demote`)\n * the tier ladder by re-wrapping its body key.\n *\n * Each function takes a small {@link TiersContext} (the exact `this.*` the\n * moving methods touched) instead of `this`, mirroring the `record-keys/`\n * siblings. Behaviour is byte-identical to the inline code it replaced.\n *\n * The crux is `cekCache`: `elevate`/`demote`/`getAtTier` write the SAME `Lru`\n * reference `Collection` owns (never a copy) so a per-record CEK re-wrap stays\n * synchronous with the cache the kernel's write/read path also mutates. The\n * cross-tier event sink stays collection-resident and is reached via the\n * `emitCrossTierEvent` callback.\n *\n * Internal service — not exported as a `@noy-db/hub/*` subpath.\n */\nexport { withTiers } from './active.js'\nexport { NO_TIERS, type TiersStrategy } from './strategy.js'\nexport { TiersNotEnabledError } from '../../kernel/errors.js'\nimport { encrypt, decrypt, unwrapCek, rewrapBodyToDek, applyRewrappedBody, isDeleteMarker, isTombstoneShape, type RecordCodec, type EnclaveKey, type SealedShredSlot } from '../../kernel/enclave/index.js'\nimport { TierDemoteDeniedError, UnsupportedTierCompositionError, PersistedIndexCompensationError } from '../../kernel/errors.js'\nimport { dekKey, assertTierAccess } from '../../with-party/team/tiers.js'\nimport type { UnlockedKeyring } from '../../with-party/team/keyring.js'\nimport type { Lru } from '../../kernel/cache/index.js'\nimport type { BlobFieldsConfig } from '../../with-shape/blobs/blob-compaction.js'\nimport {\n NOYDB_FORMAT_VERSION,\n type NoydbStore,\n type EncryptedEnvelope,\n type GhostRecord,\n type TierMode,\n type CrossTierAccessEvent,\n} from '../../kernel/types.js'\n\n/** Everything the moving tier methods touched on `this.*`, as a flat context. */\nexport interface TiersContext<T> {\n /** Collection name — the crypto AAD scope and tier-DEK key prefix. */\n readonly name: string\n /** Vault namespace the records live under. */\n readonly vault: string\n /** The ciphertext store. */\n readonly adapter: NoydbStore\n /** The caller's unlocked keyring (tier-DEK holdings + role + userId). */\n readonly keyring: UnlockedKeyring\n /** The record codec — decrypts a tier-0 envelope to T. */\n readonly codec: RecordCodec<T>\n /**\n * The collection's per-record CEK cache (SHARED reference, not a copy).\n * `elevate`/`demote`/`getAtTier` re-wrap a record's CEK and `set` it here so\n * the move stays synchronous with the cache the kernel's read/write path\n * owns. `null` → no caching.\n */\n readonly cekCache: Lru<string, EnclaveKey> | null\n /**\n * Sync the collection's decoded-record cache after a tier move rewraps the\n * envelope (#691). The lazy LRU is evicted on EVERY call — it is never\n * re-seeded, since a lazy `#getRaw` miss refetches and decodes the fresh\n * envelope via its adapter fallback (a stale LRU entry after a tier-0\n * `putAtTier` overwrite was the #702 lazy-mode gap). `null` → also evict\n * the eager cache entry; an `entry` → set the eager cache to the given\n * decoded record/version (used by `demote()` landing back at tier 0 and\n * `putAtTier`'s tier-0 write, #702 — the record is tier-0, so it must stay\n * plain-`get()`-readable in-session).\n */\n syncCache(id: string, entry: { record: T; version: number } | null): void\n /**\n * Sync the collection's indexes after a tier move rewraps the envelope\n * (#709). Persisted `_idx/<field>/<recordId>` side-cars hold the indexed\n * field's PLAINTEXT value and are always encrypted under the tier-0 DEK,\n * whatever the record's own tier — the same leak class `forget()` fixed\n * via `purgePersistedIndexes` (\"forget() crypto-shreds the body but keeps\n * the collection DEK, under which these side-cars are encrypted — so\n * without this they leave the indexed field VALUES readable\"). `null` →\n * the record just left tier 0: purge its persisted side-cars and drop its\n * in-memory index entries. A record → it is tier-0 again: (re)build its\n * entries from that record. Call this BEFORE {@link syncCache} — the\n * implementation reads the pre-write cached value as \"previous\" to clean\n * up stale index buckets, so it must run while that entry is still live.\n * `version` stamps a rebuilt side-car's own envelope version — every\n * caller has one in scope (the envelope it just wrote), including the\n * purge (`null`) branches, which pass it along even though it's ignored\n * there (#720: keeps the parameter honestly required, no dead default).\n * `priorEnvelope` (#720) — the RAW envelope read BEFORE this call's own\n * overwrite (already in scope as `existing`/`envelope` at every record-\n * branch call site), so a lazy same-tier value change (a `putAtTier(0)`\n * dropping an indexed field, a `demote(0)` off an elevated record) can\n * still be tier-gate-decoded as \"previous\" once the live envelope itself\n * has already moved past it. The `null`-record branches never pass one —\n * see the implementation's doc comment for why they don't need to.\n */\n syncIndexes(id: string, record: T | null, version: number, priorEnvelope?: EncryptedEnvelope): Promise<void>\n /** Sync the collection's SEARCH artifacts after a tier move (#721). Both the\n * lexical `_ftindex` blob and the `_vec/<id>` embedding are encrypted under\n * the tier-0 DEK and hold the record's derived plaintext (full field text /\n * a text-invertible vector), so leaving them means elevation never hid what\n * the record was searchable by — the `forget()` precedent, unapplied to\n * elevate. `null` → the record left tier 0: purge its `_vec`, and\n * invalidate the `_ftindex` blob (the cache-driven rebuild then excludes\n * it). A record → it is tier-0 again: re-embed it, and invalidate\n * `_ftindex` (rebuild includes it). No-op fast when the collection has no\n * search. */\n syncSearch(id: string, record: T | null, version?: number): Promise<void>\n /**\n * Sync a record's `_history` snapshots after a tier move rewraps the live\n * envelope (#712, at-rest hardening). Each snapshot carries its own key\n * material (a direct DEK wrap, or a per-record `_cek` itself wrapped under\n * the tier DEK) — `elevate()`'s live-body rewrap moves only the live\n * envelope, leaving prior versions decryptable at rest under the tier the\n * record has moved away from (the same leak class `forget()`'s\n * `tombstoneHistory` closed for erasure). `fromDek`/`toDek` are the SAME\n * DEKs the caller already resolved for the live rewrap — reuse them, don't\n * recompute. No-op when the strategy is `NO_HISTORY`.\n */\n syncHistory(id: string, fromDek: EnclaveKey, toDek: EnclaveKey): Promise<void>\n /**\n * Save a raw pre-move envelope as a `_history` snapshot (#728). Tier moves\n * (`putAtTier`/`elevate`/`demote`) bump `_v` and overwrite the live\n * envelope without ever snapshotting the version that existed just before\n * the move — so `history()` silently lost it. The caller (`tiers/index.ts`)\n * builds `envelope` by reusing the SAME `rewrapBodyToDek(envelope, fromDek,\n * toDek)` result it already computed for the live write, so the snapshot\n * lands wrapped under the DESTINATION tier's DEK — never `ctx.codec.\n * encryptRecord`, which always resolves the tier-0 DEK and would leak the\n * pre-move body whenever `fromTier > 0`. No-op when history is disabled\n * (folds the `historyConfig.enabled` gate so `tiers/index.ts` stays simple).\n */\n saveHistorySnapshot(id: string, envelope: EncryptedEnvelope): Promise<void>\n /**\n * `true` iff a real history strategy is wired (not `NO_HISTORY`) AND not\n * explicitly disabled via `historyConfig.enabled`. `putAtTier` checks this\n * BEFORE decrypting `existing` to build a snapshot (#728, #737 regression\n * fix) — unlike elevate/demote, which reuse a `body` rewrap their live\n * write needs unconditionally, putAtTier's live write never decrypts the\n * prior envelope, so building a snapshot is the ONLY reason it would\n * touch (and potentially throw decrypting) `existing`'s ciphertext. A\n * derivation-free, history-disabled collection must not pay that decode —\n * or risk it, on a corrupted/inaccessible prior body — same law\n * `hasDerivedOutputs` already enforces for the derived-outputs decode.\n */\n readonly historyEnabled: boolean\n /**\n * Rehome a record's blob attachments after a tier move (#724 Arc 10 Task\n * 2, at-rest hardening). A blob's home tier is its owning record's tier —\n * the `_blob` DEK is tier-scoped (`dekKey('_blob', tier)`), so a solo-\n * owned blob's content CEK must move with the record or it stays\n * tier-0-unwrappable at rest even once the live body has moved (the same\n * leak class `syncHistory` closes for `_history` snapshots). Delegates to\n * `BlobSet.rehomeForTier`; no-op fast when the collection has no blob\n * fields. Order-independent with the other sync hooks — touches only\n * blob side structures, not this collection's own cache/indexes.\n */\n syncBlobs(id: string, fromTier: number, toTier: number): Promise<void>\n /**\n * Purge a record's tier-0-era plaintext ledger deltas after a tier move\n * lands it above tier 0 (#729). The audit ledger is a flat, vault-wide,\n * hash-chained log — a record's reverse-JSON-Patch deltas in\n * `_ledger_deltas` are encrypted under one collection-wide `_ledger` DEK\n * (not the record's own tier DEK), so `elevate()`/`putAtTier()`'s live-body\n * rewrap never moves them: they stay readable at rest to any tier-0\n * caller, the same leak class `syncHistory` closed for `_history`\n * snapshots. Chain-safe: `verify()` reads only `_ledger` entry fields\n * (including `deltaHash`, which lives on the entry, not the deleted delta\n * row), never `_ledger_deltas`, so a purge cannot break it.\n * **Irreversible** — the deleted plaintext cannot be restored by\n * `demote()`. **Metadata-retained** — the `_ledger` entries (that the\n * record was mutated, at which version/timestamp/actor) are untouched;\n * only the delta *content* is purged. No-op when the collection has no\n * ledger (`withHistory()` not enabled).\n */\n syncLedger(id: string): Promise<void>\n /**\n * Sync a record's derived outputs (materialized-view rows, rollup\n * contributions, `withDerivation` outputs) after a tier move (#722).\n * Those outputs are computed from this record and written to OTHER\n * (output) collections via a plain `put` at tier 0 — `elevate()`'s live-\n * body rewrap moves only the source envelope, so the output rows keep\n * holding the source's tier-0-era plaintext, the same leak class\n * `syncSearch`/`syncIndexes` closed for THIS collection's own artifacts.\n * `elevated` (landing tier > 0) reuses the SAME onDelete fanout\n * `forgetDerivedFanout` drives for `forget()` (`kernel/via/dispatch.ts`)\n * — minus its `'ref'` cascade edge (elevate/demote never erase a\n * *different* record) — to recompute this record's derived outputs as\n * REMOVED. Recompute is tier-safe: the fanout's own source scan reads\n * the elevated-excluding cache (#701/#709/#712), so it naturally omits\n * the now-elevated record instead of re-embedding its plaintext. Landing\n * back at tier 0 (`elevated === false`) is the reverse (Task 2, #722):\n * `syncDerivedOutputs` instead runs the ordinary local-write add-\n * dispatchers (`dispatchDerivations`/`dispatchMaterializedViews`, the\n * same ones a plain `put()` fires) to restore the contribution —\n * reversible, since the source's plaintext survives the elevate/demote\n * rewrap round-trip. `record` is the decoded source record (PRE-move on\n * the remove side, POST-move on the add side); only the rollup edge\n * consumes it on remove, both add-dispatchers require it on add; `null`\n * (tombstone/delete-marker) skips both. `version` stamps the add-\n * dispatchers' `_derivedFrom.sourceVersion` metadata — reuses the version\n * the tier op already has, no re-read; unused on the remove side. No-op\n * fast when the collection has no MV/derivation source — each dispatcher\n * below checks its own source before doing any work.\n */\n syncDerived(id: string, record: T | null, elevated: boolean, version?: number): Promise<void>\n /**\n * `true` iff the collection has a materialized-view or derivation source\n * attached. Gates the {@link syncDerived} pre-move decode on the remove\n * direction (`elevate()`, `putAtTier(tier>0)`, `demote()`'s intermediate-\n * tier branch): those sites decode the record SOLELY to feed\n * `syncDerived(..., true)`, whose dispatchers no-op immediately when the\n * collection has no MV/derivation source — so a no-derivation collection\n * would otherwise pay a full record-body decrypt on every tier move for\n * nothing (#722 perf regression). `false` short-circuits the decode to\n * `null`, which `syncDerivedOutputs` already treats safely.\n */\n readonly hasDerivedOutputs: boolean\n /** Emit `_source`/`_sourceTs` provenance fields when a source is supplied. */\n readonly provenance: boolean\n /** Declared tiers, or null when the feature is off. */\n readonly tiers: ReadonlySet<number> | null\n /** Above-tier read visibility mode (`'invisibility'` | `'ghost'`). */\n readonly tierMode: TierMode\n /** Resolve a tier DEK by its `dekKey(name, tier)`. */\n getDEK(key: string): Promise<EnclaveKey>\n /** Fire a cross-tier access event (sink stays collection-resident). */\n emitCrossTierEvent(event: CrossTierAccessEvent): void\n /**\n * Register this record's ref in the encrypted `_subject_index` (#766).\n * `putAtTier`'s raw `ctx.adapter.put` bypasses `Collection.put()`'s write-\n * hook pipeline — the `onAfterWrite` hook that normally does this — so a\n * record whose FIRST persistence is `putAtTier` (sensitive from birth, a\n * documented legitimate use) would otherwise never enter the index and\n * stay unreachable by `vault.forget()`. Idempotent (safe on every call,\n * including a repeat write of an already-registered record) and a no-op\n * when the collection declares no forget-subject field.\n */\n addSubjectRef(id: string, record: T): Promise<void>\n}\n\nexport function assertTiersEnabled<T>(ctx: TiersContext<T>): void {\n if (!ctx.tiers) {\n throw new Error(\n `Collection \"${ctx.name}\": hierarchical tiers are not enabled. ` +\n `Pass { tiers: [0, 1, 2, …] } to vault.collection() to opt in.`,\n )\n }\n}\n\nexport function assertDeclaredTier<T>(ctx: TiersContext<T>, tier: number): void {\n if (tier < 0 || !Number.isInteger(tier)) {\n throw new Error(`Collection \"${ctx.name}\": tier must be a non-negative integer, got ${tier}`)\n }\n if (tier === 0) return\n if (!ctx.tiers || !ctx.tiers.has(tier)) {\n throw new Error(\n `Collection \"${ctx.name}\": tier ${tier} is not declared in { tiers: [...] }`,\n )\n }\n}\n\n/**\n * Tier-composition guard (#724 / Arc 7 of the tier-invisibility campaign).\n *\n * Refuses `tiers` declared together with a derived-artifact feature whose\n * crypto has not yet been made tier-aware — i.e. a feature `elevate()` /\n * `demote()` does not re-key when a record moves tiers, so an elevated\n * record's data for that feature would stay readable at tier 0.\n *\n * **Status:** the original call site (`Collection` constructor, beside\n * `buildUniqueConstraintSet`) was removed by Arc 10 Task 1 (#724) — the\n * `tiers + blobFields` refusal below was superseded by a runtime read gate\n * on `collection.blob(id)` (`with-shape/blobs/blob-set.ts`), so this\n * function currently has no caller. Relocated here from\n * `with-lookup/indexing/unique-constraints.ts` (#733) so the tier-domain\n * guard lives in the tier domain, rather than an indexing file it only\n * borrowed for a grandfathered import specifier.\n *\n * ## #724 verified: `tiers + blobFields` leaks\n *\n * `collection.blob(id)` (`Collection.blob()`) never checks the live\n * record's tier before returning a `BlobSet` handle, and `BlobSet`'s crypto\n * is entirely orthogonal to the tier ladder:\n * - the slot map (`_blob_slots_{collection}/{id}`) is encrypted under the\n * collection's TIER-0 DEK (`getDEK(name)` ≡ `dekKey(name, 0)` — see\n * `dekKey` in `with-party/team/tiers.ts`), never a tier-N DEK;\n * - chunk content (`_blob_index` / `_blob_chunks`) is encrypted under the\n * vault-shared `_blob` DEK (`BLOB_COLLECTION`), which has no tier\n * dimension at all.\n * `elevate()`/`demote()` (this file) rewrap only the live record body,\n * `_history` snapshots (#712), and index side-cars — blob slots and chunks\n * are untouched. A caller whose keyring never held the record's elevated\n * tier DEK can still open `collection.blob(id).get(slot)` and read full\n * plaintext, even though `collection.get(id)` correctly reports the same\n * record as invisible. See `__tests__/tier-composition-guard.test.ts` for\n * the reproduction.\n *\n * ## Safe today — no check needed here\n *\n * Field indexes, search (`textIndexes`/`embeddings`), `withHistory`\n * (snapshot keys are rewrapped by `elevate()`/`demote()`, #712), and the\n * decoded-record cache (evicted on tier moves) are already tier-safe. They\n * are not enumerated below; only known-unsafe features are refused, so any\n * combination not named here passes silently by default.\n *\n * ## Deliberately NOT handled here\n *\n * - `ledger` — `withHistory` threads a ledger writer onto every tiered\n * collection by default in shipped usage; refusing `tiers + ledger` would\n * break that default. A ledger-specific tier-rekey handler (rewrap ledger\n * entries, or scope ledger visibility to tier) is a separate arc.\n * - materialized-view (MV) output — an MV's fanout spec lives on the\n * SOURCE collection, not on the MV collection's own config, so it is not\n * visible at `vault.collection()` registration time and this guard cannot\n * detect it structurally.\n */\nexport function assertTierComposition(\n collectionName: string,\n cfg: { readonly tiers: boolean; readonly blobFields: BlobFieldsConfig | undefined },\n): void {\n if (!cfg.tiers) return\n if (cfg.blobFields !== undefined && Object.keys(cfg.blobFields).length > 0) {\n throw new UnsupportedTierCompositionError(\n 'blobs',\n `Collection \"${collectionName}\": blobFields are not supported together with tiers (#724) — ` +\n `blob chunk content is encrypted under a vault-shared DEK that elevate()/demote() do not ` +\n `re-key, so an elevated record's blob attachments would remain readable at tier 0. Use a ` +\n `non-tiered collection for blob-bearing records until a blob tier-rekey handler ships.`,\n )\n }\n}\n\nfunction isElevatorOrOwner<T>(ctx: TiersContext<T>): boolean {\n return ctx.keyring.role === 'owner' || ctx.keyring.role === 'admin'\n}\n\n/**\n * Structural surface {@link syncDerivedOutputs} needs from the owning\n * collection — the SAME onDelete dispatchers `forgetDerivedFanout` already\n * drives (`kernel/via/dispatch.ts`), reused as-is (#722). Narrower than\n * `Collection<T>` (avoids an import cycle: collection.ts constructs\n * `TiersContext` FROM `this`, which already satisfies this shape\n * structurally — no cast needed at the `tiersContext()` call site).\n */\nexport interface DerivedOutputsHost<T> {\n dispatchMaterializedViewsOnDelete(id: string): Promise<{ deleted: number; residueUndecodable: string[]; residueDeclined: string[] }>\n dispatchArrayDerivationsOnDelete(id: string, eraseRecordShapeToo?: boolean): Promise<number>\n dispatchRollupsOnDelete(id: string, deleted: T): Promise<unknown>\n /** Task 2 (#722) add-direction: the same local-write dispatchers an ordinary `put()` fires. */\n dispatchMaterializedViews(id: string, record: T): Promise<void>\n dispatchDerivations(id: string, record: T, version: number): Promise<void>\n}\n\n/**\n * The {@link TiersContext.syncDerived} implementation collection.ts's\n * `tiersContext()` binds `this` into (#722). See that field's doc comment\n * for the design; `host` is `this` — every dispatcher below is a public\n * Collection method, already self-guarding (no-op) when the collection has\n * no materialized-view/derivation source, so no separate fast-path check is\n * needed here.\n */\nexport async function syncDerivedOutputs<T>(\n host: DerivedOutputsHost<T>,\n id: string,\n record: T | null,\n elevated: boolean,\n version?: number,\n): Promise<void> {\n if (elevated) {\n await host.dispatchMaterializedViewsOnDelete(id)\n await host.dispatchArrayDerivationsOnDelete(id, true)\n if (record !== null) await host.dispatchRollupsOnDelete(id, record)\n return\n }\n // Task 2 (#722) recompute-as-add: the record rejoined tier 0 (demote(→0) /\n // putAtTier(0)) — restore its contribution via the SAME local-write\n // dispatchers an ordinary `put()` fires (`collection.ts`'s\n // `_onRecordMutated('local-write')`), same order. `record === null` only\n // when a caller demotes a tombstone/delete-marker to 0 — nothing to add.\n if (record === null) return\n await host.dispatchDerivations(id, record, version ?? 0)\n await host.dispatchMaterializedViews(id, record)\n}\n\n/**\n * tier-aware put. Encrypts the record with the collection's tier-N DEK and\n * stamps `_tier: N` on the envelope. The caller's keyring must hold the tier-N\n * DEK (directly, by delegation, or by virtue of being the grantor); otherwise\n * throws `TierNotGrantedError`.\n *\n * accepts an optional `elevation` context. When present, the emitted cross-tier\n * event is stamped with `authorization: 'elevation'`, the elevation's reason,\n * and the caller's pre-elevation tier. `vault.elevate(...).collection().put`\n * threads this through; direct `putAtTier` calls leave it undefined and fall\n * back to the inherent-write event shape.\n */\nexport async function putAtTier<T>(\n ctx: TiersContext<T>,\n id: string,\n record: T,\n tier: number,\n opts?: { elevation?: { reason: string; fromTier: number }; source?: string; sourceTs?: string },\n): Promise<TierMoveResult> {\n assertTiersEnabled(ctx)\n assertDeclaredTier(ctx, tier)\n assertTierAccess(ctx.keyring, ctx.name, tier)\n\n const key = dekKey(ctx.name, tier)\n const dek = await ctx.getDEK(key)\n\n const existing = await ctx.adapter.get(ctx.vault, ctx.name, id)\n // #712/whole-branch-fix-1: putAtTier moves the record OFF its existing\n // tier, same as elevate/demote — so the caller must also be cleared for\n // that existing tier, not just the target. Without this, a member holding\n // only the target tier's DEK could putAtTier over a record parked at a\n // tier they've never been granted; the from-tier `getDEK` below would then\n // silently MINT a fresh DEK for that tier into their keyring (a\n // non-cleared caller creating tier key material inside the trust\n // boundary — see `assertTierAccess`'s doc comment). Gate here, BEFORE any\n // from-tier getDEK/syncHistory call, so the mint never happens; owner/\n // admin/custodian still bypass (they may mint, same as elevate/demote).\n assertTierAccess(ctx.keyring, ctx.name, existing?._tier ?? 0)\n const version = existing ? existing._v + 1 : 1\n\n // #728 review-fix-2: resolve the from-tier DEK and snapshot the PRE-move\n // version BEFORE the live write below, mirroring elevate()/demote() —\n // originally this ran much later (after syncIndexes/syncLedger/syncDerived/\n // syncSearchResilient), so a throw in any of those left the live record\n // already moved with `existing` — its sole pre-move copy — gone, silently\n // losing the version #728 exists to preserve. `fromKey`/`fromDek` are\n // resolved ONCE here and reused below for the trailing syncHistory rewrap.\n const fromKey = dekKey(ctx.name, existing?._tier ?? 0)\n const fromDek = await ctx.getDEK(fromKey)\n if (existing && ctx.historyEnabled) {\n // Snapshot the version this write is about to overwrite — same law as\n // Collection.put()'s history save, extended to putAtTier (#728). Rewrap\n // under the DESTINATION `dek` via `applyRewrappedBody` (never\n // `ctx.codec.encryptRecord`, which always resolves the tier-0 DEK and\n // would leak this body at rest whenever the prior tier was > 0).\n // Untagged (`_tier`/`_elevatedBy` stripped), same as elevate()/\n // demote()'s snapshots. `_v` stays the PRE-move version (unbumped), as\n // `preCarried` already carries it.\n const body = await rewrapBodyToDek(existing, fromDek, dek)\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- excluded from ...preCarried\n const { _tier: _preTier, _elevatedBy: _preElevatedBy, ...preCarried } = existing\n await ctx.saveHistorySnapshot(id, applyRewrappedBody(preCarried, body))\n }\n\n const json = JSON.stringify(record)\n const { iv, data } = await encrypt(json, dek)\n const envelope: EncryptedEnvelope = {\n _noydb: NOYDB_FORMAT_VERSION,\n _v: version,\n _ts: new Date().toISOString(),\n _iv: iv,\n _data: data,\n _by: ctx.keyring.userId,\n ...(tier > 0 && { _tier: tier }),\n ...(ctx.provenance && opts?.source !== undefined ? { _source: opts.source, _sourceTs: opts.sourceTs ?? new Date().toISOString() } : {}),\n }\n\n await ctx.adapter.put(ctx.vault, ctx.name, id, envelope)\n\n // #766: putAtTier bypasses Collection.put()'s onAfterWrite hook — register\n // the subject ref directly so a sensitive-from-birth first write stays\n // reachable by vault.forget(). No-op when no forget-subject field is declared.\n await ctx.addSubjectRef(id, record)\n\n // #702/#709: keep the record cache AND indexes coherent with the raw\n // write — same law as elevate/demote (#691): tier > 0 → invisible on\n // tier-0 surfaces (evict + purge); tier 0 → an ordinary write (re-seed +\n // reindex). syncIndexes runs first — it needs the pre-write cache entry\n // as \"previous\", which syncCache is about to overwrite/evict.\n if (tier > 0) {\n await ctx.syncIndexes(id, null, version)\n ctx.syncCache(id, null)\n // #729: the record lands above tier 0 — purge its tier-0-era plaintext\n // ledger deltas (irreversible; a tier-0 putAtTier(0) has nothing to purge).\n await ctx.syncLedger(id)\n // #722: recompute this record's derived outputs now that it left tier 0\n // — same law as elevate() below. `existing` is the PRE-write envelope;\n // decode it only here (the rollup edge is the only consumer). Gated by\n // `hasDerivedOutputs` — no-derivation collections skip this decrypt\n // entirely (perf regression review finding, Arc 9 #722). Whole-branch\n // review: `existing` may sit at ANY prior tier (including a\n // `putAtTier`-origin tier>0 record with no `_cek` ever minted) — decode\n // it under ITS OWN tier's DEK via `codec.decryptRecordAtDek`, not\n // `ctx.codec.decryptRecord`'s tier-unaware default, which threw\n // `TamperedError` here.\n await ctx.syncDerived(\n id,\n ctx.hasDerivedOutputs && existing\n ? await ctx.codec.decryptRecordAtDek(existing, await ctx.getDEK(dekKey(ctx.name, existing._tier ?? 0)), id)\n : null,\n true,\n )\n } else {\n const rec = await ctx.codec.decryptRecord(envelope, { id, sealedAsHandles: true })\n await ctx.syncIndexes(id, rec, envelope._v, existing ?? undefined)\n ctx.syncCache(id, rec !== null ? { record: rec, version: envelope._v } : null)\n // #722 Task 2: the record is written at tier 0 — restore its\n // contribution to every derived output (reuse the decode above, no\n // double-decrypt).\n await ctx.syncDerived(id, rec, false, envelope._v)\n }\n // #721: search artifacts follow the same tier > 0 → purge / tier 0 →\n // re-embed law as syncIndexes above. No ordering dependency on syncCache —\n // the _ftindex rebuild is deferred to the next retrieve() and _vec re-embed\n // reads `record` directly, not the cache. #774: a permanently stuck\n // compensation must not abort the write — the record put above already\n // landed — so it is caught and surfaced as residue instead, same as\n // elevate()/demote() (#764).\n const searchResidue = await syncSearchResilient(ctx, id, tier > 0 ? null : record, envelope._v)\n\n if (tier > 0) {\n ctx.emitCrossTierEvent({\n actor: ctx.keyring.userId,\n collection: ctx.name,\n id,\n tier,\n authorization: opts?.elevation ? 'elevation' : 'inherent',\n op: 'put',\n ts: envelope._ts,\n ...(opts?.elevation && {\n reason: opts.elevation.reason,\n elevatedFrom: opts.elevation.fromTier,\n }),\n })\n }\n\n // #712/whole-branch-fix-2: syncHistory runs LAST — after syncIndexes/\n // syncCache/syncSearch (and the cross-tier emit) have already landed. Its\n // own throw (adapter I/O, the Fix-1 assertion, the rewrap) then strands\n // only the `_history` artifact instead of leaving indexes/cache/search\n // unsynced behind an already-moved live record (would conditionally\n // reopen #709/#721/#723 on the error path). A direct putAtTier also moves\n // the record's tier — its history snapshots (if any exist from an earlier\n // tier-0 put()) must follow, same as elevate/demote. Skip when the record\n // was already at this tier (no move, nothing to rewrap). `fromKey`/\n // `fromDek` were already resolved above (review-fix-2), before the live\n // write, for the pre-move snapshot — reused here rather than recomputed.\n if (fromKey !== key) {\n await ctx.syncHistory(id, fromDek, dek)\n // #724: this putAtTier moved the record's tier — rehome its blobs too.\n await ctx.syncBlobs(id, existing?._tier ?? 0, tier)\n }\n return { searchResidue }\n}\n\n/**\n * tier-aware get. When the stored record is at a tier the caller cannot\n * decrypt:\n * - `'invisibility'` mode (default) → returns `null`.\n * - `'ghost'` mode → returns a `GhostRecord` placeholder with the tier and\n * the record id (the record exists but contents are withheld).\n *\n * Fully-cleared reads return the plaintext record and fire a cross-tier event\n * when `_tier > 0`.\n */\nexport async function getAtTier<T>(ctx: TiersContext<T>, id: string): Promise<T | GhostRecord | null> {\n assertTiersEnabled(ctx)\n const envelope = await ctx.adapter.get(ctx.vault, ctx.name, id)\n if (!envelope) return null\n const tier = envelope._tier ?? 0\n if (tier === 0) {\n return ctx.codec.decryptRecord(envelope, { id })\n }\n\n const key = dekKey(ctx.name, tier)\n if (!ctx.keyring.deks.has(key)) {\n if (ctx.tierMode === 'ghost') {\n return { _ghost: true, _tier: tier } as GhostRecord\n }\n return null\n }\n\n const dek = await ctx.getDEK(key)\n // A tiered record may carry a per-record CEK (e.g. a CEK record\n // elevated via `elevate()`): the CEK is wrapped under the TIER DEK, so\n // unwrap under the tier DEK then decrypt the body under the CEK. Legacy\n // tiered records decrypt directly under the tier DEK.\n let plaintext: string\n let cek: EnclaveKey | undefined\n if (envelope._cek !== undefined) {\n cek = await unwrapCek(envelope._cek, dek)\n ctx.cekCache?.set(id, cek, 1)\n plaintext = await decrypt(envelope._iv, envelope._data, cek)\n } else {\n plaintext = await decrypt(envelope._iv, envelope._data, dek)\n }\n let record = JSON.parse(plaintext) as T\n\n // #635: this manual-decrypt leg never went through `decryptRecord`, so it\n // never processed `_sealed` slots — an elevated classified record read\n // back through here would return the body WITHOUT its sealed fields\n // (silent omission). `applySealedSlots` is the same post-processing\n // `decryptRecord`'s tier-0 branch above gets for free, extracted so it can\n // take OUR already-unwrapped `cek` (unwrapped under the TIER dek above)\n // instead of resolving one itself under the collection DEK the way\n // `decryptRecord`/`resolveEnvelopeCek` would — which would be the wrong\n // key for a `_cek` wrapped under a tier DEK. `sealedAsHandles` is omitted\n // (default false) to match this function's OWN tier-0 branch above\n // (`ctx.codec.decryptRecord(envelope, { id })`, no `sealedAsHandles`) —\n // both tiers return sealed fields inline-decrypted, not as handles.\n if (envelope._sealed !== undefined) {\n record = await ctx.codec.applySealedSlots(record, envelope._sealed, cek, { id })\n }\n\n ctx.emitCrossTierEvent({\n actor: ctx.keyring.userId,\n collection: ctx.name,\n id,\n tier,\n authorization: isElevatorOrOwner(ctx) ? 'inherent' : 'delegation',\n op: 'get',\n ts: new Date().toISOString(),\n })\n\n return record\n}\n\n/**\n * list ids grouped by the caller's readability. Returns only ids whose tier the\n * caller can read. Above-tier ids are omitted in `'invisibility'` mode and\n * included (with tier metadata) in `'ghost'` mode.\n */\nexport async function listAtTier<T>(ctx: TiersContext<T>): Promise<Array<{ id: string; tier: number; readable: boolean }>> {\n assertTiersEnabled(ctx)\n const ids = await ctx.adapter.list(ctx.vault, ctx.name)\n const out: Array<{ id: string; tier: number; readable: boolean }> = []\n for (const id of ids) {\n const env = await ctx.adapter.get(ctx.vault, ctx.name, id)\n if (!env) continue\n const tier = env._tier ?? 0\n const readable = tier === 0 || ctx.keyring.deks.has(dekKey(ctx.name, tier))\n if (!readable && ctx.tierMode === 'invisibility') continue\n out.push({ id, tier, readable })\n }\n return out\n}\n\n/**\n * Result of a completed `elevate()`/`demote()` (#764). The record move\n * (put + indexes/cache/ledger/derived/history/blobs sync) always completes —\n * `searchResidue: true` means only the search-index side (`_vec`/`_ftindex`)\n * was left in a needs-retry state (a stuck `PersistedIndexCompensationError`),\n * mirroring `forget()`'s `indexResidue` posture rather than aborting the move.\n */\nexport interface TierMoveResult {\n readonly searchResidue: boolean\n}\n\n/**\n * Run {@link TiersContext.syncSearch} with #764 resilience: a permanently\n * stuck compensation (`PersistedIndexCompensationError`, thrown by\n * `PersistedIndexStore` when its compensating `remove()` of a stale\n * `_ftindex` blob keeps failing) must not abort `elevate()`/`demote()` — the\n * record's tier-move `put` has already landed, and aborting here would leave\n * `syncLedger`/`syncDerived`/`syncHistory`/`syncBlobs` unsynced behind it (the\n * partial-completion hazard #764 names). Same posture `forget()` takes for\n * `_purgeSearchIndex` (`vault.ts`'s `indexResidue`) — everything else (a\n * genuinely unexpected search-hook error) still aborts, undisturbed.\n */\nasync function syncSearchResilient<T>(ctx: TiersContext<T>, id: string, record: T | null, version?: number): Promise<boolean> {\n try {\n await ctx.syncSearch(id, record, version)\n return false\n } catch (e) {\n if (e instanceof PersistedIndexCompensationError) return true\n throw e\n }\n}\n\n/**\n * elevate a record to a higher tier. Re-encrypts with the target tier's DEK.\n * The caller must hold DEKs for both the current tier (to decrypt) and the\n * target tier (to re-encrypt). Stamps `_elevatedBy` with the caller id so\n * `demote()` can check the reverse operation.\n */\nexport async function elevate<T>(ctx: TiersContext<T>, id: string, toTier: number): Promise<TierMoveResult> {\n assertTiersEnabled(ctx)\n assertDeclaredTier(ctx, toTier)\n assertTierAccess(ctx.keyring, ctx.name, toTier)\n\n const envelope = await ctx.adapter.get(ctx.vault, ctx.name, id)\n if (!envelope || isDeleteMarker(envelope) || isTombstoneShape(envelope)) {\n throw new Error(`Record \"${id}\" not found in collection \"${ctx.name}\"`)\n }\n const fromTier = envelope._tier ?? 0\n if (toTier === fromTier) return { searchResidue: false }\n if (toTier < fromTier) {\n throw new Error(`Use demote() to lower the tier of \"${id}\" from ${fromTier} to ${toTier}`)\n }\n // Caller must have access at the existing tier to decrypt.\n if (fromTier > 0) assertTierAccess(ctx.keyring, ctx.name, fromTier)\n\n const fromKey = dekKey(ctx.name, fromTier)\n const toKey = dekKey(ctx.name, toTier)\n const fromDek = await ctx.getDEK(fromKey)\n const toDek = await ctx.getDEK(toKey)\n\n // Per-record CEK composes with tiers: the body key is unchanged (history\n // chain identity preserved); only the wrapping key moves with the tier.\n // Legacy (no `_cek`) records take the direct-DEK path unchanged.\n const now = new Date().toISOString()\n const body = await rewrapBodyToDek(envelope, fromDek, toDek)\n if (body.cek) ctx.cekCache?.set(id, body.cek, 1)\n // #728: snapshot the PRE-move version into `_history` before it's\n // overwritten below — reuses `body` (already fromDek→toDek) via\n // `applyRewrappedBody` (never `ctx.codec.encryptRecord`, which always\n // resolves the tier-0 DEK and would leak this body at rest whenever\n // `fromTier > 0`). Untagged (`_tier`/`_elevatedBy` stripped) so the\n // history read-gate doesn't hide it PERMANENTLY once the record demotes\n // back — at-rest protection comes from the ciphertext being under\n // `toDek`, not from the tag. `_v` stays the PRE-move version (unbumped),\n // exactly as `envelope` already carries it.\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- excluded from ...snapshot\n const { _tier: _preTier, _elevatedBy: _preElevatedBy, ...snapshot } = applyRewrappedBody(envelope, body)\n await ctx.saveHistorySnapshot(id, snapshot)\n // #662: spread every slot the source carries (_sealed/_det/_vdig/_bidx/\n // _source/_sourceTs/_debug) through UNCHANGED, then override only\n // the fields a tier move manages. rewrapBodyToDek preserves the CEK, so no\n // passenger slot is re-keyed by the move. The old field-literal dropped every\n // unlisted slot.\n const next: EncryptedEnvelope = {\n ...envelope,\n _noydb: NOYDB_FORMAT_VERSION,\n _v: envelope._v + 1,\n _ts: now,\n _iv: body._iv,\n _data: body._data,\n _by: ctx.keyring.userId,\n _tier: toTier,\n _elevatedBy: ctx.keyring.userId,\n ...(body._cek !== undefined ? { _cek: body._cek } : {}),\n }\n await ctx.adapter.put(ctx.vault, ctx.name, id, next)\n // Evict/purge only once the write landed — a throwing put must not blind\n // the eager cache or leave a readable index behind for a still-valid\n // tier-0 record (same ordering as demote). #709: syncIndexes runs first —\n // it needs the pre-elevation cache entry as \"previous\" to clean the\n // eager index bucket; the persisted side-car purge is content-free.\n await ctx.syncIndexes(id, null, next._v)\n ctx.syncCache(id, null)\n // #721: same purge law as syncIndexes above — the record left tier 0, so\n // its _vec sidecar is purged and _ftindex is invalidated. #764: a\n // permanently stuck compensation must not abort the move — the record put\n // above already landed — so it is caught and surfaced as residue instead.\n const searchResidue = await syncSearchResilient(ctx, id, null)\n // #729: elevate always lands the record at tier > 0 — purge its\n // tier-0-era plaintext ledger deltas (irreversible; entry metadata stays).\n await ctx.syncLedger(id)\n // #722: recompute this record's derived outputs now that it left tier 0 —\n // same law as putAtTier(tier>0) above. `envelope` is the PRE-move envelope\n // (captured before the rewrap), decoded only here (the rollup edge is the\n // only consumer of the record content). Gated by `hasDerivedOutputs` — no-\n // derivation collections skip this decrypt entirely (perf regression\n // review finding, Arc 9 #722). Whole-branch review: decode under `fromDek`\n // via `codec.decryptRecordAtDek` — `fromTier` may be > 0 here (a prior\n // elevate), and `ctx.codec.decryptRecord`'s tier-unaware CEK resolution\n // threw `TamperedError` whenever this op's own rewrap hadn't just primed\n // the cekCache (non-`perRecordKeys` collections; `_cek`-absent bodies).\n await ctx.syncDerived(id, ctx.hasDerivedOutputs ? await ctx.codec.decryptRecordAtDek(envelope, fromDek, id) : null, true)\n\n ctx.emitCrossTierEvent({\n actor: ctx.keyring.userId,\n collection: ctx.name,\n id,\n tier: toTier,\n authorization: 'elevation',\n op: 'elevate',\n ts: now,\n })\n\n // #712/whole-branch-fix-2: syncHistory runs LAST — after syncIndexes/\n // syncCache/syncSearch/the cross-tier emit have already landed, so its own\n // throw (adapter I/O, the rewrap) strands only the `_history` artifact\n // instead of leaving indexes/cache/search unsynced behind an already-moved\n // live record (would conditionally reopen #709/#721/#723 on the error\n // path). Rewraps prior-version history snapshots to the SAME toTier DEK —\n // otherwise they stay decryptable at rest under fromDek forever.\n await ctx.syncHistory(id, fromDek, toDek)\n // #724: rehome any solo-owned blob attachments to the target tier's\n // `_blob` DEK — same at-rest law as syncHistory above.\n await ctx.syncBlobs(id, fromTier, toTier)\n return { searchResidue }\n}\n\n/**\n * demote a record to a lower tier. Allowed only for the user who performed the\n * last elevation or an owner.\n */\nexport async function demote<T>(ctx: TiersContext<T>, id: string, toTier: number): Promise<TierMoveResult> {\n assertTiersEnabled(ctx)\n if (toTier < 0) throw new Error(`Cannot demote to negative tier ${toTier}`)\n\n const envelope = await ctx.adapter.get(ctx.vault, ctx.name, id)\n if (!envelope || isDeleteMarker(envelope) || isTombstoneShape(envelope)) {\n throw new Error(`Record \"${id}\" not found in collection \"${ctx.name}\"`)\n }\n const fromTier = envelope._tier ?? 0\n if (toTier === fromTier) return { searchResidue: false }\n if (toTier > fromTier) {\n throw new Error(`Use elevate() to raise the tier of \"${id}\" from ${fromTier} to ${toTier}`)\n }\n const isOwner = ctx.keyring.role === 'owner'\n const isOriginalElevator = envelope._elevatedBy === ctx.keyring.userId\n if (!isOwner && !isOriginalElevator) {\n throw new TierDemoteDeniedError(id, fromTier)\n }\n // Caller must still hold the DEK of the current tier to decrypt.\n assertTierAccess(ctx.keyring, ctx.name, fromTier)\n if (toTier > 0) assertDeclaredTier(ctx, toTier)\n\n const fromDek = await ctx.getDEK(dekKey(ctx.name, fromTier))\n const toDek = await ctx.getDEK(dekKey(ctx.name, toTier))\n\n // CEK re-wrap on demote — same body key, moved from the source tier\n // DEK to the target tier DEK. Legacy records take the direct-DEK path.\n const now = new Date().toISOString()\n const body = await rewrapBodyToDek(envelope, fromDek, toDek)\n if (body.cek) ctx.cekCache?.set(id, body.cek, 1)\n // #662: same passenger carry-through as elevate(). demote additionally CLEARS\n // `_elevatedBy` (the demote right is consumed) and OMITS `_tier` at tier 0, so\n // both are destructured out of the spread rather than carried.\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- excluded from ...carried\n const { _elevatedBy, _tier: _priorTier, ...carried } = envelope\n // #728: snapshot the PRE-move version into `_history` — reuses `body`\n // (already fromDek→toDek) and `carried` (envelope minus `_tier`/\n // `_elevatedBy`, computed above for the live write) via\n // `applyRewrappedBody`, so a tier-0-landing demote's snapshot is untagged\n // just like an ordinary put() entry, same law as elevate()'s snapshot\n // above. `_v` stays the PRE-move version (unbumped), as `carried` already\n // carries it.\n await ctx.saveHistorySnapshot(id, applyRewrappedBody(carried, body))\n const next: EncryptedEnvelope = {\n ...carried,\n _noydb: NOYDB_FORMAT_VERSION,\n _v: envelope._v + 1,\n _ts: now,\n _iv: body._iv,\n _data: body._data,\n _by: ctx.keyring.userId,\n ...(toTier > 0 ? { _tier: toTier } : {}),\n ...(body._cek !== undefined ? { _cek: body._cek } : {}),\n }\n await ctx.adapter.put(ctx.vault, ctx.name, id, next)\n\n // #691/#709: landing back at tier 0 makes this a plain tier-0 record\n // again — it must stay plain-get()-readable AND indexed in the same\n // session, so re-seed the eager cache and rebuild its index entries from\n // the just-written (now tier-0-keyed) envelope through the canonical\n // codec path, the same decode collection.ts's own eager hydration / lazy\n // cache-miss paths use — reused for both, no double-decrypt. A demote\n // that lands on an intermediate elevated tier (toTier > 0) still evicts\n // + purges: the record stays above tier 0 and must remain invisible on\n // tier-0 surfaces, unindexed. syncIndexes runs first in both branches —\n // it needs the pre-demote cache entry as \"previous\".\n // #764: a permanently stuck search-index compensation must not abort the\n // move — the record put above already landed — so it is caught in both\n // branches and surfaced as residue on the return value instead.\n let searchResidue: boolean\n if (toTier === 0) {\n const rec = await ctx.codec.decryptRecord(next, { id, sealedAsHandles: true })\n await ctx.syncIndexes(id, rec, next._v, envelope)\n ctx.syncCache(id, rec !== null ? { record: rec, version: next._v } : null)\n // #721: reuse the decode above — no double-decrypt. The record is tier-0\n // again, so re-embed its _vec and invalidate _ftindex to include it.\n searchResidue = await syncSearchResilient(ctx, id, rec, next._v)\n // #722 Task 2: the record rejoined tier 0 — restore its contribution to\n // every derived output (reuse the decode above, no double-decrypt).\n await ctx.syncDerived(id, rec, false, next._v)\n } else {\n await ctx.syncIndexes(id, null, next._v)\n ctx.syncCache(id, null)\n // #721: still above tier 0 — purge _vec, invalidate _ftindex.\n searchResidue = await syncSearchResilient(ctx, id, null)\n // #722 Task 2: still elevated (an intermediate tier) — the source's\n // derived outputs stay recompute-as-removed, same law as elevate()\n // above. `envelope` is the PRE-move envelope captured at function\n // entry; decoded only here (the rollup edge is the only consumer).\n // Gated by `hasDerivedOutputs` — no-derivation collections skip this\n // decrypt entirely (perf regression review finding, Arc 9 #722).\n // Whole-branch review: decode under `fromDek` via\n // `codec.decryptRecordAtDek` — this branch's `fromTier` is always > 0 (a\n // demote FROM tier `fromTier` TO an intermediate `toTier` > 0), so\n // `ctx.codec.decryptRecord`'s tier-unaware CEK resolution threw\n // `TamperedError` here exactly as it did in elevate() above\n // (code-identical bug, same fix).\n await ctx.syncDerived(id, ctx.hasDerivedOutputs ? await ctx.codec.decryptRecordAtDek(envelope, fromDek, id) : null, true)\n }\n\n ctx.emitCrossTierEvent({\n actor: ctx.keyring.userId,\n collection: ctx.name,\n id,\n tier: fromTier,\n authorization: 'elevation',\n op: 'demote',\n ts: now,\n })\n\n // #712/whole-branch-fix-2: syncHistory runs LAST — after syncIndexes/\n // syncCache/syncSearch/the cross-tier emit have already landed, so its own\n // throw (adapter I/O, the rewrap) strands only the `_history` artifact\n // instead of leaving indexes/cache/search unsynced behind an already-moved\n // live record (would conditionally reopen #709/#721/#723 on the error\n // path). Rewraps history snapshots the same direction as the live body —\n // demote restores tier-0 (or intermediate-tier) readability at rest.\n await ctx.syncHistory(id, fromDek, toDek)\n // #724: rehome any solo-owned blob attachments the same direction as the\n // live body — restores tier-0 (or intermediate-tier) readability at rest.\n await ctx.syncBlobs(id, fromTier, toTier)\n return { searchResidue }\n}\n\n/**\n * Classify a live envelope's `_sealed` slots for crypto-shred completeness\n * (#M-1). Thin delegate to {@link RecordCodec.classifySealedShred}; surfaced\n * here because `vault.ts` forget() reaches in via `collection._classifySealedShred`.\n */\nexport function classifySealedShred<T>(\n ctx: TiersContext<T>,\n live: EncryptedEnvelope,\n): Promise<{ readonly slots: readonly SealedShredSlot[] }> {\n return ctx.codec.classifySealedShred(live)\n}\n\n// #837 — classifySealedShred's slot type must be nameable from this entry.\nexport type { SealedShredSlot } from '../../kernel/enclave/index.js'\n"],"mappings":"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","names":["putAtTier","getAtTier","listAtTier","elevate","demote"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/kernel/validation.ts","../src/with-party/team/echo-secret.ts","../src/with-party/team/reserved-secret-collections.ts","../src/with-party/team/keyring.ts"],"sourcesContent":["/**\n * Secret validation — phrase format (per the three-tier session-tiers\n * design, locked 2026-05-04).\n *\n * Secrets are **phrases**: multiple simple words, easy to remember,\n * structurally constrained so a weak choice cannot silently collapse the\n * security floor. The format is intentionally narrow: lowercase letters\n * and single spaces only, no punctuation, no symbols, no digits.\n *\n * - Default minimum: 6 words (~77 bits with the 7,776-word EFF list).\n * - Strict minimum: 8 words (~103 bits).\n * - Per-word minimum: 3 characters (excludes \"a\", \"is\", \"of\").\n * - Adjacent repeats rejected (\"the the\").\n *\n * The hub runs validation default-on at every secret ingress\n * (`createOwnerKeyring`, `grant`, `rotateSecret`); test fixtures and\n * CLI scripts override via `{ allowWeakSecret: true }`.\n *\n * @module\n */\nimport { NoydbError, ValidationError } from './errors.js'\n\n/** All reasons a phrase can be rejected. */\nexport type WeakSecretReason =\n | 'empty'\n | 'invalid-chars'\n | 'leading-or-trailing-space'\n | 'double-space'\n | 'too-few-words'\n | 'word-too-short'\n | 'repeated-adjacent'\n\n/** Per-vault knobs. Aligns with `VaultPolicy.secret`. */\nexport interface SecretPolicy {\n /** Minimum number of words. Default 6. Strict policy uses 8. */\n readonly minWords?: number\n /** Minimum characters per word. Default 3. */\n readonly minWordLength?: number\n /** Reject adjacent identical words (\"the the\"). Default true. */\n readonly rejectRepeatedAdjacent?: boolean\n /**\n * Override the default character-class rule (`/^[a-z]+( [a-z]+)*$/`).\n *\n * The hub's strict default is lowercase-letters-and-single-spaces\n * because that's what the EFF wordlist generator emits and what\n * most attacker password lists are keyed on. Use this knob to allow\n * digits, uppercase, hyphens, or non-Latin scripts when the\n * consumer's audience needs them — e.g.:\n *\n * ```ts\n * // Thai + English mix with digits permitted\n * pattern: /^[\\p{L}0-9 ]+( [\\p{L}0-9 ]+)*$/u\n *\n * // Allow uppercase + hyphens (secret-with-hyphens style)\n * pattern: /^[A-Za-z]+([- ][A-Za-z]+)*$/\n * ```\n *\n * The OTHER structural rules still apply (min-words split by space,\n * min-word-length, repeated-adjacent, leading/trailing whitespace,\n * double-space). For non-space-delimited word semantics, use\n * {@link customValidator} instead.\n *\n */\n readonly pattern?: RegExp\n /**\n * Replace ALL validation entirely with a custom function. When set,\n * none of the other SecretPolicy fields apply — the consumer\n * owns every rule (word splitting, character classes, entropy\n * thresholds, allowlist/denylist). Use sparingly; this is the\n * escape hatch for domain-specific phrase formats:\n *\n * - Localized wordlists with non-space word boundaries\n * - BIP-39 seed phrases (24 words, fixed wordlist, etc.)\n * - Organization-specific HR password policies\n *\n * The returned `SecretValidationResult` is what\n * {@link assertStrongSecret} dispatches on — `ok: true` accepts;\n * `ok: false` throws `WeakSecretError` with the supplied reason.\n *\n */\n readonly customValidator?: (phrase: string) => SecretValidationResult\n}\n\n/** Result of a check. Discriminated union — compile-time exhaustive. */\nexport type SecretValidationResult =\n | { readonly ok: true; readonly words: number }\n | {\n readonly ok: false\n readonly reason: WeakSecretReason\n readonly minimum?: number\n readonly got?: number\n }\n\n/**\n * Thrown by `assertStrongSecret()` and by every hub ingress\n * point (`createOwnerKeyring`, `grant`, `rotateSecret`) when a\n * supplied phrase fails the structural rules above.\n */\nexport class WeakSecretError extends NoydbError {\n readonly reason: WeakSecretReason\n readonly suggestion: string\n constructor(reason: WeakSecretReason, suggestion: string) {\n super('WEAK_SECRET', `Weak secret (${reason}). ${suggestion}`)\n this.name = 'WeakSecretError'\n this.reason = reason\n this.suggestion = suggestion\n }\n}\n\nconst DEFAULT_MIN_WORDS = 6\nconst DEFAULT_MIN_WORD_LENGTH = 3\n\nconst SUGGESTIONS: Record<WeakSecretReason, string> = {\n empty: 'Provide a phrase of at least 6 lowercase words separated by single spaces.',\n 'invalid-chars':\n 'Use only lowercase letters [a-z] and single spaces. No punctuation, symbols, digits, or uppercase.',\n 'leading-or-trailing-space': 'Trim leading and trailing spaces.',\n 'double-space': 'Use exactly one space between words.',\n 'too-few-words':\n 'Use at least 6 words by default (8 under strict policy). Example: \"correct horse battery staple printer toaster\".',\n 'word-too-short': 'Each word must be at least 3 characters. Drop short fillers like \"a\", \"is\", \"of\".',\n 'repeated-adjacent': 'Avoid repeating the same word twice in a row.',\n}\n\n/**\n * Inspect a phrase against the format rules and return a structured\n * verdict. Never throws — callers either branch on `ok` or pass the\n * result to {@link assertStrongSecret} for the throwing flavour.\n */\nexport function validateSecret(\n s: string,\n opts?: SecretPolicy,\n): SecretValidationResult {\n // Escape hatch: customValidator owns the entire decision. None of\n // the structural rules below run when this is set — the consumer is\n // responsible for the full validation contract.\n if (opts?.customValidator) {\n return opts.customValidator(s)\n }\n\n const minWords = opts?.minWords ?? DEFAULT_MIN_WORDS\n const minWordLength = opts?.minWordLength ?? DEFAULT_MIN_WORD_LENGTH\n const rejectRepeated = opts?.rejectRepeatedAdjacent ?? true\n\n if (s.length === 0) {\n return { ok: false, reason: 'empty' }\n }\n\n if (s !== s.trim()) {\n return { ok: false, reason: 'leading-or-trailing-space' }\n }\n\n if (s.includes(' ')) {\n return { ok: false, reason: 'double-space' }\n }\n\n // The default character class is lowercase-letters-and-spaces;\n // consumers can override via SecretPolicy.pattern (e.g. to\n // allow digits, uppercase, or non-Latin scripts). Word splitting\n // below remains space-based — for non-space word semantics the\n // consumer should use customValidator instead.\n const charPattern = opts?.pattern ?? /^[a-z]+( [a-z]+)*$/\n if (!charPattern.test(s)) {\n return { ok: false, reason: 'invalid-chars' }\n }\n\n const words = s.split(' ')\n\n if (words.length < minWords) {\n return { ok: false, reason: 'too-few-words', minimum: minWords, got: words.length }\n }\n\n for (const w of words) {\n if (w.length < minWordLength) {\n return { ok: false, reason: 'word-too-short', minimum: minWordLength, got: w.length }\n }\n }\n\n if (rejectRepeated) {\n for (let i = 1; i < words.length; i++) {\n if (words[i] === words[i - 1]) {\n return { ok: false, reason: 'repeated-adjacent' }\n }\n }\n }\n\n return { ok: true, words: words.length }\n}\n\n/**\n * Throw {@link WeakSecretError} when the phrase fails. Used by\n * `createOwnerKeyring`, `grant`, and `rotateSecret` at ingress.\n *\n * Pass `{ allowWeakSecret: true }` to bypass — intended for test\n * fixtures, CLI scripts, and dev environments. The override never\n * loosens the cryptographic key derivation; it only relaxes the\n * structural-strength gate.\n */\nexport function assertStrongSecret(\n s: string,\n opts?: SecretPolicy & { allowWeakSecret?: boolean },\n): void {\n if (opts?.allowWeakSecret) return\n const result = validateSecret(s, opts)\n if (result.ok) return\n throw new WeakSecretError(result.reason, SUGGESTIONS[result.reason])\n}\n\n/**\n * Estimate the entropy of a phrase, given the EFF 7,776-word list as\n * the assumed wordlist. ~12.9 bits per word.\n *\n * Returns 0 for any input that fails the phrase format — character-class\n * estimates aren't comparable to phrase entropy, and surfacing 0 makes\n * weak inputs visible in any UI that displays an entropy meter.\n */\nexport function estimateEntropy(secret: string): number {\n const result = validateSecret(secret)\n if (!result.ok) return 0\n return Math.round(result.words * Math.log2(7776))\n}\n\n/**\n * Internal compatibility shim. Older code paths used the throwing\n * `validateSecret(s)` directly; some still do via re-exports. Routes\n * to the new `assertStrongSecret` so the contract holds for both\n * shapes during the transition. New code should call\n * {@link assertStrongSecret} directly.\n *\n * @internal\n */\nexport function legacyAssertSecret(s: string): void {\n try {\n assertStrongSecret(s)\n } catch (err) {\n if (err instanceof WeakSecretError) {\n throw new ValidationError(err.message)\n }\n throw err\n }\n}\n\n/** Dedicated floors for the echo mode's three parts (spec resolved Q1/Q5). */\nexport interface EchoSecretPolicy {\n /** Floor for the typed prompt — the brute-forceable-in-isolation part. */\n readonly prompt?: SecretPolicy\n /** Policy for the combined parts (defaults to the standard whole-secret rules). */\n readonly combined?: SecretPolicy\n}\n\nexport const DEFAULT_ECHO_PROMPT_MIN_WORDS = 3\n/**\n * Default per-word character floor for echo-mode validation (prompt AND\n * combined checks). Relaxed to 1 from the standard 3-char floor\n * ({@link DEFAULT_MIN_WORD_LENGTH}) — echo secrets are natural-language\n * sentences (spec #940's canonical example is Italian), and Romance\n * languages are full of 2-letter function words (\"mi\", \"da\", \"al\") that\n * the standard floor would otherwise reject even though the combined\n * secret is plenty strong. Word-COUNT floors (prompt 3, combined 6) are\n * unchanged — this only relaxes per-word length.\n */\nexport const DEFAULT_ECHO_MIN_WORD_LENGTH = 1\n\n/** Which of `validateEchoSecret`'s three internal checks produced a result. */\ntype EchoSecretCheck = 'empty' | 'prompt' | 'combined'\n\n/**\n * `validateEchoSecret` plus which check produced the result — lets\n * `assertStrongEchoSecret` pick the prompt-specific suggestion without\n * re-running `validateSecret` on the failure path. Internal: NOT\n * package-exported (the public `validateEchoSecret`/`SecretValidationResult`\n * contract stays exactly as before).\n */\nfunction validateEchoSecretDetailed(\n parts: { readonly prompt: string; readonly echo: string; readonly key: string },\n opts?: EchoSecretPolicy,\n): { readonly result: SecretValidationResult; readonly failedCheck: EchoSecretCheck } {\n // Trimmed: a whitespace-only part carries no secret material, so it is\n // 'empty' rather than a confusing spacing complaint.\n for (const part of [parts.prompt, parts.echo, parts.key]) {\n if (part.trim().length === 0) return { result: { ok: false, reason: 'empty' }, failedCheck: 'empty' }\n }\n // Field-by-field construction (not a spread of `opts?.prompt`) so an\n // explicit `{ prompt: { minWords: undefined } }` override falls back to\n // the ECHO prompt default (3) rather than reintroducing `undefined` as\n // an own property that would then hit `validateSecret`'s STANDARD\n // default (6) — a spread copies an explicit-`undefined` key verbatim and\n // clobbers the default set here. Each optional field is included only\n // when actually set (`exactOptionalPropertyTypes`).\n const promptPolicy: SecretPolicy = {\n minWords: opts?.prompt?.minWords ?? DEFAULT_ECHO_PROMPT_MIN_WORDS,\n minWordLength: opts?.prompt?.minWordLength ?? DEFAULT_ECHO_MIN_WORD_LENGTH,\n ...(opts?.prompt?.rejectRepeatedAdjacent !== undefined\n && { rejectRepeatedAdjacent: opts.prompt.rejectRepeatedAdjacent }),\n ...(opts?.prompt?.pattern !== undefined && { pattern: opts.prompt.pattern }),\n ...(opts?.prompt?.customValidator !== undefined && { customValidator: opts.prompt.customValidator }),\n }\n const promptResult = validateSecret(parts.prompt, promptPolicy)\n if (!promptResult.ok) return { result: promptResult, failedCheck: 'prompt' }\n // Same field-by-field construction as promptPolicy above (not a spread of\n // `opts?.combined`) so the combined check's minWordLength defaults to the\n // echo floor too, while minWords keeps the standard whole-secret default\n // (6) when the caller doesn't override it.\n const combinedPolicy: SecretPolicy = {\n ...(opts?.combined?.minWords !== undefined && { minWords: opts.combined.minWords }),\n minWordLength: opts?.combined?.minWordLength ?? DEFAULT_ECHO_MIN_WORD_LENGTH,\n ...(opts?.combined?.rejectRepeatedAdjacent !== undefined\n && { rejectRepeatedAdjacent: opts.combined.rejectRepeatedAdjacent }),\n ...(opts?.combined?.pattern !== undefined && { pattern: opts.combined.pattern }),\n ...(opts?.combined?.customValidator !== undefined && { customValidator: opts.combined.customValidator }),\n }\n const combinedResult = validateSecret(`${parts.prompt} ${parts.echo} ${parts.key}`, combinedPolicy)\n return { result: combinedResult, failedCheck: 'combined' }\n}\n\n/** Validate a 3-part echo secret. Never throws. */\nexport function validateEchoSecret(\n parts: { readonly prompt: string; readonly echo: string; readonly key: string },\n opts?: EchoSecretPolicy,\n): SecretValidationResult {\n return validateEchoSecretDetailed(parts, opts).result\n}\n\n/**\n * Suggestion copy for a failure that came from the PROMPT's dedicated floor.\n * The generic `SUGGESTIONS` copy quotes the 6-word whole-secret floor, which\n * misdirects an owner whose prompt only has to clear 3 words.\n */\nfunction promptSuggestion(reason: WeakSecretReason, minWords: number): string {\n if (reason === 'too-few-words') {\n return `The prompt (the part you type first) must be at least ${minWords} words. Example: \"sono chiamato vicio\".`\n }\n return `The prompt (the part you type first) is the problem. ${SUGGESTIONS[reason]}`\n}\n\n/** Throwing form of {@link validateEchoSecret}. */\nexport function assertStrongEchoSecret(\n parts: { readonly prompt: string; readonly echo: string; readonly key: string },\n opts?: EchoSecretPolicy & { allowWeakSecret?: boolean },\n): void {\n if (opts?.allowWeakSecret) return\n const { result, failedCheck } = validateEchoSecretDetailed(parts, opts)\n if (result.ok) return\n\n const promptMinWords = opts?.prompt?.minWords ?? DEFAULT_ECHO_PROMPT_MIN_WORDS\n const suggestion = failedCheck === 'prompt'\n ? promptSuggestion(result.reason, promptMinWords)\n : SUGGESTIONS[result.reason]\n\n throw new WeakSecretError(result.reason, suggestion)\n}\n","/**\n * Echo-block builders + verifiers (spec\n * design-history/2026-08-02-echo-secret-design.md, #940).\n *\n * Builds and verifies the on-keyring {@link KeyringEchoBlock} declaration\n * for the 3-part echo ceremony (prompt → echo → key). This module owns:\n *\n * - Minting the prompt/echo canary verifiers (the \"canary trick\": AES-KW\n * wrap a constant 32-byte key under `deriveSecretKey(part, salt, …)`;\n * verify = unwrap succeeds).\n * - Building the reveal blob per the hybrid reveal policy (spec decision\n * 4): `portable` (AES-GCM under the prompt, own salt), `sealed`\n * (opaque to hub, unsealed only by the enrolling device's\n * {@link DeviceSealProvider}), or `none`.\n * - Resolving the echo for display (`resolveEchoReveal`, `null` ⇒\n * degraded typed-echo path) and verifying a typed echo\n * (`verifyTypedEcho`) for that degraded path.\n *\n * Deriving the actual KEK from all three parts together is\n * `deriveEchoKey`/`encodeEchoParts` (`kernel/enclave/crypto.ts`) — a\n * separate concern from this module's per-part verifiers.\n *\n * Known, accepted disclosure: `prompt_verifier` and `echo_verifier` are\n * offline brute-force oracles for anyone holding the keyring file (600K\n * PBKDF2 per guess), and the `sealed` reveal blob is unpadded so its length\n * discloses the echo's UTF-8 byte length — see the threat model in\n * `design-history/2026-08-02-echo-secret-design.md`.\n *\n * @module\n */\n\nimport {\n deriveSecretKey,\n generateSalt,\n generateIV,\n wrapKey,\n bufferToBase64,\n base64ToBuffer,\n encodeEchoParts,\n type EchoSecretParts,\n} from '../../kernel/enclave/index.js'\nimport type { KeyringEchoBlock } from '../../kernel/types.js'\nimport type { DeviceSealProvider } from './device-seal.js'\nimport { WrongPromptError } from '../../kernel/errors.js'\n\n/** Same iteration floor as the KEK (spec resolved question 1). */\nexport const ECHO_KDF_ITERATIONS = 600_000\n\nconst VERIFIER_PLAINTEXT = new Uint8Array(32).fill(0x5a)\nconst subtle = globalThis.crypto.subtle\n\nasync function getVerifierKey(): Promise<CryptoKey> {\n return subtle.importKey('raw', VERIFIER_PLAINTEXT as BufferSource, { name: 'AES-GCM' }, true, ['encrypt'])\n}\n\nasync function mintVerifier(part: string, salt: Uint8Array): Promise<string> {\n const kek = await deriveSecretKey(part, salt, { iterations: ECHO_KDF_ITERATIONS, keyUsage: 'aes-kw' })\n return wrapKey(await getVerifierKey(), kek)\n}\n\n/**\n * `saltB64` and `verifier` are both decoded INSIDE the guarded region so a\n * tampered/corrupt base64 salt or verifier surfaces as `false`, not a raw\n * `DOMException` (InvalidCharacterError) escaping to the caller.\n */\nasync function checkVerifier(verifier: string, part: string, saltB64: string): Promise<boolean> {\n try {\n const kek = await deriveSecretKey(part, base64ToBuffer(saltB64), { iterations: ECHO_KDF_ITERATIONS, keyUsage: 'aes-kw' })\n await subtle.unwrapKey('raw', base64ToBuffer(verifier) as BufferSource, kek, 'AES-KW', { name: 'AES-GCM' }, false, ['encrypt'])\n return true\n } catch {\n return false\n }\n}\n\nexport type EchoRevealChoice =\n | { readonly kind: 'portable' }\n | { readonly kind: 'sealed'; readonly deviceSeal: DeviceSealProvider }\n | { readonly kind: 'none' }\n\n/** Build the on-keyring echo declaration for a fresh enrollment. */\nexport async function buildEchoBlock(\n parts: EchoSecretParts,\n reveal: EchoRevealChoice,\n maskHint?: string,\n): Promise<KeyringEchoBlock> {\n // Validate the 3-part shape through the SAME chokepoint the KEK derivation\n // uses (result deliberately discarded — only its guard is wanted here). A\n // malformed parts object would otherwise be TextEncoder-coerced below and\n // mint verifiers that no owner-derivable KEK matches.\n encodeEchoParts(parts)\n const promptSalt = generateSalt()\n const echoSalt = generateSalt()\n let revealField: KeyringEchoBlock['reveal']\n if (reveal.kind === 'portable') {\n const blobSalt = generateSalt()\n const iv = generateIV()\n const gcmKey = await deriveSecretKey(parts.prompt, blobSalt, { iterations: ECHO_KDF_ITERATIONS, keyUsage: 'aes-gcm' })\n const ct = new Uint8Array(\n await subtle.encrypt({ name: 'AES-GCM', iv: iv as BufferSource }, gcmKey, new TextEncoder().encode(parts.echo)),\n )\n revealField = { kind: 'portable', blob: bufferToBase64(ct), iv: bufferToBase64(iv), salt: bufferToBase64(blobSalt) }\n } else if (reveal.kind === 'sealed') {\n const sealed = await reveal.deviceSeal.seal(new TextEncoder().encode(parts.echo))\n revealField = { kind: 'sealed', blob: bufferToBase64(sealed), provider_hint: reveal.deviceSeal.id }\n } else {\n revealField = { kind: 'none' }\n }\n return {\n v: 1,\n prompt_salt: bufferToBase64(promptSalt),\n prompt_verifier: await mintVerifier(parts.prompt, promptSalt),\n echo_salt: bufferToBase64(echoSalt),\n echo_verifier: await mintVerifier(parts.echo, echoSalt),\n reveal: revealField,\n ...(maskHint !== undefined ? { mask_hint: maskHint } : {}),\n }\n}\n\nexport async function verifyPrompt(block: KeyringEchoBlock, prompt: string): Promise<boolean> {\n return checkVerifier(block.prompt_verifier, prompt, block.prompt_salt)\n}\n\n/**\n * Resolve the echo for display. `null` ⇒ degraded path: the player\n * must ask the owner to TYPE the echo (verify via {@link verifyTypedEcho}).\n * Callers MUST verify the prompt first — this assumes it.\n *\n * @throws WrongPromptError when the prompt cannot open the portable reveal\n * (callers should verifyPrompt first for a boolean check).\n */\nexport async function resolveEchoReveal(\n block: KeyringEchoBlock,\n prompt: string,\n deviceSeal?: DeviceSealProvider,\n): Promise<string | null> {\n if (block.reveal.kind === 'portable') {\n try {\n const gcmKey = await deriveSecretKey(prompt, base64ToBuffer(block.reveal.salt), {\n iterations: ECHO_KDF_ITERATIONS,\n keyUsage: 'aes-gcm',\n })\n const plain = await subtle.decrypt(\n { name: 'AES-GCM', iv: base64ToBuffer(block.reveal.iv) as BufferSource },\n gcmKey,\n base64ToBuffer(block.reveal.blob) as BufferSource,\n )\n return new TextDecoder().decode(plain)\n } catch {\n // Wrong prompt (or corrupt salt/iv/blob) — a wrong prompt must NOT\n // silently degrade to the typed-echo path, that would weaken mutual\n // auth. Surface a typed error instead of the raw DOMException.\n throw new WrongPromptError()\n }\n }\n if (block.reveal.kind === 'sealed' && deviceSeal !== undefined) {\n if (deviceSeal.id !== block.reveal.provider_hint) {\n // Legitimate foreign-device-with-its-own-provider case (spec's\n // degradation matrix) — degrade to the typed-echo path rather than\n // attempting (and failing) an unseal under the wrong provider.\n return null\n }\n // Same provider id: a thrown error here is a genuine tamper/wrong-device\n // anomaly and must surface, not be silently masked.\n const plain = await deviceSeal.unseal(base64ToBuffer(block.reveal.blob))\n return new TextDecoder().decode(plain)\n }\n return null\n}\n\nexport async function verifyTypedEcho(block: KeyringEchoBlock, echo: string): Promise<boolean> {\n return checkVerifier(block.echo_verifier, echo, block.echo_salt)\n}\n","/**\n * Secret-bearing reserved collections — the `_`-prefixed reserved collections\n * whose record CONTENTS are directly-usable secrets (transport OAuth tokens,\n * connection strings, API keys), as opposed to the *operational* reserved\n * collections (`_ledger`, `_history`, `_sync`, …) whose contents are metadata\n * (collection names, record ids, ciphertext hashes).\n *\n * This distinction matters at two trust-boundary seams:\n *\n * 1. `vault.collection()` — a secret-bearing reserved name must never be\n * reachable through the generic public collection handle. It is served\n * ONLY by its dedicated, role-gated API (e.g. `_sync_credentials` →\n * `getCredential`/`putCredential`, which enforce owner/admin access).\n *\n * 2. `grant()` DEK propagation — a sub-admin grantee (operator, viewer,\n * client, custodian) must NOT receive a secret-bearing collection's DEK.\n * Operational reserved DEKs still propagate to every role (grantees must\n * write ledger/history entries on every mutation), but handing a\n * sub-admin the `_sync_credentials` DEK would let them decrypt the firm's\n * transport secrets — a plaintext leak, not a metadata leak.\n *\n * Kept dependency-free so both the kernel (`vault.ts`) and the team layer\n * (`keyring.ts`) can import it without an import cycle.\n */\n\n/** Reserved collection holding per-adapter sync transport secrets. */\nexport const SYNC_CREDENTIALS_COLLECTION = '_sync_credentials'\n\n/**\n * Reserved (pre-allocated) name for the future credential broker (#479).\n * Not yet implemented — reserved now so that neither the public collection\n * handle nor grant propagation can ever expose it before its dedicated,\n * role-gated API lands.\n */\nexport const BROKER_COLLECTION = '_broker'\n\n/**\n * The set of reserved collection names whose record contents are\n * directly-usable secrets. Membership here means: never served via\n * `vault.collection()`, never propagated to sub-admin grantees.\n */\nexport const SECRET_BEARING_RESERVED_COLLECTIONS: ReadonlySet<string> = new Set([\n SYNC_CREDENTIALS_COLLECTION,\n BROKER_COLLECTION,\n])\n\n/** True when `name` is a secret-bearing reserved collection. */\nexport function isSecretBearingReservedCollection(name: string): boolean {\n return SECRET_BEARING_RESERVED_COLLECTIONS.has(name)\n}\n","import type { NoydbStore, KeyringFile, KeyringAuthenticator, Role, Permissions, GrantOptions, RevokeOptions, UpdateUserOptions, UserInfo, EncryptedEnvelope, ExportCapability, ExportFormat, ImportCapability, VaultPolicyOnDisk, UserEnvelope } from '../../kernel/types.js'\nimport { NOYDB_KEYRING_VERSION, NOYDB_FORMAT_VERSION } from '../../kernel/types.js'\nimport { USER_ENVELOPE_COLLECTION } from '../../kernel/constants.js'\nimport {\n deriveKey,\n deriveEchoKey,\n generateDEK,\n generateSalt,\n wrapKey,\n unwrapKey,\n rekeyEnvelopeIfNeeded,\n bufferToBase64,\n base64ToBuffer,\n type EnclaveKey,\n type EchoSecretParts,\n} from '../../kernel/enclave/index.js'\nimport { NoAccessError, PermissionDeniedError, PrivilegeEscalationError, KeyringExpiredError, KeyringCorruptError, InvalidKeyError, ValidationError, DirectoryDisabledError, EchoCeremonyRequiredError } from '../../kernel/errors.js'\nimport { readDirectoryConfig } from '../directory/storage.js'\nimport { readUserVisibility, deleteUserVisibility } from '../directory/visibility.js'\nimport {\n assertStrongSecret,\n assertStrongEchoSecret,\n type SecretPolicy,\n type EchoSecretPolicy,\n} from '../../kernel/validation.js'\nimport { buildEchoBlock } from './echo-secret.js'\nimport type { DeviceSealProvider } from './device-seal.js'\nimport {\n saveUserEnvelope,\n loadUserEnvelope as loadUserEnvelopeFn,\n deleteUserEnvelope,\n} from '../directory/user-envelope/index.js'\nimport { isSecretBearingReservedCollection } from './reserved-secret-collections.js'\n\n// ─── Roles that can grant/revoke ───────────────────────────────────────\n\n/**\n * Roles that an `admin` is allowed to grant and revoke.\n *\n * Includes `'admin'` itself: the model bottlenecked all admin\n * onboarding through the single `owner` principal, which made lateral\n * delegation impossible and left a single-owner bus-factor risk\n * unresolved even when multiple trusted humans existed. opens up\n * admin↔admin lateral delegation, with two guardrails:\n *\n * 1. **No privilege escalation.** Enforced in `grant()`: every DEK\n * wrapped into the new admin's keyring must be present in the\n * grantor's own DEK set. Today this is structurally trivially\n * true (admin grants always inherit the full caller DEK set),\n * but the check is wired in so future per-collection admin scoping\n * cannot accidentally bypass it. See `PrivilegeEscalationError`.\n *\n * 2. **Cascade on revoke.** Enforced in `revoke()`: when an admin is\n * revoked, every admin they (transitively) granted is either\n * revoked too (`cascade: 'strict'`, default) or left in place with\n * a console warning (`cascade: 'warn'`). The walk uses the\n * `granted_by` field on each keyring file as the parent pointer.\n */\n// FR-6: 'custodian' is deliberately ABSENT here. An admin must not be able\n// to mint (or revoke) a custodian — only the (sealed Deed) owner can, because\n// the custodian is the de-facto operational authority and granting one is an\n// ownership-level act. `canGrant(admin, 'custodian')` therefore returns false.\nconst ADMIN_GRANTABLE_TARGETS: readonly Role[] = ['operator', 'viewer', 'client', 'admin']\n\nfunction canGrant(callerRole: Role, targetRole: Role): boolean {\n if (callerRole === 'owner') return true\n // FR-6: a custodian can never grant — it holds the DEKs to OPERATE the\n // vault but no authority to widen the principal set. Explicit + fail-safe\n // (it would also fall through to `return false`, but spell it out).\n if (callerRole === 'custodian') return false\n if (callerRole === 'admin') return ADMIN_GRANTABLE_TARGETS.includes(targetRole)\n return false\n}\n\nfunction canRevoke(callerRole: Role, targetRole: Role): boolean {\n if (targetRole === 'owner') return false // owner cannot be revoked\n if (callerRole === 'owner') return true\n // FR-6: a custodian can never revoke — same non-owning rationale as canGrant.\n if (callerRole === 'custodian') return false\n if (callerRole === 'admin') return ADMIN_GRANTABLE_TARGETS.includes(targetRole)\n return false\n}\n\n/**\n * Whether `callerRole` can mutate a keyring whose role is (or becomes)\n * `targetRole`. Used by `updateKeyringIdentity`.\n *\n * Mirrors `canGrant`'s hierarchy: admins manage admin/operator/viewer/\n * client laterally; admins cannot create or destroy `owner`-shaped\n * keyrings. Owner can do anything.\n *\n * Both the OLD role and the NEW role must satisfy this check —\n * otherwise admin could elevate themselves (`admin → owner`) or demote\n * an owner (`owner → admin`) under cover of \"update.\"\n */\nfunction canUpdateRole(callerRole: Role, targetRole: Role): boolean {\n if (callerRole === 'owner') return true\n if (callerRole === 'admin') return ADMIN_GRANTABLE_TARGETS.includes(targetRole)\n return false\n}\n\n// ─── Unlocked Keyring ──────────────────────────────────────────────────\n\n/** In-memory representation of an unlocked keyring. */\nexport interface UnlockedKeyring {\n readonly userId: string\n readonly displayName: string\n readonly role: Role\n readonly permissions: Permissions\n readonly deks: Map<string, EnclaveKey>\n /**\n * Unwrapped counterpart of {@link KeyringFile.pending_deks} — an\n * uncommitted rotation (#1074). Empty in the normal state.\n */\n readonly pendingDeks?: Map<string, EnclaveKey> | undefined\n /**\n * The KEK, when this keyring was unlocked via tier 1 (secret) or\n * a wrap-KEK tier-2 method (WebAuthn / OIDC). `null` when the\n * keyring was opened via:\n *\n * - Unencrypted mode (no KEK exists)\n * - Tier-3 PIN quick-resume (`@noy-db/on-pin`)\n * - Wrap-DEKs tier-2 unlock (`@noy-db/on-password`'s\n * `verifyPasswordSlot`)\n * - Session-state restore (`session/session.ts`)\n * - Dev-unlock fixture (`session/dev-unlock.ts`)\n *\n * Consumers performing tier-1 operations that need the KEK\n * (DEK rewrap, keyring persist, delegation issue/unwrap) must\n * null-check and throw a clear error if absent — re-authenticate\n * at tier 1 first to recover the KEK.\n *\n * Tightened from `EnclaveKey` to `EnclaveKey | null`; the runtime\n * contract has always allowed null, the type now matches reality.\n */\n readonly kek: EnclaveKey | null\n readonly salt: Uint8Array\n /**\n * Debug-plaintext layout flag. Set only on the plaintext keyring created\n * in `encrypt: false` + `debugPlaintext: true` mode — it lives here\n * because its lifecycle is identical to the plaintext keyring's (no\n * encrypted vault ever has it, so this never widens the encrypted surface).\n * When true, user-collection records are written with their fields inlined\n * beside the envelope metadata (`_debug: 1`) so native store tooling can\n * read them without unwrapping `_data`.\n */\n readonly debugPlaintext?: boolean\n /**\n * `@noy-db/as-*` export capability. Absent when the\n * keyring was written before this RFC landed — role-based defaults\n * apply via `hasExportCapability`.\n */\n readonly exportCapability?: ExportCapability\n /**\n * `@noy-db/as-*` import capability. Absent when the\n * keyring was written before the import-capability extension\n * landed — default-closed semantics\n * apply via `hasImportCapability` (no plaintext format granted, no\n * bundle import granted, regardless of role).\n */\n readonly importCapability?: ImportCapability\n /**\n * Tier-2 authenticator slots — readonly snapshot loaded from the\n * keyring file. Mutations go through `enrollAuthenticator` /\n * `removeAuthenticator`, which write back via\n * `persistKeyring`. Always defined; loads with an empty array for\n * keyrings written before the multi-slot extension landed.\n */\n readonly authenticators: readonly KeyringAuthenticator[]\n /**\n * Reserved per-keyring policy override (forward-compat for Option C\n * — see {@link VaultPolicyOnDisk}). v1.0 round-trips this field but\n * never enforces it; the gate engine uses `_meta/policy` only.\n */\n readonly policy?: VaultPolicyOnDisk\n}\n\n// ─── Secret canary ─────────────────────────────────────────────────\n//\n// The canary is a fixed 256-bit AES-GCM key (32 zero bytes), wrapped\n// under the keyring's KEK with AES-KW. Because AES-KW is deterministic\n// (RFC 3394 fixed IV), wrapping the same constant under the same KEK\n// always yields the same ciphertext — so every write site can mint\n// fresh on each persist without round-tripping a `canary` field\n// through UnlockedKeyring.\n//\n// On load, the canary unwraps cleanly iff the KEK is correct AND the\n// canary bytes on disk are intact. Combined with each-DEK try/catch,\n// this distinguishes wrong-secret (canary fails AND every DEK fails)\n// from corruption (canary succeeds OR at least one DEK succeeds) —\n// closing the all-DEKs-corrupt and single-DEK ambiguities that the\n// pre-canary heuristic left open.\n\nconst CANARY_PLAINTEXT_BYTES = new Uint8Array(32)\nlet canaryKeyPromise: Promise<EnclaveKey> | null = null\n\nfunction getCanaryKey(): Promise<EnclaveKey> {\n if (canaryKeyPromise === null) {\n canaryKeyPromise = globalThis.crypto.subtle.importKey(\n 'raw',\n CANARY_PLAINTEXT_BYTES as BufferSource,\n { name: 'AES-GCM', length: 256 },\n true, // extractable so AES-KW can wrap it\n ['encrypt', 'decrypt'],\n )\n }\n return canaryKeyPromise\n}\n\n/** Mint a fresh wrapped-canary string. Deterministic for a given KEK. */\nexport async function mintKeyringCanary(kek: EnclaveKey): Promise<string> {\n const canaryKey = await getCanaryKey()\n return wrapKey(canaryKey, kek)\n}\n\n/** Try to unwrap the canary. Returns true iff KEK + canary bytes are intact. */\nasync function verifyKeyringCanary(wrappedCanary: string, kek: EnclaveKey): Promise<boolean> {\n try {\n await unwrapKey(wrappedCanary, kek)\n return true\n } catch {\n return false\n }\n}\n\n// ─── Load / Create ─────────────────────────────────────────────────────\n\n/** Options for {@link loadKeyring} (#846b). */\nexport interface LoadKeyringOptions {\n /** The user whose keyring to unlock. */\n readonly userId: string\n /**\n * That user's secret — a single string for a standard keyring, or the\n * structured 3-part {@link EchoSecretParts} for an echo keyring (spec\n * #940, AG-1: a single string can never unlock an echo keyring).\n */\n readonly secret: string | EchoSecretParts\n}\n\n/**\n * KDF dispatch for tier-1 unlock (spec #940). An echo keyring accepts\n * ONLY structured parts (AG-1 — no single string is key-equivalent);\n * a standard keyring accepts only a string.\n */\nexport async function deriveKekForKeyring(\n file: KeyringFile,\n secret: string | EchoSecretParts,\n salt: Uint8Array,\n): Promise<EnclaveKey> {\n if (file.echo !== undefined) {\n if (typeof secret === 'string') throw new EchoCeremonyRequiredError()\n return deriveEchoKey(secret, salt)\n }\n if (typeof secret !== 'string') {\n throw new ValidationError('This keyring uses a standard secret — pass a string, not echo parts.')\n }\n return deriveKey(secret, salt)\n}\n\n// ─── Raw keyring reads (#951) ───────────────────────────────────────────\n//\n// Single sanctioned reader of a keyring file's `_data`, plus the fetch+parse\n// and expiry-gate wrappers built on it. Every keyring.ts / echo-ceremony.ts\n// call site that used to inline `JSON.parse(env._data) as KeyringFile`\n// (each with its own missing-row semantics — throw / skip / continue) now\n// goes through `readKeyringFile`, which stays neutral on the missing-row\n// case (`undefined`) and lets each caller keep its own decision.\n\n/** Parse a raw keyring envelope. Single sanctioned reader of `_data` for keyring files. */\nexport function parseKeyringEnvelope(envelope: EncryptedEnvelope): KeyringFile {\n return JSON.parse(envelope._data) as KeyringFile\n}\n\n/** Fetch + parse a user's keyring file; undefined when the row is missing. */\nexport async function readKeyringFile(\n store: NoydbStore,\n vault: string,\n userId: string,\n): Promise<{ readonly envelope: EncryptedEnvelope; readonly file: KeyringFile } | undefined> {\n const envelope = await store.get(vault, '_keyring', userId)\n if (!envelope) return undefined\n return { envelope, file: parseKeyringEnvelope(envelope) }\n}\n\n/**\n * Shared expiry gate — refuse to unwrap an expired slot. Call before any\n * KEK derivation so an expired slot doesn't leak timing on the secret.\n * Comparison uses Date.parse → ms-since-epoch; an unparseable expires_at is\n * treated as \"no expiry\" so a malformed value can't silently lock users out\n * (it'll surface in tests).\n */\nexport function assertKeyringNotExpired(file: KeyringFile): void {\n if (file.expires_at !== undefined) {\n const cutoff = Date.parse(file.expires_at)\n if (Number.isFinite(cutoff) && Date.now() >= cutoff) {\n throw new KeyringExpiredError({ userId: file.user_id, expiresAt: file.expires_at })\n }\n }\n}\n\n/** Load and unlock a user's keyring for a vault. */\nexport async function loadKeyring(\n store: NoydbStore,\n vault: string,\n opts: LoadKeyringOptions,\n): Promise<UnlockedKeyring> {\n const { userId, secret } = opts\n const found = await readKeyringFile(store, vault, userId)\n\n if (!found) {\n throw new NoAccessError(`No keyring found for user \"${userId}\" in vault \"${vault}\"`)\n }\n\n const { file: keyringFile } = found\n\n assertKeyringNotExpired(keyringFile)\n\n const salt = base64ToBuffer(keyringFile.salt)\n const kek = await deriveKekForKeyring(keyringFile, secret, salt)\n\n // Verify the canary first when present. A canary success proves the\n // KEK is correct independent of any DEK byte — so subsequent DEK\n // unwrap failures are unambiguously corruption, not wrong-pass. A\n // canary failure with at least one DEK success indicates the KEK\n // is correct but the canary itself is corrupt.\n // `null` sentinel = legacy keyring without canary; falls back to the\n // multi-DEK heuristic.\n const canaryOk: boolean | null = keyringFile.canary !== undefined\n ? await verifyKeyringCanary(keyringFile.canary, kek)\n : null\n\n // Unwrap each DEK independently — collect successes and failures.\n const deks = new Map<string, EnclaveKey>()\n const failedCollections: string[] = []\n let firstUnwrapError: unknown = null\n for (const [collName, wrappedDek] of Object.entries(keyringFile.deks)) {\n try {\n const dek = await unwrapKey(wrappedDek, kek)\n deks.set(collName, dek)\n } catch (err) {\n failedCollections.push(collName)\n if (firstUnwrapError === null) firstUnwrapError = err\n }\n }\n\n // #1074 — unwrap any uncommitted rotation key. Deliberately NOT folded into\n // `failedCollections`: a pending DEK that fails to unwrap means the\n // interrupted rotation cannot be resumed, which is worth surfacing on its own\n // terms, but it must not make an otherwise-healthy keyring read as corrupt.\n const pendingDeks = new Map<string, EnclaveKey>()\n for (const [collName, wrapped] of Object.entries(keyringFile.pending_deks ?? {})) {\n try {\n pendingDeks.set(collName, await unwrapKey(wrapped, kek))\n } catch {\n // Unresumable; `deks` still holds the pre-rotation key, so the records\n // the interrupted run had not reached remain readable.\n }\n }\n\n if (canaryOk === true) {\n // KEK proven correct by the canary. Any DEK failure is corruption.\n if (failedCollections.length > 0) {\n throw new KeyringCorruptError({ failedCollections, intactCount: deks.size })\n }\n } else if (canaryOk === false) {\n // Canary failed. If any DEK unwrapped, KEK is correct → canary bytes\n // are corrupted (rare; reported under the '_canary' sentinel).\n if (deks.size > 0) {\n throw new KeyringCorruptError({\n failedCollections: [...failedCollections, '_canary'],\n intactCount: deks.size,\n })\n }\n // Canary failed AND no DEK unwrapped — wrong KEK (or whole-file\n // corruption). Surface the original InvalidKeyError so\n // onInvalidKey: 'reset' can fire its documented recovery path.\n throw firstUnwrapError instanceof Error ? firstUnwrapError : new InvalidKeyError()\n } else {\n // Legacy keyring (no canary). Fall back to the multi-DEK heuristic.\n if (failedCollections.length > 0) {\n if (deks.size > 0) {\n throw new KeyringCorruptError({ failedCollections, intactCount: deks.size })\n }\n throw firstUnwrapError instanceof Error ? firstUnwrapError : new InvalidKeyError()\n }\n }\n\n return {\n userId: keyringFile.user_id,\n displayName: keyringFile.display_name,\n role: keyringFile.role,\n permissions: keyringFile.permissions,\n deks,\n pendingDeks,\n kek,\n salt,\n authenticators: keyringFile.authenticators ?? [],\n ...(keyringFile.export_capability !== undefined && { exportCapability: keyringFile.export_capability }),\n ...(keyringFile.import_capability !== undefined && { importCapability: keyringFile.import_capability }),\n ...(keyringFile.policy !== undefined && { policy: keyringFile.policy }),\n }\n}\n\n/**\n * Open-policy pre-gate: decide create-vs-fail-closed **before** any\n * vault write. `openVault` must not self-provision an owner keyring into a\n * vault held by other principals; create-on-open is allowed only for a\n * genuinely-new vault (no `_keyring/*` at all). Capability-free — one\n * `store.list`. Returns when the open may proceed (the caller is a member, or\n * the vault is genuinely-new and `create` is allowed, in which case the caller\n * falls through to the normal `createOwnerKeyring` path); throws `NoAccessError`\n * otherwise. Placed before managed-secret secret resolution (which persists\n * on first open), so a fail-closed open writes nothing.\n */\nexport async function assertKeyringOpenAllowed(\n store: NoydbStore,\n vault: string,\n userId: string,\n create: boolean,\n): Promise<void> {\n const keyringUsers = await store.list(vault, '_keyring')\n if (keyringUsers.includes(userId)) return // caller is a member → load existing\n if (!create) {\n throw new NoAccessError(`Vault \"${vault}\" not opened: create disabled and no keyring for \"${userId}\".`)\n }\n if (keyringUsers.length > 0) {\n throw new NoAccessError(\n `No keyring for user \"${userId}\" in vault \"${vault}\" (held by other principals) — refusing to self-provision.`,\n )\n }\n // empty → genuinely-new vault → caller proceeds to the create path\n}\n\n/**\n * Options for {@link createOwnerKeyring} (#846b). The `SecretPolicy` knobs are\n * flattened into the same bag rather than nested, so `assertStrongSecret` can\n * take `opts` directly.\n */\nexport interface CreateOwnerKeyringOptions extends SecretPolicy {\n /** The owner to create. */\n readonly userId: string\n /**\n * The owner's secret — a plain string for a standard keyring, or the\n * structured 3-part {@link EchoSecretParts} to enroll an echo keyring\n * (spec #940). The shape alone selects the KDF and whether an `echo`\n * block is written.\n */\n readonly secret: string | EchoSecretParts\n /** Gate creation on the phrase-format strength rules. Default false. */\n readonly validate?: boolean\n /** Escape hatch for fixtures and migrations — skips the strength gate. */\n readonly allowWeakSecret?: boolean\n /**\n * Echo enrollment only: device-local sealer for the reveal blob. Present\n * ⇒ `reveal: 'sealed'` (attacker-B resistance); absent ⇒ `reveal:\n * 'portable'` (spec resolved question 4). Ignored for a string secret.\n */\n readonly deviceSeal?: DeviceSealProvider\n /** Echo enrollment only: optional display hint for the masked echo. */\n readonly echoMaskHint?: string\n /**\n * Echo enrollment only: strength-policy knobs for the 3-part secret's\n * prompt / combined floors — the parts-path counterpart of the\n * flattened `SecretPolicy` fields this options bag already extends\n * (those apply to a STRING secret via `assertStrongSecret`; this\n * applies to {@link EchoSecretParts} via `assertStrongEchoSecret`).\n * Ignored for a string secret.\n */\n readonly echoSecretPolicy?: EchoSecretPolicy\n}\n\n/**\n * Create the initial owner keyring for a new vault.\n *\n * Pass `{ validate: true }` (or any `SecretPolicy` knob) to gate creation\n * on the phrase-format strength rules — `Noydb` threads this from\n * `NoydbOptions.validateSecret`. Direct callers (CLI, scripts,\n * test fixtures) opt in explicitly.\n */\nexport async function createOwnerKeyring(\n store: NoydbStore,\n vault: string,\n opts: CreateOwnerKeyringOptions,\n): Promise<UnlockedKeyring> {\n const { userId, secret } = opts\n if (opts.validate && !opts.allowWeakSecret) {\n // `buildEchoBlock` only type-validates the 3-part secret via the\n // `encodeEchoParts` chokepoint — the STRENGTH gate for it still has to\n // fire here, exactly like `assertStrongSecret` does for a string.\n if (typeof secret === 'string') assertStrongSecret(secret, opts)\n else {\n assertStrongEchoSecret(secret, {\n ...opts.echoSecretPolicy,\n ...(opts.allowWeakSecret !== undefined && { allowWeakSecret: opts.allowWeakSecret }),\n })\n }\n }\n const salt = generateSalt()\n const kek = typeof secret === 'string'\n ? await deriveKey(secret, salt)\n : await deriveEchoKey(secret, salt)\n\n // Eager-provision the _users DEK at owner creation. This guarantees\n // every subsequent grant inherits it via the existing\n // collName.startsWith('_') propagation in grant() — so multi-principal\n // user-envelope reads (alice reading bob's profile) work for new\n // vaults without any per-keyring DEK rotation. Pre-existing vaults\n // get the DEK lazily on first vault.user.* access (which only\n // materializes a single-principal DEK that won't propagate\n // retroactively — that's the documented \"lazy creation for\n // pre-existing keyrings\" rollout note in the spec).\n const userEnvelopeDek = await generateDEK()\n const wrappedUserEnvelopeDek = await wrapKey(userEnvelopeDek, kek)\n const canary = await mintKeyringCanary(kek)\n\n const keyringFile: KeyringFile = {\n _noydb_keyring: NOYDB_KEYRING_VERSION,\n user_id: userId,\n display_name: userId,\n role: 'owner',\n permissions: {},\n deks: { [USER_ENVELOPE_COLLECTION]: wrappedUserEnvelopeDek },\n salt: bufferToBase64(salt),\n created_at: new Date().toISOString(),\n granted_by: userId,\n canary,\n // The presence of this block is what makes the keyring an echo keyring —\n // `deriveKekForKeyring` reads it to refuse a single-string unlock (AG-1).\n ...(typeof secret !== 'string'\n ? {\n echo: await buildEchoBlock(\n secret,\n opts.deviceSeal ? { kind: 'sealed', deviceSeal: opts.deviceSeal } : { kind: 'portable' },\n opts.echoMaskHint,\n ),\n }\n : {}),\n }\n\n await writeKeyringFile(store, vault, userId, keyringFile)\n\n return {\n userId,\n displayName: userId,\n role: 'owner',\n permissions: {},\n deks: new Map([[USER_ENVELOPE_COLLECTION, userEnvelopeDek]]),\n kek,\n salt,\n authenticators: [],\n }\n}\n\n// ─── Grant ─────────────────────────────────────────────────────────────\n\n/** Grant access to a new user. Caller must have grant privilege. */\nexport async function grant(\n store: NoydbStore,\n vault: string,\n callerKeyring: UnlockedKeyring,\n options: GrantOptions,\n): Promise<void> {\n if (!callerKeyring.kek) {\n throw new ValidationError(\n 'grant: caller keyring has no KEK — tier-2 wrap-DEKs and tier-3 PIN-resume ' +\n 'sessions cannot grant access to other users. Re-authenticate at tier 1 ' +\n '(secret) before granting.',\n )\n }\n\n if (!canGrant(callerKeyring.role, options.role)) {\n throw new PermissionDeniedError(\n `Role \"${callerKeyring.role}\" cannot grant role \"${options.role}\"`,\n )\n }\n\n // PRESENCE check — distinct from, and unskippable by, the strength check\n // below. `GrantOptions.secret` is a required field, so only an untypechecked\n // call site can omit it (a build script outside the typecheck project, a\n // stale `passphrase` key surviving the 0.4.0-pre rename). Before #1004 that\n // call derived a KEK from `undefined`, minted a real keyring slot, and\n // surfaced as `InvalidKeyError` whenever the grantee first tried to unlock —\n // arbitrarily far from the call that caused it. Fail at the grant instead.\n // `allowWeakSecret` deliberately does NOT skip this: it waives the strength\n // POLICY, not the existence of a secret.\n const secret: unknown = options.secret\n if (typeof secret !== 'string' || secret.trim() === '') {\n throw new ValidationError(\n `grant: \\`secret\\` is required and must be a non-empty string (got ${\n secret === undefined ? 'undefined' : typeof secret === 'string' ? 'an empty string' : typeof secret\n }). The grantee's key is derived from it, so a missing secret produces a ` +\n 'keyring slot nobody can unlock. Note the 0.4.0-pre rename: the option is ' +\n '`secret`, not `passphrase`.',\n )\n }\n\n // Optional strength validation — opt-in via grant({ validateSecret: true })\n // or via the calling Noydb's NoydbOptions.validateSecret flag.\n // The override `allowWeakSecret: true` skips even when validate is on.\n if (\n (options as { validateSecret?: boolean }).validateSecret &&\n !options.allowWeakSecret\n ) {\n assertStrongSecret(options.secret)\n }\n\n // Determine which collections the new user gets access to\n const permissions = resolvePermissions(options.role, options.permissions)\n\n // Derive the new user's KEK from their secret\n const newSalt = generateSalt()\n const newKek = await deriveKey(options.secret, newSalt)\n\n // Only owner and admin may ever hold a secret-bearing reserved DEK\n // (`_sync_credentials`, `_broker`) — they are the roles the dedicated\n // credential API admits. Every other grantee (custodian, viewer, operator,\n // client) is excluded, even from the \"wrap ALL\" branches below: a custodian\n // operates the DATA but must never read the firm's transport secrets (see\n // `requireAdminAccess` in sync-credentials.ts), and handing any sub-admin\n // one of these DEKs is a plaintext leak, not a metadata leak.\n const granteeMayHoldSecrets =\n options.role === 'owner' || options.role === 'admin'\n\n // A grantee's DEKs can only ever be wrapped HERE, at grant time: wrapping\n // needs the grantee's KEK, which is derived from a secret the vault never\n // stores, so there is no later moment at which a newly-minted collection DEK\n // could be back-filled into an existing keyring. #1004: granting\n // `{ invoices: 'rw' }` before `invoices` existed therefore wrapped nothing\n // and left a permanently blind slot. Mint the DEK now so the grant is\n // honoured whichever order the caller works in.\n //\n // Only for collections that do not exist yet. If a collection HAS records\n // and the grantor still lacks its DEK, minting would fabricate a key that\n // decrypts nothing AND would hand the anti-privilege-escalation check below\n // a DEK the grantor never legitimately held — turning a structural guarantee\n // into a no-op. Leave those unwrapped and let the read path deny.\n let mintedForGrant = false\n for (const collName of Object.keys(permissions)) {\n // `'*'` is a marker, not a collection — minting a DEK for it would create a\n // literal `*` collection and cover nothing (#1010).\n if (collName === PERMISSION_WILDCARD) continue\n if (isSecretBearingReservedCollection(collName) && !granteeMayHoldSecrets) continue\n if (callerKeyring.deks.has(collName)) continue\n if (await collectionHasRecords(store, vault, collName)) continue\n callerKeyring.deks.set(collName, await generateDEK())\n mintedForGrant = true\n }\n // Only when we actually minted: the grantor's own keyring file has to record\n // the new DEK, or their next write to that collection would mint a SECOND,\n // different one and orphan the copy we are about to wrap for the grantee.\n if (mintedForGrant) await persistKeyring(store, vault, callerKeyring)\n\n // Wrap the appropriate DEKs with the new user's KEK\n const wrappedDeks: Record<string, string> = {}\n for (const collName of Object.keys(permissions)) {\n if (collName === PERMISSION_WILDCARD) continue\n // Never hand a secret-bearing reserved DEK to a sub-admin, even if the\n // grantor explicitly names it in `permissions` — that path is served\n // only by the owner/admin-gated credential API, not per-collection grants.\n if (isSecretBearingReservedCollection(collName) && !granteeMayHoldSecrets) continue\n const dek = callerKeyring.deks.get(collName)\n if (dek) {\n wrappedDeks[collName] = await wrapKey(dek, newKek)\n }\n }\n\n // For owner/admin/custodian/viewer roles, wrap ALL known DEKs.\n // FR-6: a custodian operates EVERY collection, so — like admin — it must\n // receive every collection DEK on grant. Without this branch a custodian\n // could neither read nor write and the role would be inert.\n // #1010 — `permissions: { '*': ... }` puts a permission-scoped grantee on the\n // same footing as the whole-vault roles: every DEK the grantor holds. Note the\n // inherent limit the caller must know about — this covers the collections that\n // exist NOW. A wildcard cannot enumerate collections created later, and a DEK\n // can only ever be wrapped at grant time, so a later collection needs a\n // re-grant (the read path says so explicitly).\n if (\n options.role === 'owner' ||\n options.role === 'admin' ||\n options.role === 'custodian' ||\n options.role === 'viewer' ||\n permissionsAreWildcard(options.permissions)\n ) {\n for (const [collName, dek] of callerKeyring.deks) {\n if (collName in wrappedDeks) continue\n if (isSecretBearingReservedCollection(collName) && !granteeMayHoldSecrets) continue\n wrappedDeks[collName] = await wrapKey(dek, newKek)\n }\n }\n\n // For ALL roles, propagate system-prefixed collection DEKs\n // (`_ledger`, `_history`, `_sync`, …). These are internal collections\n // that any user with access to the vault must be able to\n // read and write — for example, the hash-chained ledger writes\n // an entry on every put/delete, so operators and clients with write\n // access to a single data collection still need the `_ledger` DEK.\n //\n // Trade-off: a granted user can decrypt every system-collection\n // entry, including ones they would not otherwise have access to\n // (e.g., an operator on `invoices` can read ledger entries for\n // mutations in `salaries`). This is a metadata leak, not a\n // plaintext leak — the ledger entries record collection names,\n // record ids, and ciphertext hashes, but never plaintext records.\n // Per-collection ledger DEKs are tracked as a follow-up.\n //\n // EXCEPTION — secret-bearing reserved collections (`_sync_credentials`,\n // `_broker`) whose record CONTENTS are directly-usable secrets are NOT\n // propagated to sub-admin grantees. Unlike the operational collections\n // above, handing an operator/viewer/client/custodian one of these DEKs\n // IS a plaintext leak (the firm's transport OAuth tokens). Only owner and\n // admin — the roles the dedicated `getCredential`/`putCredential` API\n // admits — receive them, so the legit admin-reads-existing-credential\n // flow (which needs the DEK to decrypt, not regenerate) still works.\n for (const [collName, dek] of callerKeyring.deks) {\n if (!collName.startsWith('_') || collName in wrappedDeks) continue\n if (isSecretBearingReservedCollection(collName) && !granteeMayHoldSecrets) continue\n wrappedDeks[collName] = await wrapKey(dek, newKek)\n }\n\n // Anti-privilege-escalation check. Every DEK we just\n // wrapped into the new keyring must come from the caller's own DEK\n // set — the grantor cannot give the grantee access to a collection\n // they themselves can't read. Today this is structurally trivially\n // satisfied because every wrapped DEK was looked up in\n // `callerKeyring.deks` above, but the explicit check is wired in\n // so a future change (per-collection admin scoping, escrow-based\n // re-wrapping, etc.) cannot accidentally let a widening grant\n // through. See `PrivilegeEscalationError` for the rationale.\n for (const collName of Object.keys(wrappedDeks)) {\n if (!callerKeyring.deks.has(collName)) {\n throw new PrivilegeEscalationError(collName)\n }\n }\n\n const canary = await mintKeyringCanary(newKek)\n const keyringFile: KeyringFile = {\n _noydb_keyring: NOYDB_KEYRING_VERSION,\n user_id: options.userId,\n display_name: options.displayName,\n role: options.role,\n permissions,\n deks: wrappedDeks,\n salt: bufferToBase64(newSalt),\n created_at: new Date().toISOString(),\n granted_by: callerKeyring.userId,\n canary,\n ...(options.exportCapability !== undefined && { export_capability: options.exportCapability }),\n ...(options.importCapability !== undefined && { import_capability: options.importCapability }),\n }\n\n await writeKeyringFile(store, vault, options.userId, keyringFile)\n\n // User envelope bootstrap. Seeded with `options.initialProfile` if\n // provided, otherwise an empty `{}`. Encrypted with the caller's\n // _users DEK — which is the same DEK that was wrapped into the new\n // keyring's `wrappedDeks[USER_ENVELOPE_COLLECTION]` above (system-\n // collection propagation), so the new user can decrypt it on first\n // open. Skipped silently if the caller has no _users DEK (pre-feature\n // vault upgrade path — documented \"lazy creation for pre-existing\n // keyrings\" in the spec).\n const userEnvelopeDek = callerKeyring.deks.get(USER_ENVELOPE_COLLECTION)\n if (userEnvelopeDek) {\n const initialPayload = options.initialProfile ?? {}\n await saveUserEnvelope(\n store,\n vault,\n options.userId,\n initialPayload,\n userEnvelopeDek,\n )\n }\n}\n\n// ─── Revoke ────────────────────────────────────────────────────────────\n\n/**\n * Walk every keyring in the vault to find admins that the given\n * `rootUserId` (transitively) granted, via the `granted_by` parent\n * pointer recorded on each keyring file.\n *\n * Returns the set of descendant admin user-ids in DFS order, NOT\n * including the root itself. Non-admin descendants are excluded\n * because operators/viewers/clients cannot grant other users — they\n * are leaves in the delegation tree and cleaning them up is the\n * caller's job (or the next rotate, since they'd lose key access\n * anyway when the cascading admin's collections rotate).\n *\n * The walk uses a visited set keyed by user-id so cycles introduced\n * by re-grants (admin-A revoked, then re-granted later by admin-B who\n * was originally granted by A) terminate cleanly.\n */\nasync function findAdminDescendants(\n store: NoydbStore,\n vault: string,\n rootUserId: string,\n): Promise<string[]> {\n const allUserIds = await store.list(vault, '_keyring')\n\n // Build a map: parentUserId → child KeyringFiles. We only ever\n // descend into admins, so non-admin children are skipped at the\n // edge level rather than after a recursive call.\n const childrenByParent = new Map<string, string[]>()\n for (const userId of allUserIds) {\n const found = await readKeyringFile(store, vault, userId)\n if (!found) continue\n const kf = found.file\n // Only admins can grant, so only admins have a delegation subtree to\n // cascade. FR-6: a custodian is intentionally EXCLUDED here — it cannot\n // grant (canGrant(custodian,*) === false), so it is never a cascade root\n // and never a descendant edge. Treating it as an admin-descendant would\n // wrongly sweep it into an admin's revoke cascade.\n if (kf.role !== 'admin') continue // only admins can grant — leaves are uninteresting\n if (kf.user_id === rootUserId) continue // self-edges are noise\n const list = childrenByParent.get(kf.granted_by) ?? []\n list.push(kf.user_id)\n childrenByParent.set(kf.granted_by, list)\n }\n\n const visited = new Set<string>()\n const order: string[] = []\n const stack: string[] = [...(childrenByParent.get(rootUserId) ?? [])]\n while (stack.length > 0) {\n const next = stack.pop()!\n if (visited.has(next)) continue\n visited.add(next)\n order.push(next)\n for (const grandchild of childrenByParent.get(next) ?? []) {\n if (!visited.has(grandchild)) stack.push(grandchild)\n }\n }\n return order\n}\n\n/** Revoke a user's access. Optionally rotate keys for affected collections. */\nexport async function revoke(\n store: NoydbStore,\n vault: string,\n callerKeyring: UnlockedKeyring,\n options: RevokeOptions,\n): Promise<void> {\n // Load the target's keyring to check their role\n const targetFound = await readKeyringFile(store, vault, options.userId)\n if (!targetFound) {\n // #1077 — the entry may be absent because a PREVIOUS revoke deleted it and\n // then failed during rotation. `revoke()` deletes first and rotates second,\n // with no transaction, so that window is reachable by any store error.\n //\n // Throwing here is what made the state dangerous: the operator retries,\n // sees \"has no keyring\", reads it as \"already revoked, nothing to do\", and\n // stops — while the keys were never rotated. The failure was silent\n // precisely because it looked like success.\n //\n // An uncommitted rotation on the caller's own keyring is the evidence that\n // this happened (`pending_deks`, #1074). Resume it rather than reporting a\n // not-found: finishing the interrupted job is what the operator asked for,\n // and it makes retrying `revoke()` idempotent instead of misleading.\n const pending = [...(callerKeyring.pendingDeks?.keys() ?? [])]\n if (pending.length > 0) {\n await rotateKeys(store, vault, callerKeyring, { collections: pending })\n return\n }\n throw new NoAccessError(`User \"${options.userId}\" has no keyring in vault \"${vault}\"`)\n }\n\n const targetKeyring = targetFound.file\n\n if (!canRevoke(callerKeyring.role, targetKeyring.role)) {\n throw new PermissionDeniedError(\n `Role \"${callerKeyring.role}\" cannot revoke role \"${targetKeyring.role}\"`,\n )\n }\n\n // Cascade-on-revoke. Only meaningful when the target is\n // an admin — operators/viewers/clients cannot grant other users so\n // they have no delegation subtree to walk.\n const cascadeMode = options.cascade ?? 'strict'\n const usersToRevoke: string[] = [options.userId]\n const affectedCollections = new Set(Object.keys(targetKeyring.deks))\n\n if (targetKeyring.role === 'admin') {\n const descendants = await findAdminDescendants(store, vault, options.userId)\n if (descendants.length > 0) {\n if (cascadeMode === 'warn') {\n // Diagnostic mode: leave the descendants in place but make\n // them visible. The owner / a different admin can clean up\n // manually. The single console.warn is intentionally noisy\n // (a list, not a count) so the operator sees exactly which\n // keyrings will become orphans.\n console.warn(\n `[noy-db] revoke(${options.userId}): cascade='warn' — leaving ` +\n `${descendants.length} descendant admin(s) in place: ` +\n `${descendants.join(', ')}. These admins were granted by the revoked user ` +\n `(transitively) and will become orphans in the delegation tree.`,\n )\n } else {\n // Strict mode (default): pull every descendant into the\n // revoke set. We collect their affected collections too so\n // the single rotation pass at the end covers everything.\n for (const userId of descendants) {\n const descFound = await readKeyringFile(store, vault, userId)\n if (!descFound) continue\n usersToRevoke.push(userId)\n for (const c of Object.keys(descFound.file.deks)) affectedCollections.add(c)\n }\n }\n }\n }\n\n // Delete every keyring in the revoke set. Order doesn't matter\n // because each keyring file is independent on disk; we don't have\n // referential integrity to maintain across deletes.\n for (const userId of usersToRevoke) {\n await store.delete(vault, '_keyring', userId)\n // Cascade-delete the principal's user envelope. Idempotent — no\n // error when the envelope was never written (e.g. the user was\n // granted but never authenticated to write their own profile).\n await deleteUserEnvelope(store, vault, userId)\n // Also drop the visibility sidecar at `_meta/visibility/<userId>`.\n // If the same `userId` is re-granted later (rare for humans,\n // possible for service accounts and test fixtures), the new\n // principal must start with a fresh visibility state instead of\n // silently inheriting the revoked user's `hidden` flag.\n await deleteUserVisibility(store, vault, userId)\n }\n\n // Single rotation pass at the end. The cost is O(records in\n // affected collections), NOT O(records × cascade depth) — every\n // descendant's collections were unioned into `affectedCollections`\n // before we got here, so the rotation re-encrypts each affected\n // record exactly once regardless of how deep the cascade went.\n //\n // #1043 — this is UNCONDITIONAL, and it is what makes revocation mean\n // anything. Revocation's first act is `store.delete(vault, '_keyring',\n // userId)`, and the store is untrusted by design: it can simply decline.\n // The revoked member's old keyring file is authentic — it unwraps under\n // their own KEK and its canary verifies — so nothing in `loadKeyring` can\n // tell it is stale. Rotation is the only step the store cannot suppress,\n // because it re-keys the records themselves. A probe confirmed both halves:\n // with rotation the revoked member is locked out entirely; without it,\n // revocation is a complete no-op and they keep reading data written after\n // they were revoked.\n if (affectedCollections.size > 0) {\n await rotateKeys(store, vault, callerKeyring, { collections: [...affectedCollections] })\n }\n}\n\n// ─── Update User ───────────────────────────────────────────────────────\n\n/**\n * Mutate `role`, `displayName`, and/or `permissions` on an existing\n * keyring. Pure plaintext-header rewrite — no DEK rewrap, no KEK\n * required, no authenticator slots touched. Tier-2 enrollments and\n * recovery codes survive the operation.\n *\n * Role-elevation guard: BOTH the old role AND the new role must\n * satisfy `canUpdateRole(callerRole, _)`. This blocks the two\n * privilege-escalation shapes:\n * - admin elevates someone (or themselves) to owner\n * - admin demotes an owner to a role they then control\n *\n * Owner is always allowed. Admin manages admin / operator / viewer /\n * client laterally.\n *\n * Identity preserved: same userId, same DEK wrappings. Last-write-wins\n * through the standard keyring put (same concurrency story as `grant`\n * and `revoke`).\n *\n * @throws `NoAccessError` when no keyring exists for the target.\n * @throws `PermissionDeniedError` when the role hierarchy rejects.\n * @throws `ValidationError` when the diff is empty (nothing to update).\n *\n */\nexport async function updateKeyringIdentity(\n store: NoydbStore,\n vault: string,\n callerKeyring: UnlockedKeyring,\n options: UpdateUserOptions,\n): Promise<void> {\n if (\n options.role === undefined &&\n options.displayName === undefined &&\n options.permissions === undefined\n ) {\n throw new ValidationError(\n `updateUser: at least one of role / displayName / permissions must be provided ` +\n `(userId: \"${options.userId}\").`,\n )\n }\n\n const found = await readKeyringFile(store, vault, options.userId)\n if (!found) {\n throw new NoAccessError(\n `updateUser: user \"${options.userId}\" has no keyring in vault \"${vault}\".`,\n )\n }\n const target = found.file\n\n // Role-elevation guard. The OLD role must be one this caller is\n // allowed to manage, AND the NEW role (if changing) must be too.\n // Two-sided check: blocks admin→owner promotion (new side) and\n // demoting an owner (old side).\n if (!canUpdateRole(callerKeyring.role, target.role)) {\n throw new PermissionDeniedError(\n `Role \"${callerKeyring.role}\" cannot update a keyring with role \"${target.role}\"`,\n )\n }\n if (\n options.role !== undefined &&\n options.role !== target.role &&\n !canUpdateRole(callerKeyring.role, options.role)\n ) {\n throw new PermissionDeniedError(\n `Role \"${callerKeyring.role}\" cannot promote target to role \"${options.role}\"`,\n )\n }\n\n const next: KeyringFile = {\n ...target,\n ...(options.role !== undefined && { role: options.role }),\n ...(options.displayName !== undefined && {\n // null clears the field (stored as \"\"); a string sets it.\n display_name: options.displayName ?? '',\n }),\n ...(options.permissions !== undefined && { permissions: options.permissions }),\n }\n\n await writeKeyringFile(store, vault, options.userId, next)\n}\n\n// ─── Key Rotation ──────────────────────────────────────────────────────\n\n/**\n * Rotate DEKs for specified collections:\n * 1. Generate new DEKs\n * 2. Re-encrypt all records in affected collections\n * 3. Re-wrap new DEKs for all remaining users\n */\n/**\n * What a DEK rotation leaves behind (#854).\n *\n * A member's DEKs are wrapped under that member's KEK, and a KEK derives only\n * from that member's secret — so the caller cannot re-wrap a fresh DEK for\n * anyone else. That is the zero-knowledge property working as designed, but it\n * means rotation necessarily DROPS the rotated collections from every other\n * member's keyring rather than re-keying them.\n *\n * `needsRegrant` names exactly who lost what, so the caller can re-run\n * `grant()` instead of discovering the access loss later.\n */\nexport interface RotateResult {\n /** (member, collection) pairs whose access was dropped by the rotation. */\n readonly needsRegrant: ReadonlyArray<{ readonly userId: string; readonly collection: string }>\n}\n\n/** Options for {@link rotateKeys} (#846b — was a bare `string[]`). */\nexport interface RotateKeysOptions {\n /** Collections whose DEKs are re-minted. */\n readonly collections: readonly string[]\n}\n\nexport async function rotateKeys(\n store: NoydbStore,\n vault: string,\n callerKeyring: UnlockedKeyring,\n opts: RotateKeysOptions,\n): Promise<RotateResult> {\n const { collections } = opts\n // FR-6: re-keying is an owner-only meta-capability. A custodian operates the\n // vault fully but must NOT rotate — rotation would let it mint fresh DEKs and\n // strip the sealed owner's access, breaking the inalienability floor.\n if (callerKeyring.role === 'custodian') {\n throw new PermissionDeniedError(\n 'custodian cannot rotate keys (FR-6: re-key is an owner-only meta-capability; use the Deed owner)',\n )\n }\n // Generate new DEKs for each affected collection\n // #1074 part 2 — RESUME. A pending DEK means a previous rotation of this\n // collection was interrupted after persisting its key but before committing.\n // Reuse it: minting a fresh one would strand every record the interrupted run\n // already moved, since nothing would hold the key they were sealed under.\n //\n // This is what makes `rotateKeys` its own resume path — re-running it after a\n // crash finishes the job rather than starting a second, incompatible one.\n const newDeks = new Map<string, EnclaveKey>()\n for (const collName of collections) {\n newDeks.set(collName, callerKeyring.pendingDeks?.get(collName) ?? await generateDEK())\n }\n\n // Persist the new DEKs BEFORE rewriting a single record. This is the whole\n // fix: previously the key existed only in memory until after the loop, so any\n // interruption left already-rewritten records sealed under a key that was\n // never saved — permanently unreadable, not merely un-migrated.\n //\n // `deks` still holds the OLD key here, so reads during the window continue to\n // work for records the loop has not reached. Records it HAS reached are\n // unreadable until the rotation is resumed — degraded, but recoverable, which\n // is the property that was missing.\n // `pendingDeks` is optional on the public `UnlockedKeyring` — absent simply\n // means no rotation is in flight — so materialise it on first use rather than\n // forcing every constructor of the type to write an empty map.\n const pending = callerKeyring.pendingDeks ?? new Map<string, EnclaveKey>()\n ;(callerKeyring as { pendingDeks?: Map<string, EnclaveKey> }).pendingDeks = pending\n for (const [collName, newDek] of newDeks) {\n pending.set(collName, newDek)\n }\n await persistKeyring(store, vault, callerKeyring)\n\n // Re-encrypt all records in affected collections\n //\n // FORWARD REQUIREMENT (not implemented here — DEK rotation predates\n // perRecordKeys/classified fields): a future perRecordKeys-aware DEK\n // rotation MUST DROP `_bidx` here rather than carry it forward. `_bidx` is\n // rooted in the (soon-to-be-dead) old DEK; a stale tag surviving under the\n // new DEK is unreturnable garbage (no key can ever re-derive it to match a\n // query again) that still LEAKS the old equality partition. Unlike\n // `rotateRecordCek` (record-keys/sealing.ts), which carries `_bidx`\n // verbatim because it is CEK-rotation-only and the DEK is unchanged, a DEK\n // rotation changes the root the tag is derived from, so index coverage can\n // only regrow per-record, the next time each record is `put()` under the\n // new DEK.\n //\n // #1074 — CRASH-SAFETY HAZARD, still open, and the scope is GENERAL.\n //\n // This was previously filed as D-5 and described narrowly: \"in a mixed\n // collection, if a later record throws\". That framing understated it and is\n // why it sat. The hazard applies to ANY interruption of this loop — a thrown\n // error, a process kill, a lost connection — because the new DEK exists only\n // in memory until `persistKeyring` runs AFTER every record is rewritten.\n // Interrupt it and the already-rewritten records are sealed under a DEK that\n // was never persisted: permanently unreadable, not merely un-migrated.\n //\n // Fixed separately (see #1074): the new DEK must be persisted BEFORE the loop\n // so a resume can find it. Doing that needs the keyring to hold two\n // generations transiently, so it is its own change rather than part of this\n // one. What IS fixed here is everything the loop does to a record it reaches.\n for (const collName of collections) {\n const oldDek = callerKeyring.deks.get(collName)\n const newDek = newDeks.get(collName)!\n if (!oldDek) continue\n\n const ids = await store.list(vault, collName)\n for (const id of ids) {\n const envelope = await store.get(vault, collName, id)\n if (!envelope || !envelope._iv) continue\n\n // #1074 — one enclave helper does the whole per-record rotation:\n // carries every slot except `_bidx`, and re-wraps a per-record CEK\n // instead of trying to decrypt its body under the DEK. Both were wrong\n // inline here, and `enclave-body-only` is why they moved — envelope\n // surgery belongs in the enclave, where the guard can see it.\n // Returns null when this record is already under `newDek` — the\n // resumed-rotation case. A record readable under NEITHER key rethrows\n // rather than being skipped: walking silently past unreadable records\n // would turn a loud failure into permanent quiet loss.\n const newEnvelope = await rekeyEnvelopeIfNeeded(envelope, oldDek, newDek)\n if (newEnvelope !== null) await store.put(vault, collName, id, newEnvelope)\n }\n }\n\n // COMMIT (#1074 part 2). Every record is now under the new key, so promote\n // it and clear the pending marker. A keyring that still carries `pending_deks`\n // after this point means the rotation did not reach here.\n for (const [collName, newDek] of newDeks) {\n callerKeyring.deks.set(collName, newDek)\n callerKeyring.pendingDeks?.delete(collName)\n }\n await persistKeyring(store, vault, callerKeyring)\n\n // Drop the rotated collections from every OTHER member's keyring.\n //\n // The old comment here said these were \"re-wrapped\", which is what\n // `Noydb.rotate`'s jsdoc promised too — but re-wrapping is impossible and\n // always was (#854). See the note below.\n const needsRegrant: Array<{ userId: string; collection: string }> = []\n const userIds = await store.list(vault, '_keyring')\n for (const userId of userIds) {\n if (userId === callerKeyring.userId) continue\n\n const userFound = await readKeyringFile(store, vault, userId)\n if (!userFound) continue\n\n const userKeyringFile = userFound.file\n // A user's DEKs are wrapped with that user's KEK, and a KEK derives only\n // from the user's secret — the caller can never derive another user's\n // KEK, so re-wrapping the new DEKs for them is impossible. Rotation\n // therefore REMOVES the rotated collections' DEK entries (and permissions)\n // from each remaining user's keyring: secure (revoked keys are gone) but\n // the owner must re-run grant() for those users/collections.\n\n const updatedDeks = { ...userKeyringFile.deks }\n const updatedPermissions = { ...userKeyringFile.permissions }\n for (const collName of collections) {\n // Report only what the member actually held — a user who never had the\n // collection does not \"need a re-grant\".\n if (collName in updatedDeks || collName in updatedPermissions) {\n needsRegrant.push({ userId, collection: collName })\n }\n delete updatedDeks[collName]\n delete updatedPermissions[collName]\n }\n\n const updatedKeyring: KeyringFile = {\n ...userKeyringFile,\n deks: updatedDeks,\n permissions: updatedPermissions,\n }\n\n await writeKeyringFile(store, vault, userId, updatedKeyring)\n }\n\n return { needsRegrant }\n}\n\n// ─── Change Secret ─────────────────────────────────────────────────────\n\n/** Options for {@link changeSecret} (#846b). */\nexport interface ChangeSecretOptions extends SecretPolicy {\n /** The replacement secret. */\n readonly newSecret: string\n /** Escape hatch for fixtures and migrations — skips the strength gate. */\n readonly allowWeakSecret?: boolean\n}\n\n/**\n * Change the user's secret. Re-wraps every DEK under the new KEK.\n *\n * Validates the new secret against the strength rules unless\n * `allowWeakSecret: true` is passed. Mirrors `rotateSecret`'s\n * default-on validation contract.\n *\n * `db.team.rotateSecret()` adds a `checkGate('rotate-secret')` step\n * on top of this primitive and additionally requires the OLD secret\n * for re-derivation; `changeSecret` reuses the cached unlocked KEK so\n * the OLD secret is not retyped.\n */\nexport async function changeSecret(\n store: NoydbStore,\n vault: string,\n keyring: UnlockedKeyring,\n opts: ChangeSecretOptions,\n): Promise<UnlockedKeyring> {\n // An echo keyring rotates via `rotateSecret` only — `changeSecret`\n // rebuilds the keyring file literal below and would silently DROP the\n // `echo` block, permanently losing the ceremony. Guard on the file as\n // persisted (not `keyring`, which carries no echo info) before any\n // derivation.\n const existingFound = await readKeyringFile(store, vault, keyring.userId)\n if (existingFound && existingFound.file.echo !== undefined) throw new EchoCeremonyRequiredError()\n\n const { newSecret } = opts\n if (!opts.allowWeakSecret) {\n assertStrongSecret(newSecret, opts)\n }\n const newSalt = generateSalt()\n const newKek = await deriveKey(newSecret, newSalt)\n\n // Re-wrap all DEKs with the new KEK\n const wrappedDeks: Record<string, string> = {}\n for (const [collName, dek] of keyring.deks) {\n wrappedDeks[collName] = await wrapKey(dek, newKek)\n }\n\n const canary = await mintKeyringCanary(newKek)\n const keyringFile: KeyringFile = {\n _noydb_keyring: NOYDB_KEYRING_VERSION,\n user_id: keyring.userId,\n display_name: keyring.displayName,\n role: keyring.role,\n permissions: keyring.permissions,\n deks: wrappedDeks,\n salt: bufferToBase64(newSalt),\n created_at: new Date().toISOString(),\n granted_by: keyring.userId,\n canary,\n }\n\n await writeKeyringFile(store, vault, keyring.userId, keyringFile)\n\n return {\n userId: keyring.userId,\n displayName: keyring.displayName,\n role: keyring.role,\n permissions: keyring.permissions,\n deks: keyring.deks, // Same DEKs, different wrapping\n kek: newKek,\n salt: newSalt,\n // Tier-2 slots are NOT preserved through `changeSecret` —\n // each slot wraps the OLD KEK, so the new keyring has no\n // authenticator slots until the user re-enrolls. The higher-level\n // `db.team.rotateSecret()` preserves slots by rewrapping the\n // KEK reference, not the KEK itself.\n authenticators: [],\n ...(keyring.policy !== undefined && { policy: keyring.policy }),\n }\n}\n\n// ─── Bundle recipients ──────────────────────────────────────────\n\n/**\n * Echo-mode recipient secret (spec #940). A pod slot may embed the reveal\n * (`'portable'`, default) or omit it (`'none'`) — `'sealed'` is\n * live-device-only and unavailable here: `writePod` has no device context\n * to seal the reveal blob against.\n */\nexport interface EchoRecipientSecret extends EchoSecretParts {\n readonly reveal?: 'portable' | 'none'\n}\n\n/**\n * Recipient slot in a re-keyed `.noydb` bundle. Each slot becomes its\n * own keyring file inside the bundle, sealed with its own secret.\n * Same role/permission semantics as `db.grant()` but no store side\n * effect — the slot only exists inside the bundle bytes.\n *\n * @public\n */\nexport interface PodRecipient {\n /** User id stamped onto the keyring file in the bundle. */\n readonly id: string\n /** Optional display name. Defaults to `id`. */\n readonly displayName?: string\n /**\n * Secret the recipient will type to unlock — a plain string for a\n * standard keyring, or the structured 3-part {@link EchoRecipientSecret}\n * to embed an echo keyring so the anti-phishing ceremony travels with\n * the pod (spec #940).\n */\n readonly secret: string | EchoRecipientSecret\n /** Role on the destination vault. Defaults to `'viewer'`. */\n readonly role?: Role\n /**\n * Per-collection permissions. When omitted, role defaults apply.\n * Restricting permissions here ALSO restricts which DEKs are wrapped\n * into the slot — a slot with `{ invoices: 'ro' }` cannot decrypt\n * other collections even though their ciphertext sits in the bundle.\n */\n readonly permissions?: Permissions\n /**\n * Optional `as-*` export grants on the destination vault.\n * Mirrors the `exportCapability` field on a live keyring.\n */\n readonly exportCapability?: ExportCapability\n /**\n * Optional `as-*` import grants on the destination vault.\n * Mirrors the `importCapability` field on a live keyring.\n * Default-closed: no plaintext format granted, no bundle import.\n */\n readonly importCapability?: ImportCapability\n /**\n * Optional bundle-slot expiry. ISO-8601 timestamp; past the\n * cutoff this slot's keyring refuses to load with\n * `KeyringExpiredError`. Time-boxed audit access pattern: \"this\n * slot works for 30 days then becomes opaque to its holder.\"\n */\n readonly expiresAt?: string\n}\n\n/**\n * Build a `KeyringFile` for one bundle recipient, given the source\n * vault's unwrapped DEKs. Mirrors `grant()` minus the store write —\n * the produced file is meant to be embedded in the bundle's\n * `keyrings` map, never persisted to the source vault.\n *\n * Privilege-escalation check still runs: every DEK wrapped into the\n * recipient's keyring must come from the source's own DEK set.\n *\n * @internal\n */\nexport async function buildRecipientKeyringFile(\n callerKeyring: UnlockedKeyring,\n recipient: PodRecipient,\n): Promise<KeyringFile> {\n if (!callerKeyring.kek) {\n throw new ValidationError(\n 'buildRecipientKeyringFile: caller keyring has no KEK — tier-2 wrap-DEKs ' +\n 'and tier-3 PIN-resume sessions cannot create bundle recipients. ' +\n 'Re-authenticate at tier 1 (secret) before building a bundle.',\n )\n }\n\n const role: Role = recipient.role ?? 'viewer'\n const permissions = resolvePermissions(role, recipient.permissions)\n\n const newSalt = generateSalt()\n const newKek = typeof recipient.secret === 'string'\n ? await deriveKey(recipient.secret, newSalt)\n : await deriveEchoKey(recipient.secret, newSalt)\n\n const wrappedDeks: Record<string, string> = {}\n\n // Collections the recipient was explicitly granted permission to.\n for (const collName of Object.keys(permissions)) {\n const dek = callerKeyring.deks.get(collName)\n if (dek) {\n wrappedDeks[collName] = await wrapKey(dek, newKek)\n }\n }\n\n // owner / admin / custodian / viewer: wrap every known DEK (matches grant).\n // FR-6: a custodian recipient operates every collection, so it receives all\n // DEKs just like admin (kept in lockstep with grant()'s all-DEKs branch).\n if (role === 'owner' || role === 'admin' || role === 'custodian' || role === 'viewer') {\n for (const [collName, dek] of callerKeyring.deks) {\n if (!(collName in wrappedDeks)) {\n wrappedDeks[collName] = await wrapKey(dek, newKek)\n }\n }\n }\n\n // Always propagate system-prefixed collection DEKs (`_ledger`, etc.) —\n // the recipient needs them to verify the bundle on import.\n for (const [collName, dek] of callerKeyring.deks) {\n if (collName.startsWith('_') && !(collName in wrappedDeks)) {\n wrappedDeks[collName] = await wrapKey(dek, newKek)\n }\n }\n\n // Anti-privilege-escalation: every wrapped DEK must come from the\n // caller's own DEK set. Belt-and-braces with the lookups above.\n for (const collName of Object.keys(wrappedDeks)) {\n if (!callerKeyring.deks.has(collName)) {\n throw new PrivilegeEscalationError(collName)\n }\n }\n\n const canary = await mintKeyringCanary(newKek)\n return {\n _noydb_keyring: NOYDB_KEYRING_VERSION,\n user_id: recipient.id,\n display_name: recipient.displayName ?? recipient.id,\n role,\n permissions,\n deks: wrappedDeks,\n salt: bufferToBase64(newSalt),\n created_at: new Date().toISOString(),\n granted_by: callerKeyring.userId,\n canary,\n ...(recipient.exportCapability !== undefined\n ? { export_capability: recipient.exportCapability }\n : {}),\n ...(recipient.importCapability !== undefined\n ? { import_capability: recipient.importCapability }\n : {}),\n ...(recipient.expiresAt !== undefined\n ? { expires_at: recipient.expiresAt }\n : {}),\n // The presence of this block is what makes the recipient's slot an\n // echo keyring — mirrors `createOwnerKeyring`'s same dance so the\n // ceremony (prompt → echo → key) travels with the pod, not just a\n // live owner keyring.\n ...(typeof recipient.secret !== 'string'\n ? { echo: await buildEchoBlock(recipient.secret, { kind: recipient.secret.reveal ?? 'portable' }) }\n : {}),\n }\n}\n\n// ─── List Users ────────────────────────────────────────────────────────\n\n/** List all users with access to a vault. */\nexport async function listUsers(\n store: NoydbStore,\n vault: string,\n): Promise<UserInfo[]> {\n const userIds = await store.list(vault, '_keyring')\n const users: UserInfo[] = []\n\n for (const userId of userIds) {\n const found = await readKeyringFile(store, vault, userId)\n if (!found) continue\n const kf = found.file\n users.push({\n userId: kf.user_id,\n displayName: kf.display_name,\n role: kf.role,\n permissions: kf.permissions,\n createdAt: kf.created_at,\n grantedBy: kf.granted_by,\n })\n }\n\n return users\n}\n\n/**\n * Optional filter knobs for {@link listUsersWithEnvelopes}.\n *\n * - `includeHidden` — when true, principals with `_meta/visibility/<id>`\n * set to `{ hidden: true }` are returned alongside everyone else.\n * Requires `owner` or `admin` callerRole; lower roles get\n * {@link import('../../kernel/errors.js').PermissionDeniedError}.\n */\nexport interface ListUsersOptions {\n readonly includeHidden?: boolean\n}\n\n/**\n * Joined enumeration: every keyring + its `_users/<keyringId>`\n * envelope side by side. Convenience for admin UIs that want to\n * render team-member lists with profile data (\"Bob — operator —\n * 'Bob the Auditor' avatar X locale fr-FR\") in a single pass.\n *\n * `userEnvelopeDek` is the vault's `_users` collection DEK\n * (`vault.getDEK('_users')`); used to decrypt every envelope.\n *\n * `callerRole` drives the directory-visibility checks:\n *\n * - When the vault's `_meta/directory` document has `enabled: false`,\n * only `owner` and `admin` callers may enumerate; anyone else gets\n * {@link import('../../kernel/errors.js').DirectoryDisabledError}.\n * - Principals with `_meta/visibility/<id>` set to `{ hidden: true }`\n * are filtered out by default. `owner`/`admin` callers can pass\n * `{ includeHidden: true }` to see them; lower roles passing that\n * option get `PermissionDeniedError`.\n *\n * Honest caveat: these filters are a UX hint, not a security\n * boundary. The keyring file is still listed at `_keyring/*` and the\n * envelope ciphertext at `_users/*`. A caller with direct store access\n * — or a caller that calls this function with `callerRole: 'owner'`\n * unconditionally — sees every principal. The protection is only as\n * strong as the role the calling layer passes in. The hub-level wrapper\n * on `Vault` sources `callerRole` from the unlocked keyring's `role`\n * field, which is signed-by-construction (it lives in the user's own\n * keyring file). See `https://github.com/vLannaAi/noy-db-docs/blob/main/content/docs/services/user-envelope.md` →\n * \"Directory visibility\".\n *\n * Principals without a persisted envelope (legacy keyrings predating\n * the user-envelope feature) come back with `envelope: null`. The\n * caller chooses how to render — usually \"fall back to keyring's\n * `displayName`\".\n *\n * Order matches `listUsers()` (store-defined; sort if you need a\n * stable display order).\n */\nexport async function listUsersWithEnvelopes<T = unknown>(\n store: NoydbStore,\n vault: string,\n userEnvelopeDek: EnclaveKey,\n callerRole: Role,\n options: ListUsersOptions = {},\n): Promise<Array<{ user: UserInfo; envelope: UserEnvelope<T> | null }>> {\n // FR-6: custodian is INTENTIONALLY treated as NON-privileged here (SAFER\n // default — review flag). Directory-privilege is a team-MANAGEMENT capability\n // (bypassing the visibility toggle + listing hidden principals); a custodian\n // is non-owning and cannot grant/revoke, so it should not see the hidden\n // team membership. It still gets the normal (non-hidden) directory view.\n const isPrivileged = callerRole === 'owner' || callerRole === 'admin'\n\n // 1. Vault-level directory toggle.\n const dirConfig = await readDirectoryConfig(store, vault)\n if (dirConfig?.enabled === false && !isPrivileged) {\n throw new DirectoryDisabledError(vault)\n }\n\n // 2. `includeHidden` requires admin/owner.\n if (options.includeHidden && !isPrivileged) {\n throw new PermissionDeniedError(\n 'Permission denied — listUsersWithEnvelopes({ includeHidden: true }) requires owner or admin role',\n )\n }\n\n const users = await listUsers(store, vault)\n const out: Array<{ user: UserInfo; envelope: UserEnvelope<T> | null }> = []\n for (const user of users) {\n if (!options.includeHidden) {\n const visibility = await readUserVisibility(store, vault, user.userId)\n if (visibility?.hidden) continue\n }\n const envelope = await loadUserEnvelopeFn<T>(\n store,\n vault,\n user.userId,\n userEnvelopeDek,\n )\n out.push({ user, envelope })\n }\n return out\n}\n\n\n// ─── DEK Management ────────────────────────────────────────────────────\n\n/** Ensure a DEK exists for a collection. Generates one if new. */\n/**\n * Does this collection already hold persisted records?\n *\n * The one question that separates \"I am creating this collection\" from \"I was\n * never given the key to this collection\" (#1004). Kept deliberately narrow:\n * it asks the store, not the schema registry, because the store is the only\n * authority on what ciphertext actually exists.\n */\nasync function collectionHasRecords(\n store: NoydbStore,\n vault: string,\n collectionName: string,\n): Promise<boolean> {\n const ids = await store.list(vault, collectionName)\n return ids.length > 0\n}\n\n/**\n * Re-read this principal's persisted keyring and adopt the DEK for one\n * collection if it has appeared there since this handle unlocked (#1010).\n *\n * Returns the DEK (also caching it into `keyring.deks`) or `null` when the\n * persisted keyring genuinely does not carry it. Only ever called on the\n * miss path, so an up-to-date handle never pays for it.\n *\n * @internal\n */\nasync function reloadPersistedDEK(\n store: NoydbStore,\n vault: string,\n keyring: UnlockedKeyring,\n collectionName: string,\n): Promise<EnclaveKey | null> {\n if (!keyring.kek) return null\n const found = await readKeyringFile(store, vault, keyring.userId)\n const wrapped = found?.file.deks[collectionName]\n if (wrapped === undefined) return null\n try {\n const dek = await unwrapKey(wrapped, keyring.kek)\n keyring.deks.set(collectionName, dek)\n return dek\n } catch {\n // A DEK that will not unwrap under this KEK is corruption, not access —\n // fall through to the caller's denial rather than masking it as success.\n return null\n }\n}\n\nexport async function ensureCollectionDEK(\n store: NoydbStore,\n vault: string,\n keyring: UnlockedKeyring,\n): Promise<(collectionName: string) => Promise<EnclaveKey>> {\n // Dedupe concurrent first-time DEK creates per collection. Without\n // this, two concurrent `getDEK('foo')` calls both pass the `existing`\n // check (the Map is empty), both generate fresh DEKs, and the second\n // `set` overwrites the first — making any envelope encrypted with\n // the discarded DEK fail to decrypt later (TamperedError on read).\n // Pre-existing race exposed by the multi-writer ledger work.\n const inFlight = new Map<string, Promise<EnclaveKey>>()\n return async (collectionName: string): Promise<EnclaveKey> => {\n const existing = keyring.deks.get(collectionName)\n if (existing) return existing\n const pending = inFlight.get(collectionName)\n if (pending) return pending\n\n const promise = (async () => {\n // #1004 — minting on a DEK miss is only correct when the caller is\n // ENTITLED to the collection (they are creating it, or they were granted\n // it and it did not exist at grant time). For an unentitled caller the\n // miss IS the denial, and minting fabricated a key that decrypts none of\n // the stored envelopes — so the denial re-emerged from the enclave as an\n // AES-GCM tag failure, i.e. `TamperedError`, the signal reserved for\n // genuine ciphertext corruption. In a zero-knowledge design the DEK is\n // the access control, so an unentitled miss is exactly `NoAccessError`.\n //\n // Entitlement is read straight off the keyring — no store round-trip, so\n // the authorized mint path costs exactly what it did before.\n //\n // System collections (`_ledger`, `_meta`, `_history`, the fanout\n // sidecars …) are exempt: their DEKs are propagated to every role at\n // grant time and they are minted lazily by machinery running on behalf\n // of a user who may hold no explicit permission for them. They are never\n // addressable by user code, so nothing is being protected by denying\n // here — only internal writes would break.\n if (!collectionName.startsWith('_')) {\n if (!hasAccess(keyring, collectionName)) {\n throw new NoAccessError(\n `No access — user does not have a key for collection \"${collectionName}\". ` +\n 'Grant them this collection in `permissions` to give them one.',\n )\n }\n // #1010 — the ENTITLED half of the same defect. Being allowed to read a\n // collection is not the same as holding its key: a grant only ever\n // wraps the DEKs that exist at grant time, and re-wrapping later is\n // impossible because it needs the grantee's KEK, derived from a secret\n // the vault never stores. So a principal granted BEFORE a collection\n // existed is entitled to it and has no key for it — and minting one\n // here produced a key that decrypts nothing, resurfacing as\n // `TamperedError`. This is reachable for every whole-vault role\n // (`admin`, `viewer`, `custodian`) and for a `'*'` wildcard grantee.\n //\n // Costs one `list()`, and only on this path: an entitled principal\n // whose keyring is missing a DEK. Creating a genuinely new collection\n // finds no records and mints exactly as before.\n if (await collectionHasRecords(store, vault, collectionName)) {\n // Before denying: the in-memory keyring may simply be STALE. A DEK is\n // minted lazily by whichever handle first touches a collection and\n // persisted to the keyring file — so a second live handle for the\n // SAME principal (a second tab, a second `createNoydb` over one\n // store) that was opened before the collection existed holds a\n // keyring snapshot without it. That principal is not unauthorized;\n // their own key is already on disk. Re-read it.\n //\n // This is what separates \"my snapshot is behind\" from \"I was granted\n // before this collection existed\": only the latter finds nothing.\n const refreshed = await reloadPersistedDEK(store, vault, keyring, collectionName)\n if (refreshed) return refreshed\n throw new NoAccessError(\n `No access — user \"${keyring.userId}\" is entitled to collection \"${collectionName}\" ` +\n 'but holds no key for it, because the collection was created AFTER their grant. ' +\n 'A collection DEK can only be wrapped at grant time (wrapping needs the grantee\\'s ' +\n 'secret, which the vault never stores), so this cannot be back-filled — re-grant ' +\n 'the user to give them the key.',\n )\n }\n }\n const dek = await generateDEK()\n keyring.deks.set(collectionName, dek)\n await persistKeyring(store, vault, keyring)\n return dek\n })()\n inFlight.set(collectionName, promise)\n try {\n return await promise\n } finally {\n inFlight.delete(collectionName)\n }\n }\n}\n\n// ─── Permission Checks ─────────────────────────────────────────────────\n\n/**\n * The `Permissions` catch-all key (#1010). `Permissions` has always documented\n * `'*'` as \"the wildcard collection matching all collections in the vault\", but\n * nothing expanded it — a grantee handed the documented catch-all got no keys\n * at all. It is honoured in three places, and they must agree: the DEK wrapping\n * in `grant()`, and both permission checks below.\n */\nexport const PERMISSION_WILDCARD = '*'\n\n/** Does this permission map hand over the whole vault? */\nexport function permissionsAreWildcard(permissions: Permissions | undefined): boolean {\n return permissions !== undefined && PERMISSION_WILDCARD in permissions\n}\n\n/** Check if a user has write permission for a collection. */\nexport function hasWritePermission(keyring: UnlockedKeyring, collectionName: string): boolean {\n // FR-6: custodian writes every collection (admin-level operational rw).\n if (keyring.role === 'owner' || keyring.role === 'admin' || keyring.role === 'custodian') return true\n if (keyring.role === 'viewer' || keyring.role === 'client') return false\n if (keyring.permissions[PERMISSION_WILDCARD] === 'rw') return true\n return keyring.permissions[collectionName] === 'rw'\n}\n\n/** Check if a user has any access to a collection. */\nexport function hasAccess(keyring: UnlockedKeyring, collectionName: string): boolean {\n // FR-6: custodian reads every collection (admin-level operational access).\n if (\n keyring.role === 'owner' ||\n keyring.role === 'admin' ||\n keyring.role === 'custodian' ||\n keyring.role === 'viewer'\n )\n return true\n if (PERMISSION_WILDCARD in keyring.permissions) return true\n return collectionName in keyring.permissions\n}\n\n// ─── Helpers ───────────────────────────────────────────────────────────\n\n/** Persist a keyring file to the store. */\nexport async function persistKeyring(\n store: NoydbStore,\n vault: string,\n keyring: UnlockedKeyring,\n): Promise<void> {\n if (!keyring.kek) {\n throw new ValidationError(\n 'persistKeyring: keyring.kek is null — cannot wrap DEKs without the KEK. ' +\n 'This typically means the keyring was opened via tier-3 PIN resume, ' +\n 'session restore, or a wrap-DEKs tier-2 unlock. Re-authenticate at ' +\n 'tier 1 (secret) before persisting.',\n )\n }\n // Carry the `echo` block forward (#940). This function rebuilds the file\n // from the `UnlockedKeyring`, which deliberately carries no echo info (same\n // rationale as the `changeSecret` guard), so it has to be read back off the\n // persisted file — otherwise the first DEK-provisioning write would drop it\n // and silently degrade an echo keyring into a standard one that no secret\n // shape can open (the KEK stays echo-derived).\n const existingFound = await readKeyringFile(store, vault, keyring.userId)\n const existingEcho = existingFound?.file.echo\n // Same carry-forward rationale as `echo`, for two fields that describe the\n // keyring's ORIGIN rather than its current contents. `UnlockedKeyring` does\n // not carry either, so rebuilding the file from it defaulted `granted_by` to\n // the holder themselves and stamped a fresh `created_at` — meaning any\n // DEK-provisioning write silently re-parented the holder to themselves and\n // collapsed the admin delegation subtree that `granted_by` encodes.\n const existingGrantedBy = existingFound?.file.granted_by\n const existingCreatedAt = existingFound?.file.created_at\n\n const wrappedDeks: Record<string, string> = {}\n for (const [collName, dek] of keyring.deks) {\n wrappedDeks[collName] = await wrapKey(dek, keyring.kek)\n }\n // #1074 — an uncommitted rotation's key must survive a crash, so it is\n // persisted under its own field rather than mixed into `deks`, which would\n // make readers treat it as current before any record had moved.\n const wrappedPending: Record<string, string> = {}\n for (const [collName, dek] of keyring.pendingDeks ?? []) {\n wrappedPending[collName] = await wrapKey(dek, keyring.kek)\n }\n const canary = await mintKeyringCanary(keyring.kek)\n\n const keyringFile: KeyringFile = {\n _noydb_keyring: NOYDB_KEYRING_VERSION,\n user_id: keyring.userId,\n display_name: keyring.displayName,\n role: keyring.role,\n permissions: keyring.permissions,\n deks: wrappedDeks,\n ...(Object.keys(wrappedPending).length > 0 ? { pending_deks: wrappedPending } : {}),\n salt: bufferToBase64(keyring.salt),\n created_at: existingCreatedAt ?? new Date().toISOString(),\n granted_by: existingGrantedBy ?? keyring.userId,\n canary,\n ...(keyring.exportCapability !== undefined && { export_capability: keyring.exportCapability }),\n ...(keyring.importCapability !== undefined && { import_capability: keyring.importCapability }),\n ...(keyring.authenticators.length > 0 && { authenticators: keyring.authenticators }),\n ...(keyring.policy !== undefined && { policy: keyring.policy }),\n ...(existingEcho !== undefined && { echo: existingEcho }),\n }\n\n await writeKeyringFile(store, vault, keyring.userId, keyringFile)\n}\n\n// ─── Export capability ──────────────────────────────────────\n\n/**\n * Role-based default policy for the encrypted-bundle capability.\n *\n * Applied when `keyring.exportCapability` is absent or\n * `exportCapability.bundle` is undefined:\n *\n * - `owner` / `admin` → `true` (happy-path backup without friction)\n * - `operator` / `viewer` / `client` → `false` (explicit grant required)\n *\n * Rationale: a bundle is inert without the KEK, so an owner backing up\n * their own vault doesn't need friction; a non-admin role producing a\n * bundle for an external party does, because the bundle outlives\n * keyring revocation.\n */\nfunction defaultBundleCapability(role: Role): boolean {\n // FR-6: custodian is INTENTIONALLY not in the default-true set (SAFER\n // default — review flag). A bundle is an external artifact that outlives\n // keyring revocation; the owner can still grant a custodian\n // `exportCapability.bundle = true` explicitly when an offline backup is\n // part of the custody contract. Defaulting it off keeps a custodian from\n // silently minting a revocation-surviving copy of the whole vault.\n return role === 'owner' || role === 'admin'\n}\n\n/**\n * Check whether a keyring is authorised for a given `@noy-db/as-*`\n * export tier.\n *\n * - `tier: 'plaintext'` — returns true iff `exportCapability.plaintext`\n * contains the requested `format` or the `'*'` wildcard. Default for\n * every role is empty — no grant, no plaintext export.\n * - `tier: 'bundle'` — returns `exportCapability.bundle` if present, or\n * the role-based default otherwise (owner/admin → true, else false).\n *\n * `@noy-db/as-*` packages MUST call this before invoking the underlying\n * export primitive. Rogue forks that skip the check are caught by code\n * review — the single-entry-point contract is a convention, not a\n * runtime invariant. Vault-level gated wrappers\n * (`vault.exportRecords` / `exportBlobs` / `writeBundle`) will land in a\n * follow-up PR to enforce at the primitive level.\n */\nexport function hasExportCapability(\n keyring: UnlockedKeyring,\n tier: 'plaintext',\n format: ExportFormat,\n): boolean\nexport function hasExportCapability(\n keyring: UnlockedKeyring,\n tier: 'bundle',\n): boolean\nexport function hasExportCapability(\n keyring: UnlockedKeyring,\n tier: 'plaintext' | 'bundle',\n format?: ExportFormat,\n): boolean {\n const cap = keyring.exportCapability\n if (tier === 'plaintext') {\n const allowed = cap?.plaintext ?? []\n return allowed.includes('*') || (format !== undefined && allowed.includes(format))\n }\n // tier === 'bundle'\n return cap?.bundle ?? defaultBundleCapability(keyring.role)\n}\n\n/**\n * Same-shape inspector for an `ExportCapability` value that isn't yet\n * attached to a keyring (e.g. for previewing a grant before applying).\n * Role must be supplied separately so bundle defaults can be computed.\n */\nexport function evaluateExportCapability(\n capability: ExportCapability | undefined,\n role: Role,\n tier: 'plaintext',\n format: ExportFormat,\n): boolean\nexport function evaluateExportCapability(\n capability: ExportCapability | undefined,\n role: Role,\n tier: 'bundle',\n): boolean\nexport function evaluateExportCapability(\n capability: ExportCapability | undefined,\n role: Role,\n tier: 'plaintext' | 'bundle',\n format?: ExportFormat,\n): boolean {\n if (tier === 'plaintext') {\n const allowed = capability?.plaintext ?? []\n return allowed.includes('*') || (format !== undefined && allowed.includes(format))\n }\n return capability?.bundle ?? defaultBundleCapability(role)\n}\n\n// ─── Import capability (issue ) ────────────────────────────────────\n\n/**\n * Check whether a keyring is authorised for a given `@noy-db/as-*`\n * import tier (issue ).\n *\n * - `tier: 'plaintext'` — true iff `importCapability.plaintext`\n * contains the requested `format` or the `'*'` wildcard.\n * - `tier: 'bundle'` — true iff `importCapability.bundle === true`.\n *\n * **Default-closed for every role on every dimension** — including\n * owner. Import is more dangerous than export (corrupts vs leaks), so\n * the policy refuses to assume intent. Owners must positively grant\n * the capability via `vault.grant({ importCapability: ... })`.\n */\nexport function hasImportCapability(\n keyring: UnlockedKeyring,\n tier: 'plaintext',\n format: ExportFormat,\n): boolean\nexport function hasImportCapability(\n keyring: UnlockedKeyring,\n tier: 'bundle',\n): boolean\nexport function hasImportCapability(\n keyring: UnlockedKeyring,\n tier: 'plaintext' | 'bundle',\n format?: ExportFormat,\n): boolean {\n const cap = keyring.importCapability\n if (tier === 'plaintext') {\n const allowed = cap?.plaintext ?? []\n return allowed.includes('*') || (format !== undefined && allowed.includes(format))\n }\n // tier === 'bundle' — closed default for every role\n return cap?.bundle === true\n}\n\n/**\n * Same-shape inspector for an `ImportCapability` value that isn't yet\n * attached to a keyring (e.g. previewing a grant before applying).\n * `role` is accepted for symmetry with `evaluateExportCapability` even\n * though the import policy ignores it — bundle defaults are\n * role-agnostic and closed.\n */\nexport function evaluateImportCapability(\n capability: ImportCapability | undefined,\n role: Role,\n tier: 'plaintext',\n format: ExportFormat,\n): boolean\nexport function evaluateImportCapability(\n capability: ImportCapability | undefined,\n role: Role,\n tier: 'bundle',\n): boolean\nexport function evaluateImportCapability(\n capability: ImportCapability | undefined,\n _role: Role,\n tier: 'plaintext' | 'bundle',\n format?: ExportFormat,\n): boolean {\n if (tier === 'plaintext') {\n const allowed = capability?.plaintext ?? []\n return allowed.includes('*') || (format !== undefined && allowed.includes(format))\n }\n return capability?.bundle === true\n}\n\nfunction resolvePermissions(role: Role, explicit?: Permissions): Permissions {\n // FR-6: custodian is full-access-by-role (hasAccess/hasWritePermission\n // short-circuit on the role), so — like admin — its permissions map is\n // empty. Returning {} also prevents a caller-supplied `permissions` from\n // accidentally NARROWING a custodian below its role guarantee.\n if (role === 'owner' || role === 'admin' || role === 'custodian' || role === 'viewer') return {}\n return explicit ?? {}\n}\n\nasync function writeKeyringFile(\n store: NoydbStore,\n vault: string,\n userId: string,\n keyringFile: KeyringFile,\n): Promise<void> {\n const envelope = {\n _noydb: NOYDB_FORMAT_VERSION,\n _v: 1,\n _ts: new Date().toISOString(),\n _iv: '',\n _data: JSON.stringify(keyringFile),\n }\n await store.put(vault, '_keyring', userId, envelope)\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/with-party/team/tiers.ts"],"sourcesContent":["/**\n * Hierarchical access — tier-aware keyring helpers.\n *\n * The keyring's existing `deks: Map<string, CryptoKey>` is keyed by\n * collection name. extends the key space:\n *\n * `'invoices'` — tier-0 DEK (unchanged from v0.x)\n * `'invoices#1'` — tier-1 DEK\n * `'invoices#2'` — tier-2 DEK\n *\n * Tier 0 keeps the bare collection name so any keyring written\n * before tiers existed loads without migration. Tiers ≥ 1 use `#N`\n * suffixes that\n * would be invalid as user-supplied collection names (see\n * `ReservedCollectionNameError` — `#` is reserved).\n *\n * @module\n */\n\nimport type { UnlockedKeyring } from './keyring.js'\nimport { TierNotGrantedError } from '../../kernel/errors.js'\n\n/** Canonical DEK key for a given collection + tier. Tier 0 → bare name. */\nexport function dekKey(collection: string, tier: number): string {\n if (tier <= 0) return collection\n return `${collection}#${tier}`\n}\n\n/**\n * Returns the user's effective clearance for a given collection: the\n * maximum tier for which their keyring holds a DEK. Falls back to 0\n * when the user has only the tier-0 DEK (or none — the getDEK caller\n * will raise separately).\n */\nexport function effectiveClearance(keyring: UnlockedKeyring, collection: string): number {\n let max = 0\n const prefix = `${collection}#`\n for (const key of keyring.deks.keys()) {\n if (!key.startsWith(prefix)) continue\n const suffix = key.slice(prefix.length)\n const n = Number.parseInt(suffix, 10)\n if (Number.isFinite(n) && n > max) max = n\n }\n return max\n}\n\n/**\n * Assert the caller is cleared for the requested tier. Owners and\n * admins always pass (they can mint any new tier DEK on demand);\n * other roles must already hold the tier DEK — via a prior grant or\n * an active delegation — otherwise this throws `TierNotGrantedError`.\n *\n * This gate runs BEFORE `getDEK()` on the mutation path so a\n * non-cleared operator never has the opportunity to silently\n * auto-create a tier DEK they shouldn't have.\n */\nexport function assertTierAccess(\n keyring: UnlockedKeyring,\n collection: string,\n tier: number,\n): void {\n if (tier <= 0) return\n // FR-6: custodian operates every collection at every tier (admin-level\n // operational authority), so it may mint a tier DEK on demand like admin.\n if (keyring.role === 'owner' || keyring.role === 'admin' || keyring.role === 'custodian') return\n if (!keyring.deks.has(dekKey(collection, tier))) {\n throw new TierNotGrantedError(collection, tier)\n }\n}\n"],"mappings":";;;;;AAuBO,SAAS,OAAO,YAAoB,MAAsB;AAC/D,MAAI,QAAQ,EAAG,QAAO;AACtB,SAAO,GAAG,UAAU,IAAI,IAAI;AAC9B;AA8BO,SAAS,iBACd,SACA,YACA,MACM;AACN,MAAI,QAAQ,EAAG;AAGf,MAAI,QAAQ,SAAS,WAAW,QAAQ,SAAS,WAAW,QAAQ,SAAS,YAAa;AAC1F,MAAI,CAAC,QAAQ,KAAK,IAAI,OAAO,YAAY,IAAI,CAAC,GAAG;AAC/C,UAAM,IAAI,oBAAoB,YAAY,IAAI;AAAA,EAChD;AACF;","names":[]}
|