@noy-db/hub 0.8.0 → 0.9.0-pre.0
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/CHANGELOG.md +28 -0
- package/README.md +8 -8
- package/codemods/0.4.0-pre.json +1 -1
- package/codemods/0.8.0-pre.json +1 -1
- package/dist/api-MSJXVYQG.js +18 -0
- package/dist/as/index.js +33 -23
- package/dist/as/index.js.map +1 -1
- package/dist/at/index.js +8 -8
- package/dist/attestation/index.js +18 -18
- package/dist/{backup-YE2VQ26S.js → backup-7RKNZMQO.js} +11 -11
- package/dist/blobs/index.js +12 -12
- package/dist/broker/index.js +19 -11
- package/dist/broker/index.js.map +1 -1
- package/dist/by/index.js +2 -2
- package/dist/capsule/contract.d.ts +1 -1
- package/dist/capsule/contract.js +6 -6
- package/dist/capsule/enclave-aes/broker/proof.d.ts +12 -3
- package/dist/capsule/enclave-aes/index.d.ts +1 -1
- package/dist/capsule/enclave-aes/index.js +40 -14
- package/dist/capsule/enclave-aes/index.js.map +1 -1
- package/dist/capsule/index.d.ts +1 -1
- package/dist/capsule/plumbing/record-codec.d.ts +7 -0
- package/dist/{capsule-VOLX3QRQ.js → capsule-57BDK7LG.js} +8 -8
- package/dist/cargo/index.js +32 -32
- package/dist/{chunk-A4XMIZT3.js → chunk-226V6XXJ.js} +41 -6
- package/dist/chunk-226V6XXJ.js.map +1 -0
- package/dist/{chunk-LNSVGRHO.js → chunk-2A56DSRI.js} +2 -2
- package/dist/chunk-2DX4JCRI.js +1 -0
- package/dist/{chunk-SV2PM2OY.js → chunk-2ELOCEXE.js} +1 -1
- package/dist/chunk-2ELOCEXE.js.map +1 -0
- package/dist/{chunk-6QVKPHBF.js → chunk-2WJKJZSO.js} +2 -2
- package/dist/{chunk-TKVUE4JF.js → chunk-2ZFD2UJC.js} +9 -9
- package/dist/{chunk-2AFX33YA.js → chunk-3V6LZHUO.js} +3 -3
- package/dist/chunk-3V6LZHUO.js.map +1 -0
- package/dist/{chunk-4JPMZTJY.js → chunk-3Y5JLSXS.js} +2 -2
- package/dist/{chunk-XSPFDUDG.js → chunk-45VHWE4C.js} +51 -16
- package/dist/chunk-45VHWE4C.js.map +1 -0
- package/dist/{chunk-LGNDFRYC.js → chunk-4CFHL5RY.js} +1 -1
- package/dist/chunk-4CFHL5RY.js.map +1 -0
- package/dist/{chunk-EZGV6F47.js → chunk-4J3MNVVX.js} +1 -1
- package/dist/{chunk-EZGV6F47.js.map → chunk-4J3MNVVX.js.map} +1 -1
- package/dist/{chunk-O2IMQSNA.js → chunk-4UIP3QNW.js} +2 -2
- package/dist/{chunk-EJLV62NI.js → chunk-55P6OBB7.js} +2 -2
- package/dist/{chunk-Y3LPTBG7.js → chunk-5HNW4ZTA.js} +6 -9
- package/dist/chunk-5HNW4ZTA.js.map +1 -0
- package/dist/{chunk-TEHHROZL.js → chunk-5LMCVBMY.js} +5 -5
- package/dist/{chunk-4V3Q35WG.js → chunk-5UUJE4FP.js} +3 -3
- package/dist/{chunk-JWWYRR4Q.js → chunk-5XONHAFD.js} +2 -2
- package/dist/{chunk-PD64727P.js → chunk-6QLCS7ET.js} +2 -2
- package/dist/chunk-6QLCS7ET.js.map +1 -0
- package/dist/{chunk-QBLT6POL.js → chunk-6WS35E6X.js} +2 -2
- package/dist/{chunk-IUBMZPHM.js → chunk-6Y5E4SJU.js} +5 -6
- package/dist/chunk-6Y5E4SJU.js.map +1 -0
- package/dist/{chunk-GZT3NQRA.js → chunk-72XBVN4J.js} +8 -2
- package/dist/{chunk-GZT3NQRA.js.map → chunk-72XBVN4J.js.map} +1 -1
- package/dist/{chunk-EYG2CY2X.js → chunk-7BMOS4JQ.js} +11 -11
- package/dist/chunk-7BMOS4JQ.js.map +1 -0
- package/dist/{chunk-UA372JHH.js → chunk-7CT6SX4M.js} +3 -3
- package/dist/chunk-7CT6SX4M.js.map +1 -0
- package/dist/{chunk-EZOAKKTB.js → chunk-ABU6A4Z5.js} +2 -2
- package/dist/{chunk-VKI2EB3N.js → chunk-AJFPAHMW.js} +3 -3
- package/dist/{chunk-DQESF6RP.js → chunk-APFFJN5T.js} +29 -16
- package/dist/chunk-APFFJN5T.js.map +1 -0
- package/dist/{chunk-2ODVB6LZ.js → chunk-B65CC6Z6.js} +2 -2
- package/dist/{chunk-MBMTND6D.js → chunk-BJH77UY2.js} +2 -2
- package/dist/{chunk-VAORSCSU.js → chunk-BMHQHH7Q.js} +2 -2
- package/dist/chunk-BMHQHH7Q.js.map +1 -0
- package/dist/{chunk-FFJFE4T7.js → chunk-BNFNQNEP.js} +2 -2
- package/dist/chunk-BU2ABHFW.js +74 -0
- package/dist/chunk-BU2ABHFW.js.map +1 -0
- package/dist/{chunk-5WWDGFOS.js → chunk-BUE7R3B5.js} +2 -2
- package/dist/{chunk-5FE75J4S.js → chunk-BVF3SXD7.js} +5 -5
- package/dist/{chunk-VFHCKG5M.js → chunk-BYHCKX73.js} +3 -3
- package/dist/{chunk-BVCUTXE3.js → chunk-C53ER7AG.js} +2 -2
- package/dist/{chunk-MILI22XQ.js → chunk-CAD7N6HQ.js} +3 -3
- package/dist/{chunk-5KC7QYEC.js → chunk-CB4467G3.js} +5 -5
- package/dist/{chunk-JIBUPZHA.js → chunk-D4V5JJAK.js} +4 -4
- package/dist/{chunk-DLD5LGDS.js → chunk-D5A3GCZ4.js} +3 -3
- package/dist/{chunk-FOEDQATO.js → chunk-DHU2AQ6T.js} +2 -2
- package/dist/{chunk-VHBKYN2J.js → chunk-DL6BZKP3.js} +2 -2
- package/dist/{chunk-NBQ3KK6X.js → chunk-EA4VDI5D.js} +4 -4
- package/dist/chunk-EA4VDI5D.js.map +1 -0
- package/dist/{chunk-E2N2T5ZO.js → chunk-ECGXA4LT.js} +2 -2
- package/dist/{chunk-PP3K67AJ.js → chunk-EMAWOGVH.js} +3 -3
- package/dist/{chunk-WXE4W2U6.js → chunk-EUD3HQM4.js} +4 -4
- package/dist/{chunk-MXOQ5JCG.js → chunk-FSQNGPHH.js} +171 -124
- package/dist/chunk-FSQNGPHH.js.map +1 -0
- package/dist/{chunk-LIOVX5TJ.js → chunk-FZXRLNJO.js} +3 -3
- package/dist/{chunk-WPOYWFSE.js → chunk-G46JSSVX.js} +7 -7
- package/dist/{chunk-JSMTXZR3.js → chunk-G5EDYVSE.js} +8 -6
- package/dist/chunk-G5EDYVSE.js.map +1 -0
- package/dist/chunk-G66YXUTY.js +60 -0
- package/dist/chunk-G66YXUTY.js.map +1 -0
- package/dist/{chunk-FFWI7UH7.js → chunk-GAGT3YZN.js} +1 -1
- package/dist/chunk-GAGT3YZN.js.map +1 -0
- package/dist/{chunk-AEQGFRU4.js → chunk-GCUMOKTN.js} +3 -3
- package/dist/{chunk-244QBK7Z.js → chunk-GETUTTZU.js} +4 -4
- package/dist/{chunk-244QBK7Z.js.map → chunk-GETUTTZU.js.map} +1 -1
- package/dist/{chunk-V3RWOOSO.js → chunk-GOJQ5DZA.js} +4 -4
- package/dist/{chunk-UEFOQH74.js → chunk-GRS2HIV6.js} +2 -2
- package/dist/{chunk-3L2NBKDR.js → chunk-H3YM66EI.js} +2 -2
- package/dist/{chunk-3A5DUZ56.js → chunk-IVMFIKNA.js} +6 -6
- package/dist/{chunk-3A5DUZ56.js.map → chunk-IVMFIKNA.js.map} +1 -1
- package/dist/{chunk-747G6CT3.js → chunk-J4KAPKBJ.js} +2 -2
- package/dist/{chunk-H23XHRTT.js → chunk-JA25QG5M.js} +211 -6
- package/dist/chunk-JA25QG5M.js.map +1 -0
- package/dist/{chunk-T7IR4N2S.js → chunk-JBMMWYOH.js} +3 -1
- package/dist/{chunk-T7IR4N2S.js.map → chunk-JBMMWYOH.js.map} +1 -1
- package/dist/{chunk-MXKI4ABF.js → chunk-JPSL5GIZ.js} +3 -3
- package/dist/{chunk-2GHDLTPE.js → chunk-JSWKVZCR.js} +2 -2
- package/dist/{chunk-BNWA3WG5.js → chunk-JZ4EDEIP.js} +3 -3
- package/dist/{chunk-GUZET4SG.js → chunk-K673OO6B.js} +4 -4
- package/dist/chunk-K673OO6B.js.map +1 -0
- package/dist/{chunk-5UMK7GRI.js → chunk-K7RW7FI4.js} +5 -5
- package/dist/{chunk-N7EVQD5S.js → chunk-L2F3CFCH.js} +4 -4
- package/dist/{chunk-2PVXF5CS.js → chunk-L5A3KR7R.js} +3 -3
- package/dist/{chunk-O46CHQT7.js → chunk-LGGIFKJP.js} +4 -4
- package/dist/{chunk-Y2RKVFKR.js → chunk-MQ7YWBQ5.js} +2 -2
- package/dist/{chunk-4CIQDWRB.js → chunk-MRJDLXI3.js} +14 -8
- package/dist/{chunk-4CIQDWRB.js.map → chunk-MRJDLXI3.js.map} +1 -1
- package/dist/{chunk-YRXFFWG4.js → chunk-NIXRB5BP.js} +2 -2
- package/dist/{chunk-RHVMZSUU.js → chunk-NV4OESHC.js} +1 -1
- package/dist/chunk-NV4OESHC.js.map +1 -0
- package/dist/{chunk-V7ADQF5O.js → chunk-NYJBYVBG.js} +4 -4
- package/dist/{chunk-23D6M6VM.js → chunk-NZULV7LK.js} +4 -4
- package/dist/{chunk-HW7CMKUG.js → chunk-OFD2HD6F.js} +1 -1
- package/dist/{chunk-HW7CMKUG.js.map → chunk-OFD2HD6F.js.map} +1 -1
- package/dist/{chunk-BPWYQ25R.js → chunk-OJU67C23.js} +4 -4
- package/dist/{chunk-BPWYQ25R.js.map → chunk-OJU67C23.js.map} +1 -1
- package/dist/{chunk-AUKKUPVB.js → chunk-OK57EATL.js} +2 -2
- package/dist/{chunk-42ALPZ2B.js → chunk-OM3SEDI4.js} +4 -4
- package/dist/{chunk-42ALPZ2B.js.map → chunk-OM3SEDI4.js.map} +1 -1
- package/dist/{chunk-OYW3I5M4.js → chunk-PCYPXLEX.js} +6 -6
- package/dist/{chunk-2MG3SW7Q.js → chunk-PFCTSXHU.js} +2 -2
- package/dist/{chunk-FVH5QYY2.js → chunk-PGC35KXE.js} +2 -2
- package/dist/{chunk-NCO25JZO.js → chunk-PT2YCQVK.js} +4 -4
- package/dist/{chunk-SYNY3VB5.js → chunk-PTJY5R3O.js} +2 -2
- package/dist/{chunk-QMWY2FDS.js → chunk-PUBI455O.js} +2 -2
- package/dist/{chunk-VMV5SBE4.js → chunk-QDHHHI5Q.js} +5 -5
- package/dist/{chunk-JQYKPKG6.js → chunk-QGFEBNHO.js} +4 -4
- package/dist/{chunk-52MGURXO.js → chunk-QGSNJD3B.js} +2 -2
- package/dist/chunk-QJNAHPVT.js +91 -0
- package/dist/chunk-QJNAHPVT.js.map +1 -0
- package/dist/{chunk-VVOLDVIR.js → chunk-QSHATAXJ.js} +2 -2
- package/dist/{chunk-G2MVBDFP.js → chunk-QXJXG7DN.js} +39 -9
- package/dist/chunk-QXJXG7DN.js.map +1 -0
- package/dist/{chunk-KZXDLBY6.js → chunk-RDOEOPOI.js} +5 -5
- package/dist/{chunk-FLPCETP6.js → chunk-RIZTZJX6.js} +2 -2
- package/dist/{chunk-H427H63K.js → chunk-RNACRT5H.js} +2 -2
- package/dist/chunk-RWFQVNXR.js +24 -0
- package/dist/chunk-RWFQVNXR.js.map +1 -0
- package/dist/{chunk-JOSPH3E5.js → chunk-SFCD6OU7.js} +2 -2
- package/dist/{chunk-IMQKWR46.js → chunk-SNM7LSYG.js} +4 -5
- package/dist/chunk-SNM7LSYG.js.map +1 -0
- package/dist/{chunk-Q2XRFWOI.js → chunk-SPPOUJLN.js} +2 -2
- package/dist/{chunk-EGVJ7TM4.js → chunk-SWXLZ4HX.js} +8 -9
- package/dist/chunk-SWXLZ4HX.js.map +1 -0
- package/dist/{chunk-H7JBY4P7.js → chunk-TFWFPZRU.js} +13 -13
- package/dist/chunk-TFWFPZRU.js.map +1 -0
- package/dist/{chunk-VQDZYB2B.js → chunk-TMIJAR2X.js} +3 -3
- package/dist/{chunk-VQDZYB2B.js.map → chunk-TMIJAR2X.js.map} +1 -1
- package/dist/{chunk-RB4DOPGZ.js → chunk-TZYTMZZY.js} +10 -10
- package/dist/{chunk-Q4UHJ5SN.js → chunk-UAKY6E4N.js} +2 -2
- package/dist/chunk-UHFGK6YA.js +40 -0
- package/dist/chunk-UHFGK6YA.js.map +1 -0
- package/dist/{chunk-DIZLQVAA.js → chunk-UNSQZSZP.js} +2 -2
- package/dist/{chunk-4T3SE27M.js → chunk-UOZOWN73.js} +2 -2
- package/dist/{chunk-OKSTSITU.js → chunk-V55JASXO.js} +3 -3
- package/dist/{chunk-37EPTBBR.js → chunk-VSUQI5Q6.js} +32 -4
- package/dist/chunk-VSUQI5Q6.js.map +1 -0
- package/dist/{chunk-XQRFSKYM.js → chunk-WFWFXDNS.js} +2 -2
- package/dist/{chunk-N7UDP5VU.js → chunk-WHOKGHJ3.js} +7 -7
- package/dist/{chunk-7R2Q7CPL.js → chunk-X2RP4TO5.js} +7 -7
- package/dist/{chunk-U2FZNEWJ.js → chunk-XA34THLF.js} +8 -6
- package/dist/{chunk-U2FZNEWJ.js.map → chunk-XA34THLF.js.map} +1 -1
- package/dist/{chunk-UIYESRD6.js → chunk-XITHENBI.js} +9 -9
- package/dist/{chunk-UIYESRD6.js.map → chunk-XITHENBI.js.map} +1 -1
- package/dist/{chunk-3XJQBBZD.js → chunk-XKBTJGFP.js} +2 -2
- package/dist/{chunk-SMV2HRDU.js → chunk-XXNWU5QZ.js} +2 -2
- package/dist/{chunk-TZZBXPZK.js → chunk-XXQ5XIYY.js} +2 -2
- package/dist/{chunk-3EWS67C7.js → chunk-YFFMPZPY.js} +2 -2
- package/dist/{chunk-M5B2W6YY.js → chunk-YKGJ44Z7.js} +3 -3
- package/dist/{chunk-5A4345TG.js → chunk-YQFBH5ZJ.js} +3 -3
- package/dist/{chunk-DHHGL42E.js → chunk-Z3ZXVB2Y.js} +22 -6
- package/dist/chunk-Z3ZXVB2Y.js.map +1 -0
- package/dist/{chunk-OIEO4WDL.js → chunk-ZBUAX4EF.js} +2 -2
- package/dist/chunk-ZBUAX4EF.js.map +1 -0
- package/dist/{chunk-HS554XAF.js → chunk-ZHQJAV6D.js} +2 -2
- package/dist/{chunk-IUT3GWP3.js → chunk-ZMVSKYTJ.js} +17 -93
- package/dist/chunk-ZMVSKYTJ.js.map +1 -0
- package/dist/{chunk-AL6ML4LA.js → chunk-ZVSKSUGI.js} +5 -5
- package/dist/{chunk-JDEYF3PJ.js → chunk-ZWF3EWZA.js} +2 -2
- package/dist/chunk-ZWF3EWZA.js.map +1 -0
- package/dist/classified/index.js +5 -5
- package/dist/{classified-marker-SIYKUU5L.js → classified-marker-KHDOYIJB.js} +3 -3
- package/dist/collection-facade-LZIF7GWD.js +41 -0
- package/dist/{computed-FAD7SED4.js → computed-XMFTHAXV.js} +3 -3
- package/dist/consent/index.js +8 -8
- package/dist/cover/index.js +8 -8
- package/dist/coverage/index.js +1 -1
- package/dist/custody/index.js +12 -11
- package/dist/{dead-filter-4F3LWYKH.js → dead-filter-CXV5NS67.js} +10 -10
- package/dist/debug/index.js +7 -7
- package/dist/{delegation-NANZDG4N.js → delegation-ONLM4JFI.js} +49 -25
- package/dist/delegation-ONLM4JFI.js.map +1 -0
- package/dist/derivations/index.js +24 -23
- package/dist/derive-3YSIS36W.js +20 -0
- package/dist/directory/index.js +9 -9
- package/dist/{dispatch-JDZNGEB3.js → dispatch-2MQFMEGY.js} +20 -20
- package/dist/{dispatch-H4HV32AM.js → dispatch-OFDA4WQV.js} +5 -5
- package/dist/executor-E6YDJ7YA.js +8 -0
- package/dist/executor-NJX3YQQ4.js +35 -0
- package/dist/executor-WWM3DD4P.js +8 -0
- package/dist/export-accessible-RLHU5G4W.js +22 -0
- package/dist/extract-partition-QII2EDCS.js +35 -0
- package/dist/{fanout-sidecar-L7NHPIOC.js → fanout-sidecar-BO67PCPO.js} +8 -8
- package/dist/find-6M7GYDUZ.js +10 -0
- package/dist/forget/index.js +25 -8
- package/dist/forget/index.js.map +1 -1
- package/dist/guards/index.js +4 -4
- package/dist/history/index.js +12 -12
- package/dist/i18n/index.js +13 -13
- package/dist/index.d.ts +8 -6
- package/dist/index.js +122 -110
- package/dist/index.js.map +1 -1
- package/dist/indexing/index.js +6 -6
- package/dist/introspection/index.js +15 -11
- package/dist/issue-LPEEEMPP.js +19 -0
- package/dist/kernel/cache/index.d.ts +1 -1
- package/dist/kernel/collection.d.ts +1 -0
- package/dist/kernel/errors.d.ts +33 -4
- package/dist/kernel/export-scope.d.ts +40 -0
- package/dist/kernel/noydb.d.ts +3 -1
- package/dist/kernel/put-many-atomic.d.ts +1 -1
- package/dist/kernel/query/live/live.d.ts +2 -1
- package/dist/kernel/query/sort-key.d.ts +0 -2
- package/dist/kernel/tx-write-gate.d.ts +2 -4
- package/dist/kernel/types.d.ts +93 -6
- package/dist/kernel/validation.d.ts +0 -10
- package/dist/kernel/vault.d.ts +4 -0
- package/dist/kernel/via/reconcile.d.ts +1 -42
- package/dist/{ledger-5ZMGE44K.js → ledger-MSJGWKGW.js} +10 -10
- package/dist/liberate-LEKDYKTC.js +25 -0
- package/dist/{link-set-MFXC24VK.js → link-set-QXXSPPHL.js} +10 -10
- package/dist/{managed-secret-MFTFT22X.js → managed-secret-VSK7ANQI.js} +9 -9
- package/dist/materialized-views/index.js +26 -26
- package/dist/{migrate-cek-DZRFFOHW.js → migrate-cek-GEBFYBDC.js} +2 -2
- package/dist/money/index.js +5 -5
- package/dist/noydb-F2AUF6KL.js +98 -0
- package/dist/on/index.js +25 -0
- package/dist/overlay-views/index.js +4 -4
- package/dist/periods/index.js +11 -11
- package/dist/pod/index.js +69 -67
- package/dist/{pod-handle-A3LET7NO.js → pod-handle-YE7CT66T.js} +8 -8
- package/dist/policy/index.js +9 -9
- package/dist/port/as/active.d.ts +4 -0
- package/dist/port/as/index.d.ts +1 -0
- package/dist/port/by/index.d.ts +14 -5
- package/dist/port/on/index.d.ts +26 -0
- package/dist/port/to/index.d.ts +1 -1
- package/dist/port/with/broker-strategy.d.ts +14 -0
- package/dist/port/with/coverage-strategy.d.ts +17 -1
- package/dist/port/with/formats-strategy.d.ts +34 -1
- package/dist/port/with/lookup-strategy.d.ts +85 -4
- package/dist/port/with/service-bus.d.ts +2 -1
- package/dist/portability/index.js +8 -8
- package/dist/{post-register-7ND27CF2.js → post-register-QC5TEAW3.js} +6 -6
- package/dist/query/all/index.js +16 -16
- package/dist/query/index.js +8 -8
- package/dist/query/live/index.js +7 -7
- package/dist/query/reduce/index.js +10 -10
- package/dist/query/relate/index.js +10 -10
- package/dist/reduce/index.js +8 -8
- package/dist/register-SN72QALV.js +30 -0
- package/dist/registry-253FF7C6.js +35 -0
- package/dist/registry-3AZWQTDT.js +23 -0
- package/dist/registry-7UEBTHWG.js +8 -0
- package/dist/registry-IVJ67NFQ.js +8 -0
- package/dist/request-withdrawal-UKVJZKX2.js +28 -0
- package/dist/{reveal-B2TLP4JQ.js → reveal-YQTRNQFH.js} +7 -7
- package/dist/revoke-G2KXJQA5.js +24 -0
- package/dist/satellites/index.js +1 -1
- package/dist/schema-update/index.js +2 -2
- package/dist/sealed-record/index.js +11 -11
- package/dist/search/index.js +4 -4
- package/dist/{seed-XNSE4U3S.js → seed-JMQTRKRE.js} +121 -27
- package/dist/seed-JMQTRKRE.js.map +1 -0
- package/dist/sequence/index.js +8 -8
- package/dist/session/index.js +8 -8
- package/dist/shadow/index.js +2 -2
- package/dist/signer-IUIYR3CX.js +23 -0
- package/dist/snapshots/index.js +9 -9
- package/dist/{stale-6GCFMOOR.js → stale-SKV33PBU.js} +9 -9
- package/dist/storage-NPBYP2SM.js +21 -0
- package/dist/{store-coordination-provider-C4BPT5XF.js → store-coordination-provider-Z6HKPOGM.js} +9 -9
- package/dist/sync/index.js +17 -11
- package/dist/sync/index.js.map +1 -1
- package/dist/team/index.js +17 -15
- package/dist/tiers/index.js +10 -10
- package/dist/to/index.js +2 -2
- package/dist/transactions/index.js +3 -3
- package/dist/util/index.js +1 -1
- package/dist/vault-head/index.js +8 -8
- package/dist/{verify-UKALW7C2.js → verify-KWQIWLGH.js} +7 -7
- package/dist/via/classified/presets.d.ts +29 -0
- package/dist/via/classified/resolve.d.ts +7 -0
- package/dist/via/geo/binding.d.ts +0 -4
- package/dist/via/lookup/index.d.ts +32 -0
- package/dist/via/money/binding.d.ts +0 -9
- package/dist/walk-F4OLXI5H.js +20 -0
- package/dist/with-audit/attestation/strategy.d.ts +2 -1
- package/dist/with-audit/attestation/vault-facade.d.ts +7 -0
- package/dist/with-audit/forget/completeness.d.ts +66 -0
- package/dist/with-audit/forget/index.d.ts +2 -0
- package/dist/with-audit/forget/strategy.d.ts +65 -0
- package/dist/with-audit/periods/periods.d.ts +0 -9
- package/dist/with-audit/periods/reserved-collections.d.ts +7 -0
- package/dist/with-audit/portability/request-withdrawal.d.ts +0 -1
- package/dist/with-audit/portability/withdraw-accessible.d.ts +0 -1
- package/dist/with-audit/tiers/index.d.ts +1 -1
- package/dist/with-audit/tiers/strategy.d.ts +19 -0
- package/dist/with-cargo/extract-partition.d.ts +0 -46
- package/dist/with-commit/numbering/index.d.ts +0 -2
- package/dist/with-commit/tx/transaction.d.ts +2 -24
- package/dist/with-commit/vault-head/strategy.d.ts +7 -0
- package/dist/with-fork/snapshots/noydb-facade.d.ts +7 -0
- package/dist/with-formula/derivations/trigger-match.d.ts +8 -5
- package/dist/with-formula/materialized-views/emit-memo.d.ts +0 -2
- package/dist/with-lookup/embeddings/index.d.ts +1 -2
- package/dist/with-lookup/indexing/collection-facade.d.ts +1 -15
- package/dist/with-lookup/indexing/eager-indexes.d.ts +2 -1
- package/dist/with-lookup/search/collection-facade.d.ts +0 -12
- package/dist/with-party/broker/index.d.ts +2 -2
- package/dist/with-party/broker/seed.d.ts +25 -10
- package/dist/with-party/policy/noydb-facade.d.ts +1 -31
- package/dist/with-party/team/delegation.d.ts +58 -2
- package/dist/with-party/team/echo-secret.d.ts +0 -2
- package/dist/with-party/team/keyring.d.ts +1 -21
- package/dist/with-party/team/magic-link-grant.d.ts +11 -1
- package/dist/with-party/team/noydb-facade.d.ts +5 -0
- package/dist/with-party/team/reserved-secret-collections.d.ts +10 -8
- package/dist/with-party/team/roster-tag.d.ts +42 -1
- package/dist/with-party/team/tiers.d.ts +36 -5
- package/dist/with-shape/blobs/blob-set.d.ts +23 -0
- package/dist/with-shape/blobs/legacy-sweep.d.ts +16 -0
- package/dist/with-shape/introspection/describe.d.ts +1 -2
- package/dist/with-shape/introspection/index.d.ts +1 -0
- package/dist/with-shape/introspection/types.d.ts +2 -1
- package/dist/with-shape/schema-update/fence.d.ts +7 -0
- package/dist/with-sync/engine.d.ts +30 -1
- package/dist/with-sync/reserved-mirror.d.ts +89 -0
- package/dist/with-sync/strategy.d.ts +7 -0
- package/dist/with-sync/tab-write-relay.d.ts +0 -10
- package/dist/withdraw-accessible-ZKPLZ66E.js +25 -0
- package/package.json +3 -3
- package/dist/api-OS747DS4.js +0 -18
- package/dist/chunk-2AFX33YA.js.map +0 -1
- package/dist/chunk-2RH777HH.js +0 -126
- package/dist/chunk-2RH777HH.js.map +0 -1
- package/dist/chunk-37EPTBBR.js.map +0 -1
- package/dist/chunk-7XMHMVTS.js +0 -20
- package/dist/chunk-7XMHMVTS.js.map +0 -1
- package/dist/chunk-A4XMIZT3.js.map +0 -1
- package/dist/chunk-DHHGL42E.js.map +0 -1
- package/dist/chunk-DQESF6RP.js.map +0 -1
- package/dist/chunk-EGVJ7TM4.js.map +0 -1
- package/dist/chunk-EYG2CY2X.js.map +0 -1
- package/dist/chunk-FFWI7UH7.js.map +0 -1
- package/dist/chunk-G2MVBDFP.js.map +0 -1
- package/dist/chunk-GUZET4SG.js.map +0 -1
- package/dist/chunk-H23XHRTT.js.map +0 -1
- package/dist/chunk-H7JBY4P7.js.map +0 -1
- package/dist/chunk-IMQKWR46.js.map +0 -1
- package/dist/chunk-IUBMZPHM.js.map +0 -1
- package/dist/chunk-IUT3GWP3.js.map +0 -1
- package/dist/chunk-JDEYF3PJ.js.map +0 -1
- package/dist/chunk-JSMTXZR3.js.map +0 -1
- package/dist/chunk-LGNDFRYC.js.map +0 -1
- package/dist/chunk-MXOQ5JCG.js.map +0 -1
- package/dist/chunk-NBQ3KK6X.js.map +0 -1
- package/dist/chunk-OIEO4WDL.js.map +0 -1
- package/dist/chunk-PD64727P.js.map +0 -1
- package/dist/chunk-RHVMZSUU.js.map +0 -1
- package/dist/chunk-SV2PM2OY.js.map +0 -1
- package/dist/chunk-UA372JHH.js.map +0 -1
- package/dist/chunk-UBSFPGVJ.js +0 -1
- package/dist/chunk-VAORSCSU.js.map +0 -1
- package/dist/chunk-XSPFDUDG.js.map +0 -1
- package/dist/chunk-Y3LPTBG7.js.map +0 -1
- package/dist/collection-facade-CS7CA2XO.js +0 -47
- package/dist/delegation-NANZDG4N.js.map +0 -1
- package/dist/derive-XLOJVXPU.js +0 -20
- package/dist/executor-FMPVN6DT.js +0 -35
- package/dist/executor-LBBBRTN5.js +0 -8
- package/dist/executor-OSRZST2U.js +0 -8
- package/dist/export-accessible-RAOJAAU5.js +0 -22
- package/dist/extract-partition-JD5CUSTG.js +0 -37
- package/dist/find-JRN4RNQ5.js +0 -10
- package/dist/issue-YIVBZUTH.js +0 -19
- package/dist/liberate-WZSZM2W6.js +0 -24
- package/dist/noydb-ITIGNKUF.js +0 -96
- package/dist/register-DT5HZ2LZ.js +0 -30
- package/dist/registry-BHRZGZHF.js +0 -8
- package/dist/registry-ET6ZFFWA.js +0 -34
- package/dist/registry-HXFGLGTG.js +0 -8
- package/dist/registry-THCPDEOD.js +0 -23
- package/dist/request-withdrawal-AV2MJJKY.js +0 -30
- package/dist/revoke-6DWIEOCV.js +0 -24
- package/dist/seed-XNSE4U3S.js.map +0 -1
- package/dist/signer-MHGW3OIL.js +0 -23
- package/dist/storage-BYN5LO6Z.js +0 -21
- package/dist/walk-4VPK7AS4.js +0 -20
- package/dist/withdraw-accessible-HL7GACUE.js +0 -27
- /package/dist/{api-OS747DS4.js.map → api-MSJXVYQG.js.map} +0 -0
- /package/dist/{backup-YE2VQ26S.js.map → backup-7RKNZMQO.js.map} +0 -0
- /package/dist/{capsule-VOLX3QRQ.js.map → capsule-57BDK7LG.js.map} +0 -0
- /package/dist/{chunk-LNSVGRHO.js.map → chunk-2A56DSRI.js.map} +0 -0
- /package/dist/{chunk-UBSFPGVJ.js.map → chunk-2DX4JCRI.js.map} +0 -0
- /package/dist/{chunk-6QVKPHBF.js.map → chunk-2WJKJZSO.js.map} +0 -0
- /package/dist/{chunk-TKVUE4JF.js.map → chunk-2ZFD2UJC.js.map} +0 -0
- /package/dist/{chunk-4JPMZTJY.js.map → chunk-3Y5JLSXS.js.map} +0 -0
- /package/dist/{chunk-O2IMQSNA.js.map → chunk-4UIP3QNW.js.map} +0 -0
- /package/dist/{chunk-EJLV62NI.js.map → chunk-55P6OBB7.js.map} +0 -0
- /package/dist/{chunk-TEHHROZL.js.map → chunk-5LMCVBMY.js.map} +0 -0
- /package/dist/{chunk-4V3Q35WG.js.map → chunk-5UUJE4FP.js.map} +0 -0
- /package/dist/{chunk-JWWYRR4Q.js.map → chunk-5XONHAFD.js.map} +0 -0
- /package/dist/{chunk-QBLT6POL.js.map → chunk-6WS35E6X.js.map} +0 -0
- /package/dist/{chunk-EZOAKKTB.js.map → chunk-ABU6A4Z5.js.map} +0 -0
- /package/dist/{chunk-VKI2EB3N.js.map → chunk-AJFPAHMW.js.map} +0 -0
- /package/dist/{chunk-2ODVB6LZ.js.map → chunk-B65CC6Z6.js.map} +0 -0
- /package/dist/{chunk-MBMTND6D.js.map → chunk-BJH77UY2.js.map} +0 -0
- /package/dist/{chunk-FFJFE4T7.js.map → chunk-BNFNQNEP.js.map} +0 -0
- /package/dist/{chunk-5WWDGFOS.js.map → chunk-BUE7R3B5.js.map} +0 -0
- /package/dist/{chunk-5FE75J4S.js.map → chunk-BVF3SXD7.js.map} +0 -0
- /package/dist/{chunk-VFHCKG5M.js.map → chunk-BYHCKX73.js.map} +0 -0
- /package/dist/{chunk-BVCUTXE3.js.map → chunk-C53ER7AG.js.map} +0 -0
- /package/dist/{chunk-MILI22XQ.js.map → chunk-CAD7N6HQ.js.map} +0 -0
- /package/dist/{chunk-5KC7QYEC.js.map → chunk-CB4467G3.js.map} +0 -0
- /package/dist/{chunk-JIBUPZHA.js.map → chunk-D4V5JJAK.js.map} +0 -0
- /package/dist/{chunk-DLD5LGDS.js.map → chunk-D5A3GCZ4.js.map} +0 -0
- /package/dist/{chunk-FOEDQATO.js.map → chunk-DHU2AQ6T.js.map} +0 -0
- /package/dist/{chunk-VHBKYN2J.js.map → chunk-DL6BZKP3.js.map} +0 -0
- /package/dist/{chunk-E2N2T5ZO.js.map → chunk-ECGXA4LT.js.map} +0 -0
- /package/dist/{chunk-PP3K67AJ.js.map → chunk-EMAWOGVH.js.map} +0 -0
- /package/dist/{chunk-WXE4W2U6.js.map → chunk-EUD3HQM4.js.map} +0 -0
- /package/dist/{chunk-LIOVX5TJ.js.map → chunk-FZXRLNJO.js.map} +0 -0
- /package/dist/{chunk-WPOYWFSE.js.map → chunk-G46JSSVX.js.map} +0 -0
- /package/dist/{chunk-AEQGFRU4.js.map → chunk-GCUMOKTN.js.map} +0 -0
- /package/dist/{chunk-V3RWOOSO.js.map → chunk-GOJQ5DZA.js.map} +0 -0
- /package/dist/{chunk-UEFOQH74.js.map → chunk-GRS2HIV6.js.map} +0 -0
- /package/dist/{chunk-3L2NBKDR.js.map → chunk-H3YM66EI.js.map} +0 -0
- /package/dist/{chunk-747G6CT3.js.map → chunk-J4KAPKBJ.js.map} +0 -0
- /package/dist/{chunk-MXKI4ABF.js.map → chunk-JPSL5GIZ.js.map} +0 -0
- /package/dist/{chunk-2GHDLTPE.js.map → chunk-JSWKVZCR.js.map} +0 -0
- /package/dist/{chunk-BNWA3WG5.js.map → chunk-JZ4EDEIP.js.map} +0 -0
- /package/dist/{chunk-5UMK7GRI.js.map → chunk-K7RW7FI4.js.map} +0 -0
- /package/dist/{chunk-N7EVQD5S.js.map → chunk-L2F3CFCH.js.map} +0 -0
- /package/dist/{chunk-2PVXF5CS.js.map → chunk-L5A3KR7R.js.map} +0 -0
- /package/dist/{chunk-O46CHQT7.js.map → chunk-LGGIFKJP.js.map} +0 -0
- /package/dist/{chunk-Y2RKVFKR.js.map → chunk-MQ7YWBQ5.js.map} +0 -0
- /package/dist/{chunk-YRXFFWG4.js.map → chunk-NIXRB5BP.js.map} +0 -0
- /package/dist/{chunk-V7ADQF5O.js.map → chunk-NYJBYVBG.js.map} +0 -0
- /package/dist/{chunk-23D6M6VM.js.map → chunk-NZULV7LK.js.map} +0 -0
- /package/dist/{chunk-AUKKUPVB.js.map → chunk-OK57EATL.js.map} +0 -0
- /package/dist/{chunk-OYW3I5M4.js.map → chunk-PCYPXLEX.js.map} +0 -0
- /package/dist/{chunk-2MG3SW7Q.js.map → chunk-PFCTSXHU.js.map} +0 -0
- /package/dist/{chunk-FVH5QYY2.js.map → chunk-PGC35KXE.js.map} +0 -0
- /package/dist/{chunk-NCO25JZO.js.map → chunk-PT2YCQVK.js.map} +0 -0
- /package/dist/{chunk-SYNY3VB5.js.map → chunk-PTJY5R3O.js.map} +0 -0
- /package/dist/{chunk-QMWY2FDS.js.map → chunk-PUBI455O.js.map} +0 -0
- /package/dist/{chunk-VMV5SBE4.js.map → chunk-QDHHHI5Q.js.map} +0 -0
- /package/dist/{chunk-JQYKPKG6.js.map → chunk-QGFEBNHO.js.map} +0 -0
- /package/dist/{chunk-52MGURXO.js.map → chunk-QGSNJD3B.js.map} +0 -0
- /package/dist/{chunk-VVOLDVIR.js.map → chunk-QSHATAXJ.js.map} +0 -0
- /package/dist/{chunk-KZXDLBY6.js.map → chunk-RDOEOPOI.js.map} +0 -0
- /package/dist/{chunk-FLPCETP6.js.map → chunk-RIZTZJX6.js.map} +0 -0
- /package/dist/{chunk-H427H63K.js.map → chunk-RNACRT5H.js.map} +0 -0
- /package/dist/{chunk-JOSPH3E5.js.map → chunk-SFCD6OU7.js.map} +0 -0
- /package/dist/{chunk-Q2XRFWOI.js.map → chunk-SPPOUJLN.js.map} +0 -0
- /package/dist/{chunk-RB4DOPGZ.js.map → chunk-TZYTMZZY.js.map} +0 -0
- /package/dist/{chunk-Q4UHJ5SN.js.map → chunk-UAKY6E4N.js.map} +0 -0
- /package/dist/{chunk-DIZLQVAA.js.map → chunk-UNSQZSZP.js.map} +0 -0
- /package/dist/{chunk-4T3SE27M.js.map → chunk-UOZOWN73.js.map} +0 -0
- /package/dist/{chunk-OKSTSITU.js.map → chunk-V55JASXO.js.map} +0 -0
- /package/dist/{chunk-XQRFSKYM.js.map → chunk-WFWFXDNS.js.map} +0 -0
- /package/dist/{chunk-N7UDP5VU.js.map → chunk-WHOKGHJ3.js.map} +0 -0
- /package/dist/{chunk-7R2Q7CPL.js.map → chunk-X2RP4TO5.js.map} +0 -0
- /package/dist/{chunk-3XJQBBZD.js.map → chunk-XKBTJGFP.js.map} +0 -0
- /package/dist/{chunk-SMV2HRDU.js.map → chunk-XXNWU5QZ.js.map} +0 -0
- /package/dist/{chunk-TZZBXPZK.js.map → chunk-XXQ5XIYY.js.map} +0 -0
- /package/dist/{chunk-3EWS67C7.js.map → chunk-YFFMPZPY.js.map} +0 -0
- /package/dist/{chunk-M5B2W6YY.js.map → chunk-YKGJ44Z7.js.map} +0 -0
- /package/dist/{chunk-5A4345TG.js.map → chunk-YQFBH5ZJ.js.map} +0 -0
- /package/dist/{chunk-HS554XAF.js.map → chunk-ZHQJAV6D.js.map} +0 -0
- /package/dist/{chunk-AL6ML4LA.js.map → chunk-ZVSKSUGI.js.map} +0 -0
- /package/dist/{classified-marker-SIYKUU5L.js.map → classified-marker-KHDOYIJB.js.map} +0 -0
- /package/dist/{collection-facade-CS7CA2XO.js.map → collection-facade-LZIF7GWD.js.map} +0 -0
- /package/dist/{computed-FAD7SED4.js.map → computed-XMFTHAXV.js.map} +0 -0
- /package/dist/{dead-filter-4F3LWYKH.js.map → dead-filter-CXV5NS67.js.map} +0 -0
- /package/dist/{derive-XLOJVXPU.js.map → derive-3YSIS36W.js.map} +0 -0
- /package/dist/{dispatch-JDZNGEB3.js.map → dispatch-2MQFMEGY.js.map} +0 -0
- /package/dist/{dispatch-H4HV32AM.js.map → dispatch-OFDA4WQV.js.map} +0 -0
- /package/dist/{executor-FMPVN6DT.js.map → executor-E6YDJ7YA.js.map} +0 -0
- /package/dist/{executor-LBBBRTN5.js.map → executor-NJX3YQQ4.js.map} +0 -0
- /package/dist/{executor-OSRZST2U.js.map → executor-WWM3DD4P.js.map} +0 -0
- /package/dist/{export-accessible-RAOJAAU5.js.map → export-accessible-RLHU5G4W.js.map} +0 -0
- /package/dist/{extract-partition-JD5CUSTG.js.map → extract-partition-QII2EDCS.js.map} +0 -0
- /package/dist/{fanout-sidecar-L7NHPIOC.js.map → fanout-sidecar-BO67PCPO.js.map} +0 -0
- /package/dist/{find-JRN4RNQ5.js.map → find-6M7GYDUZ.js.map} +0 -0
- /package/dist/{issue-YIVBZUTH.js.map → issue-LPEEEMPP.js.map} +0 -0
- /package/dist/{ledger-5ZMGE44K.js.map → ledger-MSJGWKGW.js.map} +0 -0
- /package/dist/{liberate-WZSZM2W6.js.map → liberate-LEKDYKTC.js.map} +0 -0
- /package/dist/{link-set-MFXC24VK.js.map → link-set-QXXSPPHL.js.map} +0 -0
- /package/dist/{managed-secret-MFTFT22X.js.map → managed-secret-VSK7ANQI.js.map} +0 -0
- /package/dist/{migrate-cek-DZRFFOHW.js.map → migrate-cek-GEBFYBDC.js.map} +0 -0
- /package/dist/{noydb-ITIGNKUF.js.map → noydb-F2AUF6KL.js.map} +0 -0
- /package/dist/{pod-handle-A3LET7NO.js.map → pod-handle-YE7CT66T.js.map} +0 -0
- /package/dist/{post-register-7ND27CF2.js.map → post-register-QC5TEAW3.js.map} +0 -0
- /package/dist/{register-DT5HZ2LZ.js.map → register-SN72QALV.js.map} +0 -0
- /package/dist/{registry-BHRZGZHF.js.map → registry-253FF7C6.js.map} +0 -0
- /package/dist/{registry-ET6ZFFWA.js.map → registry-3AZWQTDT.js.map} +0 -0
- /package/dist/{registry-HXFGLGTG.js.map → registry-7UEBTHWG.js.map} +0 -0
- /package/dist/{registry-THCPDEOD.js.map → registry-IVJ67NFQ.js.map} +0 -0
- /package/dist/{request-withdrawal-AV2MJJKY.js.map → request-withdrawal-UKVJZKX2.js.map} +0 -0
- /package/dist/{reveal-B2TLP4JQ.js.map → reveal-YQTRNQFH.js.map} +0 -0
- /package/dist/{revoke-6DWIEOCV.js.map → revoke-G2KXJQA5.js.map} +0 -0
- /package/dist/{signer-MHGW3OIL.js.map → signer-IUIYR3CX.js.map} +0 -0
- /package/dist/{stale-6GCFMOOR.js.map → stale-SKV33PBU.js.map} +0 -0
- /package/dist/{storage-BYN5LO6Z.js.map → storage-NPBYP2SM.js.map} +0 -0
- /package/dist/{store-coordination-provider-C4BPT5XF.js.map → store-coordination-provider-Z6HKPOGM.js.map} +0 -0
- /package/dist/{verify-UKALW7C2.js.map → verify-KWQIWLGH.js.map} +0 -0
- /package/dist/{walk-4VPK7AS4.js.map → walk-F4OLXI5H.js.map} +0 -0
- /package/dist/{withdraw-accessible-HL7GACUE.js.map → withdraw-accessible-ZKPLZ66E.js.map} +0 -0
|
@@ -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 async checkUnique(ctx, id, record) {\n // Skip the dynamic import entirely on the overwhelmingly common case:\n // a tiered collection with no `unique` index declared.\n if (!ctx.uniqueKeys) return\n const { checkUniqueAcrossTiers } = await import('./index.js')\n return checkUniqueAcrossTiers(ctx, id, record)\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 * #1358 — unique-constraint scan across every tier whose DEK the writer\n * holds. Called by the ORDINARY `Collection.put()` on a tiered collection\n * (`putAtTier` calls the core directly): the tier-0 mirror cannot see an\n * elevated record, because #709 purges its index entries on elevation.\n * A tier the writer cannot read is outside the guarantee.\n */\n checkUnique<T>(ctx: TiersContext<T>, id: string, record: T): Promise<void>\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 // NOT a throw, unlike its five siblings: this one is called from the\n // ORDINARY put path of any collection that declared `tiers` — and a\n // collection can declare `tiers` without wiring `withTiers()`. Without the\n // engine no record can ever leave tier 0, so there is nothing above tier 0\n // to scan and the tier-0 mirror is already the whole truth. Throwing here\n // would break plain `put()` on such a collection for no gain.\n async checkUnique() { /* no tier engine ⇒ every record is at tier 0 */ },\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 { recordAadFor, type RecordIdentity, buildRecordAad, buildRecordEnvelope, encrypt, decrypt, unwrapCek, rewrapBodyToDek, applyRewrappedBody, isDeleteMarker, isTombstoneShape, type RecordCodec, type EnclaveKey, type SealedShredSlot } from '../../capsule/index.js'\nimport { TierDemoteDeniedError, UnsupportedTierCompositionError, PersistedIndexCompensationError, UniqueConstraintError } 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'\nimport { sealedBodyArgs } from '../../capsule/index.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 /**\n * #1358 — the unique-constraint keys a record occupies, or `[]` when the\n * record is exempt (null-distinct) . `undefined` when the collection\n * declares no `unique` index, which is what makes the tier scan free for\n * every other collection. Deliberately a plain callback rather than the\n * `UniqueConstraintSet` itself: the tier domain must not import the\n * indexing domain, and this is the whole of what it needs.\n */\n readonly uniqueKeys?: ((record: unknown) => ReadonlyArray<{ readonly fields: readonly string[]; readonly key: string }>) | undefined\n /**\n * #1358 — the collection's own tier-0 unique pre-flight (the eager map or\n * the lazy `_idx/` probe). `putAtTier` runs it before its write because it\n * bypasses `Collection.put()` entirely; the tier-0 `put()` path calls it\n * directly and does not need this. `undefined` ⇒ no unique index declared.\n */\n readonly checkUniqueLocal?: ((id: string, record: unknown) => Promise<void>) | undefined\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 /**\n * Persist a `_history` snapshot of `version`, sealed against the address it\n * will be STORED at (#1041).\n *\n * Takes a **sealer**, not a finished envelope, because the destination\n * identity is not knowable here: only the history strategy owns the\n * `_history` layout. It hands the address in; the caller seals to it.\n *\n * ⚠️ The identity passed to `rewrapBodyToDek` must be what a READER will\n * recompute — `recordAadFor(address, finalEnvelope)`. A tier-move snapshot\n * strips `_tier`/`_elevatedBy` but KEEPS `_by`, so the destination is\n * `{...address, by: envelope._by}` with no tier. Sealing against the bare\n * address would produce history nothing can open, and the failure would\n * surface only when someone reads it.\n */\n saveHistorySnapshot(\n id: string,\n version: number,\n seal: (address: RecordIdentity) => Promise<EncryptedEnvelope>,\n ): 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 // #1358: `putAtTier` is the \"separate path that bypasses unique\n // enforcement\" the old registration-time refusal named. Both halves run\n // here, BEFORE any key resolution or write, so a violation leaves nothing\n // behind: the tier-0 mirror check the collection owns, then the scan of\n // every above-tier record this writer can decrypt.\n await ctx.checkUniqueLocal?.(id, record)\n await checkUniqueAcrossTiers(ctx, id, record)\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 preFrom: RecordIdentity = {\n // The PRE-move version — this identity opens what is already stored.\n collection: ctx.name, id, version: existing._v,\n ...(existing._tier !== undefined ? { tier: existing._tier } : {}),\n ...(existing._by !== undefined ? { by: existing._by } : {}),\n }\n await ctx.saveHistorySnapshot(id, existing._v, async (address) => {\n // The snapshot is UNTAGGED (`_tier` stripped) and keeps `_by`, so that is\n // the identity a reader will recompute from it (#1041).\n const to: RecordIdentity = { ...address, ...(existing._by !== undefined ? { by: existing._by } : {}) }\n const body = await rewrapBodyToDek(preFrom, to, 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 return applyRewrappedBody(preCarried, body)\n })\n }\n\n const json = JSON.stringify(record)\n const identity = { collection: ctx.name, id, version, by: ctx.keyring.userId, ...(tier > 0 ? { tier } : {}) }\n const { iv, data } = await encrypt(json, dek, buildRecordAad(identity))\n const envelope = buildRecordEnvelope(\n identity,\n {\n iv,\n data,\n // `_tier` is stamped from `identity.tier` above — the single source.\n ...(ctx.provenance && opts?.source !== undefined\n ? { provenance: { source: opts.source, sourceTs: opts.sourceTs ?? new Date().toISOString() } }\n : {}),\n },\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({ collection: ctx.name, id }, existing, await ctx.getDEK(dekKey(ctx.name, existing._tier ?? 0)))\n : null,\n true,\n )\n } else {\n const rec = await ctx.codec.decryptRecord({ collection: ctx.name, id }, envelope, { 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({ collection: ctx.name, id }, envelope)\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 record = await decryptElevated<T>(ctx, id, envelope, key)\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 * Decrypt an ABOVE-TIER envelope under `dekKey` — the manual leg\n * {@link getAtTier} used to inline (#635/#1041). Extracted so the\n * unique-constraint tier scan (#1358) reuses exactly this key handling\n * instead of growing a second copy that could drift from it.\n *\n * A tiered record may carry a per-record CEK (e.g. a CEK record elevated via\n * `elevate()`): the CEK is wrapped under the TIER DEK, so unwrap under the\n * tier DEK then decrypt the body under the CEK. Legacy tiered records decrypt\n * directly under the tier DEK.\n *\n * This leg decrypts manually rather than through `decryptRecord`, so it must\n * build the AAD itself — from the address it fetched from plus the envelope's\n * own `_tier`/`_by`, exactly as `recordAadFor` does everywhere else (#1041).\n *\n * #635: it never went through `decryptRecord`, so it never processed\n * `_sealed` slots — an elevated classified record read back through here\n * would otherwise return the body WITHOUT its sealed fields (silent\n * omission). `applySealedSlots` takes OUR already-unwrapped `cek` (unwrapped\n * under the TIER dek) instead of resolving one under the collection DEK the\n * way `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 `getAtTier`'s tier-0 branch: both tiers return\n * sealed fields inline-decrypted, not as handles.\n *\n * The caller MUST have established readability first (`keyring.deks.has(key)`\n * or owner/elevator standing) — this function does not gate.\n */\nasync function decryptElevated<T>(ctx: TiersContext<T>, id: string, envelope: EncryptedEnvelope, key: string): Promise<T> {\n const dek = await ctx.getDEK(key)\n const aad = recordAadFor({ collection: ctx.name, id }, envelope)\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(...sealedBodyArgs(envelope, 'getAtTier'), cek, aad)\n } else {\n plaintext = await decrypt(...sealedBodyArgs(envelope, 'getAtTier'), dek, aad)\n }\n let record = JSON.parse(plaintext) as T\n if (envelope._sealed !== undefined) {\n record = await ctx.codec.applySealedSlots(record, envelope._sealed, cek, { id })\n }\n return record\n}\n\n/**\n * Unique-constraint enforcement across TIERS (#1358).\n *\n * The in-memory / persisted mirrors that back ordinary unique enforcement\n * hold TIER-0 records only — #709 purges an elevated record's index entries\n * precisely so elevation actually hides its field values. That is correct,\n * and it is also exactly why a tier-0 `put()` would otherwise be free to\n * re-take a value an elevated record still holds. This closes that by\n * scanning the above-tier records the writer can decrypt.\n *\n * ## The guarantee, stated exactly\n *\n * Uniqueness is enforced across **every tier whose DEK this writer holds**.\n * A tier the writer cannot read is **outside the guarantee**: those records\n * are not merely skipped-with-a-warning, they are unreadable, so no local\n * check of any design can see them. A duplicate against such a record is\n * accepted, and stays accepted. Do not describe this as \"enforced across all\n * tiers\" anywhere.\n *\n * ## Cost\n *\n * Zero for a collection with no unique index (`ctx.uniqueKeys` is undefined\n * and this returns immediately) and for one with no elevated records (the\n * scan reads envelope headers only until it meets a `_tier > 0` row it can\n * read). On a tiered collection that DOES hold both, it is a list + get per\n * write — the price of the guarantee, paid only where it was asked for.\n *\n * No cross-tier ACCESS event is emitted per scanned record: this is a\n * constraint pre-flight over material the writer is already cleared for, not\n * a read the caller asked for. The write it gates emits its own event.\n */\nexport async function checkUniqueAcrossTiers<T>(ctx: TiersContext<T>, id: string, record: T): Promise<void> {\n const keysOf = ctx.uniqueKeys\n if (!keysOf || !ctx.tiers) return\n const mine = keysOf(record)\n if (mine.length === 0) return // null-distinct exempt, or nothing declared\n\n for (const otherId of await ctx.adapter.list(ctx.vault, ctx.name)) {\n if (otherId === id || otherId.startsWith('_')) continue\n const env = await ctx.adapter.get(ctx.vault, ctx.name, otherId)\n if (!env) continue\n const tier = env._tier ?? 0\n if (tier === 0) continue // already covered by the tier-0 mirror\n const key = dekKey(ctx.name, tier)\n if (!ctx.keyring.deks.has(key)) continue // unreadable ⇒ outside the guarantee\n let other: T\n try {\n other = await decryptElevated<T>(ctx, otherId, env, key)\n } catch {\n continue // a row this writer cannot open is, for this purpose, unreadable\n }\n const theirs = new Map(keysOf(other).map(k => [`${k.fields.join('\\u0000')}\\u0000${k.key}`, k]))\n for (const k of mine) {\n if (theirs.has(`${k.fields.join('\\u0000')}\\u0000${k.key}`)) {\n throw new UniqueConstraintError(ctx.name, id, k.fields, otherId)\n }\n }\n }\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 moveFrom: RecordIdentity = {\n // Opens at the version currently stored; the live write below re-seals at\n // `_v + 1`, so the two ends of a move are NOT the same identity (#1093).\n collection: ctx.name, id, version: envelope._v,\n ...(fromTier > 0 ? { tier: fromTier } : {}),\n ...(envelope._by !== undefined ? { by: envelope._by } : {}),\n }\n const moveTo: RecordIdentity = {\n collection: ctx.name, id, version: envelope._v + 1,\n ...(toTier > 0 ? { tier: toTier } : {}),\n by: ctx.keyring.userId,\n }\n const body = await rewrapBodyToDek(moveFrom, moveTo, 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 //\n // A SECOND rewrap, not a reuse of `body` above. The live write and the\n // snapshot now land at different identities, so they need different AAD and\n // therefore different ciphertext — sharing one rewrap would produce bytes\n // that open at one address and not the other (#1041).\n await ctx.saveHistorySnapshot(id, envelope._v, async (address) => {\n const to: RecordIdentity = { ...address, ...(envelope._by !== undefined ? { by: envelope._by } : {}) }\n const histBody = await rewrapBodyToDek(moveFrom, to, envelope, fromDek, toDek)\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- excluded from ...snapshot\n const { _tier: _preTier, _elevatedBy: _preElevatedBy, ...snapshot } = applyRewrappedBody(envelope, histBody)\n return snapshot\n })\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 // NOT a `buildRecordEnvelope` site, deliberately (#1051). This is an envelope\n // TRANSFORM, not a construction: the spread is load-bearing (#662) — it carries\n // every passenger slot (`_sealed`/`_det`/`_vdig`/`_bidx`/`_source`/`_debug`)\n // through unchanged, which a fresh literal would drop. That drop is exactly the\n // defect #1074 found in `rotateKeys`. Its body already goes through the enclave\n // (`rewrapBodyToDek`), which is where a tier move will supply AAD.\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({ collection: ctx.name, id }, envelope, fromDek) : 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 moveFrom: RecordIdentity = {\n // Opens at the version currently stored; the live write below re-seals at\n // `_v + 1`, so the two ends of a move are NOT the same identity (#1093).\n collection: ctx.name, id, version: envelope._v,\n ...(fromTier > 0 ? { tier: fromTier } : {}),\n ...(envelope._by !== undefined ? { by: envelope._by } : {}),\n }\n const moveTo: RecordIdentity = {\n collection: ctx.name, id, version: envelope._v + 1,\n ...(toTier > 0 ? { tier: toTier } : {}),\n by: ctx.keyring.userId,\n }\n const body = await rewrapBodyToDek(moveFrom, moveTo, 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 // A SECOND rewrap — see elevate()'s note. The snapshot's destination\n // identity differs from the live write's, so the bytes must differ too.\n await ctx.saveHistorySnapshot(id, envelope._v, async (address) => {\n const to: RecordIdentity = { ...address, ...(envelope._by !== undefined ? { by: envelope._by } : {}) }\n const histBody = await rewrapBodyToDek(moveFrom, to, envelope, fromDek, toDek)\n return applyRewrappedBody(carried, histBody)\n })\n // Transform, not construction — see the note on elevate()'s `next` above.\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({ collection: ctx.name, id }, next, { 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({ collection: ctx.name, id }, envelope, fromDek) : 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 '../../capsule/index.js'\n"],"mappings":"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","names":["putAtTier","getAtTier","listAtTier","elevate","demote","checkUniqueAcrossTiers"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/port/with/team-strategy.ts","../src/with-party/team/wrapped-deks.ts","../src/with-party/team/recovery.ts","../src/with-party/team/magic-link-grant.ts","../src/with-party/team/rotate-recover.ts","../src/with-party/team/peer-recover.ts","../src/with-party/auth-introspection/index.ts","../src/with-party/team/authenticators.ts"],"sourcesContent":["/**\n * Multi-user team capability contract (#267 Track A tail — the\n * keyring-grant → team split). Lives on the `/with` port (the one seam the\n * kernel spine may import statically) so `Noydb` can hold the `NO_TEAM`\n * floor default without a spine→service static import.\n *\n * The active engine (`withTeam()` in `with-party/team/active.ts`)\n * statically links the grant/revoke/rotate keyring engines from\n * `keyring.ts` — bundle-charged to consumers of the `@noy-db/hub/team`\n * subpath, never to the single-user floor. {@link NO_TEAM} (the floor\n * default) throws {@link TeamNotEnabledError}.\n *\n * `Noydb.grant` / `Noydb.revoke` / `Noydb.rotate` delegate here, passing\n * their `TeamFacade` — the facade owns the policy-gate + keyring plumbing\n * (`runGrant` / `runRevoke` / `runRotate`) and receives the engine as an\n * argument from the strategy.\n *\n * Deliberately NOT gated (single-user primitives): owner keyring creation,\n * unlock, `listUsers`, `updateUser`, secret rotate/recover, and the\n * `createDeedOwner` free function (no createNoydb instance to gate against —\n * the same carve-out as `liberateVault`).\n * @internal\n */\nimport type { GrantOptions, RevokeOptions, FactorProofBundle } from '../../kernel/types.js'\nimport type { RotateResult, RosterVerifyResult, QuarantineResult } from '../../with-party/team/keyring.js'\nimport { TeamNotEnabledError } from '../../kernel/errors.js'\nimport type { TeamFacade } from '../../with-party/team/noydb-facade.js'\n\nexport interface TeamStrategy {\n grant(team: TeamFacade, vault: string, options: GrantOptions, factors?: FactorProofBundle): Promise<void>\n revoke(team: TeamFacade, vault: string, options: RevokeOptions, factors?: FactorProofBundle): Promise<void>\n rotate(team: TeamFacade, vault: string, collections: string[]): Promise<RotateResult>\n /** #1121 — read-only sweep naming every `_keyring` file that fails authentication. */\n verifyRoster(team: TeamFacade, vault: string): Promise<RosterVerifyResult>\n /** #1121 — remove an unauthenticatable `_keyring` file and re-key behind it. */\n quarantineKeyring(team: TeamFacade, vault: string, userId: string, factors?: FactorProofBundle): Promise<QuarantineResult>\n}\n\n/**\n * No-op stub — the floor default. Every multi-user team operation throws\n * {@link TeamNotEnabledError}; opt in with `teamStrategy: withTeam()` in\n * createNoydb. @internal\n */\nexport const NO_TEAM: TeamStrategy = {\n async grant() { throw new TeamNotEnabledError() },\n async revoke() { throw new TeamNotEnabledError() },\n async rotate() { throw new TeamNotEnabledError() },\n async verifyRoster() { throw new TeamNotEnabledError() },\n async quarantineKeyring() { throw new TeamNotEnabledError() },\n}\n","/**\n * **Wrap-DEKs primitive** — a single canonical shape for the\n * pattern of \"serialize a DEK set, encrypt it under a credential-derived\n * AES-GCM key.\" Used by:\n *\n * - **tier-0** — paper recovery entries (`_meta/recovery-paper`),\n * credential = the printed code.\n * - **tier-2** — password authenticator slots (`KeyringFile.authenticators`,\n * `wrapKind: 'deks'`), credential = the user's password.\n *\n * **Not** used by `@noy-db/on-pin` — tier-3 wraps the DEK set under\n * the same conceptual pattern but at **100,000 PBKDF2 iterations**\n * (vs the 600,000 here), because the protection window for a PIN\n * slot is short (idle-timeout-bounded, typically 15 min) and 600k\n * iterations would make every PIN-resume noticeably slow. The wire\n * formats are deliberately incompatible. See `@noy-db/on-pin`'s\n * `PIN_PBKDF2_ITERATIONS` and the threat-model rationale in its\n * module docstring.\n *\n * Previously, the same crypto lived in two places: `mintPaperRecoveryEntry`\n * (in `team/recovery.ts`) and `enrollPasswordAuthenticator` (in\n * `@noy-db/on-password`). Both functions did identical work — PBKDF2\n * the credential, AES-GCM-encrypt the JSON-serialized DEK set — but\n * their implementations had drifted apart enough that fixing a bug\n * in one wouldn't fix the other.\n *\n * This module owns the canonical implementation. Consumers compose:\n *\n * - `mintPaperRecoveryEntry` is now a thin wrapper that calls\n * `mintWrappedDeksBlob` and adds `{ codeId, enrolledAt }`.\n * - `enrollPasswordAuthenticator` calls `mintWrappedDeksBlob` and\n * wraps the result in the slot envelope.\n *\n * @module\n */\n\nimport {\n deriveSecretKey,\n generateSalt,\n encryptBytes,\n decryptBytes,\n bufferToBase64,\n base64ToBuffer,\n exportDekSet,\n importDekSet,\n type EnclaveKey,\n} from '../../capsule/index.js'\nimport { InvalidKeyError, TamperedError } from '../../kernel/errors.js'\n\nconst PBKDF2_ITERATIONS = 600_000\n\n// ─── Type ──────────────────────────────────────────────────────────────\n\n/**\n * The wrap-DEKs primitive — a serialized + AES-GCM-encrypted DEK set\n * keyed under a credential-derived key.\n *\n * All three fields are base64-encoded so the blob is JSON-safe and\n * round-trips through `_meta/*` envelopes (which carry plaintext\n * JSON in `_data`).\n *\n * Composition: `PaperRecoveryEntry extends WrappedDeksBlob` plus\n * `{ codeId, enrolledAt }`. `KeyringAuthenticatorWrappingDEKs`\n * carries the same three fields with `salt` stored in `meta` for\n * slot-format back-compat (defers moving it to top-level).\n */\nexport interface WrappedDeksBlob {\n /** Base64 PBKDF2 salt for the credential-derived wrapping key. */\n readonly salt: string\n /** Base64 AES-GCM IV used for the `wrappedDeks` ciphertext. */\n readonly iv: string\n /** Base64 AES-GCM ciphertext of `{ deks: { collection: base64rawDek } }`. */\n readonly wrappedDeks: string\n}\n\n// ─── Mint ──────────────────────────────────────────────────────────────\n\n/**\n * Mint a fresh `WrappedDeksBlob` from a DEK set + a string credential.\n *\n * Generates a random salt + IV, derives a 256-bit AES-GCM key via\n * PBKDF2-SHA256(credential, salt, 600K), serializes the DEK set as\n * `{ deks: { coll: rawBase64 } }`, and AES-GCM-encrypts.\n *\n * The `credential` is the user-typed string (recovery code, password,\n * PIN). Caller normalization rules apply (e.g. paper\n * recovery uppercase-strips the code before reaching this function).\n *\n * @param deks - DEK set to wrap. Serialized through the enclave's\n * `exportDekSet` door (the hub mints DEKs extractable;\n * consumers feeding non-extractable keys will get\n * `InvalidAccessError` from WebCrypto).\n * @param credential - String input the consumer minted (paper code,\n * password, PIN). Treated as opaque bytes by PBKDF2.\n */\nexport async function mintWrappedDeksBlob(\n deks: Map<string, EnclaveKey>,\n credential: string,\n): Promise<WrappedDeksBlob> {\n const salt = generateSalt()\n const wrappingKey = await deriveWrappingKey(credential, salt)\n\n // Serialize the DEK set as JSON `{ deks: { collection: base64 } }`.\n const plaintext = new TextEncoder().encode(JSON.stringify({ deks: await exportDekSet(deks) }))\n const { iv, data } = await encryptBytes(plaintext, wrappingKey)\n\n return { salt: bufferToBase64(salt), iv, wrappedDeks: data }\n}\n\n// ─── Unwrap ────────────────────────────────────────────────────────────\n\n/**\n * Reverse of {@link mintWrappedDeksBlob}. Re-derives the wrapping key\n * from the credential + stored salt, AES-GCM-decrypts the wrapped DEK\n * set, and re-imports each DEK as an extractable AES-GCM CryptoKey.\n *\n * Throws (AES-GCM auth tag failure) when the credential doesn't\n * match the blob. Callers iterating over multiple blobs (e.g. paper\n * recovery's \"try every entry until one matches\") should catch.\n */\nexport async function unwrapDeksFromBlob(\n blob: WrappedDeksBlob,\n credential: string,\n): Promise<Map<string, EnclaveKey>> {\n const wrappingKey = await deriveWrappingKey(credential, base64ToBuffer(blob.salt))\n let plaintext: Uint8Array\n try {\n plaintext = await decryptBytes(blob.iv, blob.wrappedDeks, wrappingKey)\n } catch (e) {\n // The barrel reports an AEAD failure as TamperedError — which means\n // exactly \"did not authenticate under THIS key\" and is the enclave's to\n // throw (lanna-db #4). Here the key was derived from a user-supplied\n // credential, so the overwhelmingly likely cause is a wrong recovery code\n // / password / share set, and saying \"tampered\" is the misdiagnosis\n // #1288 reported. Translate to the class whose contract already reads\n // \"wrong secret or corrupted keyring\"; on-password does the same one\n // layer up. Anything else propagates untouched.\n if (e instanceof TamperedError) {\n throw new InvalidKeyError('Wrapped DEK set did not open — the credential does not match this blob, or the blob is corrupted')\n }\n throw e\n }\n const parsed = JSON.parse(new TextDecoder().decode(plaintext)) as { deks: Record<string, string> }\n return importDekSet(parsed.deks)\n}\n\n// ─── Internals ─────────────────────────────────────────────────────────\n\nasync function deriveWrappingKey(credential: string, salt: Uint8Array): Promise<CryptoKey> {\n return deriveSecretKey(credential, salt, { iterations: PBKDF2_ITERATIONS, keyUsage: 'aes-gcm' })\n}\n","/**\n * Recovery profile persistence + dispatch.\n *\n * Wires the **paper** profile end-to-end through\n * `@noy-db/on-recovery`. The other three profiles (Shamir,\n * multi-channel, admin-mediated) ship the API surface and throw\n * {@link RecoveryProfileNotImplementedError} during use; per-profile\n * dispatch lands in follow-up issues.\n *\n * Storage layout:\n *\n * ```\n * _meta/recovery-paper — JSON { entries: RecoveryCodeEntry[] } produced by `on-recovery`.\n * _meta/recovery-shamir — reserved\n * _meta/recovery-multi — reserved\n * _meta/recovery-admin — reserved\n * ```\n *\n * Like `_meta/policy` and `_meta/handle`, the documents are plain JSON\n * with empty `_iv` — the recovery-code wrapping is what protects the\n * KEK; the entries themselves are inert without the user's code.\n *\n * @module\n */\nimport type { NoydbStore } from '../../kernel/types.js'\nimport { buildRecordEnvelope, bufferToBase64, generateRecoverySecret } from '../../capsule/index.js'\nimport type { EnclaveKey } from '../../capsule/index.js'\nimport {\n mintWrappedDeksBlob,\n unwrapDeksFromBlob,\n type WrappedDeksBlob,\n} from './wrapped-deks.js'\nimport type { NoydbShamir } from './noydb-shamir.js'\n\n/**\n * One paper recovery code as persisted in `_meta/recovery-paper`.\n *\n * The hub's KEK is intentionally non-extractable (see `crypto.ts`),\n * so the recovery entry can't AES-KW-wrap the KEK directly. Instead\n * we wrap a serialized DEK set: the entry holds the AES-GCM\n * ciphertext of `{ deks: { collection: rawDekBase64 } }`. Recovery\n * deserializes the DEK set, then mints a fresh KEK from the new\n * secret and rewraps the DEKs under it.\n *\n * This is the same pattern `@noy-db/on-pin` uses for tier-3 quick\n * resume — the cryptographic guarantee is identical (AES-GCM with a\n * PBKDF2-derived key), and it sidesteps the non-extractable-KEK\n * constraint cleanly.\n *\n * Type-level composition: `PaperRecoveryEntry extends\n * WrappedDeksBlob` — the three crypto fields (`salt`, `iv`,\n * `wrappedDeks`) come from the shared primitive; `codeId` and\n * `enrolledAt` are paper-recovery's own metadata. Wire format\n * unchanged.\n */\nexport interface PaperRecoveryEntry extends WrappedDeksBlob {\n readonly codeId: string\n readonly enrolledAt: string\n}\n\nexport interface PaperRecoveryDoc {\n readonly _noydb_recovery: 1\n readonly profile: 'paper'\n readonly entries: ReadonlyArray<PaperRecoveryEntry>\n}\n\nconst PAPER_DOC_ID = 'recovery-paper'\n\n/** Read the paper-recovery entries. Returns empty array when absent. */\nexport async function loadPaperRecoveryEntries(\n store: NoydbStore,\n vault: string,\n): Promise<ReadonlyArray<PaperRecoveryEntry>> {\n const env = await store.get(vault, '_meta', PAPER_DOC_ID)\n if (!env) return []\n try {\n const doc = JSON.parse(env._data ?? '') as PaperRecoveryDoc\n if (doc.profile !== 'paper' || !Array.isArray(doc.entries)) return []\n return doc.entries\n } catch {\n return []\n }\n}\n\n/** Replace the paper-recovery entries (used after burn-on-recovery). */\nexport async function savePaperRecoveryEntries(\n store: NoydbStore,\n vault: string,\n entries: ReadonlyArray<PaperRecoveryEntry>,\n): Promise<void> {\n const doc: PaperRecoveryDoc = {\n _noydb_recovery: 1,\n profile: 'paper',\n entries,\n }\n const envelope = buildRecordEnvelope(\n { collection: '_meta', id: PAPER_DOC_ID, version: 1 },\n { iv: '', data: JSON.stringify(doc) },\n )\n await store.put(vault, '_meta', PAPER_DOC_ID, envelope)\n}\n\n/** Drop a single paper-recovery entry (burn-on-use). */\nexport async function burnPaperRecoveryEntry(\n store: NoydbStore,\n vault: string,\n codeId: string,\n): Promise<void> {\n const entries = await loadPaperRecoveryEntries(store, vault)\n const remaining = entries.filter((e) => e.codeId !== codeId)\n await savePaperRecoveryEntries(store, vault, remaining)\n}\n\n/** Whether at least one recovery profile has any enrolled entries. */\nexport async function hasRecoveryEnrolled(\n store: NoydbStore,\n vault: string,\n): Promise<boolean> {\n const paper = await loadPaperRecoveryEntries(store, vault)\n if (paper.length > 0) return true\n const shamir = await loadShamirRecoveryEntries(store, vault)\n return shamir.length > 0\n}\n\n/**\n * Whether at least one **strong** recovery profile is enrolled.\n *\n * \"Strong\" excludes paper-alone — under managed-secret mode the\n * user has no memorized secret, so a stolen/lost paper sheet\n * would be a single point of total loss. Strong profiles today:\n *\n * - `shamir` (k-of-n threshold; survives loss of up to n-k shares)\n * - `multi-channel` (when shipped — follow-up slice)\n * - `admin-mediated` (when shipped — follow-up slice)\n *\n * Managed mode requires this check to pass before `openVault` returns.\n */\nexport async function hasStrongRecoveryEnrolled(\n store: NoydbStore,\n vault: string,\n): Promise<boolean> {\n const shamir = await loadShamirRecoveryEntries(store, vault)\n return shamir.length > 0\n // When multi-channel / admin-mediated land, extend this check.\n}\n\n// ─── Shamir recovery ─────────────────────────────────────────────────────\n\n/**\n * One Shamir-recovery entry as persisted in `_meta/recovery-shamir`.\n *\n * Like {@link PaperRecoveryEntry}, the entry composes\n * {@link WrappedDeksBlob} (DEKs wrapped under a fresh ephemeral\n * recovery secret) with profile-specific metadata. Unlike paper, the\n * \"credential\" was never visible to the user — it was 32 random\n * bytes split into N Shamir shares at enrollment. The shares ARE\n * the credential; the user holds them, the hub never sees them\n * again after `enrollRecovery` returns.\n *\n * The recovery secret is base64-encoded and\n * passed as the `credential` arg to\n * {@link mintWrappedDeksBlob} / {@link unwrapDeksFromBlob}. The\n * PBKDF2 round over high-entropy input is harmless overhead — it\n * keeps the shared primitive unchanged while letting Shamir reuse\n * the same wrapping pipeline as paper.\n */\nexport interface ShamirRecoveryEntry extends WrappedDeksBlob {\n /** Stable id for this entry. Allows multiple Shamir splits to coexist. */\n readonly entryId: string\n /** Threshold — minimum shares to reconstruct. */\n readonly k: number\n /** Total shares minted at enrollment. */\n readonly n: number\n /** x-coordinates of the n minted shares. Informational. Omitted as of 0.2\n * (string-level provider doesn't expose share x-coords); kept optional so\n * pre-0.2 entries still read. */\n readonly xCoords?: ReadonlyArray<number>\n /** ISO timestamp. */\n readonly enrolledAt: string\n /** Optional caller-supplied label (e.g., \"2-of-3 board escrow\"). */\n readonly label?: string\n}\n\nexport interface ShamirRecoveryDoc {\n readonly _noydb_recovery: 1\n readonly profile: 'shamir'\n readonly entries: ReadonlyArray<ShamirRecoveryEntry>\n}\n\nconst SHAMIR_DOC_ID = 'recovery-shamir'\n\n/** Read the Shamir-recovery entries. Returns empty array when absent. */\nexport async function loadShamirRecoveryEntries(\n store: NoydbStore,\n vault: string,\n): Promise<ReadonlyArray<ShamirRecoveryEntry>> {\n const env = await store.get(vault, '_meta', SHAMIR_DOC_ID)\n if (!env) return []\n try {\n const doc = JSON.parse(env._data ?? '') as ShamirRecoveryDoc\n if (doc.profile !== 'shamir' || !Array.isArray(doc.entries)) return []\n return doc.entries\n } catch {\n return []\n }\n}\n\n/** Replace the Shamir-recovery entries (used by enrollment and rotation). */\nexport async function saveShamirRecoveryEntries(\n store: NoydbStore,\n vault: string,\n entries: ReadonlyArray<ShamirRecoveryEntry>,\n): Promise<void> {\n const doc: ShamirRecoveryDoc = {\n _noydb_recovery: 1,\n profile: 'shamir',\n entries,\n }\n const envelope = buildRecordEnvelope(\n { collection: '_meta', id: SHAMIR_DOC_ID, version: 1 },\n { iv: '', data: JSON.stringify(doc) },\n )\n await store.put(vault, '_meta', SHAMIR_DOC_ID, envelope)\n}\n\n/**\n * Mint a fresh Shamir recovery entry from a DEK set.\n *\n * 1. Generates a 32-byte recovery secret.\n * 2. Wraps the DEK set under that secret via\n * {@link mintWrappedDeksBlob} (the recovery secret is base64-\n * encoded as the credential string — PBKDF2 over high-entropy\n * input is harmless overhead).\n * 3. Splits the recovery secret via Shamir into `n` shares with\n * threshold `k`.\n * 4. Zeros the in-memory recovery secret after wrapping + splitting.\n *\n * Returns:\n * - `entry` — the {@link ShamirRecoveryEntry} to persist.\n * - `shareStrings` — the `n` Base32-encoded share strings to\n * return to the caller. The HUB MUST NOT PERSIST THESE; once\n * returned they are the user's responsibility.\n *\n * @param deks - DEK set to wrap.\n * @param entryId - Stable id for this entry (caller-supplied or\n * hub-generated).\n * @param k - Threshold (>= 2).\n * @param n - Total shares (k <= n <= 255).\n * @param label - Optional caller label.\n */\nexport async function mintShamirRecoveryEntry(\n provider: NoydbShamir,\n deks: Map<string, EnclaveKey>,\n entryId: string,\n k: number,\n n: number,\n label?: string,\n): Promise<{ entry: ShamirRecoveryEntry; shareStrings: string[] }> {\n const recoverySecret = generateRecoverySecret()\n try {\n const credential = bufferToBase64(recoverySecret)\n const blob = await mintWrappedDeksBlob(deks, credential)\n const shareStrings = provider.splitToShares(recoverySecret, k, n)\n const entry: ShamirRecoveryEntry = {\n ...blob, entryId, k, n,\n enrolledAt: new Date().toISOString(),\n ...(label !== undefined && { label }),\n }\n return { entry, shareStrings }\n } finally {\n recoverySecret.fill(0)\n }\n}\n\n/**\n * Decrypt a Shamir recovery entry to recover the raw DEK set.\n *\n * Combines K or more `shares`, reconstructs the recovery secret,\n * unwraps the DEKs via {@link unwrapDeksFromBlob}.\n *\n * Throws (AES-GCM auth-tag mismatch) when the shares don't combine\n * to the secret originally used to mint the entry — typically\n * because they came from a different enrollment or were tampered\n * with. Callers iterating multiple entries should catch.\n */\nexport async function unwrapDeksFromShamirEntry(\n provider: NoydbShamir,\n entry: ShamirRecoveryEntry,\n shareStrings: readonly string[],\n): Promise<Map<string, EnclaveKey>> {\n if (shareStrings.length < entry.k) {\n throw new Error(\n `Insufficient shares: this Shamir entry needs ${entry.k} of ${entry.n}, `\n + `but ${shareStrings.length} were provided.`,\n )\n }\n const secret = provider.combineShares(shareStrings)\n try {\n return await unwrapDeksFromBlob(entry, bufferToBase64(secret))\n } finally {\n secret.fill(0)\n }\n}\n\n\n/**\n * Generate one paper-recovery entry from an unlocked DEK set.\n *\n * Returns the serializable entry (persisted via\n * {@link savePaperRecoveryEntries}). The recovery flow unwraps the\n * DEK set, then mints a fresh KEK from the user's new secret.\n *\n * Thin wrapper over {@link mintWrappedDeksBlob} — the crypto\n * lives in the shared primitive; this function just adds paper-\n * recovery's own metadata (`codeId`, `enrolledAt`).\n *\n * @param deks Map of collection-name → DEK (extractable).\n * @param code The plaintext recovery code (caller-supplied;\n * pair this with `@noy-db/on-recovery`'s code\n * generator/parser if available).\n * @param codeId Stable id used by `burnPaperRecoveryEntry`.\n */\nexport async function mintPaperRecoveryEntry(\n deks: Map<string, EnclaveKey>,\n code: string,\n codeId: string,\n): Promise<PaperRecoveryEntry> {\n const blob = await mintWrappedDeksBlob(deks, code)\n return {\n ...blob,\n codeId,\n enrolledAt: new Date().toISOString(),\n }\n}\n\n/**\n * Decrypt a recovery entry to recover the raw DEK set. Used by the\n * `recoverSecret` flow after the user's code has been parsed.\n *\n * Thin wrapper over {@link unwrapDeksFromBlob}.\n *\n * @throws when the code does not match the entry (AES-GCM auth tag fail).\n */\nexport async function unwrapDeksFromPaperEntry(\n entry: PaperRecoveryEntry,\n code: string,\n): Promise<Map<string, EnclaveKey>> {\n return unwrapDeksFromBlob(entry, code)\n}\n\n// The wrap crypto lives in the canonical wrap-DEKs primitive at\n// `./wrapped-deks.ts` and is reached via\n// `mintWrappedDeksBlob` / `unwrapDeksFromBlob`; base64 comes from the\n// enclave barrel.\n","/**\n * Magic-link-bound cross-user delegation grants.\n *\n * This module is the **core storage + encryption layer** that lets a\n * grantor issue a tier-DEK to a user whose KEK they do not know. The\n * trust bridge is provided by the `@noy-db/on-magic-link` package:\n *\n * 1. Grantor picks a grantee identity (user id + email handle).\n * 2. Grantor mints a magic-link token (ULID) via `createMagicLinkToken`.\n * 3. Grantor derives a **content key** + a **KEK** from\n * `(serverSecret, token, vault)` using HKDF-SHA256 with separate\n * `info` tags — both callers (grantor and grantee) can derive the\n * same keys given the same inputs.\n * 4. Grantor persists a record in `_magic_link_grants/<token>`:\n * - envelope `_data` is AES-GCM encrypted under the content key\n * - the inner `wrappedDek` is AES-KW wrapped under the KEK\n * 5. Grantee receives the URL, derives the same content key + KEK,\n * loads the grant, decrypts the envelope, unwraps the tier DEK.\n *\n * ## Why a separate collection from `_delegations`\n *\n * `_delegations` envelopes are encrypted under a DEK shared across\n * every vault user (audit-visibility). External auditors / client\n * portal users have NO pre-existing keyring, so they cannot read that\n * DEK. Magic-link grants live in their own collection whose envelope\n * encryption is derived purely from the magic-link URL + server secret\n * — nothing else is required to decrypt.\n *\n * ## Batch grants\n *\n * One magic-link token may point to MULTIPLE grants (e.g. the client\n * portal case: invoices + payments + etax all share one link). Each\n * grant is persisted under a distinct record id:\n *\n * `<token>` for the single-grant / primary entry\n * `<token>:<index>` for subsequent entries\n *\n * `listMagicLinkGrants(store, vault, token)` enumerates every record\n * whose id begins with `<token>` so the claimant can materialize all\n * DEKs in one pass.\n *\n * ## Revocation\n *\n * `store.delete(vault, _magic_link_grants, <token>)` immediately\n * invalidates the link — even if the URL was captured and the server\n * secret leaked, no payload remains to decrypt.\n *\n * @module\n */\n\nimport type { NoydbStore } from '../../kernel/types.js'\nimport type { UnlockedKeyring } from './keyring.js'\nimport { buildRecordAad, buildRecordEnvelope, encrypt, openEnvelopeJson, wrapKey, unwrapKey, sha256Bytes, hkdfAesGcmKey, type EnclaveKey } from '../../capsule/index.js'\nimport { dekKey } from './tiers.js'\nimport { DelegationTargetMissingError } from '../../kernel/errors.js'\n\n/** Reserved collection holding magic-link grant envelopes. */\nexport const MAGIC_LINK_GRANTS_COLLECTION = '_magic_link_grants'\n\n/** HKDF `info` for the AES-GCM content key. Version-namespaced. */\nexport const MAGIC_LINK_CONTENT_INFO_PREFIX = 'noydb-magic-link-content-v1:'\n\n/** HKDF `info` for the AES-KW KEK. Matches `@noy-db/on-magic-link`. */\nexport const MAGIC_LINK_KEK_INFO_PREFIX = 'noydb-magic-link-v1:'\n\n// ─── Types ──────────────────────────────────────────────────────────────\n\n/**\n * Decrypted payload of a magic-link grant record. Mirrors\n * `DelegationToken` in `team/delegation.ts` but tracked separately\n * because the two flows persist under different collections + envelope\n * encryption schemes.\n */\nexport interface MagicLinkGrantPayload {\n readonly id: string\n readonly toUser: string\n readonly fromUser: string\n readonly tier: number\n /** Collection name or `null` for the vault-wide tier DEK. */\n readonly collection: string | null\n /** Optional specific record id scope. */\n readonly record?: string\n /** ISO timestamp — grant expires at this instant. */\n readonly until: string\n /** AES-KW-wrapped tier DEK, unwrap with the magic-link KEK. */\n readonly wrappedDek: string\n /** ISO timestamp the grant was issued. */\n readonly createdAt: string\n /** Optional caller-provided label (surfaced in audit UIs). */\n readonly note?: string\n}\n\nexport interface IssueMagicLinkGrantOptions {\n readonly toUser: string\n readonly tier: number\n readonly collection?: string\n readonly record?: string\n readonly until: Date | string\n readonly note?: string\n}\n\nexport interface MagicLinkGrantRecord {\n /** Store record id — `<token>` or `<token>:<index>` for batch entries. */\n readonly recordId: string\n readonly payload: MagicLinkGrantPayload\n}\n\n// ─── Key derivation ─────────────────────────────────────────────────────\n\n/**\n * Derive the AES-GCM content key from the same HKDF inputs used for\n * the magic-link KEK. Different `info` suffix → domain-separated key.\n *\n * Exported so the `@noy-db/on-magic-link` package can share the exact\n * derivation path without cross-dependency between the two modules.\n */\nexport async function deriveMagicLinkContentKey(\n serverSecret: string | Uint8Array<ArrayBuffer>,\n token: string,\n vault: string,\n): Promise<EnclaveKey> {\n const ikmBytes =\n serverSecret instanceof Uint8Array\n ? serverSecret\n : new TextEncoder().encode(serverSecret)\n const salt = await sha256Bytes(new TextEncoder().encode(token))\n const info = new TextEncoder().encode(MAGIC_LINK_CONTENT_INFO_PREFIX + vault)\n return hkdfAesGcmKey(ikmBytes, salt, info)\n}\n\n// ─── Issue ──────────────────────────────────────────────────────────────\n\n/**\n * Persist a magic-link grant record. Caller derives + provides both\n * the content key and the KEK; this function performs the wrap/encrypt\n * and writes the envelope.\n *\n * `recordId` lets the caller use either the bare token (primary grant)\n * or a suffixed id (batch entry). The writer is responsible for\n * collision-avoidance across batch entries.\n */\nexport async function writeMagicLinkGrant(\n store: NoydbStore,\n vault: string,\n grantor: UnlockedKeyring,\n contentKey: EnclaveKey,\n grantKek: EnclaveKey,\n recordId: string,\n opts: IssueMagicLinkGrantOptions,\n): Promise<MagicLinkGrantRecord> {\n const collectionName = opts.collection ?? null\n const sourceKey = collectionName\n ? dekKey(collectionName, opts.tier)\n : `__any#${opts.tier}`\n const sourceDek = grantor.deks.get(sourceKey)\n if (!sourceDek) {\n throw new DelegationTargetMissingError(\n `grantor cannot find tier ${opts.tier} DEK for ${collectionName ?? '(any)'}`,\n )\n }\n const wrappedDek = await wrapKey(sourceDek, grantKek)\n\n const until = typeof opts.until === 'string' ? opts.until : opts.until.toISOString()\n const createdAt = new Date().toISOString()\n const payload: MagicLinkGrantPayload = {\n id: recordId,\n toUser: opts.toUser,\n fromUser: grantor.userId,\n tier: opts.tier,\n collection: collectionName,\n ...(opts.record && { record: opts.record }),\n until,\n wrappedDek,\n createdAt,\n ...(opts.note && { note: opts.note }),\n }\n\n const identity = { collection: MAGIC_LINK_GRANTS_COLLECTION, id: recordId, by: grantor.userId, version: 1 }\n const { iv, data } = await encrypt(JSON.stringify(payload), contentKey, buildRecordAad(identity))\n const envelope = buildRecordEnvelope(\n identity,\n { ts: createdAt, iv, data},\n )\n await store.put(vault, MAGIC_LINK_GRANTS_COLLECTION, recordId, envelope)\n return { recordId, payload }\n}\n\n// ─── Claim ──────────────────────────────────────────────────────────────\n\n/**\n * Fetch + decrypt a single magic-link grant record by id. Returns null\n * when the record is absent OR when decryption fails (wrong server\n * secret, wrong vault, tampered envelope) — callers treat a null as\n * \"this URL is not valid for this server\".\n *\n * The returned payload's `wrappedDek` is still AES-KW-wrapped; the\n * caller unwraps it with the magic-link KEK to obtain the tier DEK.\n */\nexport async function readMagicLinkGrantRecord(\n store: NoydbStore,\n vault: string,\n contentKey: EnclaveKey,\n recordId: string,\n): Promise<MagicLinkGrantPayload | null> {\n const env = await store.get(vault, MAGIC_LINK_GRANTS_COLLECTION, recordId)\n if (!env) return null\n try {\n const json = await openEnvelopeJson({ collection: MAGIC_LINK_GRANTS_COLLECTION, id: recordId }, env, contentKey)\n return JSON.parse(json) as MagicLinkGrantPayload\n } catch {\n return null\n }\n}\n\n/**\n * Enumerate every grant record sharing the magic-link `token` prefix\n * (i.e. the primary `<token>` entry plus any `<token>:*` batch entries).\n * Expired grants are still returned — the caller filters on `until`.\n */\nexport async function listMagicLinkGrants(\n store: NoydbStore,\n vault: string,\n contentKey: EnclaveKey,\n token: string,\n): Promise<MagicLinkGrantPayload[]> {\n const ids = await store.list(vault, MAGIC_LINK_GRANTS_COLLECTION)\n const matching = ids.filter(id => id === token || id.startsWith(`${token}:`))\n const out: MagicLinkGrantPayload[] = []\n for (const id of matching) {\n const payload = await readMagicLinkGrantRecord(store, vault, contentKey, id)\n if (payload) out.push(payload)\n }\n return out\n}\n\n/**\n * Unwrap the tier DEK from a grant payload using the magic-link KEK.\n * Thin wrapper around `unwrapKey` — provided so the claimant can avoid\n * importing `crypto.js` directly.\n */\nexport async function unwrapMagicLinkGrant(\n payload: MagicLinkGrantPayload,\n grantKek: EnclaveKey,\n): Promise<EnclaveKey> {\n return unwrapKey(payload.wrappedDek, grantKek)\n}\n\n/**\n * Delete a magic-link grant (primary + every batch entry sharing the\n * token). Safe to call when nothing exists.\n */\nexport async function revokeMagicLinkGrant(\n store: NoydbStore,\n vault: string,\n token: string,\n): Promise<number> {\n const ids = await store.list(vault, MAGIC_LINK_GRANTS_COLLECTION)\n const matching = ids.filter(id => id === token || id.startsWith(`${token}:`))\n for (const id of matching) {\n await store.delete(vault, MAGIC_LINK_GRANTS_COLLECTION, id)\n }\n return matching.length\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────\n\n/**\n * Compose the batch-entry record id. `index === 0` → bare token.\n * Subsequent entries use `<token>:<index>` so `store.list()` can\n * enumerate them all by common prefix.\n */\nexport function magicLinkGrantRecordId(token: string, index: number): string {\n return index === 0 ? token : `${token}:${index}`\n}\n\n/**\n * True when the payload's `until` is in the past relative to `now`.\n * Kept here (rather than inlined) so the semantics stay aligned with\n * the canonical `DelegationToken` expiry check.\n */\nexport function isMagicLinkGrantExpired(\n payload: MagicLinkGrantPayload,\n now: Date = new Date(),\n): boolean {\n return payload.until <= now.toISOString()\n}\n","/**\n * Tier-1 change flows — `rotateSecret` (user remembers old) and\n * `recoverSecret` (user supplies a recovery proof).\n *\n * The two flows share the post-verification half — fresh salt, fresh\n * KEK, rewrap every DEK — and differ only in how they re-derive the\n * old KEK:\n *\n * - **Rotate**: derive from the supplied `oldSecret`.\n * - **Recover (paper)**: unwrap from a `RecoveryCodeEntry` using a\n * user-supplied recovery code. The entry is burned on success.\n *\n * The non-paper recovery profiles (Shamir, multi-channel,\n * admin-mediated) are not yet wired — calling them throws\n * {@link RecoveryProfileNotImplementedError} with a tracking link.\n *\n * @module\n */\nimport type { NoydbStore, KeyringFile, KeyringEchoBlock } from '../../kernel/types.js'\nimport { NOYDB_KEYRING_VERSION } from '../../kernel/types.js'\nimport {\n buildRecordEnvelope,\n deriveKey,\n deriveEchoKey,\n generateSalt,\n wrapKey,\n unwrapKey,\n bufferToBase64,\n base64ToBuffer,\n type EnclaveKey,\n type EchoSecretParts,\n} from '../../capsule/index.js'\nimport { InvalidKeyError, NoAccessError, RecoveryProfileNotImplementedError } from '../../kernel/errors.js'\nimport {\n loadPaperRecoveryEntries,\n burnPaperRecoveryEntry,\n unwrapDeksFromPaperEntry,\n loadShamirRecoveryEntries,\n unwrapDeksFromShamirEntry,\n type PaperRecoveryEntry,\n type ShamirRecoveryEntry,\n} from './recovery.js'\nimport type { NoydbShamir } from './noydb-shamir.js'\nimport {\n assertStrongSecret,\n assertStrongEchoSecret,\n type SecretPolicy,\n type EchoSecretPolicy,\n} from '../../kernel/validation.js'\nimport type { UnlockedKeyring } from './keyring.js'\nimport { mintKeyringCanary, deriveKekForKeyring, readKeyringFile } from './keyring.js'\nimport { assertRosterAuthenticated, mintRosterTag } from './roster-tag.js'\nimport { nextRosterEpoch } from './roster-epoch.js'\nimport { ROSTER_KEY_ID } from '../../kernel/constants.js'\nimport { buildEchoBlock } from './echo-secret.js'\nimport type { NoydbDeviceSeal } from './device-seal.js'\nimport type { KeyringAuthenticator } from '../../kernel/types.js'\nimport type { EnrollAuthenticatorOptions } from './authenticators.js'\nimport { ValidationError } from '../../kernel/errors.js'\n\n/**\n * Echo-enrollment knobs for a rotation whose NEW secret is 3-part (spec #940).\n *\n * Deliberately NOT exported: it is reachable structurally through\n * {@link RotateSecretInput.echoOptions}, and exporting it would mean adding it\n * to the team + root barrels for a type consumers never need to name.\n */\ninterface RotateEchoOptions {\n /**\n * Device-local sealer for the reveal blob. Present ⇒ `reveal: 'sealed'`;\n * absent ⇒ `reveal: 'portable'`. Mirrors\n * {@link CreateOwnerKeyringOptions.deviceSeal}.\n */\n readonly deviceSeal?: NoydbDeviceSeal\n /** Optional display hint for the masked echo. */\n readonly maskHint?: string\n}\n\n/**\n * The `echo` field for the keyring file a tier-1 change is about to write.\n *\n * Every write site rebuilds the persisted file WITHOUT the old block (a\n * `{ ...file }` spread would carry it) and then re-attaches whatever the NEW\n * secret's shape demands:\n *\n * - parts ⇒ a FRESH block (the standard→echo upgrade, and the re-mint on an\n * echo→echo rotation — the old block's verifiers are bound to the old parts).\n * - string ⇒ `{}`, i.e. no block at all (the echo→standard downgrade).\n *\n * Getting this wrong bricks the keyring: a stale block makes\n * `deriveKekForKeyring` demand the echo ceremony and derive via the echo KDF,\n * while the DEKs sit wrapped under a string-derived KEK — neither the old parts\n * nor the new string can open it.\n */\nasync function echoFieldForNewSecret(\n newSecret: string | EchoSecretParts,\n echoOptions?: RotateEchoOptions,\n): Promise<{ echo?: KeyringEchoBlock }> {\n if (typeof newSecret === 'string') return {}\n return {\n echo: await buildEchoBlock(\n newSecret,\n echoOptions?.deviceSeal\n ? { kind: 'sealed', deviceSeal: echoOptions.deviceSeal }\n : { kind: 'portable' },\n echoOptions?.maskHint,\n ),\n }\n}\n\n/** Derive the KEK for a freshly-chosen tier-1 secret, dispatching on its shape. */\nasync function deriveKekForNewSecret(\n newSecret: string | EchoSecretParts,\n salt: Uint8Array,\n): Promise<EnclaveKey> {\n return typeof newSecret === 'string'\n ? deriveKey(newSecret, salt)\n : deriveEchoKey(newSecret, salt)\n}\n\n/** Strength gate for a freshly-chosen tier-1 secret, dispatching on its shape. */\nfunction assertStrongNewSecret(\n newSecret: string | EchoSecretParts,\n secretPolicy?: SecretPolicy,\n echoSecretPolicy?: EchoSecretPolicy,\n): void {\n if (typeof newSecret === 'string') assertStrongSecret(newSecret, secretPolicy)\n else assertStrongEchoSecret(newSecret, echoSecretPolicy)\n}\n\n/**\n * Context handed to a {@link SlotRewrapCeremony} when `rotateSecret`\n * preserves a tier-2 slot. The ceremony's job is to re-derive its\n * method-specific wrapping material (PRF assertion, PBKDF2 of the\n * password, etc.) and wrap the freshly rewrapped DEK set under\n * the new wrapping key.\n *\n * Two surfaces are exposed:\n *\n * - `newDeks` — the rewrapped (extractable) DEK set the slot will\n * wrap. This is what `mintPaperRecoveryEntry` / `enrollPassword-\n * Authenticator` / `wrapKeyringSummary` (in `@noy-db/on-webauthn`)\n * all consume; effectively the canonical input for every\n * post-Path C tier-2 ceremony.\n *\n * - `newKek` — the freshly-derived KEK (extractable for the\n * ceremony scope only). Only relevant for forward-compatibility\n * with a hypothetical future on-* package that wants to wrap the\n * KEK itself under a method-derived key. None of the shipped\n * on-* packages need this; they all operate on `newDeks`.\n *\n * The ceremony MUST preserve `oldSlot.id` and `oldSlot.method` in the\n * returned `EnrollAuthenticatorOptions`. Hub validates these — a\n * mismatch throws `ValidationError` (prevents slot-type swap mid-\n * rotation, e.g. converting a webauthn slot to a password slot under\n * cover of preservation).\n */\nexport interface SlotRewrapContext {\n readonly newKek: EnclaveKey\n readonly newDeks: Map<string, EnclaveKey>\n readonly oldSlot: KeyringAuthenticator\n}\n\n/**\n * Callback that re-enrolls one tier-2 slot during `rotateSecret`.\n * Returns the new slot's `EnrollAuthenticatorOptions` — same shape\n * the consumer would pass to `db.enrollAuthenticator` for a fresh\n * enrollment. Hub persists the result atomically with the rotation.\n */\nexport type SlotRewrapCeremony = (\n ctx: SlotRewrapContext,\n) => Promise<EnrollAuthenticatorOptions>\n\n/** Caller payload for {@link rotateSecret}. */\nexport interface RotateSecretInput {\n /**\n * The secret currently opening the keyring — a string for a standard\n * keyring, the 3-part {@link EchoSecretParts} for an echo one. The shape\n * must MATCH the stored keyring: `deriveKekForKeyring` refuses a string on\n * an echo keyring (`EchoCeremonyRequiredError`, AG-1) and parts on a\n * standard one (`ValidationError`).\n */\n readonly oldSecret: string | EchoSecretParts\n /**\n * The secret to rotate TO. Its shape — independent of `oldSecret`'s —\n * decides the keyring's mode after rotation: parts enroll an `echo` block\n * (standard→echo upgrade, or a re-mint on echo→echo), a string removes any\n * existing one (echo→standard downgrade).\n */\n readonly newSecret: string | EchoSecretParts\n /**\n * Strength-policy knobs applied when `newSecret` is a STRING. Ignored\n * for 3-part {@link EchoSecretParts} — see `echoSecretPolicy` for the\n * parts-path counterpart.\n */\n readonly secretPolicy?: SecretPolicy\n /**\n * Strength-policy knobs applied when `newSecret` is 3-part\n * {@link EchoSecretParts} (prompt / combined floors). Ignored for a\n * string `newSecret` — see `secretPolicy` for the string-path\n * counterpart.\n */\n readonly echoSecretPolicy?: EchoSecretPolicy\n readonly allowWeakSecret?: boolean\n /**\n * Echo-enrollment knobs for the block minted when `newSecret` is 3-part.\n * Ignored for a string `newSecret` (no block is written).\n */\n readonly echoOptions?: RotateEchoOptions\n /**\n * Explicit confirmation for an echo→standard rotation. When the stored\n * keyring has an `echo` block AND `newSecret` is a plain string, the\n * rotation is a security-posture downgrade — and that shape is decided\n * purely by argument shape, so it must be opted into explicitly. Without\n * this flag such a rotation throws `ValidationError` BEFORE any write\n * (and before deriving the new KEK). Upgrades (string→parts) and\n * same-mode rotations (string→string, parts→parts) never require it.\n */\n readonly allowModeDowngrade?: boolean\n /**\n * Map of slot id → re-enrolment ceremony. Slots whose id appears\n * here are PRESERVED across rotation (the ceremony re-derives the\n * method-specific wrapping under the new keyring); slots whose id\n * is absent are DROPPED (the pre-slot-ceremony behavior).\n *\n * Without this map, `rotateSecret` wipes every tier-2 slot. Consumers building a\n * \"rotate without losing my biometric\" flow supply ceremonies for\n * each slot they want to keep.\n *\n * If a ceremony throws, the entire rotation throws — no partial\n * state. Callers wrap individual ceremonies in try/catch + return\n * a sentinel if they want graceful degradation per slot.\n *\n * Added when slot-ceremony rewrapping landed.\n */\n readonly slotCeremonies?: { readonly [slotId: string]: SlotRewrapCeremony }\n}\n\n/**\n * Re-derive the user's KEK from `oldSecret`, rewrap every DEK\n * under a freshly-derived KEK from `newSecret`, and persist.\n *\n * Tier-2 authenticator slots are dropped UNLESS the caller supplies\n * a `slotCeremonies` map — each ceremony re-derives its\n * method-specific wrapping under the new keyring, and hub persists\n * the rewrapped slots atomically with the rotation. Slots whose id\n * isn't in the map are still dropped.\n *\n * Either secret may be a string or 3-part {@link EchoSecretParts}; the two\n * shapes are independent, so a rotation doubles as the standard↔echo\n * upgrade/downgrade ceremony (spec #940).\n *\n * @throws `InvalidKeyError` if `oldSecret` does not unwrap the keyring.\n * @throws `EchoCeremonyRequiredError` if `oldSecret` is a string but the\n * keyring is an echo keyring.\n * @throws `WeakSecretError` if `newSecret` fails the strength rule.\n * @throws `ValidationError` if a ceremony's result mismatches the\n * slot's id or method (anti-slot-swap guard).\n */\nexport async function rotateSecret(\n store: NoydbStore,\n vault: string,\n userId: string,\n input: RotateSecretInput,\n): Promise<UnlockedKeyring> {\n if (!input.allowWeakSecret) {\n assertStrongNewSecret(input.newSecret, input.secretPolicy, input.echoSecretPolicy)\n }\n\n const found = await readKeyringFile(store, vault, userId)\n if (!found) {\n throw new NoAccessError(`No keyring found for user \"${userId}\" in vault \"${vault}\".`)\n }\n const { file } = found\n const oldSalt = base64ToBuffer(file.salt)\n // Dispatches on the STORED keyring's mode, so a string presented to an echo\n // keyring (or parts to a standard one) is refused before any unwrap.\n const oldKek = await deriveKekForKeyring(file, input.oldSecret, oldSalt)\n\n // Unwrap every DEK with the OLD KEK first — this also validates the\n // secret (a bad KEK throws InvalidKeyError on the first unwrap).\n const deks = new Map<string, EnclaveKey>()\n for (const [coll, wrapped] of Object.entries(file.deks)) {\n deks.set(coll, await unwrapKey(wrapped, oldKek))\n }\n\n // #1096 — this function returns an `UnlockedKeyring` whose `role` and\n // `permissions` come from the plaintext header, so it must authenticate the\n // roster like `loadKeyring` does. Checked BEFORE anything is written: a\n // forged roster must be refused, not rewrapped under a fresh secret and\n // thereby blessed.\n await assertRosterAuthenticated(file, deks, userId)\n\n // Echo→standard is a security-posture downgrade decided by argument shape\n // alone — require explicit opt-in, and refuse before deriving the new KEK\n // or writing anything.\n if (file.echo !== undefined && typeof input.newSecret === 'string' && !input.allowModeDowngrade) {\n throw new ValidationError(\n 'Rotating an echo-secret keyring to a plain string secret is a security-posture downgrade — pass allowModeDowngrade: true to confirm.',\n )\n }\n\n const newSalt = generateSalt()\n const newKek = await deriveKekForNewSecret(input.newSecret, newSalt)\n\n // Rewrap with the new KEK.\n const wrappedDeks: Record<string, string> = {}\n for (const [coll, dek] of deks) {\n wrappedDeks[coll] = await wrapKey(dek, newKek)\n }\n\n // Slot rewrap. Without slotCeremonies, we drop every existing\n // slot. With a ceremony map, slots whose id appears in the map\n // are preserved; the rest are dropped.\n const oldSlots = file.authenticators ?? []\n const newSlots: KeyringAuthenticator[] = []\n if (input.slotCeremonies && oldSlots.length > 0) {\n for (const oldSlot of oldSlots) {\n const ceremony = input.slotCeremonies[oldSlot.id]\n if (!ceremony) continue // drop — not in slotCeremonies map\n\n const result = await ceremony({ newKek, newDeks: deks, oldSlot })\n\n // Anti-slot-swap guard. The ceremony MUST preserve identity —\n // a mismatch would let the consumer convert a webauthn slot to\n // a password slot mid-rotation, which would silently change\n // the security profile of the slot under cover of \"rotation.\"\n if (result.id !== oldSlot.id) {\n throw new ValidationError(\n `slotCeremonies['${oldSlot.id}'] returned id=\"${result.id}\". ` +\n 'The id must match the rotated slot — a ceremony cannot ' +\n 'change a slot\\'s identity.',\n )\n }\n if (result.method !== oldSlot.method) {\n throw new ValidationError(\n `slotCeremonies['${oldSlot.id}'] returned method=\"${result.method}\", ` +\n `expected \"${oldSlot.method}\". The method must match the rotated ` +\n 'slot — a ceremony cannot change the auth method (e.g. webauthn ' +\n '→ password) under cover of rotation.',\n )\n }\n // wrapKind absent on legacy slots / wrap-KEK enroll inputs; treat as 'kek'.\n const oldWrapKind = oldSlot.wrapKind ?? 'kek'\n const newWrapKind = result.wrapKind ?? 'kek'\n if (oldWrapKind !== newWrapKind) {\n throw new ValidationError(\n `slotCeremonies['${oldSlot.id}'] returned wrapKind=\"${newWrapKind}\", ` +\n `expected \"${oldWrapKind}\". The wrap format must match the rotated ` +\n 'slot — a ceremony cannot change the wrap shape (e.g. wrap-KEK → ' +\n 'wrap-DEKs) under cover of rotation, since that would silently ' +\n 'change the session tier produced at unlock.',\n )\n }\n\n // Build the persisted slot from the ceremony result. Mirrors\n // the same construction `enrollAuthenticator` does — wrap-DEKs\n // variants carry { wrapped_deks, iv }; wrap-KEK variants\n // carry { wrapped_kek }.\n const baseFields = {\n id: result.id,\n method: result.method,\n // Preserve original enrolled_at — rotation is rewrapping, not\n // re-enrollment. The slot's enrolment timestamp tracks when\n // the user originally added the slot, not when it was last\n // rewrapped. Forensics consumers reading enrolled_at are\n // tracking the slot's ORIGIN, not its CURRENT wrapping.\n enrolled_at: oldSlot.enrolled_at,\n enrolled_via_tier: result.enrolled_via_tier ?? oldSlot.enrolled_via_tier,\n meta: result.meta,\n } as const\n const newSlot: KeyringAuthenticator = result.wrapKind === 'deks'\n ? {\n ...baseFields,\n wrapKind: 'deks',\n wrapped_deks: result.wrapped_deks,\n iv: result.iv,\n }\n : {\n ...baseFields,\n wrapped_kek: result.wrapped_kek,\n }\n newSlots.push(newSlot)\n }\n }\n\n const canary = await mintKeyringCanary(newKek)\n // Drop the OLD echo block explicitly — a `{ ...file }` spread would carry it\n // across a downgrade and miss it on an upgrade. See echoFieldForNewSecret.\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- excluded from ...carried\n const { echo: _staleEcho, ...carried } = file\n // #1115/#1126 — RESTAMP. This is a read-modify-write of a file whose roster\n // tag now covers the DEK key SET, and the rebuild replaces `deks` wholesale.\n // Carrying the old tag across leaves the vault unopenable the moment the\n // recovered set differs from the one the tag was minted over, and fragile\n // even when it does not.\n const rebuilt = {\n ...carried,\n _noydb_keyring: NOYDB_KEYRING_VERSION,\n deks: wrappedDeks,\n salt: bufferToBase64(newSalt),\n authenticators: newSlots,\n canary,\n ...(await echoFieldForNewSecret(input.newSecret, input.echoOptions)),\n }\n // #1097 — stamped BEFORE the tag is minted, so it lands inside the\n // authenticated canonical and a store can neither edit nor strip it.\n const withEpoch = { ...rebuilt, roster_epoch: nextRosterEpoch(found?.file.roster_epoch) }\n const next: KeyringFile = {\n ...withEpoch,\n roster_tag: await mintRosterTag(withEpoch, requireRecoveredRosterKey(deks)),\n }\n\n await writeKeyringFile(store, vault, userId, next)\n\n return {\n userId: file.user_id,\n displayName: file.display_name,\n role: file.role,\n permissions: file.permissions,\n deks,\n kek: newKek,\n salt: newSalt,\n authenticators: newSlots,\n ...(file.export_capability !== undefined && { exportCapability: file.export_capability }),\n ...(file.import_capability !== undefined && { importCapability: file.import_capability }),\n }\n}\n\n/**\n * Caller payload for {@link recoverSecret}.\n *\n * `paper` and `shamir` are wired end-to-end.\n * The remaining two profiles (`multi-channel`, `admin-mediated`)\n * stay outside the union and throw\n * {@link RecoveryProfileNotImplementedError} at the runtime guard\n * when bypassed via `as unknown as RecoveryProof`.\n */\nexport type RecoveryProof =\n | { readonly profile: 'paper'; readonly payload: { readonly code: string } }\n | { readonly profile: 'shamir'; readonly payload: {\n /** Optional disambiguator when multiple Shamir entries are enrolled.\n * When omitted, hub tries each entry until one combines. */\n readonly entryId?: string\n /** K or more opaque share strings, as returned by `NoydbShamir.splitToShares`. */\n readonly shares: ReadonlyArray<string>\n } }\n\nexport interface RecoverSecretInput {\n /**\n * The secret to recover TO. As in {@link RotateSecretInput}, the shape\n * decides the recovered keyring's mode: 3-part {@link EchoSecretParts}\n * enroll a fresh (always PORTABLE) `echo` block, a string leaves the\n * keyring standard and removes any block the old one carried.\n *\n * Recovery deliberately has no `echoOptions` counterpart — a device seal\n * would bind the reveal blob to the device running the recovery, which is\n * not necessarily the device the user will unlock from. Seal it afterwards\n * with an explicit `rotateSecret`.\n *\n * A string `newSecret` on a previously-echo keyring performs the same\n * echo→standard downgrade as `rotateSecret`, but WITHOUT an\n * `allowModeDowngrade` gate — recovery is already a proof-gated reset\n * ceremony (a valid recovery code/shares), so the shape-alone check\n * `rotateSecret` needs is redundant here by design.\n */\n readonly newSecret: string | EchoSecretParts\n readonly recoveryProof: RecoveryProof\n /**\n * Strength-policy knobs applied when `newSecret` is a STRING. Ignored\n * for 3-part {@link EchoSecretParts} — see `echoSecretPolicy` for the\n * parts-path counterpart.\n */\n readonly secretPolicy?: SecretPolicy\n /**\n * Strength-policy knobs applied when `newSecret` is 3-part\n * {@link EchoSecretParts} (prompt / combined floors). Ignored for a\n * string `newSecret` — see `secretPolicy` for the string-path\n * counterpart.\n */\n readonly echoSecretPolicy?: EchoSecretPolicy\n readonly allowWeakSecret?: boolean\n /**\n * After a successful paper-recovery, replace ALL remaining recovery\n * entries with freshly-minted ones. Defaults to `true` (defensive).\n *\n * Rationale: the user just demonstrated they had access\n * to AT LEAST one code. The remaining codes from the same printed\n * sheet may also be compromised — photographed, leaked via a\n * screen-share slip, or in the hands of whoever stole the sheet.\n * Auto-rotation closes the window without requiring consumer action.\n *\n * Set to `false` to preserve the original behavior (only the matched\n * code is burned; the rest stay valid).\n *\n * Hub-side orchestration is non-atomic with the recovery itself:\n * if the rotation step fails after a successful burn, the user\n * falls back to the pre-rotation state (remaining codes still\n * valid). Strictly safer than the previous default — a failed\n * rotation degrades gracefully rather than leaving the vault\n * locked or codes dual-existing.\n */\n readonly rotateRemainingCodes?: boolean\n /**\n * Number of fresh codes to mint when `rotateRemainingCodes` is on.\n * Defaults to the count of remaining entries POST-burn (e.g. if\n * the user enrolled 8 originally and just consumed 1, defaults to\n * 7). Pass an explicit number to mint a different count — useful\n * when the consumer wants to refresh to a target N regardless of\n * how many were left.\n */\n readonly newCodeCount?: number\n /**\n * Override the default raw-code generator. The default is hub's\n * {@link generateULID} — uppercase Crockford-Base32, 26 chars,\n * passes through `normalizePaperCode` untouched.\n *\n * Pass `() => generateRawCode()` from `@noy-db/on-recovery` when\n * the consumer prefers the Base32 + checksum format with hyphenated\n * display. The `mintPaperRecoveryEntry` helper accepts any string —\n * the generator just needs to produce a high-entropy unique value.\n */\n readonly codeGenerator?: () => string\n}\n\n/**\n * Return shape of `db.recoverSecret`. `newCodes` is populated when\n * `rotateRemainingCodes` was enabled and at least one entry was\n * rotated; an empty array means no rotation happened (rotation\n * disabled, or no remaining codes after burn). Show the codes to the\n * user once — they are the canonical credential for future recovery\n * and CANNOT be retrieved again.\n */\nexport interface RecoverSecretResult {\n readonly newCodes: readonly string[]\n}\n\n/**\n * Input for {@link Noydb.rotateRecovery} — deliberate\n * recovery-credential regeneration when the user knows their\n * secret but wants a fresh sheet (paper) or fresh shares\n * (shamir). Symmetric to {@link RotateSecretInput}.\n */\nexport type RotateRecoveryOptions =\n | {\n readonly profile: 'paper'\n /** How many fresh codes to mint. Default: existing sheet size. */\n readonly count?: number\n /** Optional code generator — see {@link RecoverSecretInput.codeGenerator}. */\n readonly codeGenerator?: () => string\n }\n | {\n readonly profile: 'shamir'\n /** New threshold. */\n readonly k: number\n /** New total share count. */\n readonly n: number\n /** Disambiguator when multiple Shamir entries exist; required if there are 2+. */\n readonly entryId?: string\n /** Optional updated label. */\n readonly label?: string\n }\n\n/**\n * Result of {@link Noydb.rotateRecovery}. Shape varies by profile:\n *\n * - `paper` → `{ newCodes: string[] }` (and `entryId === 'paper-batch'`)\n * - `shamir` → `{ newShares: string[], entryId }`\n *\n * `newCodes` is populated for paper rotations; `newShares` for\n * Shamir rotations. Both are show-once — the hub does not\n * retain them.\n */\nexport interface RotateRecoveryResult {\n readonly newCodes?: readonly string[]\n readonly newShares?: readonly string[]\n readonly entryId?: string\n}\n\n/**\n * Result of {@link Noydb.enrollRecovery}. Shape varies by profile:\n *\n * - `paper` → `{ entryId: 'paper-batch' }` (caller minted the\n * entries; this is a sentinel since paper enrollments are batch-shaped).\n * - `shamir` → `{ entryId, shares: string[] }` — shares are\n * show-once; the hub does not retain them.\n */\nexport interface EnrollRecoveryResult {\n readonly entryId: string\n readonly shares?: readonly string[]\n}\n\n/**\n * Input shape for {@link Noydb.enrollRecovery} and\n * {@link Noydb.openVaultAndEnrollRecovery}. Discriminated\n * union over recovery profiles.\n *\n * - `paper`: caller pre-mints entries (typically via\n * `mintPaperRecoveryEntry` or `@noy-db/on-recovery`'s\n * `generateRecoveryCodeSet`) and passes them in. The hub stores\n * them and surfaces an opaque batch id.\n * - `shamir`: hub mints the recovery secret + the shares at\n * enrollment time. The shares are returned in\n * {@link EnrollRecoveryResult.shares} (show-once); the hub never\n * retains them.\n *\n * Multi-channel and admin-mediated will be added when the respective\n * dispatch slices ship.\n */\nexport type RecoveryEnrollmentInput =\n | { readonly profile: 'paper'; readonly entries: ReadonlyArray<PaperRecoveryEntry> }\n | {\n readonly profile: 'shamir'\n readonly k: number\n readonly n: number\n readonly label?: string\n readonly entryId?: string\n }\n\n/**\n * Reset the user's secret using a recovery proof.\n * Supports `'paper'` and `'shamir'` profiles. The other profiles throw\n * {@link RecoveryProfileNotImplementedError}.\n *\n * On success, the used recovery entry is burned (deleted from the\n * stored set).\n */\nexport async function recoverSecret(\n provider: NoydbShamir | undefined,\n store: NoydbStore,\n vault: string,\n userId: string,\n input: RecoverSecretInput,\n): Promise<UnlockedKeyring> {\n if (!input.allowWeakSecret) {\n assertStrongNewSecret(input.newSecret, input.secretPolicy, input.echoSecretPolicy)\n }\n\n // Runtime defense-in-depth: the type narrows to 'paper' | 'shamir',\n // but a consumer bypassing TS via\n // `as unknown as RecoveryProof` should still hit a clear error\n // rather than silently fall into a handler with a malformed payload.\n const profile = (input.recoveryProof as { profile: string }).profile\n if (profile === 'paper') {\n return recoverViaPaperCode(store, vault, userId, input)\n }\n if (profile === 'shamir') {\n return recoverViaShamir(provider, store, vault, userId, input)\n }\n throw new RecoveryProfileNotImplementedError(\n profile,\n 'https://github.com/noy-db/core/issues/196',\n )\n}\n\nasync function recoverViaPaperCode(\n store: NoydbStore,\n vault: string,\n userId: string,\n input: RecoverSecretInput,\n): Promise<UnlockedKeyring> {\n if (input.recoveryProof.profile !== 'paper') throw new Error('unreachable')\n const { code } = input.recoveryProof.payload\n\n const found = await readKeyringFile(store, vault, userId)\n if (!found) {\n throw new NoAccessError(`No keyring found for user \"${userId}\" in vault \"${vault}\".`)\n }\n const { file } = found\n\n const entries = await loadPaperRecoveryEntries(store, vault)\n if (entries.length === 0) {\n throw new NoAccessError(\n `No paper-recovery entries enrolled for vault \"${vault}\". ` +\n 'Enroll via `db.team.enrollRecovery({ profile: \"paper\", entries })` before relying on recovery.',\n )\n }\n\n const normalized = normalizePaperCode(code)\n let recovered: { deks: Map<string, EnclaveKey>; entry: PaperRecoveryEntry } | undefined\n for (const entry of entries) {\n try {\n const deks = await unwrapDeksFromPaperEntry(entry, normalized)\n recovered = { deks, entry }\n break\n } catch {\n // wrong code for this entry — try the next one\n }\n }\n if (!recovered) {\n throw new InvalidKeyError(\n 'Recovery code does not match any enrolled paper entry. The code may have been ' +\n 'previously used (single-use) or typed incorrectly.',\n )\n }\n\n const deks = recovered.deks\n\n // #1096 — same as the rotateSecret path: the returned keyring's authority\n // comes from the plaintext header. The roster key rides the paper entry's\n // wrapped-DEK blob, so it is in reach here even though no KEK ever is.\n await assertRosterAuthenticated(file, deks, userId)\n\n // Fresh salt + KEK from the new secret, rewrap.\n const newSalt = generateSalt()\n const newKek = await deriveKekForNewSecret(input.newSecret, newSalt)\n const wrappedDeks: Record<string, string> = {}\n for (const [coll, dek] of deks) {\n wrappedDeks[coll] = await wrapKey(dek, newKek)\n }\n\n const canary = await mintKeyringCanary(newKek)\n // Explicit rebuild — the old echo block must not ride the spread. See\n // echoFieldForNewSecret.\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- excluded from ...carried\n const { echo: _staleEcho, ...carried } = file\n // #1115/#1126 — RESTAMP, same reason as the other rebuild sites.\n const rebuilt = {\n ...carried,\n _noydb_keyring: NOYDB_KEYRING_VERSION,\n deks: wrappedDeks,\n salt: bufferToBase64(newSalt),\n authenticators: [], // tier-2 slots wrap old KEK, drop them\n canary,\n ...(await echoFieldForNewSecret(input.newSecret)),\n }\n // #1097 — stamped BEFORE the tag is minted, so it lands inside the\n // authenticated canonical and a store can neither edit nor strip it.\n const withEpoch = { ...rebuilt, roster_epoch: nextRosterEpoch(found?.file.roster_epoch) }\n const next: KeyringFile = {\n ...withEpoch,\n roster_tag: await mintRosterTag(withEpoch, requireRecoveredRosterKey(deks)),\n }\n\n // Burn first, then rewrite the keyring. The two writes are not\n // atomic — if the second fails, the safer ordering is:\n //\n // 1. Code burned, keyring untouched: user keeps their old secret\n // and loses one recovery code (recoverable: contact admin / use\n // another code).\n //\n // 2. Keyring rewritten, code unburned: user has rotated, but the\n // consumed code REMAINS VALID. Anyone with access to the paper\n // sheet can use it again. Security regression.\n //\n // Burning first picks (1) over (2).\n await burnPaperRecoveryEntry(store, vault, recovered.entry.codeId)\n await writeKeyringFile(store, vault, userId, next)\n\n return {\n userId: file.user_id,\n displayName: file.display_name,\n role: file.role,\n permissions: file.permissions,\n deks,\n kek: newKek,\n salt: newSalt,\n authenticators: [],\n ...(file.export_capability !== undefined && { exportCapability: file.export_capability }),\n ...(file.import_capability !== undefined && { importCapability: file.import_capability }),\n }\n}\n\n/**\n * Mirror of `@noy-db/on-recovery/parseRecoveryCode`. Inlined so the\n * hub does not gain a peer dep on on-recovery — both implementations\n * follow the same RFC 4648 Base32 + checksum format and round-trip\n * through the same KDF.\n *\n * Accepts hyphenated, lowercase, or whitespace-padded input.\n */\nfunction normalizePaperCode(input: string): string {\n return input.toUpperCase().replace(/[\\s\\-_]/g, '')\n}\n\n/**\n * Recover the user's keyring via the Shamir profile.\n *\n * 1. Decode each supplied share string into a {@link RawShare}.\n * 2. Load `_meta/recovery-shamir` entries.\n * 3. If `payload.entryId` is supplied, restrict to that entry; else\n * iterate over all entries and try each until one combines.\n * 4. For each candidate: filter shares to those whose `(k, n)`\n * match the entry's parameters, then attempt\n * `unwrapDeksFromShamirEntry`. AES-GCM auth-tag failure means\n * the combined secret doesn't match — try the next entry.\n * 5. With unwrapped DEKs: derive fresh KEK from `newSecret` +\n * fresh salt, rewrap, write the keyring.\n * 6. Shamir entries are NOT burned on recovery (shares reusable);\n * explicit {@link Noydb.rotateRecovery} is the refresh ceremony.\n */\nasync function recoverViaShamir(\n provider: NoydbShamir | undefined,\n store: NoydbStore,\n vault: string,\n userId: string,\n input: RecoverSecretInput,\n): Promise<UnlockedKeyring> {\n if (input.recoveryProof.profile !== 'shamir') throw new Error('unreachable')\n const { entryId: requestedEntryId, shares: shareStrings } = input.recoveryProof.payload\n\n if (shareStrings.length === 0) {\n throw new ValidationError(\n 'Shamir recovery requires at least one share; received an empty array.',\n )\n }\n\n const found = await readKeyringFile(store, vault, userId)\n if (!found) {\n throw new NoAccessError(`No keyring found for user \"${userId}\" in vault \"${vault}\".`)\n }\n const { file } = found\n\n const allEntries = await loadShamirRecoveryEntries(store, vault)\n if (allEntries.length === 0) {\n throw new NoAccessError(\n `No Shamir-recovery entries enrolled for vault \"${vault}\". `\n + 'Enroll via `db.team.enrollRecovery({ profile: \"shamir\", k, n })` before relying on recovery.',\n )\n }\n\n if (!provider) {\n throw new Error(\n \"shamir recovery requires a NoydbShamir — pass \"\n + \"shamirRecovery: shamirRecoveryProvider() from '@noy-db/on-shamir' to createNoydb()\",\n )\n }\n\n // Restrict to a specific entry when entryId supplied.\n let candidates: ReadonlyArray<ShamirRecoveryEntry>\n if (requestedEntryId !== undefined) {\n candidates = allEntries.filter(e => e.entryId === requestedEntryId)\n if (candidates.length === 0) {\n throw new NoAccessError(\n `No Shamir-recovery entry with entryId=\"${requestedEntryId}\" found `\n + `in vault \"${vault}\". Available entries: `\n + allEntries.map(e => `\"${e.entryId}\"`).join(', '),\n )\n }\n } else {\n candidates = allEntries\n }\n\n // Try each candidate entry. Pass all share strings to the provider;\n // provider.combineShares validates and throws on mismatch — the\n // AES-GCM auth-tag is an additional guard.\n let recoveredDeks: Map<string, EnclaveKey> | undefined\n for (const entry of candidates) {\n if (shareStrings.length < entry.k) {\n // Not enough shares for this entry — could still match another.\n continue\n }\n try {\n const deks = await unwrapDeksFromShamirEntry(provider, entry, shareStrings)\n recoveredDeks = deks\n break\n } catch {\n // provider.combineShares threw (malformed/mismatched shares) or\n // AES-GCM auth-tag failure → try the next entry.\n }\n }\n\n if (!recoveredDeks) {\n // Distinguish \"below-threshold\" from \"no entry matches\" so the\n // error message is actionable.\n const minK = Math.min(...candidates.map(e => e.k))\n if (shareStrings.length < minK) {\n throw new InvalidKeyError(\n `Insufficient Shamir shares to combine: the smallest enrolled threshold is ${minK}, `\n + `but only ${shareStrings.length} share${shareStrings.length === 1 ? ' was' : 's were'} provided.`,\n )\n }\n throw new InvalidKeyError(\n 'Shamir shares do not match any enrolled entry. Possible causes: '\n + 'shares were tampered with, came from a different enrollment, '\n + 'or the entry was rotated after these shares were distributed.',\n )\n }\n\n // #1096 — mirrors paper: authenticate the roster before rewrapping it under\n // a fresh secret. The roster key rides the Shamir entry's wrapped-DEK blob.\n await assertRosterAuthenticated(file, recoveredDeks, userId)\n\n // Mint fresh KEK from new secret, rewrap DEKs (mirrors paper).\n const newSalt = generateSalt()\n const newKek = await deriveKekForNewSecret(input.newSecret, newSalt)\n const wrappedDeks: Record<string, string> = {}\n for (const [coll, dek] of recoveredDeks) {\n wrappedDeks[coll] = await wrapKey(dek, newKek)\n }\n\n const canary = await mintKeyringCanary(newKek)\n // Explicit rebuild — the old echo block must not ride the spread. See\n // echoFieldForNewSecret.\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- excluded from ...carried\n const { echo: _staleEcho, ...carried } = file\n // #1115/#1126 — RESTAMP, same reason as the paper path above.\n const rebuilt = {\n ...carried,\n _noydb_keyring: NOYDB_KEYRING_VERSION,\n deks: wrappedDeks,\n salt: bufferToBase64(newSalt),\n authenticators: [], // tier-2 slots wrap old KEK, drop them on recovery\n canary,\n ...(await echoFieldForNewSecret(input.newSecret)),\n }\n // #1097 — stamped BEFORE the tag is minted, so it lands inside the\n // authenticated canonical and a store can neither edit nor strip it.\n const withEpoch = { ...rebuilt, roster_epoch: nextRosterEpoch(file.roster_epoch) }\n const next: KeyringFile = {\n ...withEpoch,\n roster_tag: await mintRosterTag(withEpoch, requireRecoveredRosterKey(recoveredDeks)),\n }\n\n // No burn: Shamir entries persist across recoveries. Explicit\n // rotateRecovery is the refresh ceremony.\n await writeKeyringFile(store, vault, userId, next)\n\n return {\n userId: file.user_id,\n displayName: file.display_name,\n role: file.role,\n permissions: file.permissions,\n deks: recoveredDeks,\n kek: newKek,\n salt: newSalt,\n authenticators: [],\n ...(file.export_capability !== undefined && { exportCapability: file.export_capability }),\n ...(file.import_capability !== undefined && { importCapability: file.import_capability }),\n }\n}\n\nasync function writeKeyringFile(\n store: NoydbStore,\n vault: string,\n userId: string,\n file: KeyringFile,\n): Promise<void> {\n // #1097 — no keyring may be WRITTEN without a roster epoch.\n //\n // Asserted on the OUTPUT rather than at each mint site. The epoch has to be\n // set BEFORE `mintRosterTag` so it lands inside the authenticated canonical,\n // and thirteen places build an authority object. Enumerating them by grep is\n // how one gets missed — and a missed site writes a file the replay check\n // cannot anchor: a field that ships empty and stays empty, inert AND\n // indistinguishable from \"nothing to report\".\n if (file.roster_epoch === undefined) {\n throw new Error(\n `writeKeyringFile: refusing to write a keyring for \"${userId}\" with no roster_epoch (#1097). ` +\n 'Stamp it with nextRosterEpoch(previous) onto the authority object BEFORE minting the roster ' +\n 'tag, so it is covered by rosterCanonical.',\n )\n }\n const envelope = buildRecordEnvelope(\n { collection: '_keyring', id: userId, version: 1 },\n { iv: '', data: JSON.stringify(file) },\n )\n await store.put(vault, '_keyring', userId, envelope)\n}\n\n/**\n * The roster key out of a RECOVERED DEK map. Recovery rebuilds the keyring file\n * from scratch, so it must restamp — and it can, because the roster key travels\n * inside the recovered set like any other `_`-prefixed slot.\n *\n * Throws rather than skipping the restamp: a recovery that cannot produce a\n * verifiable tag has produced a file nobody can open, and failing here says so\n * instead of writing it.\n */\nfunction requireRecoveredRosterKey(deks: Map<string, EnclaveKey>): EnclaveKey {\n const key = deks.get(ROSTER_KEY_ID)\n if (!key) {\n throw new ValidationError(\n `recoverSecret: the recovered DEK set carries no \"${ROSTER_KEY_ID}\" entry, so the rebuilt ` +\n 'keyring could not be re-signed. The recovery material predates the authenticated roster ' +\n 'and the vault must be re-seeded.',\n )\n }\n return key\n}\n","/**\n * Atomic peer-recovery primitive.\n *\n * `recoverUser` is a SEPARATE operation from `revoke + grant`. It\n * exists because peer-recovery has different semantics than account\n * removal-then-reissue:\n *\n * 1. **Same identity preserved.** `userId`, `role`, `permissions`,\n * capability bits, user envelope (if any), policy override (if\n * any) all survive. Only the wrapping changes.\n * 2. **No key rotation.** The existing DEKs stay valid — every\n * OTHER principal in the vault keeps their access. Rotating\n * keys would invalidate every co-user's wrapping.\n * 3. **Atomic by construction.** A single `store.put` overwrites\n * `_keyring/<userId>` with the recovered file. No revoke step\n * means no partial-failure window.\n * 4. **Owner→owner natively allowed.** Two co-owners recovering\n * each other is the explicitly-intentional case (a partner\n * forgot the master phrase). The existing `canRevoke` rule that\n * blocks owner→owner is correct for `revoke` (which is account\n * *removal*) and intentionally NOT replicated here. The policy\n * gate `peer-recover-user` carries the freshness requirement.\n * 5. **Tier-2 slots dropped.** The slots wrap the OLD KEK under\n * method-derived keys; after recovery the KEK is re-derived\n * from the new temp secret. Match `rotateSecret`'s\n * precedent — the recovered user re-enrols slots after picking\n * their own phrase.\n *\n * Caller must be at least as privileged as the target. The hub\n * `db.recoverUser` method gates this with the `peer-recover-user`\n * policy gate (the `peer-recover-user` factor-proof requirement); the function below\n * enforces only the role + anti-privilege-escalation invariants.\n *\n * @module\n */\nimport type { NoydbStore, KeyringFile, Role } from '../../kernel/types.js'\nimport { NOYDB_KEYRING_VERSION } from '../../kernel/types.js'\nimport { buildRecordEnvelope, deriveKey, generateSalt, wrapKey, bufferToBase64 } from '../../capsule/index.js'\nimport { NoAccessError, PermissionDeniedError, PrivilegeEscalationError } from '../../kernel/errors.js'\nimport { assertStrongSecret, type SecretPolicy } from '../../kernel/validation.js'\nimport type { UnlockedKeyring } from './keyring.js'\nimport { mintKeyringCanary, readKeyringFile, requireRosterKey } from './keyring.js'\nimport { mintRosterTag, assertRosterTagValid } from './roster-tag.js'\nimport { nextRosterEpoch } from './roster-epoch.js'\n\n// FR-6: 'custodian' is deliberately ABSENT — an admin cannot peer-recover a\n// custodian (mirrors ADMIN_GRANTABLE_TARGETS: custodians are owner-managed\n// only). The owner CAN recover one (canRecover returns true for owner). As a\n// CALLER, a custodian recovers nobody (it is neither owner nor admin → false).\nconst ADMIN_RECOVERABLE_TARGETS: readonly Role[] = ['operator', 'viewer', 'client', 'admin']\n\n/**\n * Whether `callerRole` may recover `targetRole`.\n *\n * Differs from `canRevoke` (in `keyring.ts`) in one critical place:\n * **owner→owner IS allowed**. Peer recovery is the explicitly\n * intentional case (a co-owner forgot their phrase); the freshness\n * binding lives in the `peer-recover-user` policy gate, not in the\n * permission predicate.\n *\n * Admins can recover everyone they could grant (operator / viewer /\n * client / admin) but NOT owners — that boundary stays as a hard\n * structural rule even under recovery.\n */\nfunction canRecover(callerRole: Role, targetRole: Role): boolean {\n if (callerRole === 'owner') return true\n if (callerRole === 'admin') return ADMIN_RECOVERABLE_TARGETS.includes(targetRole)\n return false\n}\n\n/** Input shape for {@link recoverUser}. */\nexport interface RecoverUserOptions {\n /** Target user id whose keyring is being recovered. */\n readonly userId: string\n /**\n * Temporary secret under which the new keyring is wrapped.\n * The recipient should call `db.rotateSecret` immediately on\n * acceptance to choose their own phrase — this temp acts as a\n * single-use bridge in invite / peer-recovery flows.\n */\n readonly secret: string\n /** Override the target's role. Defaults to the existing target's role. */\n readonly role?: Role\n /** Override the target's display name. Defaults to existing. */\n readonly displayName?: string\n /** Validate phrase strength against the configured policy. */\n readonly validateSecret?: boolean\n /**\n * Skip phrase strength validation even when `validateSecret` is\n * set. The escape hatch matches `grant`'s shape — used when the\n * temp phrase is a high-entropy one-shot string that doesn't need\n * to satisfy the human-typeable rules.\n */\n readonly allowWeakSecret?: boolean\n /**\n * Optional explicit phrase policy override (passed through to\n * `assertStrongSecret`). Mirrors how `grant` accepts a custom\n * `SecretPolicy` for app-specific tightening.\n */\n readonly secretPolicy?: SecretPolicy\n}\n\n/**\n * Atomically rewrap the target user's keyring under a fresh temp\n * secret. Single store write; no revoke step; no key rotation.\n *\n * Caller's responsibilities (NOT enforced here):\n * - Run the `peer-recover-user` policy gate first via\n * `Noydb.checkGate` to enforce the freshness factor proof.\n * - Communicate the temp secret to the recipient via a secure\n * channel (URL fragment, in-person, etc.) — the hub does not\n * transport secrets.\n */\nexport async function recoverUser(\n store: NoydbStore,\n vault: string,\n callerKeyring: UnlockedKeyring,\n options: RecoverUserOptions,\n): Promise<void> {\n // 1. Load the target's existing keyring file (plaintext header).\n const found = await readKeyringFile(store, vault, options.userId)\n if (!found) {\n throw new NoAccessError(\n `recoverUser: user \"${options.userId}\" has no keyring in vault \"${vault}\".`,\n )\n }\n const target = found.file\n\n // #1096 — verify BEFORE anything reads this file, because the very next line\n // adopts `target.role` as the recovered role. The attack is self-triggering:\n // a store forges a member's role, that member is locked out at load, and the\n // admin's natural remedy IS recovery — which without this check would rewrap\n // the forged authority under a fresh secret and stamp it genuine.\n const rosterKey = requireRosterKey(callerKeyring, 'recoverUser')\n await assertRosterTagValid(target, rosterKey, options.userId)\n\n const targetRole = options.role ?? target.role\n\n // 2. Permission check — caller must be allowed to recover this role.\n // Owner→owner natively allowed; admin→admin allowed; admin→owner blocked.\n if (!canRecover(callerKeyring.role, targetRole)) {\n throw new PermissionDeniedError(\n `Role \"${callerKeyring.role}\" cannot recover role \"${targetRole}\"`,\n )\n }\n // Also guard against role-uplift via the override — admin cannot\n // promote a target to owner under cover of recovery.\n if (!canRecover(callerKeyring.role, target.role)) {\n throw new PermissionDeniedError(\n `Role \"${callerKeyring.role}\" cannot recover role \"${target.role}\"`,\n )\n }\n\n // 3. Anti-privilege-escalation. Every collection the target had\n // access to must be in the caller's DEK set — the recoverer\n // cannot give the recovered user access to a collection the\n // recoverer themselves can't read. Mirrors `grant()`'s check.\n for (const coll of Object.keys(target.deks)) {\n if (!callerKeyring.deks.has(coll)) {\n throw new PrivilegeEscalationError(coll)\n }\n }\n\n // 4. Optional phrase strength validation (mirrors `grant` opt-in).\n if (options.validateSecret && !options.allowWeakSecret) {\n assertStrongSecret(options.secret, options.secretPolicy)\n }\n\n\n // 5. Mint a fresh salt + KEK from the temp secret. The DEKs\n // themselves are unchanged — only the wrapping is replaced.\n const newSalt = generateSalt()\n const newKek = await deriveKey(options.secret, newSalt)\n\n const wrappedDeks: Record<string, string> = {}\n for (const coll of Object.keys(target.deks)) {\n const callerDek = callerKeyring.deks.get(coll)\n if (!callerDek) {\n // Already caught by the anti-privilege-escalation loop above.\n // This branch is defensive belt-and-braces; if it ever fires,\n // the target had a collection the caller's deks Map disagrees\n // with — fail loud rather than silently dropping access.\n throw new PrivilegeEscalationError(coll)\n }\n wrappedDeks[coll] = await wrapKey(callerDek, newKek)\n }\n\n // 6. Build the recovered keyring file. Identity preserved; wrapping\n // refreshed; tier-2 slots dropped (they wrap the OLD KEK and\n // can't survive a tier-1 phrase change — same precedent as\n // rotateSecret). Mint a fresh canary under newKek; the\n // OLD canary on the spread `...target` would fail to verify against\n // the new KEK and trip KeyringCorruptError on next load.\n //\n // An echo block on the target is dropped for the same reason\n // (spec #940): `options.secret` is a STRING, so the rewrapped DEKs sit\n // under a string-derived KEK. Carrying the block over would make\n // `deriveKekForKeyring` demand the echo ceremony and derive via the echo\n // KDF — bricking the keyring for old parts AND new temp alike. The\n // recovered user therefore gets a STANDARD keyring; picking the echo\n // shape back up is their own `rotateSecret` away (peer recovery is a\n // single-use bridge, and widening `RecoverUserOptions.secret` to 3-part\n // parts would mean transporting three secrets over the out-of-band\n // channel — deliberately out of scope).\n const canary = await mintKeyringCanary(newKek)\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- excluded from ...carried\n const { echo: _staleEcho, ...carried } = target\n const edited: KeyringFile = {\n ...carried,\n _noydb_keyring: NOYDB_KEYRING_VERSION,\n role: targetRole,\n display_name: options.displayName ?? target.display_name,\n deks: wrappedDeks,\n salt: bufferToBase64(newSalt),\n granted_by: callerKeyring.userId,\n authenticators: [],\n canary,\n }\n // #1097 — stamped BEFORE the tag is minted, so it lands inside the\n // authenticated canonical and a store can neither edit nor strip it.\n const withEpoch = { ...edited, roster_epoch: nextRosterEpoch(target.roster_epoch) }\n // #1096 — a restamp is REQUIRED here, not merely uniform: `granted_by`\n // becomes the recoverer and `role` may change, so the carried tag no longer\n // describes the file. The recovered user's own copy of the roster key was\n // re-wrapped into `wrappedDeks` above (it is one of the target's DEKs), so\n // they can verify this on first unlock with the temp secret.\n const next: KeyringFile = {\n ...withEpoch,\n roster_tag: await mintRosterTag(withEpoch, rosterKey),\n }\n\n // 7. Single atomic write — overwrites the existing envelope.\n // Backend `put` is the canonical write primitive across every\n // `to-*` store; no partial-failure window between revoke + grant.\n const envelope = buildRecordEnvelope(\n { collection: '_keyring', id: options.userId, version: 1 },\n { iv: '', data: JSON.stringify(next) },\n )\n await store.put(vault, '_keyring', options.userId, envelope)\n}\n","/**\n * Authentication introspection.\n *\n * Three surfaces over the configured tier model and the actual\n * per-user enrollment state:\n *\n * 1. **Vault-wide English summary** — {@link describeAuthConfig}.\n * 2. **Vault-wide Mermaid diagram** — {@link diagramAuthConfig}.\n * 3. **Per-user introspection** — {@link describeUserAuth}, gated by\n * the `view-user-auth` policy gate (off by default).\n *\n * The per-user surface is held to a strict allowlist — fields not on\n * the allowlist are dropped, never rendered. The negative test in\n * `auth-introspection.test.ts` exercises the allowlist by feeding a\n * contrived keyring with fake \"secret\" fields and asserting that none\n * of them appear in the output.\n *\n * @module\n */\nimport type { NoydbStore, KeyringFile, KeyringAuthenticator, VaultPolicy, GatePolicy } from '../../kernel/types.js'\nimport { loadVaultPolicy } from '../policy/storage.js'\nimport { loadPaperRecoveryEntries } from '../team/recovery.js'\n\n/** Vault-wide English summary of the configured authentication graph. */\nexport async function describeAuthConfig(\n store: NoydbStore,\n vault: string,\n): Promise<string> {\n const policy = (await loadVaultPolicy(store, vault)) ?? defaultPolicySnapshot()\n const recoveryProfiles = await listRecoveryProfilesEnrolled(store, vault)\n\n const lines: string[] = []\n lines.push(`Vault \"${vault}\" — three-tier authentication`)\n lines.push('')\n lines.push('Tier 1 — Secret (master)')\n lines.push(` Phrase format: ${policy.secret?.minWords ?? 6}+ words, lowercase letters, ≥${policy.secret?.minWordLength ?? 3} chars/word`)\n lines.push(' Strength validator: enforced (override available for tests only)')\n lines.push('')\n lines.push('Tier 2 — Authenticate (routine login)')\n lines.push(' Allowed methods: WebAuthn (passkey), OIDC, Password')\n lines.push(' Slots per user: unlimited')\n lines.push('')\n lines.push('Tier 3 — Unlock (quick resume)')\n lines.push(' Method: PIN (per-app configurable)')\n lines.push('')\n lines.push(`Recovery profiles enrolled: ${recoveryProfiles.length === 0 ? 'none' : recoveryProfiles.join(', ')}`)\n lines.push('Managed-secret mode: off (post-1.0)')\n lines.push('')\n lines.push('Sensitive-action gates:')\n for (const [gate, gp] of Object.entries(policy.gates) as Array<[string, GatePolicy]>) {\n lines.push(` ${gate} — ${describeGatePolicy(gp)}`)\n }\n return lines.join('\\n')\n}\n\n/**\n * Render the vault's auth graph as Mermaid `flowchart TB` source. The\n * caller pipes this through Mermaid (CLI or browser) to get an SVG.\n */\nexport async function diagramAuthConfig(\n store: NoydbStore,\n vault: string,\n): Promise<string> {\n const policy = (await loadVaultPolicy(store, vault)) ?? defaultPolicySnapshot()\n const lines: string[] = []\n lines.push('flowchart TB')\n lines.push(` vault[\"Vault: ${escapeMermaid(vault)}\"]`)\n lines.push(' tier1[\"Tier 1<br/>Secret\"]')\n lines.push(' tier2[\"Tier 2<br/>Multi-slot Authenticate\"]')\n lines.push(' tier3[\"Tier 3<br/>PIN / Quick-resume\"]')\n lines.push(' vault --> tier1')\n lines.push(' tier1 --> tier2')\n lines.push(' tier2 --> tier3')\n for (const [gateName, gp] of Object.entries(policy.gates) as Array<[string, GatePolicy]>) {\n if (gp.enabled === false) continue\n const id = sanitizeId(gateName)\n const label = `${gateName}<br/>tier ≥ ${gp.minTier}`\n lines.push(` ${id}[\"${escapeMermaid(label)}\"]`)\n const tierNode = gp.minTier === 1 ? 'tier1' : gp.minTier === 2 ? 'tier2' : 'tier3'\n lines.push(` ${tierNode} --> ${id}`)\n }\n return lines.join('\\n')\n}\n\n/**\n * Render the per-user enrollment summary. Returns an empty\n * (non-throwing) string when the user has no keyring file — never\n * confirms or denies the existence of the user from the document\n * alone.\n *\n * Sanitization is strict: only the slot list, enrollment dates, and\n * recovery-profile counts are rendered. WebAuthn cred ids, OIDC\n * subject ids, password hashes, recovery codes, TOTP secrets — all\n * dropped at the allowlist boundary, not redacted.\n */\nexport async function describeUserAuth(\n store: NoydbStore,\n vault: string,\n userId: string,\n): Promise<string> {\n const env = await store.get(vault, '_keyring', userId)\n if (!env) return ''\n // #1096 — DELIBERATELY UNVERIFIED. This is a key-free plaintext-header\n // reader (no KEK, no DEKs, so no roster key to verify a `roster_tag` with).\n // The `role` reported here is therefore what the STORE says, not what an\n // authorised editor wrote. Safe because this is descriptive output, never an\n // authorisation input — do not let it become one.\n const file = JSON.parse(env._data ?? '') as KeyringFile\n\n const lines: string[] = []\n lines.push(\n `User: ${file.user_id} (joined ${file.created_at.slice(0, 10)}, role: ${file.role})`,\n )\n lines.push('')\n lines.push('Tier 2 enrollments:')\n if (!file.authenticators || file.authenticators.length === 0) {\n lines.push(' (none enrolled)')\n } else {\n for (const slot of file.authenticators) {\n lines.push(` - ${describeSlot(slot)}`)\n }\n }\n return lines.join('\\n')\n}\n\n/** Bulk variant for owner dashboards. */\nexport async function describeAllUsersAuth(\n store: NoydbStore,\n vault: string,\n): Promise<Array<{ userId: string; description: string }>> {\n const ids = await store.list(vault, '_keyring')\n const results: Array<{ userId: string; description: string }> = []\n for (const userId of ids) {\n const description = await describeUserAuth(store, vault, userId)\n if (description !== '') results.push({ userId, description })\n }\n return results\n}\n\n// ─── Helpers ───────────────────────────────────────────────────────────\n\nconst SLOT_FIELD_ALLOWLIST: ReadonlyArray<keyof KeyringAuthenticator> = [\n 'id',\n 'method',\n 'enrolled_at',\n 'enrolled_via_tier',\n] as const\n\nfunction describeSlot(slot: KeyringAuthenticator): string {\n // Project to the allowlist FIRST — never read meta/wrapped_kek into\n // any user-facing string. The allowlist is the only path values\n // take to the renderer; off-allowlist fields are dropped, not redacted.\n const sanitized: Partial<KeyringAuthenticator> = {}\n for (const key of SLOT_FIELD_ALLOWLIST) {\n if (key in slot) {\n // @ts-expect-error narrow assignment from allowlist iteration\n sanitized[key] = slot[key]\n }\n }\n const date = (sanitized.enrolled_at ?? '').slice(0, 10)\n return `${sanitized.method ?? '?'} (id=${sanitized.id ?? '?'}, enrolled ${date}, via tier ${sanitized.enrolled_via_tier ?? '?'})`\n}\n\nfunction describeGatePolicy(gp: GatePolicy): string {\n if (gp.enabled === false) return 'disabled'\n const parts: string[] = []\n parts.push(`tier ${gp.minTier}`)\n if (gp.factors && gp.factors.length > 0) {\n for (const f of gp.factors) {\n parts.push(`+ ${f.count ?? 1}× ${f.anyOf.join('|')}`)\n }\n }\n if (gp.warn?.sharedDevice === 'block') parts.push('block-on-shared-device')\n return parts.join(' ')\n}\n\nfunction defaultPolicySnapshot(): VaultPolicy {\n return {\n secret: { minWords: 6, minWordLength: 3, rejectRepeatedAdjacent: true },\n gates: {},\n }\n}\n\nasync function listRecoveryProfilesEnrolled(\n store: NoydbStore,\n vault: string,\n): Promise<ReadonlyArray<string>> {\n const enrolled: string[] = []\n const paper = await loadPaperRecoveryEntries(store, vault)\n if (paper.length > 0) enrolled.push(`paper (${paper.length} codes)`)\n return enrolled\n}\n\nfunction escapeMermaid(s: string): string {\n return s.replace(/\"/g, '\\\\\"').replace(/\\n/g, ' ')\n}\n\nfunction sanitizeId(s: string): string {\n return s.replace(/[^a-zA-Z0-9]/g, '_')\n}\n","/**\n * Tier-2 authenticator slot management.\n *\n * Each slot independently wraps the SAME KEK under a method-specific\n * derived key (LUKS pattern). Enrolling adds a slot; removing drops\n * one. Both are constant-time keyring writes — no DEK re-keying.\n *\n * The crypto for each method lives in its `@noy-db/on-*` package\n * (`on-webauthn`, `on-oidc`, `on-password`); this module accepts the\n * package's `wrapped_kek` ciphertext + `meta` payload and persists it.\n *\n * @see https://github.com/noy-db/docs/blob/main/content/docs/services/session-tiers.md → Tier 2 — Authenticate\n *\n * @module\n */\nimport type { NoydbStore, KeyringAuthenticator } from '../../kernel/types.js'\nimport { NoAccessError, ValidationError } from '../../kernel/errors.js'\nimport type { UnlockedKeyring } from './keyring.js'\nimport { persistKeyring, rotateKeys } from './keyring.js'\nimport { rotateSecret, type SlotRewrapCeremony } from './rotate-recover.js'\nimport { ROSTER_KEY_ID, BLOB_ADDRESS_KEY_ID } from '../../kernel/constants.js'\nimport type { RotateSecretInput } from './rotate-recover.js'\n\n/** Fields shared across both wrap-KEK and wrap-DEKs enroll inputs. */\ninterface EnrollAuthenticatorBase {\n readonly id: string\n readonly method: KeyringAuthenticator['method']\n /** Method-specific metadata (cred id, salt, …). */\n readonly meta: Record<string, unknown>\n /** Tier the active session held when enrolling. Defaults to 1. */\n readonly enrolled_via_tier?: 1 | 2\n}\n\n/** Wrap-KEK enroll input (WebAuthn, OIDC). */\nexport interface EnrollAuthenticatorWrappingKEKOptions extends EnrollAuthenticatorBase {\n /** Already-wrapped KEK ciphertext (base64) — produced by the on-* package. */\n readonly wrapped_kek: string\n readonly wrapKind?: 'kek'\n}\n\n/** Wrap-DEKs enroll input (password, future on-* using the unified wrap-DEKs primitive). */\nexport interface EnrollAuthenticatorWrappingDEKsOptions extends EnrollAuthenticatorBase {\n readonly wrapKind: 'deks'\n /** Base64 AES-GCM ciphertext of `{ deks: { collection: base64rawDek } }`. */\n readonly wrapped_deks: string\n /** Base64 AES-GCM IV used for the `wrapped_deks` ciphertext. */\n readonly iv: string\n}\n\n/** Discriminated union over the two enroll input shapes. */\nexport type EnrollAuthenticatorOptions =\n | EnrollAuthenticatorWrappingKEKOptions\n | EnrollAuthenticatorWrappingDEKsOptions\n\n/**\n * Append a new authenticator slot to the keyring file. Throws\n * `ValidationError` if a slot with the same id already exists — the\n * caller decides whether to remove + re-enroll.\n *\n * Accepts either wrap-KEK (WebAuthn, OIDC) or wrap-DEKs (password)\n * input. The variant is preserved verbatim into `KeyringAuthenticator`.\n */\nexport async function enrollAuthenticator(\n store: NoydbStore,\n vault: string,\n keyring: UnlockedKeyring,\n options: EnrollAuthenticatorOptions,\n): Promise<UnlockedKeyring> {\n assertSlotListVisible(keyring, 'enrollAuthenticator', vault)\n const existing = keyring.authenticators.find((a) => a.id === options.id)\n if (existing) {\n throw new ValidationError(\n `enrollAuthenticator: slot id \"${options.id}\" already exists in vault \"${vault}\". ` +\n 'Remove the slot first or pick a unique id.',\n )\n }\n\n const base = {\n id: options.id,\n method: options.method,\n enrolled_at: new Date().toISOString(),\n enrolled_via_tier: options.enrolled_via_tier ?? 1,\n meta: options.meta,\n } as const\n\n const slot: KeyringAuthenticator = options.wrapKind === 'deks'\n ? {\n ...base,\n wrapKind: 'deks',\n wrapped_deks: options.wrapped_deks,\n iv: options.iv,\n }\n : {\n ...base,\n wrapped_kek: options.wrapped_kek,\n }\n\n const next = appendSlot(keyring, slot)\n await persistKeyring(store, vault, next)\n return next\n}\n\n/**\n * Caller payload for {@link updateAuthenticator}. Mutates only\n * `meta` — the slot's id, method, and wrap material are immutable\n * through this primitive, preserving the anti-slot-swap guard.\n *\n * `meta` is **merged** at the top level: keys absent from the patch\n * are preserved, keys present overwrite. To clear a meta key, pass\n * `null` for that key explicitly. (Same top-level merge semantics as\n * `UserApi.updateMe`, non-recursive — meta is a flat label bag.)\n */\nexport interface UpdateAuthenticatorOptions {\n readonly meta?: Record<string, unknown>\n}\n\n/**\n * Mutate a tier-2 authenticator slot's `meta` blob (slot rename,\n * label changes). The slot's `id`, `method`, and wrap material\n * (`wrapped_kek` for wrap-KEK; `wrapped_deks` + `iv` for wrap-DEKs)\n * are immutable through this entry point — the anti-slot-swap guard\n * is structural, not gate-driven, so even if the policy gate is\n * weakened a future caller cannot use this path to swap one slot's\n * crypto for another's.\n *\n * `meta` patch semantics:\n * - Top-level merge — absent keys preserved, present keys overwrite\n * - `null` value — delete that meta key\n * - Non-object values (string, number, boolean, array) — replace verbatim\n *\n * @throws `NoAccessError` when no slot with the given id exists.\n * @throws `ValidationError` when no patch field is provided.\n *\n */\nexport async function updateAuthenticator(\n store: NoydbStore,\n vault: string,\n keyring: UnlockedKeyring,\n slotId: string,\n options: UpdateAuthenticatorOptions,\n): Promise<UnlockedKeyring> {\n if (options.meta === undefined) {\n throw new ValidationError(\n `updateAuthenticator: at least one of meta must be provided ` +\n `(slotId: \"${slotId}\").`,\n )\n }\n\n assertSlotListVisible(keyring, 'updateAuthenticator', vault)\n const idx = keyring.authenticators.findIndex((a) => a.id === slotId)\n if (idx === -1) {\n throw new NoAccessError(\n `updateAuthenticator: slot \"${slotId}\" not found in vault \"${vault}\".`,\n )\n }\n const existing = keyring.authenticators[idx]!\n\n // Merge at the top level. Absent keys preserved (non-recursive —\n // meta is a flat label bag in practice, no consumer nests it).\n const mergedMeta: Record<string, unknown> = { ...existing.meta }\n for (const [k, v] of Object.entries(options.meta)) {\n if (v === undefined) continue // skip\n if (v === null) {\n delete mergedMeta[k]\n continue\n }\n mergedMeta[k] = v\n }\n\n // Reconstruct the slot preserving wrapKind discrimination. The\n // immutable fields (id, method, wrapped_kek / wrapped_deks + iv,\n // enrolled_at, enrolled_via_tier) all flow through ...existing.\n const next: KeyringAuthenticator = { ...existing, meta: mergedMeta }\n const nextSlots = [...keyring.authenticators]\n nextSlots[idx] = next\n\n const nextKeyring: UnlockedKeyring = {\n ...keyring,\n authenticators: nextSlots,\n }\n await persistKeyring(store, vault, nextKeyring)\n return nextKeyring\n}\n\n/**\n * Refuse any slot-list decision taken from a keyring that cannot be\n * persisted (#1426).\n *\n * `UnlockedKeyring.authenticators` is a snapshot, and a tier-3\n * PIN-resumed / session-restored keyring carries an EMPTY one\n * alongside `kek === null` — not because the vault has no slots, but\n * because that session never unwrapped the KEK and so never read\n * them. Deciding \"the slot isn't there\" from that snapshot answers a\n * question the session cannot see the answer to.\n *\n * `persistKeyring` already refuses a null KEK, so every write path\n * here ends in a throw regardless. What this guard buys is that the\n * throw happens BEFORE the decision instead of after it — otherwise\n * `removeAuthenticator` short-circuits on the empty list and returns\n * a successful no-op, and `updateAuthenticator` reports the slot as\n * \"not found\" when the truthful answer is \"not visible from here\".\n *\n * Same class as the `echo` / `granted_by` / `expires_at` carry-forward\n * comments in `persistKeyring`: an UnlockedKeyring is a partial view,\n * and treating an absent field as a known-empty one silently destroys\n * or misreports state.\n */\nfunction assertSlotListVisible(\n keyring: UnlockedKeyring,\n op: string,\n vault: string,\n): void {\n if (keyring.kek) return\n throw new ValidationError(\n `${op}: keyring.kek is null, so the authenticator slot list for vault ` +\n `\"${vault}\" is not readable from this session and no slot decision can ` +\n 'be made. This typically means the keyring was opened via tier-3 PIN ' +\n 'resume, session restore, or a wrap-DEKs tier-2 unlock. Re-authenticate ' +\n 'at tier 1 (secret) before enrolling, updating or removing a slot.',\n )\n}\n\n/**\n * Drop a slot by id. No-op if the slot doesn't exist (idempotent —\n * removing a non-existent slot is a recoverable retry, not an error).\n *\n * ⛔ **THIS HIDES A CREDENTIAL. IT DOES NOT REVOKE ONE.** (#1445) The slot\n * leaves the keyring file and nothing else happens — no key material moves. A\n * slot blob captured before removal, or cached by an honest client earlier in\n * the same session, still unwraps the FULL LIVE DEK SET and reads every record,\n * for as long as those DEKs remain current.\n *\n * That is not a leak in one `on-*` package. `@noy-db/on-password`'s\n * `unwrapDeksWithPassword` and `@noy-db/on-webauthn`'s `unwrapKeyringSummary`\n * both carry the DEK map inside the blob and need no store, no keyring and no\n * network — the blob authenticates itself. A membership check inside a verifier\n * cannot close it, because a blob holder calls the primitive directly and never\n * reaches the verifier.\n *\n * ⭐ To actually revoke, see {@link revokeAuthenticator} — removal, then a DEK\n * rotation, then a re-wrap of the slots that stay. Every step is required and\n * the reasons are measured, not assumed; read that doc before assembling the\n * sequence by hand.\n *\n * ⚠️ Idempotency is only sound when the slot list is actually\n * visible — see {@link assertSlotListVisible} (#1426).\n *\n * @throws `ValidationError` when the session cannot see the slot list.\n */\nexport async function removeAuthenticator(\n store: NoydbStore,\n vault: string,\n keyring: UnlockedKeyring,\n slotId: string,\n): Promise<UnlockedKeyring> {\n assertSlotListVisible(keyring, 'removeAuthenticator', vault)\n const filtered = keyring.authenticators.filter((a) => a.id !== slotId)\n if (filtered.length === keyring.authenticators.length) {\n return keyring // idempotent — nothing to do\n }\n const next: UnlockedKeyring = {\n ...keyring,\n authenticators: filtered,\n }\n await persistKeyring(store, vault, next)\n return next\n}\n\n/**\n * Look up a slot by id. Returns `undefined` when no slot matches.\n * Used by tier-2 unlock dispatchers to fetch the wrapped KEK + meta\n * before invoking the method-specific verifier.\n */\nexport function findAuthenticator(\n keyring: UnlockedKeyring,\n slotId: string,\n): KeyringAuthenticator | undefined {\n return keyring.authenticators.find((a) => a.id === slotId)\n}\n\nfunction appendSlot(\n keyring: UnlockedKeyring,\n slot: KeyringAuthenticator,\n): UnlockedKeyring {\n return {\n ...keyring,\n authenticators: [...keyring.authenticators, slot],\n }\n}\n\n/** Inputs for {@link revokeAuthenticator}. */\nexport interface RevokeAuthenticatorOptions {\n /** The slot to revoke. */\n readonly slotId: string\n /**\n * The vault owner's CURRENT secret. Passed as both `oldSecret` and\n * `newSecret` to `rotateSecret`, which accepts them being equal — measured,\n * so revoking one credential does not force everyone onto a new phrase.\n */\n readonly secret: RotateSecretInput['oldSecret']\n /**\n * A re-wrap ceremony for EVERY slot that stays, keyed by slot id.\n *\n * ⛔ A slot with no ceremony is DROPPED, not preserved — that is\n * `rotateSecret`'s existing behaviour (#29 / PR5: \"without `slotCeremonies`,\n * rotation drops every slot\"), and it is the honest outcome, because after\n * step 2 its blob wraps DEKs that no longer exist. Supplying one requires\n * that credential to be present: the password for `on-password`, a live\n * assertion for `on-webauthn`. There is no way around that — re-wrapping a\n * credential is the credential's own operation.\n */\n readonly slotCeremonies?: { readonly [slotId: string]: SlotRewrapCeremony }\n}\n\n/**\n * Actually revoke an authenticator slot: remove it, mint fresh DEKs, and\n * re-wrap the slots that stay (#1445).\n *\n * ## Why this is three steps and not one\n *\n * Each was measured, because the composition looks obvious and the obvious\n * compositions do not work:\n *\n * 1. **{@link removeAuthenticator}** hides the slot. On its own it revokes\n * nothing — the captured blob carries its own DEKs.\n * 2. **`rotateKeys`** is the step that revokes. It is the ONLY one that calls\n * `generateDEK()`. Measured: a collection's DEK is byte-identical across a\n * `rotateSecret` and different across a `rotateKeys`.\n * ⚠️ `rotateSecret` alone does NOT revoke, however it reads:\n * it unwraps every DEK with the old KEK and rewraps THE SAME KEY MATERIAL\n * under the new one. A captured blob holds DEK VALUES and records are\n * encrypted with DEKs, so rewrapping them changes nothing the holder needs.\n * 3. **`rotateSecret` with `slotCeremonies`** re-wraps the remaining slots\n * around the new DEKs. Without it they survive step 2 BYTE-IDENTICAL\n * (measured: `rotateKeys` never touches `authenticators[]`) and therefore\n * silently wrap keys that no longer exist — every other credential in the\n * vault dies quietly. This step is what makes the operation safe for\n * everyone who was not being revoked.\n *\n * ⭐ Neither primitive is sufficient alone and they are not interchangeable:\n * `rotateKeys` mints keys but cannot re-wrap slots; `rotateSecret` re-wraps\n * slots but cannot mint keys.\n *\n * ## Cost\n *\n * Step 2 re-encrypts every record in every affected collection, and every\n * remaining credential holder must be present for step 3. This is expensive on\n * purpose: it is what revocation costs when the credential is a bearer token.\n *\n * @throws `ValidationError` when the session cannot see the slot list\n * ({@link assertSlotListVisible}) — a tier-1 unlock is required, and #1432\n * guarantees the KEK rotation needs is present at this point.\n */\nexport async function revokeAuthenticator(\n store: NoydbStore,\n vault: string,\n keyring: UnlockedKeyring,\n options: RevokeAuthenticatorOptions,\n): Promise<UnlockedKeyring> {\n // 1. Hide it. Also runs the #1426 visibility guard, so everything below is\n // reached only from a session that can actually see (and persist) slots.\n const next = await removeAuthenticator(store, vault, keyring, options.slotId)\n\n // 2. Mint fresh DEKs — the step that revokes.\n // The set is derived from the DEK map the way `revoke` derives its own,\n // minus the two reserved keys that are not collections: the roster key\n // (rotating it makes the vault unopenable for every other member) and the\n // blob addressing root (rotating it invalidates every stored blob eTag).\n // `rotateKeys` throws on either if named explicitly, so they are dropped\n // here at the site that gathers them implicitly.\n const collections = new Set(next.deks.keys())\n collections.delete(ROSTER_KEY_ID)\n collections.delete(BLOB_ADDRESS_KEY_ID)\n if (collections.size > 0) {\n await rotateKeys(store, vault, next, { collections: [...collections] })\n }\n\n // 3. Re-wrap the slots that stay, around the DEKs step 2 just minted.\n // `oldSecret === newSecret` is accepted (measured), so this re-wraps\n // without forcing a phrase change. A slot with no ceremony is dropped —\n // see `slotCeremonies`.\n return rotateSecret(store, vault, next.userId, {\n oldSecret: options.secret,\n newSecret: options.secret,\n ...(options.slotCeremonies !== undefined && { slotCeremonies: options.slotCeremonies }),\n })\n}\n"],"mappings":"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","names":["deks"]}
|