@noy-db/hub 0.4.0 → 0.6.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.
Files changed (438) hide show
  1. package/dist/api-67SBPMMH.js +20 -0
  2. package/dist/attestation/index.js +18 -18
  3. package/dist/{backup-X7F3TCFV.js → backup-GAYWQAFN.js} +16 -11
  4. package/dist/backup-GAYWQAFN.js.map +1 -0
  5. package/dist/blobs/index.js +20 -20
  6. package/dist/broker/index.js +12 -12
  7. package/dist/bundle-handle-5OUAI7CV.js +36 -0
  8. package/dist/bundle-handle-5OUAI7CV.js.map +1 -0
  9. package/dist/cargo/index.js +36 -36
  10. package/dist/{chunk-DQZXPGGZ.js → chunk-24UICKTB.js} +3 -3
  11. package/dist/{chunk-77AEMXFA.js → chunk-24ZLWWEO.js} +1 -1
  12. package/dist/chunk-24ZLWWEO.js.map +1 -0
  13. package/dist/{chunk-2BET5QTS.js → chunk-27TCYH6G.js} +5 -5
  14. package/dist/{chunk-MSMJJB4J.js → chunk-2KIV7BEK.js} +2 -2
  15. package/dist/{chunk-6ZNSB3H6.js → chunk-2LGDCNFJ.js} +2 -2
  16. package/dist/{chunk-GTYFIVIZ.js → chunk-35EBKWQU.js} +5 -5
  17. package/dist/chunk-35EBKWQU.js.map +1 -0
  18. package/dist/{chunk-WBKZUO5V.js → chunk-3737A6EA.js} +3 -3
  19. package/dist/{chunk-ZBYFMI6D.js → chunk-3CLZY46Y.js} +638 -300
  20. package/dist/chunk-3CLZY46Y.js.map +1 -0
  21. package/dist/{chunk-HLKXPJ4V.js → chunk-3EO7RZLJ.js} +6 -6
  22. package/dist/{chunk-JYJFGD2H.js → chunk-3FSX6LDG.js} +2 -2
  23. package/dist/{chunk-MXUT7FTE.js → chunk-3HYWYLXL.js} +2 -2
  24. package/dist/{chunk-IVYQAVWO.js → chunk-3IIPWA75.js} +11 -11
  25. package/dist/{chunk-7H3WWH6J.js → chunk-3UK6STWO.js} +3 -3
  26. package/dist/chunk-46V67XP5.js +117 -0
  27. package/dist/chunk-46V67XP5.js.map +1 -0
  28. package/dist/{chunk-NJ4DYYO5.js → chunk-4HDYQL3J.js} +2 -2
  29. package/dist/{chunk-P4JO3H6R.js → chunk-4MQDV3HZ.js} +2 -2
  30. package/dist/{chunk-X4MVWPHV.js → chunk-4UVDI4XE.js} +3 -3
  31. package/dist/{chunk-LXRTP3AQ.js → chunk-4VCWI42L.js} +2 -2
  32. package/dist/{chunk-QJNCB5SK.js → chunk-747LRSOO.js} +2 -2
  33. package/dist/{chunk-3HUYRN24.js → chunk-7F4DBNSP.js} +3 -3
  34. package/dist/{chunk-UADPR6F4.js → chunk-7G6VDWXB.js} +3 -3
  35. package/dist/{chunk-PTHKZWEH.js → chunk-AHLJSVRL.js} +2 -2
  36. package/dist/{chunk-NFZGZ4AK.js → chunk-AR2JDFAE.js} +2 -2
  37. package/dist/{chunk-3ECU57YH.js → chunk-BPTZ4K5L.js} +11 -11
  38. package/dist/{chunk-KRCBZYTZ.js → chunk-CCZJJ4I2.js} +4 -4
  39. package/dist/{chunk-NYEFP7DY.js → chunk-CJ73OLMW.js} +2 -2
  40. package/dist/{chunk-Q36YXK77.js → chunk-CYQOUTMY.js} +3 -3
  41. package/dist/{chunk-RBTVFPHF.js → chunk-DCMQR46R.js} +5 -5
  42. package/dist/{chunk-TG634KCO.js → chunk-DG27IUVI.js} +2 -2
  43. package/dist/{chunk-E23UGFVC.js → chunk-DMQCLZ4Z.js} +8 -6
  44. package/dist/chunk-DMQCLZ4Z.js.map +1 -0
  45. package/dist/{chunk-ZNWSG7XC.js → chunk-DPJTDNUF.js} +68 -34
  46. package/dist/chunk-DPJTDNUF.js.map +1 -0
  47. package/dist/{chunk-V6UQZA2H.js → chunk-E6427JU2.js} +2 -2
  48. package/dist/{chunk-JEI6JJNI.js → chunk-EK7DT5BT.js} +220 -13
  49. package/dist/chunk-EK7DT5BT.js.map +1 -0
  50. package/dist/chunk-EPFF7LOX.js +14 -0
  51. package/dist/chunk-EPFF7LOX.js.map +1 -0
  52. package/dist/{chunk-TWPK7KUJ.js → chunk-F3GVO44K.js} +6 -6
  53. package/dist/{chunk-QD46HPZG.js → chunk-FAXZQ5UF.js} +2 -2
  54. package/dist/{chunk-IG6KUWBX.js → chunk-FNSMZQS4.js} +2 -2
  55. package/dist/{chunk-BLBAQRBM.js → chunk-FUHKW252.js} +2 -2
  56. package/dist/{chunk-SU6SGWXM.js → chunk-FY343IZR.js} +8 -8
  57. package/dist/{chunk-KPZS5QAQ.js → chunk-GQ3BX5EA.js} +9 -1
  58. package/dist/chunk-GQ3BX5EA.js.map +1 -0
  59. package/dist/{chunk-DAFBJS4Y.js → chunk-GS5RT2KK.js} +4 -4
  60. package/dist/{chunk-5UQ7Y6SQ.js → chunk-HFKGIBMD.js} +3 -3
  61. package/dist/{chunk-QZWTRENR.js → chunk-HNGSV2YC.js} +133 -1
  62. package/dist/chunk-HNGSV2YC.js.map +1 -0
  63. package/dist/chunk-HNHCQ556.js +63 -0
  64. package/dist/chunk-HNHCQ556.js.map +1 -0
  65. package/dist/chunk-HS7HNYBD.js +91 -0
  66. package/dist/chunk-HS7HNYBD.js.map +1 -0
  67. package/dist/{chunk-SHEEBRZ4.js → chunk-HVQGCKY4.js} +2 -2
  68. package/dist/{chunk-35NMVJ7I.js → chunk-HXGUIPQB.js} +3 -3
  69. package/dist/{chunk-ERGECZEI.js → chunk-JEM2IGS3.js} +9 -9
  70. package/dist/chunk-JH3OHQM4.js +260 -0
  71. package/dist/chunk-JH3OHQM4.js.map +1 -0
  72. package/dist/{chunk-L3YYLT4E.js → chunk-JIUVJ4M7.js} +2 -2
  73. package/dist/{chunk-55LX6FSX.js → chunk-JRCYZEHU.js} +2 -2
  74. package/dist/{chunk-SFBYQMZM.js → chunk-JSRU5W2M.js} +5 -5
  75. package/dist/{chunk-NM5UYF6Q.js → chunk-JXIDGFKS.js} +2 -2
  76. package/dist/{chunk-VYYM3MEN.js → chunk-KHKKD62N.js} +16 -2
  77. package/dist/chunk-KHKKD62N.js.map +1 -0
  78. package/dist/{chunk-FQ5CHJR7.js → chunk-KUDZVJKQ.js} +21 -2
  79. package/dist/chunk-KUDZVJKQ.js.map +1 -0
  80. package/dist/{chunk-RO5NM6UI.js → chunk-KZ3KMDN3.js} +2 -2
  81. package/dist/{chunk-HZZP2ZIG.js → chunk-LBGPO5RL.js} +6 -6
  82. package/dist/{chunk-ZFG5EBI3.js → chunk-LTU4FLHG.js} +57 -27
  83. package/dist/chunk-LTU4FLHG.js.map +1 -0
  84. package/dist/{chunk-CLE3TMIS.js → chunk-MC2LFTQM.js} +5 -2
  85. package/dist/{chunk-467SWYT5.js → chunk-MJWCLSQL.js} +17 -8
  86. package/dist/chunk-MJWCLSQL.js.map +1 -0
  87. package/dist/{chunk-W3G5REIU.js → chunk-MSWYQLMQ.js} +2 -2
  88. package/dist/{chunk-WK7ULBTP.js → chunk-MVOWXQHN.js} +5 -3
  89. package/dist/chunk-MVOWXQHN.js.map +1 -0
  90. package/dist/{chunk-2J6UEN2V.js → chunk-MXZ5TSTR.js} +118 -4
  91. package/dist/chunk-MXZ5TSTR.js.map +1 -0
  92. package/dist/{chunk-SGDPRCC4.js → chunk-MZ5ZFO2D.js} +3 -3
  93. package/dist/{chunk-NBXFCQX4.js → chunk-NQCNSVGI.js} +9 -9
  94. package/dist/chunk-NQCNSVGI.js.map +1 -0
  95. package/dist/{chunk-EFJAAFBW.js → chunk-NQRJ5AJO.js} +2 -2
  96. package/dist/{chunk-Q2SEJMP2.js → chunk-NVU7ZMPN.js} +2 -2
  97. package/dist/{chunk-ZTLOXFPB.js → chunk-OF3L2SD6.js} +2 -2
  98. package/dist/{chunk-LLYIMSQQ.js → chunk-OMSAULZM.js} +11 -11
  99. package/dist/{chunk-SF2ZIRD6.js → chunk-OSFPNKY4.js} +3 -3
  100. package/dist/{chunk-WUHEP3HH.js → chunk-P7NGLLUF.js} +5 -5
  101. package/dist/{chunk-RQ4ZI4ZI.js → chunk-PGAPBZEK.js} +7 -7
  102. package/dist/{chunk-T2MOJF76.js → chunk-PP5CO2PR.js} +6 -6
  103. package/dist/{chunk-Q5YJCVJY.js → chunk-RLWY6Z44.js} +227 -41
  104. package/dist/chunk-RLWY6Z44.js.map +1 -0
  105. package/dist/{chunk-QGIBI6GL.js → chunk-ROWL352P.js} +5 -5
  106. package/dist/{chunk-ZHDSUZQU.js → chunk-RPXL6KSX.js} +4 -4
  107. package/dist/{chunk-DTFA5DO4.js → chunk-SKONR2OL.js} +3 -3
  108. package/dist/{chunk-AT2JJEKJ.js → chunk-SRGJET2F.js} +16 -5
  109. package/dist/chunk-SRGJET2F.js.map +1 -0
  110. package/dist/{chunk-4ZKNC6A6.js → chunk-T4GPR2IR.js} +2 -2
  111. package/dist/{chunk-NLYALJMY.js → chunk-TFZ7LLOQ.js} +113 -36
  112. package/dist/chunk-TFZ7LLOQ.js.map +1 -0
  113. package/dist/{chunk-TS5XLBZR.js → chunk-TLNNHG3A.js} +4 -4
  114. package/dist/{chunk-6UBZ6I5Z.js → chunk-UHXCLPGC.js} +2 -2
  115. package/dist/{chunk-KD4HGBJV.js → chunk-V5C5NNBR.js} +4 -4
  116. package/dist/{chunk-WYUAEZSC.js → chunk-VBY6ZLJT.js} +15 -14
  117. package/dist/chunk-VBY6ZLJT.js.map +1 -0
  118. package/dist/{chunk-AKKRXT23.js → chunk-VCETL77O.js} +34 -16
  119. package/dist/chunk-VCETL77O.js.map +1 -0
  120. package/dist/{chunk-5GQEU4DN.js → chunk-VJFMXYKP.js} +2 -2
  121. package/dist/{chunk-5DRROK5W.js → chunk-VMCCGUQB.js} +2 -2
  122. package/dist/{chunk-IXNCZTWA.js → chunk-W3TVWF6N.js} +2 -2
  123. package/dist/{chunk-W66AM5IB.js → chunk-WLYIROCJ.js} +2 -2
  124. package/dist/{chunk-OWHP3H5C.js → chunk-XB7LUDKM.js} +4 -4
  125. package/dist/{chunk-KYRDJI5O.js → chunk-XBLKM73C.js} +10 -10
  126. package/dist/{chunk-MHMLXJ3I.js → chunk-YAWK6AWF.js} +2 -2
  127. package/dist/{chunk-PWPYO73B.js → chunk-ZGGVOADG.js} +5 -5
  128. package/dist/{chunk-YGK2HMLR.js → chunk-ZK4CO2KZ.js} +9 -9
  129. package/dist/chunk-ZK4CO2KZ.js.map +1 -0
  130. package/dist/{chunk-DSDXAKIP.js → chunk-ZVF37PKE.js} +34 -5
  131. package/dist/chunk-ZVF37PKE.js.map +1 -0
  132. package/dist/{chunk-IFOH7RBI.js → chunk-ZWMO5GL5.js} +5 -5
  133. package/dist/{chunk-LRRX57UA.js → chunk-ZXJXDHNK.js} +4 -4
  134. package/dist/classified/index.js +5 -5
  135. package/dist/{classified-marker-LE54HWSX.js → classified-marker-MH3ON5OL.js} +3 -3
  136. package/dist/collection-facade-AHT7FATG.js +45 -0
  137. package/dist/{computed-AB45PS4V.js → computed-6G5M3UPH.js} +3 -3
  138. package/dist/consent/index.js +9 -9
  139. package/dist/cover/index.js +3 -3
  140. package/dist/custody/index.js +11 -11
  141. package/dist/{dead-filter-V3LVWYEJ.js → dead-filter-U575NGSH.js} +2 -2
  142. package/dist/debug/index.js +1 -1
  143. package/dist/{delegation-JDUMAF6L.js → delegation-PDYL6RWT.js} +10 -10
  144. package/dist/derivations/index.js +12 -12
  145. package/dist/derive-AKT4KU36.js +22 -0
  146. package/dist/directory/index.js +10 -10
  147. package/dist/{dispatch-6TV3DGO6.js → dispatch-4XIJSFTT.js} +5 -5
  148. package/dist/{dispatch-W7NWQ637.js → dispatch-N5HGVUGU.js} +18 -18
  149. package/dist/{enclave-YFCLPIDR.js → enclave-OPK24QDR.js} +13 -9
  150. package/dist/executor-3JZ4POTW.js +28 -0
  151. package/dist/executor-CYJQLKKI.js +9 -0
  152. package/dist/executor-S4QIKEHW.js +9 -0
  153. package/dist/export-accessible-CPTP5STN.js +23 -0
  154. package/dist/extract-partition-TWXPHVU7.js +38 -0
  155. package/dist/{fanout-sidecar-273RDMYS.js → fanout-sidecar-T2ZGLQFE.js} +9 -9
  156. package/dist/find-RBBYKXI7.js +11 -0
  157. package/dist/forget/index.js +9 -9
  158. package/dist/guards/index.js +6 -6
  159. package/dist/history/index.js +15 -15
  160. package/dist/i18n/index.js +14 -14
  161. package/dist/index.d.ts +35 -7
  162. package/dist/index.js +279 -202
  163. package/dist/index.js.map +1 -1
  164. package/dist/indexing/index.js +3 -3
  165. package/dist/introspection/index.js +12 -12
  166. package/dist/issue-N74C55X6.js +19 -0
  167. package/dist/kernel/collection-config.d.ts +4 -0
  168. package/dist/kernel/collection.d.ts +34 -8
  169. package/dist/kernel/cutover-transform.d.ts +64 -0
  170. package/dist/kernel/enclave/crypto.d.ts +24 -0
  171. package/dist/kernel/enclave/index.d.ts +2 -1
  172. package/dist/kernel/errors.d.ts +138 -0
  173. package/dist/kernel/noydb.d.ts +10 -1
  174. package/dist/kernel/put-many-atomic.d.ts +45 -0
  175. package/dist/kernel/secret-mode.d.ts +59 -0
  176. package/dist/kernel/types.d.ts +98 -7
  177. package/dist/kernel/validation.d.ts +33 -0
  178. package/dist/kernel/vault.d.ts +32 -6
  179. package/dist/{ledger-XE7JF2I2.js → ledger-DONWFC5M.js} +11 -11
  180. package/dist/liberate-KICI7AJN.js +24 -0
  181. package/dist/{link-set-CGYC3OH5.js → link-set-2TTP4VLO.js} +11 -11
  182. package/dist/managed-secret-EAOYWRYF.js +27 -0
  183. package/dist/materialized-views/index.js +19 -19
  184. package/dist/money/index.js +3 -3
  185. package/dist/noydb-6W2XGQCS.js +83 -0
  186. package/dist/overlay-views/index.js +6 -6
  187. package/dist/periods/index.js +16 -16
  188. package/dist/pod/index.js +121 -12
  189. package/dist/policy/index.js +4 -4
  190. package/dist/port/to/index.d.ts +3 -1
  191. package/dist/port/to/locator.d.ts +84 -0
  192. package/dist/port/with/device-seal-strategy.d.ts +16 -0
  193. package/dist/port/with/strategies.d.ts +2 -0
  194. package/dist/port/with/write-hooks.d.ts +6 -0
  195. package/dist/portability/index.js +8 -8
  196. package/dist/{post-register-LY35EDTJ.js → post-register-ORQOQJCS.js} +6 -6
  197. package/dist/query/index.js +4 -4
  198. package/dist/reduce/index.js +4 -4
  199. package/dist/register-LHETZRJM.js +32 -0
  200. package/dist/registry-AMAZ6QRZ.js +19 -0
  201. package/dist/registry-CZVINVQH.js +9 -0
  202. package/dist/registry-IKUR2MXD.js +9 -0
  203. package/dist/registry-JRIN447B.js +18 -0
  204. package/dist/request-withdrawal-2DN2RGCO.js +31 -0
  205. package/dist/{reveal-T34ADTPY.js → reveal-AG4SCHUQ.js} +6 -6
  206. package/dist/revoke-ZX4NM2CJ.js +24 -0
  207. package/dist/satellites/index.js +1 -1
  208. package/dist/schema-update/index.js +6 -6
  209. package/dist/sealed-record/index.js +12 -12
  210. package/dist/search/index.js +4 -4
  211. package/dist/{seed-JOV54PB3.js → seed-IRCDGIRK.js} +15 -15
  212. package/dist/seed-IRCDGIRK.js.map +1 -0
  213. package/dist/sequence/index.js +9 -9
  214. package/dist/session/index.js +9 -9
  215. package/dist/shadow/index.js +2 -2
  216. package/dist/signer-7KHD4OL2.js +25 -0
  217. package/dist/snapshots/index.js +10 -10
  218. package/dist/{stale-T3V7XEOB.js → stale-K734VACF.js} +10 -10
  219. package/dist/storage-4H2GF6RQ.js +23 -0
  220. package/dist/{store-coordination-provider-AHGFEMFA.js → store-coordination-provider-4SKLSFUG.js} +3 -3
  221. package/dist/sync/index.js +10 -10
  222. package/dist/team/index.js +27 -15
  223. package/dist/tiers/index.js +10 -10
  224. package/dist/to/index.js +28 -3
  225. package/dist/to/index.js.map +1 -1
  226. package/dist/transactions/index.js +3 -3
  227. package/dist/util/index.js +1 -1
  228. package/dist/{verify-7XWHJWUN.js → verify-RO5RTZOJ.js} +5 -5
  229. package/dist/walk-MQHYNDLZ.js +21 -0
  230. package/dist/with-audit/guards/registry.d.ts +8 -1
  231. package/dist/with-audit/guards/types.d.ts +5 -0
  232. package/dist/with-audit/periods/index.d.ts +1 -1
  233. package/dist/with-audit/periods/periods.d.ts +7 -0
  234. package/dist/with-commit/tx/atomic-eligibility.d.ts +13 -11
  235. package/dist/with-formula/derivations/registry.d.ts +6 -0
  236. package/dist/with-formula/derivations/types.d.ts +5 -0
  237. package/dist/with-lookup/indexing/eager-indexes.d.ts +10 -0
  238. package/dist/with-party/team/device-seal.d.ts +25 -0
  239. package/dist/with-party/team/echo-ceremony.d.ts +74 -0
  240. package/dist/with-party/team/echo-secret.d.ts +56 -0
  241. package/dist/with-party/team/index.d.ts +8 -0
  242. package/dist/with-party/team/keyring.d.ts +71 -9
  243. package/dist/with-party/team/rotate-recover.d.ts +99 -5
  244. package/dist/with-pod/bundle-handle.d.ts +9 -0
  245. package/dist/with-pod/bundle.d.ts +100 -2
  246. package/dist/with-pod/format.d.ts +92 -1
  247. package/dist/with-pod/index.d.ts +22 -1
  248. package/dist/with-pod/open.d.ts +117 -0
  249. package/dist/with-pod/redirect.d.ts +78 -0
  250. package/dist/with-pod/signature.d.ts +13 -0
  251. package/dist/with-shape/introspection/behaviors.d.ts +100 -0
  252. package/dist/with-shape/introspection/derivation-key.d.ts +17 -0
  253. package/dist/with-shape/introspection/describe.d.ts +36 -0
  254. package/dist/with-shape/introspection/index.d.ts +1 -0
  255. package/dist/with-shape/introspection/subsystem-matrix.d.ts +22 -0
  256. package/dist/with-shape/introspection/types.d.ts +2 -1
  257. package/dist/with-shape/links/names.d.ts +14 -0
  258. package/dist/with-shape/manifest/derive.d.ts +86 -0
  259. package/dist/with-shape/manifest/reserved-collections.d.ts +27 -0
  260. package/dist/with-shape/manifest/storage.d.ts +62 -0
  261. package/dist/with-shape/manifest/sync.d.ts +135 -0
  262. package/dist/with-shape/manifest/types.d.ts +49 -0
  263. package/dist/with-shape/manifest/writer.d.ts +32 -0
  264. package/dist/with-shape/persisted-schemas/field-ids.d.ts +29 -0
  265. package/dist/with-shape/persisted-schemas/register.d.ts +3 -0
  266. package/dist/with-shape/persisted-schemas/types.d.ts +17 -0
  267. package/dist/with-shape/schema-update/fence-controller.d.ts +34 -0
  268. package/dist/with-shape/schema-update/fence.d.ts +9 -0
  269. package/dist/with-shape/schema-update/strategies.d.ts +18 -2
  270. package/dist/with-shape/schema-update/types.d.ts +11 -0
  271. package/dist/with-sync/engine.d.ts +17 -0
  272. package/dist/with-sync/presence.d.ts +31 -1
  273. package/dist/withdraw-accessible-JSDQHXHE.js +28 -0
  274. package/dist/withdraw-accessible-JSDQHXHE.js.map +1 -0
  275. package/package.json +3 -3
  276. package/dist/api-SI2IVBQO.js +0 -20
  277. package/dist/backup-X7F3TCFV.js.map +0 -1
  278. package/dist/chunk-2J6UEN2V.js.map +0 -1
  279. package/dist/chunk-467SWYT5.js.map +0 -1
  280. package/dist/chunk-4JQK3L4V.js +0 -41
  281. package/dist/chunk-4JQK3L4V.js.map +0 -1
  282. package/dist/chunk-77AEMXFA.js.map +0 -1
  283. package/dist/chunk-AKKRXT23.js.map +0 -1
  284. package/dist/chunk-AT2JJEKJ.js.map +0 -1
  285. package/dist/chunk-DSDXAKIP.js.map +0 -1
  286. package/dist/chunk-E23UGFVC.js.map +0 -1
  287. package/dist/chunk-FQ5CHJR7.js.map +0 -1
  288. package/dist/chunk-GTYFIVIZ.js.map +0 -1
  289. package/dist/chunk-JEI6JJNI.js.map +0 -1
  290. package/dist/chunk-JQ2AX4T5.js +0 -23
  291. package/dist/chunk-JQ2AX4T5.js.map +0 -1
  292. package/dist/chunk-KPZS5QAQ.js.map +0 -1
  293. package/dist/chunk-NBXFCQX4.js.map +0 -1
  294. package/dist/chunk-NLYALJMY.js.map +0 -1
  295. package/dist/chunk-Q5YJCVJY.js.map +0 -1
  296. package/dist/chunk-QZWTRENR.js.map +0 -1
  297. package/dist/chunk-S7GIXBXV.js +0 -129
  298. package/dist/chunk-S7GIXBXV.js.map +0 -1
  299. package/dist/chunk-SBHN6QUT.js +0 -25
  300. package/dist/chunk-SBHN6QUT.js.map +0 -1
  301. package/dist/chunk-VYYM3MEN.js.map +0 -1
  302. package/dist/chunk-WK7ULBTP.js.map +0 -1
  303. package/dist/chunk-WYUAEZSC.js.map +0 -1
  304. package/dist/chunk-YGK2HMLR.js.map +0 -1
  305. package/dist/chunk-ZBYFMI6D.js.map +0 -1
  306. package/dist/chunk-ZFG5EBI3.js.map +0 -1
  307. package/dist/chunk-ZNWSG7XC.js.map +0 -1
  308. package/dist/collection-facade-DO2AABZT.js +0 -45
  309. package/dist/derive-ZPGVLE2Q.js +0 -22
  310. package/dist/executor-FPHKWDWT.js +0 -28
  311. package/dist/executor-IEBLTBCW.js +0 -9
  312. package/dist/executor-RM7XWQDU.js +0 -9
  313. package/dist/export-accessible-EKM3HGB2.js +0 -23
  314. package/dist/extract-partition-LS4ET5A6.js +0 -38
  315. package/dist/find-OZMTRIK3.js +0 -11
  316. package/dist/issue-LIA7FXBJ.js +0 -19
  317. package/dist/liberate-WZ3RHVKW.js +0 -24
  318. package/dist/noydb-ZLPVCDES.js +0 -81
  319. package/dist/register-6RIBIT3I.js +0 -25
  320. package/dist/registry-7Y5WF7GS.js +0 -9
  321. package/dist/registry-BWD2ED52.js +0 -8
  322. package/dist/registry-MUSL4AW6.js +0 -19
  323. package/dist/registry-RON4WHAP.js +0 -18
  324. package/dist/request-withdrawal-FT3ORAWN.js +0 -31
  325. package/dist/revoke-HC7WG3HE.js +0 -24
  326. package/dist/seed-JOV54PB3.js.map +0 -1
  327. package/dist/signer-CTWOH6KT.js +0 -25
  328. package/dist/storage-LLNLPWWB.js +0 -23
  329. package/dist/walk-G3NFEP5L.js +0 -21
  330. package/dist/withdraw-accessible-ANSVG6XG.js +0 -28
  331. /package/dist/{api-SI2IVBQO.js.map → api-67SBPMMH.js.map} +0 -0
  332. /package/dist/{chunk-DQZXPGGZ.js.map → chunk-24UICKTB.js.map} +0 -0
  333. /package/dist/{chunk-2BET5QTS.js.map → chunk-27TCYH6G.js.map} +0 -0
  334. /package/dist/{chunk-MSMJJB4J.js.map → chunk-2KIV7BEK.js.map} +0 -0
  335. /package/dist/{chunk-6ZNSB3H6.js.map → chunk-2LGDCNFJ.js.map} +0 -0
  336. /package/dist/{chunk-WBKZUO5V.js.map → chunk-3737A6EA.js.map} +0 -0
  337. /package/dist/{chunk-HLKXPJ4V.js.map → chunk-3EO7RZLJ.js.map} +0 -0
  338. /package/dist/{chunk-JYJFGD2H.js.map → chunk-3FSX6LDG.js.map} +0 -0
  339. /package/dist/{chunk-MXUT7FTE.js.map → chunk-3HYWYLXL.js.map} +0 -0
  340. /package/dist/{chunk-IVYQAVWO.js.map → chunk-3IIPWA75.js.map} +0 -0
  341. /package/dist/{chunk-7H3WWH6J.js.map → chunk-3UK6STWO.js.map} +0 -0
  342. /package/dist/{chunk-NJ4DYYO5.js.map → chunk-4HDYQL3J.js.map} +0 -0
  343. /package/dist/{chunk-P4JO3H6R.js.map → chunk-4MQDV3HZ.js.map} +0 -0
  344. /package/dist/{chunk-X4MVWPHV.js.map → chunk-4UVDI4XE.js.map} +0 -0
  345. /package/dist/{chunk-LXRTP3AQ.js.map → chunk-4VCWI42L.js.map} +0 -0
  346. /package/dist/{chunk-QJNCB5SK.js.map → chunk-747LRSOO.js.map} +0 -0
  347. /package/dist/{chunk-3HUYRN24.js.map → chunk-7F4DBNSP.js.map} +0 -0
  348. /package/dist/{chunk-UADPR6F4.js.map → chunk-7G6VDWXB.js.map} +0 -0
  349. /package/dist/{chunk-PTHKZWEH.js.map → chunk-AHLJSVRL.js.map} +0 -0
  350. /package/dist/{chunk-NFZGZ4AK.js.map → chunk-AR2JDFAE.js.map} +0 -0
  351. /package/dist/{chunk-3ECU57YH.js.map → chunk-BPTZ4K5L.js.map} +0 -0
  352. /package/dist/{chunk-KRCBZYTZ.js.map → chunk-CCZJJ4I2.js.map} +0 -0
  353. /package/dist/{chunk-NYEFP7DY.js.map → chunk-CJ73OLMW.js.map} +0 -0
  354. /package/dist/{chunk-Q36YXK77.js.map → chunk-CYQOUTMY.js.map} +0 -0
  355. /package/dist/{chunk-RBTVFPHF.js.map → chunk-DCMQR46R.js.map} +0 -0
  356. /package/dist/{chunk-TG634KCO.js.map → chunk-DG27IUVI.js.map} +0 -0
  357. /package/dist/{chunk-V6UQZA2H.js.map → chunk-E6427JU2.js.map} +0 -0
  358. /package/dist/{chunk-TWPK7KUJ.js.map → chunk-F3GVO44K.js.map} +0 -0
  359. /package/dist/{chunk-QD46HPZG.js.map → chunk-FAXZQ5UF.js.map} +0 -0
  360. /package/dist/{chunk-IG6KUWBX.js.map → chunk-FNSMZQS4.js.map} +0 -0
  361. /package/dist/{chunk-BLBAQRBM.js.map → chunk-FUHKW252.js.map} +0 -0
  362. /package/dist/{chunk-SU6SGWXM.js.map → chunk-FY343IZR.js.map} +0 -0
  363. /package/dist/{chunk-DAFBJS4Y.js.map → chunk-GS5RT2KK.js.map} +0 -0
  364. /package/dist/{chunk-5UQ7Y6SQ.js.map → chunk-HFKGIBMD.js.map} +0 -0
  365. /package/dist/{chunk-SHEEBRZ4.js.map → chunk-HVQGCKY4.js.map} +0 -0
  366. /package/dist/{chunk-35NMVJ7I.js.map → chunk-HXGUIPQB.js.map} +0 -0
  367. /package/dist/{chunk-ERGECZEI.js.map → chunk-JEM2IGS3.js.map} +0 -0
  368. /package/dist/{chunk-L3YYLT4E.js.map → chunk-JIUVJ4M7.js.map} +0 -0
  369. /package/dist/{chunk-55LX6FSX.js.map → chunk-JRCYZEHU.js.map} +0 -0
  370. /package/dist/{chunk-SFBYQMZM.js.map → chunk-JSRU5W2M.js.map} +0 -0
  371. /package/dist/{chunk-NM5UYF6Q.js.map → chunk-JXIDGFKS.js.map} +0 -0
  372. /package/dist/{chunk-RO5NM6UI.js.map → chunk-KZ3KMDN3.js.map} +0 -0
  373. /package/dist/{chunk-HZZP2ZIG.js.map → chunk-LBGPO5RL.js.map} +0 -0
  374. /package/dist/{chunk-CLE3TMIS.js.map → chunk-MC2LFTQM.js.map} +0 -0
  375. /package/dist/{chunk-W3G5REIU.js.map → chunk-MSWYQLMQ.js.map} +0 -0
  376. /package/dist/{chunk-SGDPRCC4.js.map → chunk-MZ5ZFO2D.js.map} +0 -0
  377. /package/dist/{chunk-EFJAAFBW.js.map → chunk-NQRJ5AJO.js.map} +0 -0
  378. /package/dist/{chunk-Q2SEJMP2.js.map → chunk-NVU7ZMPN.js.map} +0 -0
  379. /package/dist/{chunk-ZTLOXFPB.js.map → chunk-OF3L2SD6.js.map} +0 -0
  380. /package/dist/{chunk-LLYIMSQQ.js.map → chunk-OMSAULZM.js.map} +0 -0
  381. /package/dist/{chunk-SF2ZIRD6.js.map → chunk-OSFPNKY4.js.map} +0 -0
  382. /package/dist/{chunk-WUHEP3HH.js.map → chunk-P7NGLLUF.js.map} +0 -0
  383. /package/dist/{chunk-RQ4ZI4ZI.js.map → chunk-PGAPBZEK.js.map} +0 -0
  384. /package/dist/{chunk-T2MOJF76.js.map → chunk-PP5CO2PR.js.map} +0 -0
  385. /package/dist/{chunk-QGIBI6GL.js.map → chunk-ROWL352P.js.map} +0 -0
  386. /package/dist/{chunk-ZHDSUZQU.js.map → chunk-RPXL6KSX.js.map} +0 -0
  387. /package/dist/{chunk-DTFA5DO4.js.map → chunk-SKONR2OL.js.map} +0 -0
  388. /package/dist/{chunk-4ZKNC6A6.js.map → chunk-T4GPR2IR.js.map} +0 -0
  389. /package/dist/{chunk-TS5XLBZR.js.map → chunk-TLNNHG3A.js.map} +0 -0
  390. /package/dist/{chunk-6UBZ6I5Z.js.map → chunk-UHXCLPGC.js.map} +0 -0
  391. /package/dist/{chunk-KD4HGBJV.js.map → chunk-V5C5NNBR.js.map} +0 -0
  392. /package/dist/{chunk-5GQEU4DN.js.map → chunk-VJFMXYKP.js.map} +0 -0
  393. /package/dist/{chunk-5DRROK5W.js.map → chunk-VMCCGUQB.js.map} +0 -0
  394. /package/dist/{chunk-IXNCZTWA.js.map → chunk-W3TVWF6N.js.map} +0 -0
  395. /package/dist/{chunk-W66AM5IB.js.map → chunk-WLYIROCJ.js.map} +0 -0
  396. /package/dist/{chunk-OWHP3H5C.js.map → chunk-XB7LUDKM.js.map} +0 -0
  397. /package/dist/{chunk-KYRDJI5O.js.map → chunk-XBLKM73C.js.map} +0 -0
  398. /package/dist/{chunk-MHMLXJ3I.js.map → chunk-YAWK6AWF.js.map} +0 -0
  399. /package/dist/{chunk-PWPYO73B.js.map → chunk-ZGGVOADG.js.map} +0 -0
  400. /package/dist/{chunk-IFOH7RBI.js.map → chunk-ZWMO5GL5.js.map} +0 -0
  401. /package/dist/{chunk-LRRX57UA.js.map → chunk-ZXJXDHNK.js.map} +0 -0
  402. /package/dist/{classified-marker-LE54HWSX.js.map → classified-marker-MH3ON5OL.js.map} +0 -0
  403. /package/dist/{collection-facade-DO2AABZT.js.map → collection-facade-AHT7FATG.js.map} +0 -0
  404. /package/dist/{computed-AB45PS4V.js.map → computed-6G5M3UPH.js.map} +0 -0
  405. /package/dist/{dead-filter-V3LVWYEJ.js.map → dead-filter-U575NGSH.js.map} +0 -0
  406. /package/dist/{delegation-JDUMAF6L.js.map → delegation-PDYL6RWT.js.map} +0 -0
  407. /package/dist/{derive-ZPGVLE2Q.js.map → derive-AKT4KU36.js.map} +0 -0
  408. /package/dist/{dispatch-6TV3DGO6.js.map → dispatch-4XIJSFTT.js.map} +0 -0
  409. /package/dist/{dispatch-W7NWQ637.js.map → dispatch-N5HGVUGU.js.map} +0 -0
  410. /package/dist/{enclave-YFCLPIDR.js.map → enclave-OPK24QDR.js.map} +0 -0
  411. /package/dist/{executor-FPHKWDWT.js.map → executor-3JZ4POTW.js.map} +0 -0
  412. /package/dist/{executor-IEBLTBCW.js.map → executor-CYJQLKKI.js.map} +0 -0
  413. /package/dist/{executor-RM7XWQDU.js.map → executor-S4QIKEHW.js.map} +0 -0
  414. /package/dist/{export-accessible-EKM3HGB2.js.map → export-accessible-CPTP5STN.js.map} +0 -0
  415. /package/dist/{extract-partition-LS4ET5A6.js.map → extract-partition-TWXPHVU7.js.map} +0 -0
  416. /package/dist/{fanout-sidecar-273RDMYS.js.map → fanout-sidecar-T2ZGLQFE.js.map} +0 -0
  417. /package/dist/{find-OZMTRIK3.js.map → find-RBBYKXI7.js.map} +0 -0
  418. /package/dist/{issue-LIA7FXBJ.js.map → issue-N74C55X6.js.map} +0 -0
  419. /package/dist/{ledger-XE7JF2I2.js.map → ledger-DONWFC5M.js.map} +0 -0
  420. /package/dist/{liberate-WZ3RHVKW.js.map → liberate-KICI7AJN.js.map} +0 -0
  421. /package/dist/{link-set-CGYC3OH5.js.map → link-set-2TTP4VLO.js.map} +0 -0
  422. /package/dist/{noydb-ZLPVCDES.js.map → managed-secret-EAOYWRYF.js.map} +0 -0
  423. /package/dist/{register-6RIBIT3I.js.map → noydb-6W2XGQCS.js.map} +0 -0
  424. /package/dist/{post-register-LY35EDTJ.js.map → post-register-ORQOQJCS.js.map} +0 -0
  425. /package/dist/{registry-7Y5WF7GS.js.map → register-LHETZRJM.js.map} +0 -0
  426. /package/dist/{registry-BWD2ED52.js.map → registry-AMAZ6QRZ.js.map} +0 -0
  427. /package/dist/{registry-MUSL4AW6.js.map → registry-CZVINVQH.js.map} +0 -0
  428. /package/dist/{registry-RON4WHAP.js.map → registry-IKUR2MXD.js.map} +0 -0
  429. /package/dist/{request-withdrawal-FT3ORAWN.js.map → registry-JRIN447B.js.map} +0 -0
  430. /package/dist/{revoke-HC7WG3HE.js.map → request-withdrawal-2DN2RGCO.js.map} +0 -0
  431. /package/dist/{reveal-T34ADTPY.js.map → reveal-AG4SCHUQ.js.map} +0 -0
  432. /package/dist/{signer-CTWOH6KT.js.map → revoke-ZX4NM2CJ.js.map} +0 -0
  433. /package/dist/{stale-T3V7XEOB.js.map → signer-7KHD4OL2.js.map} +0 -0
  434. /package/dist/{storage-LLNLPWWB.js.map → stale-K734VACF.js.map} +0 -0
  435. /package/dist/{walk-G3NFEP5L.js.map → storage-4H2GF6RQ.js.map} +0 -0
  436. /package/dist/{store-coordination-provider-AHGFEMFA.js.map → store-coordination-provider-4SKLSFUG.js.map} +0 -0
  437. /package/dist/{verify-7XWHJWUN.js.map → verify-RO5RTZOJ.js.map} +0 -0
  438. /package/dist/{withdraw-accessible-ANSVG6XG.js.map → walk-MQHYNDLZ.js.map} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/with-sync/presence.ts","../src/with-sync/period-scope.ts","../src/with-sync/engine.ts","../src/with-sync/transaction.ts"],"sourcesContent":["/**\n * Presence handle — real-time awareness of who is viewing/editing a collection.\n * encrypted ephemeral channel keyed by collection DEK via HKDF.\n *\n * The presence key is derived from the collection DEK so:\n * - The adapter never learns user identities from presence payloads.\n * - Presence rotates automatically when the DEK rotates (revoked users\n * can no longer participate after a DEK rotation).\n *\n * Two transport strategies:\n * 1. **Pub/sub** (real-time): used when the adapter implements\n * `presencePublish` and `presenceSubscribe`.\n * 2. **Storage-poll** (fallback): presence records are written to a\n * reserved `_presence_<collection>` collection on the sync adapter\n * (if available) or local adapter, and polled periodically.\n */\n\nimport type { NoydbStore, PresencePeer } from '../kernel/types.js'\nimport { encrypt, decrypt, generateIV, bufferToBase64, derivePresenceKey, type EnclaveKey } from '../kernel/enclave/index.js'\n\n/** Options for constructing a PresenceHandle. @internal */\nexport interface PresenceHandleOpts {\n /** Local adapter for storage-poll fallback. */\n adapter: NoydbStore\n /** Remote (sync) adapter — preferred for broadcasting presence if available. */\n syncAdapter?: NoydbStore\n /** Vault name — used as part of the channel and storage key. */\n vault: string\n /** Collection name — used as HKDF `info` and channel suffix. */\n collectionName: string\n /** Calling user's ID, embedded unencrypted in storage records. */\n userId: string\n /** Whether encryption is active. When false, presence payloads are stored as JSON. */\n encrypted: boolean\n /** Callback that resolves the collection DEK (used to derive the presence key). */\n getDEK: (collectionName: string) => Promise<EnclaveKey>\n /** How long (ms) before a peer's presence is considered stale. Default: 30_000. */\n staleMs?: number\n /** Poll interval (ms) for the storage-poll fallback. Default: 5_000. */\n pollIntervalMs?: number\n}\n\n/**\n * Internal storage envelope for the storage-poll fallback.\n * Written to `_presence_<collection>` as `{ userId, lastSeen, iv, data }`.\n */\ninterface StoragePresenceRecord {\n userId: string\n lastSeen: string\n iv: string // base64 AES-GCM IV (empty when not encrypted)\n data: string // base64 ciphertext or JSON string when not encrypted\n}\n\n/** Presence handle for a single collection. */\nexport class PresenceHandle<P> {\n private readonly adapter: NoydbStore\n private readonly syncAdapter: NoydbStore | undefined\n private readonly vault: string\n private readonly collectionName: string\n private readonly userId: string\n private readonly encrypted: boolean\n private readonly getDEK: (collectionName: string) => Promise<EnclaveKey>\n private readonly staleMs: number\n private readonly pollIntervalMs: number\n private readonly channel: string\n private readonly storageCollection: string\n\n private presenceKey: EnclaveKey | null = null\n private subscribers: Array<(peers: PresencePeer<P>[]) => void> = []\n private unsubscribePubSub: (() => void) | null = null\n private pollTimer: ReturnType<typeof setInterval> | null = null\n private stopped = false\n\n constructor(opts: PresenceHandleOpts) {\n this.adapter = opts.adapter\n this.syncAdapter = opts.syncAdapter\n this.vault = opts.vault\n this.collectionName = opts.collectionName\n this.userId = opts.userId\n this.encrypted = opts.encrypted\n this.getDEK = opts.getDEK\n this.staleMs = opts.staleMs ?? 30_000\n this.pollIntervalMs = opts.pollIntervalMs ?? 5_000\n // Channel used by pub/sub adapters — vault-scoped so two collections\n // in the same vault don't bleed into each other's presence channels.\n this.channel = `${opts.vault}:${opts.collectionName}:presence`\n // Reserved collection name for the storage-poll fallback.\n this.storageCollection = `_presence_${opts.collectionName}`\n }\n\n /**\n * Announce yourself (or update your cursor/status).\n * Encrypts `payload` with the presence key and publishes it.\n */\n async update(payload: P): Promise<void> {\n if (this.stopped) return\n\n const key = await this.getPresenceKey()\n const now = new Date().toISOString()\n const plaintext = JSON.stringify({ userId: this.userId, lastSeen: now, payload })\n let encryptedPayload: string\n\n if (this.encrypted && key) {\n const iv = generateIV()\n const ivB64 = bufferToBase64(iv)\n const { data } = await encrypt(plaintext, key)\n encryptedPayload = JSON.stringify({ iv: ivB64, data })\n } else {\n encryptedPayload = plaintext\n }\n\n // Pub/sub path — publish to any adapter that supports it\n const pubAdapter = this.getPubSubAdapter()\n if (pubAdapter?.presencePublish) {\n await pubAdapter.presencePublish(this.channel, encryptedPayload)\n }\n\n // Storage-poll path — write a record to the storage adapter\n await this.writeStorageRecord(payload, now)\n }\n\n /**\n * Subscribe to presence updates. The callback receives a filtered, decrypted\n * list of all currently-active peers (excluding yourself, excluding stale).\n *\n * Returns an unsubscribe function. Also call `stop()` to release all resources.\n */\n subscribe(cb: (peers: PresencePeer<P>[]) => void): () => void {\n if (this.stopped) return () => {}\n\n this.subscribers.push(cb)\n\n // Start pub/sub listener on first subscriber\n if (this.subscribers.length === 1) {\n this.startListening()\n }\n\n return () => {\n this.subscribers = this.subscribers.filter(s => s !== cb)\n if (this.subscribers.length === 0) this.stopListening()\n }\n }\n\n /** Stop all listening and clear resources. */\n stop(): void {\n this.stopped = true\n this.stopListening()\n this.subscribers = []\n }\n\n // ─── Private ────────────────────────────────────────────────────────\n\n private async getPresenceKey(): Promise<EnclaveKey | null> {\n if (!this.encrypted) return null\n if (!this.presenceKey) {\n try {\n const dek = await this.getDEK(this.collectionName)\n this.presenceKey = await derivePresenceKey(dek, this.collectionName)\n } catch {\n // no-op — presence degrades gracefully if crypto fails\n }\n }\n return this.presenceKey\n }\n\n private getPubSubAdapter(): NoydbStore | undefined {\n // Prefer the sync adapter (it broadcasts to other devices)\n if (this.syncAdapter?.presencePublish) return this.syncAdapter\n if (this.adapter.presencePublish) return this.adapter\n return undefined\n }\n\n private startListening(): void {\n const pubAdapter = this.getPubSubAdapter()\n\n if (pubAdapter?.presenceSubscribe) {\n // Real-time pub/sub path\n this.unsubscribePubSub = pubAdapter.presenceSubscribe(\n this.channel,\n (encryptedPayload) => { void this.handlePubSubMessage(encryptedPayload) },\n )\n } else {\n // Storage-poll fallback\n this.pollTimer = setInterval(\n () => { void this.pollStoragePresence() },\n this.pollIntervalMs,\n )\n // Kick off an immediate poll\n void this.pollStoragePresence()\n }\n }\n\n private stopListening(): void {\n if (this.unsubscribePubSub) {\n this.unsubscribePubSub()\n this.unsubscribePubSub = null\n }\n if (this.pollTimer) {\n clearInterval(this.pollTimer)\n this.pollTimer = null\n }\n }\n\n private async handlePubSubMessage(encryptedPayload: string): Promise<void> {\n try {\n const peer = await this.decryptPresencePayload(encryptedPayload)\n if (!peer || peer.userId === this.userId) return\n\n const cutoff = new Date(Date.now() - this.staleMs).toISOString()\n if (peer.lastSeen < cutoff) return\n\n // Deliver only this new peer to subscribers; a full snapshot poll follows\n // on next interval. For pub/sub, we could maintain a map of active peers,\n // but for simplicity: emit a snapshot read from storage.\n await this.pollStoragePresence()\n } catch {\n // Decrypt failure — stale key or tampered payload, ignore\n }\n }\n\n private async decryptPresencePayload(\n encryptedPayload: string,\n ): Promise<{ userId: string; lastSeen: string; payload: P } | null> {\n const key = await this.getPresenceKey()\n\n if (!this.encrypted || !key) {\n return JSON.parse(encryptedPayload) as { userId: string; lastSeen: string; payload: P }\n }\n\n const { iv: ivB64, data } = JSON.parse(encryptedPayload) as { iv: string; data: string }\n const plaintext = await decrypt(ivB64, data, key)\n return JSON.parse(plaintext) as { userId: string; lastSeen: string; payload: P }\n }\n\n private async writeStorageRecord(payload: P, now: string): Promise<void> {\n const key = await this.getPresenceKey()\n const plaintext = JSON.stringify(payload)\n let iv = ''\n let data: string\n\n if (this.encrypted && key) {\n const ivBytes = generateIV()\n iv = bufferToBase64(ivBytes)\n const result = await encrypt(plaintext, key)\n data = result.data\n } else {\n data = plaintext\n }\n\n const record: StoragePresenceRecord = { userId: this.userId, lastSeen: now, iv, data }\n const json = JSON.stringify(record)\n\n // Use the sync adapter if available (so other devices can read it);\n // fall back to local adapter.\n const storeAdapter = this.syncAdapter ?? this.adapter\n const envelope = {\n _noydb: 1 as const,\n _v: 1,\n _ts: now,\n _iv: '',\n _data: json,\n }\n try {\n await storeAdapter.put(\n this.vault,\n this.storageCollection,\n this.userId,\n envelope,\n )\n } catch {\n // Presence write failure is non-fatal — the user is still present locally\n }\n }\n\n private async pollStoragePresence(): Promise<void> {\n if (this.stopped || this.subscribers.length === 0) return\n\n try {\n const storeAdapter = this.syncAdapter ?? this.adapter\n const ids = await storeAdapter.list(this.vault, this.storageCollection)\n const cutoff = new Date(Date.now() - this.staleMs).toISOString()\n const peers: PresencePeer<P>[] = []\n\n for (const id of ids) {\n if (id === this.userId) continue // skip ourselves\n const envelope = await storeAdapter.get(this.vault, this.storageCollection, id)\n if (!envelope) continue\n\n const record = JSON.parse(envelope._data) as StoragePresenceRecord\n if (record.lastSeen < cutoff) continue\n\n let peerPayload: P\n if (this.encrypted && this.presenceKey && record.iv) {\n const plaintext = await decrypt(record.iv, record.data, this.presenceKey)\n peerPayload = JSON.parse(plaintext) as P\n } else {\n peerPayload = JSON.parse(record.data) as P\n }\n\n peers.push({ userId: record.userId, payload: peerPayload, lastSeen: record.lastSeen })\n }\n\n for (const cb of this.subscribers) {\n cb(peers)\n }\n } catch {\n // Poll failure is non-fatal\n }\n }\n}\n","/**\n * #807 — period-scoped pull support for the `SyncEngine` (thin-client\n * bootstrap: first sync pulls the current period + the period summaries;\n * historical periods backfill on demand via `pull({ periods: ['<name>'] })`).\n *\n * ## Membership law\n *\n * The engine only ever sees ciphertext envelopes, so period membership at\n * the sync tier is by write-time `_ts` against the closed periods' windows\n * (`periodExclusiveUpperBound`) — the SAME store-tier law freeze (#604) and\n * archive (#613) use, NOT the business-date (`dateField`) seal the write\n * guard applies inside the trust boundary. A late-booked record (in-period\n * business date, current write-time) therefore syncs with the current\n * period — mirroring how it archives at the NEXT period's archive.\n *\n * Closed periods partition the timeline by their exclusive upper bounds:\n * the earliest closed period's window is unbounded below (a close seals\n * everything at-or-before its `endDate`), each later one starts at its\n * predecessor's bound, and the CURRENT window is everything at-or-after\n * the latest closed bound (everything, when no period was ever closed).\n *\n * ## What a period filter can never drop\n *\n * - **Period summaries** (`_periods` + freeze/archive/target-purge\n * companions) — the navigation index; always pulled in full, exempt from\n * every filter (`collections`, `periods`, `modifiedSince`).\n * - **Delete markers and tombstones** — an erasure or delete must never be\n * skipped by partial sync (the #589/#590 law, mirroring the\n * `modifiedSince` exemption). This is what makes a device that never\n * pulled period P converge instead of resurrecting P's deleted records\n * when it later backfills.\n * - **Reserved lookups** (`_dict_*`/`_lookup_*`) — vocabulary/config rows\n * are not period-partitioned data; their explicit pull loop is exempt.\n *\n * Push is NEVER period-filtered — `PushOptions` has no `periods` member at\n * all; a thin client's writes always flow up in full.\n */\nimport type { EncryptedEnvelope } from '../kernel/types.js'\nimport { ValidationError } from '../kernel/errors.js'\nimport { isTombstoneShape, isDeleteMarker } from '../kernel/enclave/index.js'\nimport {\n PERIODS_COLLECTION,\n PERIOD_FREEZES_COLLECTION,\n PERIOD_ARCHIVES_COLLECTION,\n PERIOD_TARGET_PURGES_COLLECTION,\n periodExclusiveUpperBound,\n} from '../with-audit/periods/window.js'\n\nexport { PERIODS_COLLECTION }\n\n/**\n * The reserved period-metadata collections a period-scoped pull always\n * fetches first (phase 1, \"summaries\"): the hash-chained `_periods` index\n * plus its freeze/archive/target-purge companions.\n */\nexport const PERIOD_SUMMARY_COLLECTIONS: readonly string[] = [\n PERIODS_COLLECTION,\n PERIOD_FREEZES_COLLECTION,\n PERIOD_ARCHIVES_COLLECTION,\n PERIOD_TARGET_PURGES_COLLECTION,\n]\n\n/** The subset of a decrypted `PeriodRecord` the window computation needs. */\nexport interface PeriodSyncRecord {\n readonly name: string\n readonly kind: 'closed' | 'opened'\n readonly endDate: string\n}\n\n/**\n * Vault-injected source of decrypted period records, wired via\n * `SyncEngine.setPeriodPullSource` (same injection pattern as\n * `setReservedLookupSource`). MUST re-read from the local store on every\n * call — the engine invokes it right after the summaries phase applied\n * freshly pulled `_periods` envelopes.\n */\nexport interface PeriodPullSource {\n periods(): Promise<readonly PeriodSyncRecord[]>\n}\n\nconst SHAPE_MSG =\n 'pull: `periods` must be an array of closed-period names, or `{ current: true }`.'\n\n/** Validate the raw `PullOptions.periods` value (runtime shape only). */\nexport function validatePeriodsOption(\n periods: unknown,\n): asserts periods is readonly string[] | { readonly current: true } {\n if (Array.isArray(periods)) {\n for (const p of periods) {\n if (typeof p !== 'string' || p.length === 0) throw new ValidationError(SHAPE_MSG)\n }\n return\n }\n if (\n typeof periods === 'object' && periods !== null &&\n (periods as { current?: unknown }).current === true &&\n Object.keys(periods).length === 1\n ) {\n return\n }\n throw new ValidationError(SHAPE_MSG)\n}\n\n/**\n * Resolve the selected `_ts` windows and return the pull-scope predicate:\n * `true` = the envelope is in scope for this pull. Tombstones and delete\n * markers are unconditionally in scope (see module doc).\n *\n * Throws `ValidationError` for a name that is not a closed period in the\n * (freshly synced) `_periods` index — a typo'd backfill must fail loudly,\n * never silently pull nothing.\n */\nexport function buildPeriodScope(\n option: readonly string[] | { readonly current: true },\n records: readonly PeriodSyncRecord[],\n): (envelope: EncryptedEnvelope) => boolean {\n const closed = records\n .filter((r) => r.kind === 'closed')\n .map((r) => ({ name: r.name, bound: periodExclusiveUpperBound(r.endDate) }))\n .sort((a, b) => a.bound.localeCompare(b.bound))\n\n let windows: ReadonlyArray<{ lower: string | null; upper: string | null }>\n if (Array.isArray(option)) {\n windows = (option as readonly string[]).map((name) => {\n const idx = closed.findIndex((c) => c.name === name)\n if (idx < 0) {\n const other = records.find((r) => r.name === name)\n throw new ValidationError(\n other\n ? `pull: period \"${name}\" is \"${other.kind}\" — only closed periods have a backfill window; use \\`{ current: true }\\` for the open period.`\n : `pull: no closed period named \"${name}\" in this vault's _periods index.`,\n )\n }\n return { lower: idx > 0 ? closed[idx - 1]!.bound : null, upper: closed[idx]!.bound }\n })\n } else {\n // { current: true } — everything at-or-after the latest closed bound\n // (everything, when no period was ever closed: with no closed periods\n // there is nothing historical to exclude, so nothing is silently dropped).\n windows = [{ lower: closed.length > 0 ? closed[closed.length - 1]!.bound : null, upper: null }]\n }\n\n return (envelope: EncryptedEnvelope): boolean => {\n if (isTombstoneShape(envelope) || isDeleteMarker(envelope)) return true\n return windows.some(\n (w) =>\n (w.lower === null || envelope._ts >= w.lower) &&\n (w.upper === null || envelope._ts < w.upper),\n )\n }\n}\n","import type {\n NoydbStore,\n DirtyEntry,\n Conflict,\n ConflictStrategy,\n CollectionConflictResolver,\n PushOptions,\n PullOptions,\n PushResult,\n PullResult,\n SyncStatus,\n EncryptedEnvelope,\n SyncMetadata,\n SyncTargetRole,\n ErasureEnforcement,\n} from '../kernel/types.js'\nimport { NOYDB_SYNC_VERSION } from '../kernel/types.js'\nimport { ConflictError, ValidationError } from '../kernel/errors.js'\nimport {\n PERIOD_SUMMARY_COLLECTIONS,\n PERIODS_COLLECTION,\n buildPeriodScope,\n validatePeriodsOption,\n type PeriodPullSource,\n} from './period-scope.js'\nimport type { NoydbEventEmitter } from '../kernel/events.js'\nimport type { SyncPolicy } from '../kernel/sync-policy.js'\nimport { SyncScheduler } from '../kernel/sync-policy.js'\nimport { isTombstoneShape, isDeleteMarker, envelopeBodySize } from '../kernel/enclave/index.js'\n\n/** #650 Task 4 (#647) — the declared reserved-lookup (`_dict_*`/`_lookup_*`) collection-name\n * registry a `SyncEngine` enumerates on pull. Explicit, not a blanket underscore-glob — other\n * `_`-prefixed namespaces keep their `loadAll`-skip semantics untouched. */\nexport interface ReservedLookupSource {\n /** Declared reserved-lookup collection names to enumerate on pull. */\n collections(): readonly string[]\n}\n\n/** Sync engine: dirty tracking, push, pull, conflict resolution, scheduling. */\nexport class SyncEngine {\n private readonly local: NoydbStore\n private readonly remote: NoydbStore\n private readonly strategy: ConflictStrategy\n private readonly emitter: NoydbEventEmitter\n private readonly vault: string\n readonly role: SyncTargetRole\n readonly label: string | undefined\n\n private dirty: DirtyEntry[] = []\n private lastPush: string | null = null\n private lastPull: string | null = null\n private loaded = false\n private autoSyncInterval: ReturnType<typeof setInterval> | null = null\n private isOnline = true\n\n /** Sync scheduler. Manages push/pull timing. */\n readonly scheduler: SyncScheduler | null\n\n /** Per-collection conflict resolvers registered by Collection instances. */\n private readonly conflictResolvers = new Map<string, CollectionConflictResolver>()\n\n /**\n * Expands a collection-name filter to include its satellite pair partner(s)\n * (spec #591 convergence rule 5b) — wired from vault satellite declaration\n * via `setPairExpander`. `undefined` when no satellite has ever been\n * declared for this vault: identity behavior, zero change for non-satellite\n * users.\n */\n private pairExpander?: (names: readonly string[]) => readonly string[]\n\n /** #598: refreshes Collection in-memory views after a sync-applied local write. Wired by the\n * vault at open. `action` (#640): `'delete'` for a pulled tombstone/delete-marker, `'put'`\n * otherwise — classified at `applyRemote`, the one choke point that holds the envelope. */\n private cacheInvalidator?: (collection: string, id: string, action: 'put' | 'delete') => Promise<void>\n\n /** Wire the Collection-cache invalidation hook (#598). Same injection pattern as `setPairExpander`. */\n setCacheInvalidator(fn: (collection: string, id: string, action: 'put' | 'delete') => Promise<void>): void {\n this.cacheInvalidator = fn\n }\n\n /** #638 Task 4: opens/flushes the vault's graph-dispatch touch batch around a whole `pull()`/\n * `push()` call, so N `applyRemote`d records feeding the same derivation/rollup/MV target\n * recompute once. Wired by the vault at open, mirroring `cacheInvalidator`. */\n private graphBatchController?: { begin(): void; flush(): Promise<void> }\n\n /** Wire the graph-dispatch batch controller (#638 Task 4). Same injection pattern as `setCacheInvalidator`. */\n setGraphBatchController(controller: { begin(): void; flush(): Promise<void> }): void {\n this.graphBatchController = controller\n }\n\n /** #650 Task 4 (#647): declared reserved-lookup collections `pull()` enumerates explicitly (the\n * store's `list()` does not skip `_`-prefixed names, unlike `loadAll()`). Wired by the vault at\n * open, same injection pattern as `setCacheInvalidator`/`setGraphBatchController`. */\n private reservedLookup?: ReservedLookupSource\n\n /** Wire the reserved-lookup source (#650 Task 4). */\n setReservedLookupSource(source: ReservedLookupSource): void {\n this.reservedLookup = source\n }\n\n /** #653: expands a (pair-expanded) collection-name filter to include the reserved `_dict_*`\n * collections those names depend on via their lookup fields — mirrors `pairExpander` so a\n * partial `push`/`pull({collections:[...]})` never drops the dictionary a named collection's\n * labels/membership rely on. Wired unconditionally from the vault's `dictKeyFieldRegistry` via\n * `setReservedDictExpander` (returns `[]` when the vault has no dict-backed collections); only\n * `undefined` on a standalone engine that was never vault-attached. */\n private reservedDictExpander?: (names: readonly string[]) => readonly string[]\n\n /** Wire the reserved-dict expansion function (#653). Same injection pattern as `setPairExpander`. */\n setReservedDictExpander(expander: (names: readonly string[]) => readonly string[]): void {\n this.reservedDictExpander = expander\n }\n\n /** #807: vault-injected source of decrypted period records — resolves the `_ts` windows a\n * `pull({ periods })` scopes to. Re-read AFTER the summaries phase, so a fresh device's\n * freshly pulled `_periods` index is what the windows are computed from. `undefined` on a\n * standalone engine that was never vault-attached (period-scoped pull then throws). */\n private periodPullSource?: PeriodPullSource\n\n /** Wire the period-window source (#807). Same injection pattern as `setReservedLookupSource`. */\n setPeriodPullSource(source: PeriodPullSource): void {\n this.periodPullSource = source\n }\n\n /** #807: KPI accumulator `applyRemote` feeds during a period-scoped `pull()` — pointed at the\n * active phase's counters (summaries → records) and cleared before pull returns. Approximate\n * by design: a push interleaved mid-pull on the same engine would attribute its converge\n * applies to the open phase; the counters are a download-budget KPI, not an audit source. */\n private pullByteSink: { records: number; bytes: number } | null = null\n\n constructor(opts: {\n local: NoydbStore\n remote: NoydbStore\n vault: string\n strategy: ConflictStrategy\n emitter: NoydbEventEmitter\n syncPolicy?: SyncPolicy\n role?: SyncTargetRole\n label?: string\n }) {\n this.local = opts.local\n this.remote = opts.remote\n this.vault = opts.vault\n this.strategy = opts.strategy\n this.emitter = opts.emitter\n this.role = opts.role ?? 'sync-peer'\n this.label = opts.label\n\n // Create a scheduler when the policy asks for ANY automatic behaviour.\n // #897: this used to test `push.mode !== 'manual'` alone, so a policy of\n // `{ push: manual, pull: interval }` silently got no scheduler and its pull\n // mode was ignored.\n const policy = opts.syncPolicy\n if (policy && (policy.push.mode !== 'manual' || policy.pull.mode !== 'manual')) {\n this.scheduler = new SyncScheduler(policy, {\n push: () => this.push().then(() => {}),\n // #618 — role-gate the SCHEDULER-INITIATED pull. The Noydb layer gates\n // pull-from-sink for explicit calls, but the scheduler calls the engine\n // directly and would bypass it: a backup/archive-only primary with an\n // `interval`/`on-focus` pull policy would pull ungated, reintroducing\n // #616. This guards the engine self-initiating on a timer; an explicit\n // `engine.pull()` still pulls for every role.\n // `collections` is set by a 'phased' sequence — one collection per phase.\n // A role-gated skip reports 'incomplete': nothing was pulled, so the\n // phase must not mark its collection ready.\n pull: (collections) =>\n this.role === 'sync-peer'\n ? this.pull(collections ? { collections: [...collections] } : undefined)\n // PullResult.errors accumulates WITHOUT throwing, so a partial\n // failure is only visible here — not via a rejected promise.\n .then(r => (r.errors.length === 0 ? 'complete' : 'incomplete'))\n : Promise.resolve('incomplete'),\n getDirtyCount: () => this.dirty.length,\n })\n } else {\n this.scheduler = null\n }\n }\n\n /** Start the sync scheduler. Called after vault is fully opened. */\n startScheduler(): void {\n this.scheduler?.start()\n }\n\n /** Stop the sync scheduler. Called on close. */\n stopScheduler(): void {\n this.scheduler?.stop()\n }\n\n /**\n * Register a per-collection conflict resolver.\n * Called by Collection when `conflictPolicy` is set.\n *\n * Pair-coupled (#591 rule 5b): registering for one satellite-pair member\n * registers the same resolver for both — the pair converges under one\n * resolution policy. Expands with whatever the pair-expander knows *at this\n * call*; resolvers registered before their pair existed are covered by\n * `remirrorPairResolvers` at pair-registration time.\n */\n registerConflictResolver(collection: string, resolver: CollectionConflictResolver): void {\n for (const n of this.pairExpander?.([collection]) ?? [collection]) this.conflictResolvers.set(n, resolver)\n }\n\n /**\n * Wire a satellite pair-expansion function (vault registration, #591 Task 11).\n * The closure re-reads the live `SatelliteRegistry` on every call, so pairs\n * declared after this is set are still picked up by `push`/`pull` filters.\n */\n setPairExpander(expander: (names: readonly string[]) => readonly string[]): void {\n this.pairExpander = expander\n }\n\n /**\n * Retroactively mirror per-collection conflict resolvers across a newly\n * registered satellite pair (#591 rule 5b) — covers the NORMAL declaration\n * order, where the base's `conflictPolicy` resolver was registered before\n * the satellite was declared (so the call-time expansion in\n * `registerConflictResolver` saw no pair yet). Tie-break: when BOTH members\n * already carry (different) resolvers, the FIRST name in `names` wins —\n * callers pass `[base, satellite]`, so the base's resolver is canonical.\n * Idempotent; a later explicit registration on either member still\n * overwrites both (last-wins, via the call-time expansion above).\n */\n remirrorPairResolvers(names: readonly string[]): void {\n const canonical = names.map(n => this.conflictResolvers.get(n)).find(r => r !== undefined)\n if (!canonical) return\n for (const n of names) this.conflictResolvers.set(n, canonical)\n }\n\n /** Record a local change for later push. */\n async trackChange(collection: string, id: string, action: 'put' | 'delete', version: number): Promise<void> {\n await this.ensureLoaded()\n\n // Deduplicate: if same collection+id already in dirty, update it\n const idx = this.dirty.findIndex(d => d.collection === collection && d.id === id)\n const entry: DirtyEntry = {\n vault: this.vault,\n collection,\n id,\n action,\n version,\n timestamp: new Date().toISOString(),\n }\n\n if (idx >= 0) {\n this.dirty[idx] = entry\n } else {\n this.dirty.push(entry)\n }\n\n await this.persistMeta()\n\n // Notify scheduler of the write (triggers on-change or debounce)\n this.scheduler?.notifyChange()\n }\n\n /** Remove a dirty entry (satellite fan-out revert cleanup — spec #591). */\n async removeDirty(collection: string, id: string): Promise<void> {\n const before = this.dirty.length\n this.dirty = this.dirty.filter(d => !(d.collection === collection && d.id === id))\n if (this.dirty.length !== before) await this.persistMeta()\n }\n\n /** Push dirty records to remote adapter. Accepts optional `PushOptions` for partial sync. */\n async push(options?: PushOptions): Promise<PushResult> {\n await this.ensureLoaded()\n this.graphBatchController?.begin() // #638 Task 4\n\n let pushed = 0\n const conflicts: Conflict[] = []\n const erasures: ErasureEnforcement[] = []\n const errors: Error[] = []\n const completed: number[] = []\n\n // Partial sync: expand the filter to cover satellite pair partners (#591 rule 5b), then the\n // reserved `_dict_*` collections those (expanded) names depend on (#653) — a locally-dirty\n // dict edit pushes alongside push({collections:['orders']}), symmetric with pull below.\n const expanded = options?.collections ? (this.pairExpander?.(options.collections) ?? options.collections) : null\n const filter = expanded ? new Set([...expanded, ...(this.reservedDictExpander?.(expanded) ?? [])]) : null\n\n for (let i = 0; i < this.dirty.length; i++) {\n const entry = this.dirty[i]!\n\n // Partial sync: skip collections not in the filter\n if (filter && !filter.has(entry.collection)) {\n continue\n }\n\n try {\n if (entry.action === 'delete') {\n await this.remote.delete(this.vault, entry.collection, entry.id)\n completed.push(i)\n pushed++\n } else {\n const envelope = await this.local.get(this.vault, entry.collection, entry.id)\n if (!envelope) {\n // Record was deleted locally after being marked dirty\n completed.push(i)\n continue\n }\n\n if (isTombstoneShape(envelope)) {\n // #590: a tombstone push is an erasure assertion — unconditional,\n // no CAS, no conflict resolution. Erasure always wins.\n await this.remote.put(this.vault, entry.collection, entry.id, envelope)\n completed.push(i)\n pushed++\n continue\n }\n\n try {\n await this.remote.put(\n this.vault,\n entry.collection,\n entry.id,\n envelope,\n entry.version - 1,\n )\n completed.push(i)\n pushed++\n } catch (err) {\n if (err instanceof ConflictError) {\n const remoteEnvelope = await this.remote.get(this.vault, entry.collection, entry.id)\n if (remoteEnvelope) {\n if (isTombstoneShape(remoteEnvelope)) {\n // #590: remote already shredded this record — enforce locally,\n // never resolve. Resolvers must not overrule an erasure.\n await this.applyRemote(entry.collection, entry.id, remoteEnvelope)\n erasures.push(this.reportErasure(entry.collection, entry.id, remoteEnvelope, envelope, 'push'))\n completed.push(i)\n } else if (\n remoteEnvelope._v === envelope._v &&\n isDeleteMarker(remoteEnvelope) !== isDeleteMarker(envelope) &&\n !this.conflictResolvers.get(entry.collection)\n ) {\n // #589: a same-_v delete-vs-edit tie on the push channel. handleConflict's db-level\n // 'version' default would resolve it to local-wins; the tie rule consults ONLY the\n // per-collection resolver, else delete-wins. (When a per-collection resolver IS set,\n // fall through to handleConflict, which already honors it — incl. the merged case.)\n if (isDeleteMarker(remoteEnvelope)) {\n // remote already deleted → converge locally, drop our (edit) push\n await this.applyRemote(entry.collection, entry.id, remoteEnvelope)\n completed.push(i)\n } else {\n // our local is the marker → force the delete onto the remote (unconditional put)\n await this.remote.put(this.vault, entry.collection, entry.id, envelope)\n completed.push(i)\n pushed++\n }\n } else {\n const { handled, conflict } = await this.handleConflict(\n entry.collection,\n entry.id,\n envelope,\n remoteEnvelope,\n 'push',\n )\n conflicts.push(conflict)\n if (handled === 'local') {\n await this.remote.put(this.vault, entry.collection, entry.id, conflict.local)\n completed.push(i)\n pushed++\n } else if (handled === 'remote') {\n await this.applyRemote(entry.collection, entry.id, conflict.remote)\n completed.push(i)\n } else if (handled === 'merged' && conflict.local !== envelope) {\n // Merged envelope is stored in conflict.local (the winner)\n const merged = conflict.local\n await this.remote.put(this.vault, entry.collection, entry.id, merged)\n await this.applyRemote(entry.collection, entry.id, merged)\n completed.push(i)\n pushed++\n }\n // handled === 'deferred': leave in dirty log\n }\n }\n } else {\n throw err\n }\n }\n }\n } catch (err) {\n errors.push(err instanceof Error ? err : new Error(String(err)))\n }\n }\n\n // Remove completed entries from dirty log (reverse order to preserve indices)\n for (const i of completed.sort((a, b) => b - a)) {\n this.dirty.splice(i, 1)\n }\n\n this.lastPush = new Date().toISOString()\n try {\n await this.persistMeta()\n } finally {\n // #644 item 1: flush (which clears `_graphBatch` unconditionally, vault.ts:1316-1320) must\n // run even if `persistMeta()` throws — else a throw here leaves the batch open, corrupting\n // the NEXT pull()/push() call's wave with THIS call's stale touches.\n await this.graphBatchController?.flush() // #638 Task 4\n }\n\n const result: PushResult = { pushed, conflicts, errors, erasures }\n this.emitter.emit('sync:push', result)\n return result\n }\n\n /** Pull remote records to local adapter. Accepts optional `PullOptions` for partial sync. */\n async pull(options?: PullOptions): Promise<PullResult> {\n await this.ensureLoaded()\n\n let pulled = 0\n const conflicts: Conflict[] = []\n const erasures: ErasureEnforcement[] = []\n const errors: Error[] = []\n\n // ── #807 period-scoped pull: validate the option, sync the period summaries\n // (`_periods` + companions — the navigation index, ALWAYS pulled in full,\n // exempt from every filter), then resolve the selected `_ts` windows from the\n // freshly synced index. All BEFORE the graph batch opens, so an invalid\n // option throws without leaving a batch dangling; summary applies route\n // through `applyRemote` like any pull (reserved names have no Collection\n // instance, so the invalidator/graph seams are no-ops for them). ──\n let periodScope: ((envelope: EncryptedEnvelope) => boolean) | null = null\n let phases: { summaries: { records: number; bytes: number }; records: { records: number; bytes: number } } | null = null\n if (options?.periods !== undefined) {\n validatePeriodsOption(options.periods)\n if (!this.periodPullSource) {\n throw new ValidationError(\n 'period-scoped pull requires a vault-attached sync engine (open the vault via createNoydb + openVault).',\n )\n }\n phases = { summaries: { records: 0, bytes: 0 }, records: { records: 0, bytes: 0 } }\n let remotePeriodCount = 0\n this.pullByteSink = phases.summaries\n try {\n for (const collName of PERIOD_SUMMARY_COLLECTIONS) {\n for (const id of await this.remote.list(this.vault, collName)) {\n if (collName === PERIODS_COLLECTION) remotePeriodCount++\n try {\n const remoteEnvelope = await this.remote.get(this.vault, collName, id)\n if (!remoteEnvelope) continue\n const localEnvelope = await this.local.get(this.vault, collName, id)\n if (!localEnvelope || remoteEnvelope._v > localEnvelope._v) {\n await this.applyRemote(collName, id, remoteEnvelope)\n pulled++\n }\n } catch (err) {\n errors.push(err instanceof Error ? err : new Error(String(err)))\n }\n }\n }\n } finally {\n this.pullByteSink = null\n }\n const periodRecords = await this.periodPullSource.periods()\n if (remotePeriodCount > 0 && periodRecords.length === 0) {\n throw new ValidationError(\n 'period-scoped pull: the vault holds _periods records but none are readable — enable the ' +\n 'periods service (`periodsStrategy: withPeriods()`) so pull can resolve the period windows.',\n )\n }\n periodScope = buildPeriodScope(options.periods, periodRecords)\n }\n\n this.graphBatchController?.begin() // #638 Task 4\n if (phases) this.pullByteSink = phases.records\n\n // Partial sync: expand the filter to cover satellite pair partners (#591 rule 5b), then the\n // reserved `_dict_*` collections those (expanded) names depend on (#653) — adopters never\n // name `_dict_*` in a `collections` filter, so a named collection's lookup-field dependency\n // must be pulled alongside it or its labels/membership go stale.\n const expanded = options?.collections ? (this.pairExpander?.(options.collections) ?? options.collections) : null\n const filter = expanded ? new Set([...expanded, ...(this.reservedDictExpander?.(expanded) ?? [])]) : null\n\n try {\n const remoteSnapshot = await this.remote.loadAll(this.vault)\n\n for (const [collName, records] of Object.entries(remoteSnapshot)) {\n // Partial sync: skip collections not in the filter\n if (filter && !filter.has(collName)) {\n continue\n }\n\n for (const [id, remoteEnvelope] of Object.entries(records)) {\n // Partial sync: modifiedSince filter — arriving tombstones are exempt (#590):\n // an erasure must never be skipped by partial sync.\n if (\n options?.modifiedSince &&\n remoteEnvelope._ts <= options.modifiedSince &&\n !isTombstoneShape(remoteEnvelope) &&\n !isDeleteMarker(remoteEnvelope)\n ) {\n continue\n }\n\n // #807: period scope — a record whose `_ts` falls outside every selected\n // window is skipped. Tombstones and delete markers are ALWAYS in scope\n // (built into the predicate): an erasure or delete must never be skipped\n // by partial sync, mirroring the modifiedSince exemption above — that is\n // what lets a device that never pulled a period backfill it later without\n // resurrecting its deleted records.\n if (periodScope !== null && !periodScope(remoteEnvelope)) {\n continue\n }\n\n try {\n const localEnvelope = await this.local.get(this.vault, collName, id)\n const remoteIsTombstone = isTombstoneShape(remoteEnvelope)\n\n if (!localEnvelope) {\n // New record from remote (tombstones included — durable erasure evidence)\n await this.applyRemote(collName, id, remoteEnvelope)\n pulled++\n } else if (isTombstoneShape(localEnvelope)) {\n if (remoteIsTombstone) {\n // Both shredded — keep the higher version counter\n if (remoteEnvelope._v > localEnvelope._v) await this.applyRemote(collName, id, remoteEnvelope)\n } else {\n // Terminal rule (#590): a tombstone is never overwritten by a live\n // envelope, regardless of _v. Re-assert the shred outward instead.\n erasures.push(await this.reassertTombstone(collName, id, localEnvelope, remoteEnvelope))\n }\n } else if (remoteIsTombstone) {\n // Terminal rule (#590): an arriving tombstone wins over any local live\n // envelope — even a newer, dirty one. Resolvers are never consulted.\n const wasDirty = this.dirty.some(d => d.collection === collName && d.id === id)\n await this.applyRemote(collName, id, remoteEnvelope)\n this.dirty = this.dirty.filter(d => !(d.collection === collName && d.id === id))\n pulled++\n if (wasDirty) erasures.push(this.reportErasure(collName, id, remoteEnvelope, localEnvelope, 'pull'))\n } else if (\n remoteEnvelope._v === localEnvelope._v &&\n isDeleteMarker(remoteEnvelope) !== isDeleteMarker(localEnvelope)\n ) {\n // #589: true concurrent delete-vs-edit at the SAME version. Version order\n // can't break the tie. A per-collection resolver decides if one is set;\n // otherwise DELETE wins (the db-level 'version' default is deliberately NOT\n // consulted — it would resolve a tie to local-wins).\n const resolver = this.conflictResolvers.get(collName)\n if (resolver) {\n const winner = await resolver(id, localEnvelope, remoteEnvelope)\n if (winner !== localEnvelope && winner !== null) {\n // #589 (review): a novel/merged winner (neither side verbatim) must also be\n // pushed to remote — mirrors handleConflict's 'merged' handling — so both\n // sides converge instead of local applying it while remote keeps its old copy.\n if (winner !== remoteEnvelope) {\n await this.remote.put(this.vault, collName, id, winner)\n }\n await this.applyRemote(collName, id, winner)\n this.dirty = this.dirty.filter(d => !(d.collection === collName && d.id === id))\n pulled++\n }\n // winner === localEnvelope or null → keep local (its dirty entry, if any, pushes out)\n } else if (isDeleteMarker(remoteEnvelope)) {\n // no resolver → delete wins; the incoming marker is the delete\n await this.applyRemote(collName, id, remoteEnvelope)\n this.dirty = this.dirty.filter(d => !(d.collection === collName && d.id === id))\n pulled++\n }\n // no resolver and LOCAL is the marker → keep local marker; its dirty 'put' pushes it outward\n } else if (remoteEnvelope._v > localEnvelope._v) {\n // Remote is newer — check if we have a dirty entry for this\n const isDirty = this.dirty.some(d => d.collection === collName && d.id === id)\n if (isDirty) {\n // Both changed — conflict\n const { handled, conflict } = await this.handleConflict(\n collName,\n id,\n localEnvelope,\n remoteEnvelope,\n 'pull',\n )\n conflicts.push(conflict)\n if (handled === 'remote') {\n await this.applyRemote(collName, id, conflict.remote)\n this.dirty = this.dirty.filter(d => !(d.collection === collName && d.id === id))\n pulled++\n } else if (handled === 'merged' && conflict.local !== localEnvelope) {\n const merged = conflict.local\n await this.applyRemote(collName, id, merged)\n this.dirty = this.dirty.filter(d => !(d.collection === collName && d.id === id))\n pulled++\n }\n // 'local' or 'deferred': push handles it\n } else {\n // Remote is newer, no local changes — update\n await this.applyRemote(collName, id, remoteEnvelope)\n pulled++\n }\n }\n // Same version or local is newer — skip (push will handle)\n } catch (err) {\n errors.push(err instanceof Error ? err : new Error(String(err)))\n }\n }\n }\n\n // #650 Task 4 (#647) — reserved lookup collections (`_dict_*`/`_lookup_*`) are invisible to\n // `remote.loadAll()` above (the store contract skips `_`-prefixed names) but DO sync via\n // this EXPLICIT, declared registry — not a blanket underscore-glob; other `_` namespaces\n // keep their `loadAll`-skip semantics untouched. Applied through the SAME `applyRemote` path,\n // still inside this try block, BEFORE `persistMeta`/`flush` below — so a pulled vocabulary\n // row lands before the wave (flushed at the end of `pull()`) recomputes any dependent.\n //\n // #647 fix wave 1: `LookupHandle.delete()`/`rename()` now write a version-ordered\n // delete-marker row (mirroring #589) instead of a raw adapter delete, so a removed key is\n // a normal row here too — reachable via `remote.list()` like any other id, applied by\n // version like any other write. The only reserved-tier-specific rule is the same-version\n // tie-break below: reserved lookups have no per-collection conflict-resolver concept\n // (dictionaries are admin-edited, not multi-actor-negotiated), so a converging delete\n // unconditionally wins over a same-version live edit.\n //\n // #807: the period scope deliberately does NOT apply here — vocabularies/config rows are\n // not period-partitioned data (a thin client's lookups must be complete regardless of\n // which periods it pulled), the same always-pull rule as the `_periods` summaries phase.\n for (const collName of this.reservedLookup?.collections() ?? []) {\n if (filter && !filter.has(collName)) continue\n for (const id of await this.remote.list(this.vault, collName)) {\n try {\n const remoteEnvelope = await this.remote.get(this.vault, collName, id)\n if (!remoteEnvelope) continue\n\n // Partial sync: modifiedSince filter — a delete marker is exempt, mirroring the\n // main loop's tombstone/marker exemption above: a deletion must never be silently\n // skipped by a time-window partial pull.\n if (\n options?.modifiedSince &&\n remoteEnvelope._ts <= options.modifiedSince &&\n !isDeleteMarker(remoteEnvelope)\n ) {\n continue\n }\n\n const localEnvelope = await this.local.get(this.vault, collName, id)\n if (!localEnvelope) {\n await this.applyRemote(collName, id, remoteEnvelope)\n pulled++\n } else if (\n remoteEnvelope._v === localEnvelope._v &&\n isDeleteMarker(remoteEnvelope) !== isDeleteMarker(localEnvelope)\n ) {\n // Same-version delete-vs-edit tie (#589's rule, reserved-tier default: no\n // resolver concept here, so delete always wins).\n if (isDeleteMarker(remoteEnvelope)) {\n await this.applyRemote(collName, id, remoteEnvelope)\n this.dirty = this.dirty.filter(d => !(d.collection === collName && d.id === id))\n pulled++\n }\n // else: local already holds the marker (a local delete not yet observed\n // remotely) — keep it; push re-asserts it.\n } else if (remoteEnvelope._v > localEnvelope._v) {\n await this.applyRemote(collName, id, remoteEnvelope)\n // Drop any now-superseded local dirty entry so a subsequent push doesn't\n // redundantly re-fight a CAS conflict over content pull just overwrote.\n this.dirty = this.dirty.filter(d => !(d.collection === collName && d.id === id))\n pulled++\n }\n } catch (err) {\n errors.push(err instanceof Error ? err : new Error(String(err)))\n }\n }\n }\n } catch (err) {\n errors.push(err instanceof Error ? err : new Error(String(err)))\n }\n\n this.lastPull = new Date().toISOString()\n try {\n await this.persistMeta()\n } finally {\n this.pullByteSink = null // #807: stop attributing applies to this pull's KPI counters\n // #644 item 1: flush must run even if `persistMeta()` throws (see push()'s identical guard).\n await this.graphBatchController?.flush() // #638 Task 4\n }\n\n const result: PullResult = { pulled, conflicts, errors, erasures, ...(phases !== null ? { phases } : {}) }\n this.emitter.emit('sync:pull', result)\n return result\n }\n\n /** Bidirectional sync: pull then push. */\n async sync(options?: { push?: PushOptions; pull?: PullOptions }): Promise<{ pull: PullResult; push: PushResult }> {\n const pullResult = await this.pull(options?.pull)\n const pushResult = await this.push(options?.push)\n return { pull: pullResult, push: pushResult }\n }\n\n /**\n * Push a specific subset of dirty entries (for sync transactions, ).\n * Entries are matched by collection+id from the dirty log; matched entries\n * are removed from the dirty log on success.\n */\n async pushFiltered(predicate: (entry: DirtyEntry) => boolean): Promise<PushResult> {\n await this.ensureLoaded()\n\n let pushed = 0\n const conflicts: Conflict[] = []\n const erasures: ErasureEnforcement[] = []\n const errors: Error[] = []\n const completed: number[] = []\n\n for (let i = 0; i < this.dirty.length; i++) {\n const entry = this.dirty[i]!\n if (!predicate(entry)) continue\n\n try {\n if (entry.action === 'delete') {\n await this.remote.delete(this.vault, entry.collection, entry.id)\n completed.push(i)\n pushed++\n } else {\n const envelope = await this.local.get(this.vault, entry.collection, entry.id)\n if (!envelope) {\n completed.push(i)\n continue\n }\n\n if (isTombstoneShape(envelope)) {\n // #590: a tombstone push is an erasure assertion — unconditional,\n // no CAS, no conflict resolution. Erasure always wins.\n await this.remote.put(this.vault, entry.collection, entry.id, envelope)\n completed.push(i)\n pushed++\n continue\n }\n\n try {\n await this.remote.put(\n this.vault,\n entry.collection,\n entry.id,\n envelope,\n entry.version - 1,\n )\n completed.push(i)\n pushed++\n } catch (err) {\n if (err instanceof ConflictError) {\n const remoteEnvelope = await this.remote.get(this.vault, entry.collection, entry.id)\n if (remoteEnvelope) {\n if (isTombstoneShape(remoteEnvelope)) {\n // #590: remote already shredded this record — enforce locally,\n // never resolve. Resolvers must not overrule an erasure.\n await this.applyRemote(entry.collection, entry.id, remoteEnvelope)\n erasures.push(this.reportErasure(entry.collection, entry.id, remoteEnvelope, envelope, 'push'))\n completed.push(i)\n } else if (\n remoteEnvelope._v === envelope._v &&\n isDeleteMarker(remoteEnvelope) !== isDeleteMarker(envelope) &&\n !this.conflictResolvers.get(entry.collection)\n ) {\n // #589: a same-_v delete-vs-edit tie on the push channel. handleConflict's db-level\n // 'version' default would resolve it to local-wins; the tie rule consults ONLY the\n // per-collection resolver, else delete-wins. (When a per-collection resolver IS set,\n // fall through to handleConflict, which already honors it — incl. the merged case.)\n if (isDeleteMarker(remoteEnvelope)) {\n // remote already deleted → converge locally, drop our (edit) push\n await this.applyRemote(entry.collection, entry.id, remoteEnvelope)\n completed.push(i)\n } else {\n // our local is the marker → force the delete onto the remote (unconditional put)\n await this.remote.put(this.vault, entry.collection, entry.id, envelope)\n completed.push(i)\n pushed++\n }\n } else {\n const { handled, conflict } = await this.handleConflict(\n entry.collection,\n entry.id,\n envelope,\n remoteEnvelope,\n 'push',\n )\n conflicts.push(conflict)\n if (handled === 'local') {\n await this.remote.put(this.vault, entry.collection, entry.id, conflict.local)\n completed.push(i)\n pushed++\n } else if (handled === 'remote') {\n await this.applyRemote(entry.collection, entry.id, conflict.remote)\n completed.push(i)\n } else if (handled === 'merged' && conflict.local !== envelope) {\n const merged = conflict.local\n await this.remote.put(this.vault, entry.collection, entry.id, merged)\n await this.applyRemote(entry.collection, entry.id, merged)\n completed.push(i)\n pushed++\n }\n }\n }\n } else {\n throw err\n }\n }\n }\n } catch (err) {\n errors.push(err instanceof Error ? err : new Error(String(err)))\n }\n }\n\n for (const i of completed.sort((a, b) => b - a)) {\n this.dirty.splice(i, 1)\n }\n\n this.lastPush = new Date().toISOString()\n await this.persistMeta()\n\n const result: PushResult = { pushed, conflicts, errors, erasures }\n this.emitter.emit('sync:push', result)\n return result\n }\n\n /** Get current sync status. */\n status(): SyncStatus {\n // #809 — readiness rides the status surface that already exists rather than\n // a second accessor, so an app asks one question to learn everything about\n // this vault's sync. Absent entirely unless a phased policy is running.\n const scheduled = this.scheduler?.status\n return {\n dirty: this.dirty.length,\n lastPush: this.lastPush,\n lastPull: this.lastPull,\n online: this.isOnline,\n ...(scheduled && scheduled.readiness.size > 0\n ? { readiness: scheduled.readiness, phase: scheduled.phase }\n : {}),\n }\n }\n\n // ─── Auto-Sync ───────────────────────────────────────────────────\n\n /** Start auto-sync: listen for online/offline events, optional periodic sync. */\n startAutoSync(intervalMs?: number): void {\n // Online/offline detection\n if (typeof globalThis.addEventListener === 'function') {\n globalThis.addEventListener('online', this.handleOnline)\n globalThis.addEventListener('offline', this.handleOffline)\n }\n\n // Periodic sync\n if (intervalMs && intervalMs > 0) {\n this.autoSyncInterval = setInterval(() => {\n if (this.isOnline) {\n void this.sync()\n }\n }, intervalMs)\n }\n }\n\n /** Stop auto-sync and scheduler. */\n stopAutoSync(): void {\n this.stopScheduler()\n if (typeof globalThis.removeEventListener === 'function') {\n globalThis.removeEventListener('online', this.handleOnline)\n globalThis.removeEventListener('offline', this.handleOffline)\n }\n if (this.autoSyncInterval) {\n clearInterval(this.autoSyncInterval)\n this.autoSyncInterval = null\n }\n }\n\n private handleOnline = (): void => {\n this.isOnline = true\n this.emitter.emit('sync:online', undefined as never)\n void this.sync()\n }\n\n private handleOffline = (): void => {\n this.isOnline = false\n this.emitter.emit('sync:offline', undefined as never)\n }\n\n /** Apply an envelope to the local store and refresh in-memory views (#598). `action` (#640):\n * classified HERE — the one choke point that still holds the envelope — so the\n * cacheInvalidator seam (and, downstream, the dispatch wave) can tell a pulled delete from an\n * ordinary put. `isTombstoneShape` covers a forgotten-elsewhere record arriving as a shred;\n * `isDeleteMarker` covers an ordinary (#589) delete — both route to the SAME 'delete' action\n * (sync delete ≠ forget: freshness only, no shred/residue channel on the receiving side). */\n private async applyRemote(collection: string, id: string, envelope: EncryptedEnvelope): Promise<void> {\n await this.local.put(this.vault, collection, id, envelope)\n if (this.pullByteSink !== null) {\n // #807: KPI — one applied envelope; bytes ≈ ciphertext payload size.\n this.pullByteSink.records++\n this.pullByteSink.bytes += envelopeBodySize(envelope)\n }\n const action: 'put' | 'delete' = isTombstoneShape(envelope) || isDeleteMarker(envelope) ? 'delete' : 'put'\n await this.cacheInvalidator?.(collection, id, action)\n }\n\n /** Record + emit a tombstone enforcement (#590). */\n private reportErasure(\n collection: string, id: string,\n tombstone: EncryptedEnvelope, suppressed: EncryptedEnvelope,\n direction: 'pull' | 'push',\n ): ErasureEnforcement {\n const enforcement: ErasureEnforcement = { vault: this.vault, collection, id, tombstone, suppressed, direction }\n this.emitter.emit('sync:erasure', enforcement)\n return enforcement\n }\n\n /**\n * Re-assert a local tombstone over a live remote envelope (#590): the shred\n * wins in both directions, regardless of `_v`. Bumps the tombstone to the\n * suppressed envelope's `_v` when higher so per-key version counters stay\n * monotonic on every store; `_by` (the shredding actor) is preserved.\n */\n private async reassertTombstone(\n collection: string, id: string,\n tombstone: EncryptedEnvelope, suppressedRemote: EncryptedEnvelope,\n ): Promise<ErasureEnforcement> {\n let winner = tombstone\n if (suppressedRemote._v > tombstone._v) {\n winner = { ...tombstone, _v: suppressedRemote._v, _ts: new Date().toISOString() }\n await this.applyRemote(collection, id, winner)\n }\n await this.remote.put(this.vault, collection, id, winner)\n // The re-assert already delivered the tombstone remotely — drop any dirty\n // entry so the following push doesn't redundantly re-put it.\n this.dirty = this.dirty.filter(d => !(d.collection === collection && d.id === id))\n return this.reportErasure(collection, id, winner, suppressedRemote, 'pull')\n }\n\n /**\n * Resolve a conflict, checking per-collection resolvers first,\n * then falling back to the db-level `ConflictStrategy`.\n *\n * Returns the resolved `Conflict` object (possibly with `resolve` set for\n * manual mode) and a `handled` discriminant:\n * - `'local'` — keep the local envelope; push it to remote.\n * - `'remote'` — keep the remote envelope; update local.\n * - `'merged'` — a custom merge fn produced a new envelope stored as `conflict.local`.\n * - `'deferred'` — manual mode, resolve was not called synchronously.\n */\n private async handleConflict(\n collection: string,\n id: string,\n local: EncryptedEnvelope,\n remote: EncryptedEnvelope,\n _phase: 'push' | 'pull',\n ): Promise<{ handled: 'local' | 'remote' | 'merged' | 'deferred'; conflict: Conflict }> {\n const resolver = this.conflictResolvers.get(collection)\n\n if (resolver) {\n // Per-collection resolver is responsible for emitting sync:conflict\n // (manual policy emits with a resolve callback; LWW/FWW/custom are silent).\n const winner = await resolver(id, local, remote)\n const base: Conflict = {\n vault: this.vault,\n collection,\n id,\n local,\n remote,\n localVersion: local._v,\n remoteVersion: remote._v,\n }\n if (winner === null) return { handled: 'deferred', conflict: base }\n if (winner === local) return { handled: 'local', conflict: base }\n if (winner === remote) return { handled: 'remote', conflict: base }\n // Custom merge fn produced a new envelope — store as conflict.local for the caller\n return {\n handled: 'merged',\n conflict: { ...base, local: winner, localVersion: winner._v },\n }\n }\n\n // Fall back to db-level strategy — emit once\n const baseConflict: Conflict = {\n vault: this.vault,\n collection,\n id,\n local,\n remote,\n localVersion: local._v,\n remoteVersion: remote._v,\n }\n this.emitter.emit('sync:conflict', baseConflict)\n const side = this.legacyResolve(baseConflict)\n return { handled: side, conflict: baseConflict }\n }\n\n /** DB-level ConflictStrategy resolution (legacy, kept for backward compat). */\n private legacyResolve(conflict: Conflict): 'local' | 'remote' {\n if (typeof this.strategy === 'function') {\n return this.strategy(conflict)\n }\n switch (this.strategy) {\n case 'local-wins': return 'local'\n case 'remote-wins': return 'remote'\n case 'version':\n default:\n return conflict.localVersion >= conflict.remoteVersion ? 'local' : 'remote'\n }\n }\n\n // ─── Persistence ─────────────────────────────────────────────────\n\n private async ensureLoaded(): Promise<void> {\n if (this.loaded) return\n\n const envelope = await this.local.get(this.vault, '_sync', 'meta')\n if (envelope) {\n const meta = JSON.parse(envelope._data) as SyncMetadata\n this.dirty = [...meta.dirty]\n this.lastPush = meta.last_push\n this.lastPull = meta.last_pull\n }\n\n this.loaded = true\n }\n\n private async persistMeta(): Promise<void> {\n const meta: SyncMetadata = {\n _noydb_sync: NOYDB_SYNC_VERSION,\n last_push: this.lastPush,\n last_pull: this.lastPull,\n dirty: this.dirty,\n }\n\n const envelope: EncryptedEnvelope = {\n _noydb: 1,\n _v: 1,\n _ts: new Date().toISOString(),\n _iv: '',\n _data: JSON.stringify(meta),\n }\n\n await this.local.put(this.vault, '_sync', 'meta', envelope)\n }\n}\n","import type { SyncTransactionResult } from '../kernel/types.js'\nimport type { SyncEngine } from './engine.js'\nimport type { Vault } from '../kernel/vault.js'\n\ninterface TxOp {\n readonly type: 'put' | 'delete'\n readonly collection: string\n readonly id: string\n readonly record?: unknown\n}\n\n/**\n * Sync transaction.\n *\n * Stages local writes and then pushes only those records to remote in a\n * single batch. If any record conflicts during the push, the result\n * carries `status: 'conflict'` — no automatic rollback is performed;\n * the caller handles conflict resolution.\n *\n * Obtain via `db.transaction(compartmentName)`.\n */\nexport class SyncTransaction {\n private readonly comp: Vault\n private readonly engine: SyncEngine\n private readonly ops: TxOp[] = []\n\n /** @internal — constructed by `Noydb.transaction()` */\n constructor(comp: Vault, engine: SyncEngine) {\n this.comp = comp\n this.engine = engine\n }\n\n /** Stage a record write. Does not write to any adapter until `commit()`. */\n put(collection: string, id: string, record: unknown): this {\n this.ops.push({ type: 'put', collection, id, record })\n return this\n }\n\n /** Stage a record delete. Does not write to any adapter until `commit()`. */\n delete(collection: string, id: string): this {\n this.ops.push({ type: 'delete', collection, id })\n return this\n }\n\n /**\n * Commit the transaction.\n *\n * Phase 1 — writes all staged operations to the local adapter via the\n * collection layer (encryption + dirty-log tracking).\n *\n * Phase 2 — pushes only the records that were written in this\n * transaction to the remote adapter. Existing dirty entries from\n * outside this transaction are not affected.\n *\n * If any record conflicts during the push, `status` is `'conflict'`\n * and `conflicts` lists the affected records. No automatic rollback is\n * performed.\n */\n async commit(): Promise<SyncTransactionResult> {\n // Phase 1: write all staged ops to local via collection layer\n for (const op of this.ops) {\n if (op.type === 'put') {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n await (this.comp.collection<any>(op.collection)).put(op.id, op.record as any)\n } else {\n await this.comp.collection(op.collection).delete(op.id)\n }\n }\n\n // Phase 2: push only the records from this transaction\n const opSet = new Set<string>()\n for (const op of this.ops) {\n opSet.add(`${op.collection}::${op.id}`)\n }\n\n const pushResult = await this.engine.pushFiltered(\n (entry) => opSet.has(`${entry.collection}::${entry.id}`),\n )\n\n return {\n status: pushResult.conflicts.length > 0 ? 'conflict' : 'committed',\n pushed: pushResult.pushed,\n conflicts: pushResult.conflicts,\n ...(pushResult.erasures?.length ? { erasures: pushResult.erasures } : {}),\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/kernel/validation.ts","../src/with-party/team/reserved-secret-collections.ts","../src/with-party/team/keyring.ts"],"sourcesContent":["/**\n * Secret validation — phrase format (per the three-tier session-tiers\n * design, locked 2026-05-04).\n *\n * Secrets are **phrases**: multiple simple words, easy to remember,\n * structurally constrained so a weak choice cannot silently collapse the\n * security floor. The format is intentionally narrow: lowercase letters\n * and single spaces only, no punctuation, no symbols, no digits.\n *\n * - Default minimum: 6 words (~77 bits with the 7,776-word EFF list).\n * - Strict minimum: 8 words (~103 bits).\n * - Per-word minimum: 3 characters (excludes \"a\", \"is\", \"of\").\n * - Adjacent repeats rejected (\"the the\").\n *\n * The hub runs validation default-on at every secret ingress\n * (`createOwnerKeyring`, `grant`, `rotateSecret`); test fixtures and\n * CLI scripts override via `{ allowWeakSecret: true }`.\n *\n * @module\n */\nimport { NoydbError, ValidationError } from './errors.js'\n\n/** All reasons a phrase can be rejected. */\nexport type WeakSecretReason =\n | 'empty'\n | 'invalid-chars'\n | 'leading-or-trailing-space'\n | 'double-space'\n | 'too-few-words'\n | 'word-too-short'\n | 'repeated-adjacent'\n\n/** Per-vault knobs. Aligns with `VaultPolicy.secret`. */\nexport interface SecretPolicy {\n /** Minimum number of words. Default 6. Strict policy uses 8. */\n readonly minWords?: number\n /** Minimum characters per word. Default 3. */\n readonly minWordLength?: number\n /** Reject adjacent identical words (\"the the\"). Default true. */\n readonly rejectRepeatedAdjacent?: boolean\n /**\n * Override the default character-class rule (`/^[a-z]+( [a-z]+)*$/`).\n *\n * The hub's strict default is lowercase-letters-and-single-spaces\n * because that's what the EFF wordlist generator emits and what\n * most attacker password lists are keyed on. Use this knob to allow\n * digits, uppercase, hyphens, or non-Latin scripts when the\n * consumer's audience needs them — e.g.:\n *\n * ```ts\n * // Thai + English mix with digits permitted\n * pattern: /^[\\p{L}0-9 ]+( [\\p{L}0-9 ]+)*$/u\n *\n * // Allow uppercase + hyphens (secret-with-hyphens style)\n * pattern: /^[A-Za-z]+([- ][A-Za-z]+)*$/\n * ```\n *\n * The OTHER structural rules still apply (min-words split by space,\n * min-word-length, repeated-adjacent, leading/trailing whitespace,\n * double-space). For non-space-delimited word semantics, use\n * {@link customValidator} instead.\n *\n */\n readonly pattern?: RegExp\n /**\n * Replace ALL validation entirely with a custom function. When set,\n * none of the other SecretPolicy fields apply — the consumer\n * owns every rule (word splitting, character classes, entropy\n * thresholds, allowlist/denylist). Use sparingly; this is the\n * escape hatch for domain-specific phrase formats:\n *\n * - Localized wordlists with non-space word boundaries\n * - BIP-39 seed phrases (24 words, fixed wordlist, etc.)\n * - Organization-specific HR password policies\n *\n * The returned `SecretValidationResult` is what\n * {@link assertStrongSecret} dispatches on — `ok: true` accepts;\n * `ok: false` throws `WeakSecretError` with the supplied reason.\n *\n */\n readonly customValidator?: (phrase: string) => SecretValidationResult\n}\n\n/** Result of a check. Discriminated union — compile-time exhaustive. */\nexport type SecretValidationResult =\n | { readonly ok: true; readonly words: number }\n | {\n readonly ok: false\n readonly reason: WeakSecretReason\n readonly minimum?: number\n readonly got?: number\n }\n\n/**\n * Thrown by `assertStrongSecret()` and by every hub ingress\n * point (`createOwnerKeyring`, `grant`, `rotateSecret`) when a\n * supplied phrase fails the structural rules above.\n */\nexport class WeakSecretError extends NoydbError {\n readonly reason: WeakSecretReason\n readonly suggestion: string\n constructor(reason: WeakSecretReason, suggestion: string) {\n super('WEAK_SECRET', `Weak secret (${reason}). ${suggestion}`)\n this.name = 'WeakSecretError'\n this.reason = reason\n this.suggestion = suggestion\n }\n}\n\nconst DEFAULT_MIN_WORDS = 6\nconst DEFAULT_MIN_WORD_LENGTH = 3\n\nconst SUGGESTIONS: Record<WeakSecretReason, string> = {\n empty: 'Provide a phrase of at least 6 lowercase words separated by single spaces.',\n 'invalid-chars':\n 'Use only lowercase letters [a-z] and single spaces. No punctuation, symbols, digits, or uppercase.',\n 'leading-or-trailing-space': 'Trim leading and trailing spaces.',\n 'double-space': 'Use exactly one space between words.',\n 'too-few-words':\n 'Use at least 6 words by default (8 under strict policy). Example: \"correct horse battery staple printer toaster\".',\n 'word-too-short': 'Each word must be at least 3 characters. Drop short fillers like \"a\", \"is\", \"of\".',\n 'repeated-adjacent': 'Avoid repeating the same word twice in a row.',\n}\n\n/**\n * Inspect a phrase against the format rules and return a structured\n * verdict. Never throws — callers either branch on `ok` or pass the\n * result to {@link assertStrongSecret} for the throwing flavour.\n */\nexport function validateSecret(\n s: string,\n opts?: SecretPolicy,\n): SecretValidationResult {\n // Escape hatch: customValidator owns the entire decision. None of\n // the structural rules below run when this is set — the consumer is\n // responsible for the full validation contract.\n if (opts?.customValidator) {\n return opts.customValidator(s)\n }\n\n const minWords = opts?.minWords ?? DEFAULT_MIN_WORDS\n const minWordLength = opts?.minWordLength ?? DEFAULT_MIN_WORD_LENGTH\n const rejectRepeated = opts?.rejectRepeatedAdjacent ?? true\n\n if (s.length === 0) {\n return { ok: false, reason: 'empty' }\n }\n\n if (s !== s.trim()) {\n return { ok: false, reason: 'leading-or-trailing-space' }\n }\n\n if (s.includes(' ')) {\n return { ok: false, reason: 'double-space' }\n }\n\n // The default character class is lowercase-letters-and-spaces;\n // consumers can override via SecretPolicy.pattern (e.g. to\n // allow digits, uppercase, or non-Latin scripts). Word splitting\n // below remains space-based — for non-space word semantics the\n // consumer should use customValidator instead.\n const charPattern = opts?.pattern ?? /^[a-z]+( [a-z]+)*$/\n if (!charPattern.test(s)) {\n return { ok: false, reason: 'invalid-chars' }\n }\n\n const words = s.split(' ')\n\n if (words.length < minWords) {\n return { ok: false, reason: 'too-few-words', minimum: minWords, got: words.length }\n }\n\n for (const w of words) {\n if (w.length < minWordLength) {\n return { ok: false, reason: 'word-too-short', minimum: minWordLength, got: w.length }\n }\n }\n\n if (rejectRepeated) {\n for (let i = 1; i < words.length; i++) {\n if (words[i] === words[i - 1]) {\n return { ok: false, reason: 'repeated-adjacent' }\n }\n }\n }\n\n return { ok: true, words: words.length }\n}\n\n/**\n * Throw {@link WeakSecretError} when the phrase fails. Used by\n * `createOwnerKeyring`, `grant`, and `rotateSecret` at ingress.\n *\n * Pass `{ allowWeakSecret: true }` to bypass — intended for test\n * fixtures, CLI scripts, and dev environments. The override never\n * loosens the cryptographic key derivation; it only relaxes the\n * structural-strength gate.\n */\nexport function assertStrongSecret(\n s: string,\n opts?: SecretPolicy & { allowWeakSecret?: boolean },\n): void {\n if (opts?.allowWeakSecret) return\n const result = validateSecret(s, opts)\n if (result.ok) return\n throw new WeakSecretError(result.reason, SUGGESTIONS[result.reason])\n}\n\n/**\n * Estimate the entropy of a phrase, given the EFF 7,776-word list as\n * the assumed wordlist. ~12.9 bits per word.\n *\n * Returns 0 for any input that fails the phrase format — character-class\n * estimates aren't comparable to phrase entropy, and surfacing 0 makes\n * weak inputs visible in any UI that displays an entropy meter.\n */\nexport function estimateEntropy(secret: string): number {\n const result = validateSecret(secret)\n if (!result.ok) return 0\n return Math.round(result.words * Math.log2(7776))\n}\n\n/**\n * Internal compatibility shim. Older code paths used the throwing\n * `validateSecret(s)` directly; some still do via re-exports. Routes\n * to the new `assertStrongSecret` so the contract holds for both\n * shapes during the transition. New code should call\n * {@link assertStrongSecret} directly.\n *\n * @internal\n */\nexport function legacyAssertSecret(s: string): void {\n try {\n assertStrongSecret(s)\n } catch (err) {\n if (err instanceof WeakSecretError) {\n throw new ValidationError(err.message)\n }\n throw err\n }\n}\n","/**\n * Secret-bearing reserved collections — the `_`-prefixed reserved collections\n * whose record CONTENTS are directly-usable secrets (transport OAuth tokens,\n * connection strings, API keys), as opposed to the *operational* reserved\n * collections (`_ledger`, `_history`, `_sync`, …) whose contents are metadata\n * (collection names, record ids, ciphertext hashes).\n *\n * This distinction matters at two trust-boundary seams:\n *\n * 1. `vault.collection()` — a secret-bearing reserved name must never be\n * reachable through the generic public collection handle. It is served\n * ONLY by its dedicated, role-gated API (e.g. `_sync_credentials` →\n * `getCredential`/`putCredential`, which enforce owner/admin access).\n *\n * 2. `grant()` DEK propagation — a sub-admin grantee (operator, viewer,\n * client, custodian) must NOT receive a secret-bearing collection's DEK.\n * Operational reserved DEKs still propagate to every role (grantees must\n * write ledger/history entries on every mutation), but handing a\n * sub-admin the `_sync_credentials` DEK would let them decrypt the firm's\n * transport secrets — a plaintext leak, not a metadata leak.\n *\n * Kept dependency-free so both the kernel (`vault.ts`) and the team layer\n * (`keyring.ts`) can import it without an import cycle.\n */\n\n/** Reserved collection holding per-adapter sync transport secrets. */\nexport const SYNC_CREDENTIALS_COLLECTION = '_sync_credentials'\n\n/**\n * Reserved (pre-allocated) name for the future credential broker (#479).\n * Not yet implemented — reserved now so that neither the public collection\n * handle nor grant propagation can ever expose it before its dedicated,\n * role-gated API lands.\n */\nexport const BROKER_COLLECTION = '_broker'\n\n/**\n * The set of reserved collection names whose record contents are\n * directly-usable secrets. Membership here means: never served via\n * `vault.collection()`, never propagated to sub-admin grantees.\n */\nexport const SECRET_BEARING_RESERVED_COLLECTIONS: ReadonlySet<string> = new Set([\n SYNC_CREDENTIALS_COLLECTION,\n BROKER_COLLECTION,\n])\n\n/** True when `name` is a secret-bearing reserved collection. */\nexport function isSecretBearingReservedCollection(name: string): boolean {\n return SECRET_BEARING_RESERVED_COLLECTIONS.has(name)\n}\n","import type { NoydbStore, KeyringFile, KeyringAuthenticator, Role, Permissions, GrantOptions, RevokeOptions, UpdateUserOptions, UserInfo, EncryptedEnvelope, ExportCapability, ExportFormat, ImportCapability, VaultPolicyOnDisk, UserEnvelope } from '../../kernel/types.js'\nimport { NOYDB_KEYRING_VERSION, NOYDB_FORMAT_VERSION } from '../../kernel/types.js'\nimport { USER_ENVELOPE_COLLECTION } from '../../kernel/constants.js'\nimport {\n deriveKey,\n generateDEK,\n generateSalt,\n wrapKey,\n unwrapKey,\n encrypt,\n decrypt,\n bufferToBase64,\n base64ToBuffer,\n type EnclaveKey,\n} from '../../kernel/enclave/index.js'\nimport { NoAccessError, PermissionDeniedError, PrivilegeEscalationError, KeyringExpiredError, KeyringCorruptError, InvalidKeyError, ValidationError, DirectoryDisabledError } from '../../kernel/errors.js'\nimport { readDirectoryConfig } from '../directory/storage.js'\nimport { readUserVisibility, deleteUserVisibility } from '../directory/visibility.js'\nimport { assertStrongSecret, type SecretPolicy } from '../../kernel/validation.js'\nimport {\n saveUserEnvelope,\n loadUserEnvelope as loadUserEnvelopeFn,\n deleteUserEnvelope,\n} from '../directory/user-envelope/index.js'\nimport { isSecretBearingReservedCollection } from './reserved-secret-collections.js'\n\n// ─── Roles that can grant/revoke ───────────────────────────────────────\n\n/**\n * Roles that an `admin` is allowed to grant and revoke.\n *\n * Includes `'admin'` itself: the model bottlenecked all admin\n * onboarding through the single `owner` principal, which made lateral\n * delegation impossible and left a single-owner bus-factor risk\n * unresolved even when multiple trusted humans existed. opens up\n * admin↔admin lateral delegation, with two guardrails:\n *\n * 1. **No privilege escalation.** Enforced in `grant()`: every DEK\n * wrapped into the new admin's keyring must be present in the\n * grantor's own DEK set. Today this is structurally trivially\n * true (admin grants always inherit the full caller DEK set),\n * but the check is wired in so future per-collection admin scoping\n * cannot accidentally bypass it. See `PrivilegeEscalationError`.\n *\n * 2. **Cascade on revoke.** Enforced in `revoke()`: when an admin is\n * revoked, every admin they (transitively) granted is either\n * revoked too (`cascade: 'strict'`, default) or left in place with\n * a console warning (`cascade: 'warn'`). The walk uses the\n * `granted_by` field on each keyring file as the parent pointer.\n */\n// FR-6: 'custodian' is deliberately ABSENT here. An admin must not be able\n// to mint (or revoke) a custodian — only the (sealed Deed) owner can, because\n// the custodian is the de-facto operational authority and granting one is an\n// ownership-level act. `canGrant(admin, 'custodian')` therefore returns false.\nconst ADMIN_GRANTABLE_TARGETS: readonly Role[] = ['operator', 'viewer', 'client', 'admin']\n\nfunction canGrant(callerRole: Role, targetRole: Role): boolean {\n if (callerRole === 'owner') return true\n // FR-6: a custodian can never grant — it holds the DEKs to OPERATE the\n // vault but no authority to widen the principal set. Explicit + fail-safe\n // (it would also fall through to `return false`, but spell it out).\n if (callerRole === 'custodian') return false\n if (callerRole === 'admin') return ADMIN_GRANTABLE_TARGETS.includes(targetRole)\n return false\n}\n\nfunction canRevoke(callerRole: Role, targetRole: Role): boolean {\n if (targetRole === 'owner') return false // owner cannot be revoked\n if (callerRole === 'owner') return true\n // FR-6: a custodian can never revoke — same non-owning rationale as canGrant.\n if (callerRole === 'custodian') return false\n if (callerRole === 'admin') return ADMIN_GRANTABLE_TARGETS.includes(targetRole)\n return false\n}\n\n/**\n * Whether `callerRole` can mutate a keyring whose role is (or becomes)\n * `targetRole`. Used by `updateKeyringIdentity`.\n *\n * Mirrors `canGrant`'s hierarchy: admins manage admin/operator/viewer/\n * client laterally; admins cannot create or destroy `owner`-shaped\n * keyrings. Owner can do anything.\n *\n * Both the OLD role and the NEW role must satisfy this check —\n * otherwise admin could elevate themselves (`admin → owner`) or demote\n * an owner (`owner → admin`) under cover of \"update.\"\n */\nfunction canUpdateRole(callerRole: Role, targetRole: Role): boolean {\n if (callerRole === 'owner') return true\n if (callerRole === 'admin') return ADMIN_GRANTABLE_TARGETS.includes(targetRole)\n return false\n}\n\n// ─── Unlocked Keyring ──────────────────────────────────────────────────\n\n/** In-memory representation of an unlocked keyring. */\nexport interface UnlockedKeyring {\n readonly userId: string\n readonly displayName: string\n readonly role: Role\n readonly permissions: Permissions\n readonly deks: Map<string, EnclaveKey>\n /**\n * The KEK, when this keyring was unlocked via tier 1 (secret) or\n * a wrap-KEK tier-2 method (WebAuthn / OIDC). `null` when the\n * keyring was opened via:\n *\n * - Unencrypted mode (no KEK exists)\n * - Tier-3 PIN quick-resume (`@noy-db/on-pin`)\n * - Wrap-DEKs tier-2 unlock (`@noy-db/on-password`'s\n * `verifyPasswordSlot`)\n * - Session-state restore (`session/session.ts`)\n * - Dev-unlock fixture (`session/dev-unlock.ts`)\n *\n * Consumers performing tier-1 operations that need the KEK\n * (DEK rewrap, keyring persist, delegation issue/unwrap) must\n * null-check and throw a clear error if absent — re-authenticate\n * at tier 1 first to recover the KEK.\n *\n * Tightened from `EnclaveKey` to `EnclaveKey | null`; the runtime\n * contract has always allowed null, the type now matches reality.\n */\n readonly kek: EnclaveKey | null\n readonly salt: Uint8Array\n /**\n * Debug-plaintext layout flag. Set only on the plaintext keyring created\n * in `encrypt: false` + `debugPlaintext: true` mode — it lives here\n * because its lifecycle is identical to the plaintext keyring's (no\n * encrypted vault ever has it, so this never widens the encrypted surface).\n * When true, user-collection records are written with their fields inlined\n * beside the envelope metadata (`_debug: 1`) so native store tooling can\n * read them without unwrapping `_data`.\n */\n readonly debugPlaintext?: boolean\n /**\n * `@noy-db/as-*` export capability. Absent when the\n * keyring was written before this RFC landed — role-based defaults\n * apply via `hasExportCapability`.\n */\n readonly exportCapability?: ExportCapability\n /**\n * `@noy-db/as-*` import capability. Absent when the\n * keyring was written before the import-capability extension\n * landed — default-closed semantics\n * apply via `hasImportCapability` (no plaintext format granted, no\n * bundle import granted, regardless of role).\n */\n readonly importCapability?: ImportCapability\n /**\n * Tier-2 authenticator slots — readonly snapshot loaded from the\n * keyring file. Mutations go through `enrollAuthenticator` /\n * `removeAuthenticator`, which write back via\n * `persistKeyring`. Always defined; loads with an empty array for\n * keyrings written before the multi-slot extension landed.\n */\n readonly authenticators: readonly KeyringAuthenticator[]\n /**\n * Reserved per-keyring policy override (forward-compat for Option C\n * — see {@link VaultPolicyOnDisk}). v1.0 round-trips this field but\n * never enforces it; the gate engine uses `_meta/policy` only.\n */\n readonly policy?: VaultPolicyOnDisk\n}\n\n// ─── Secret canary ─────────────────────────────────────────────────\n//\n// The canary is a fixed 256-bit AES-GCM key (32 zero bytes), wrapped\n// under the keyring's KEK with AES-KW. Because AES-KW is deterministic\n// (RFC 3394 fixed IV), wrapping the same constant under the same KEK\n// always yields the same ciphertext — so every write site can mint\n// fresh on each persist without round-tripping a `canary` field\n// through UnlockedKeyring.\n//\n// On load, the canary unwraps cleanly iff the KEK is correct AND the\n// canary bytes on disk are intact. Combined with each-DEK try/catch,\n// this distinguishes wrong-secret (canary fails AND every DEK fails)\n// from corruption (canary succeeds OR at least one DEK succeeds) —\n// closing the all-DEKs-corrupt and single-DEK ambiguities that the\n// pre-canary heuristic left open.\n\nconst CANARY_PLAINTEXT_BYTES = new Uint8Array(32)\nlet canaryKeyPromise: Promise<EnclaveKey> | null = null\n\nfunction getCanaryKey(): Promise<EnclaveKey> {\n if (canaryKeyPromise === null) {\n canaryKeyPromise = globalThis.crypto.subtle.importKey(\n 'raw',\n CANARY_PLAINTEXT_BYTES as BufferSource,\n { name: 'AES-GCM', length: 256 },\n true, // extractable so AES-KW can wrap it\n ['encrypt', 'decrypt'],\n )\n }\n return canaryKeyPromise\n}\n\n/** Mint a fresh wrapped-canary string. Deterministic for a given KEK. */\nexport async function mintKeyringCanary(kek: EnclaveKey): Promise<string> {\n const canaryKey = await getCanaryKey()\n return wrapKey(canaryKey, kek)\n}\n\n/** Try to unwrap the canary. Returns true iff KEK + canary bytes are intact. */\nasync function verifyKeyringCanary(wrappedCanary: string, kek: EnclaveKey): Promise<boolean> {\n try {\n await unwrapKey(wrappedCanary, kek)\n return true\n } catch {\n return false\n }\n}\n\n// ─── Load / Create ─────────────────────────────────────────────────────\n\n/** Options for {@link loadKeyring} (#846b). */\nexport interface LoadKeyringOptions {\n /** The user whose keyring to unlock. */\n readonly userId: string\n /** That user's secret. */\n readonly secret: string\n}\n\n/** Load and unlock a user's keyring for a vault. */\nexport async function loadKeyring(\n store: NoydbStore,\n vault: string,\n opts: LoadKeyringOptions,\n): Promise<UnlockedKeyring> {\n const { userId, secret } = opts\n const envelope = await store.get(vault, '_keyring', userId)\n\n if (!envelope) {\n throw new NoAccessError(`No keyring found for user \"${userId}\" in vault \"${vault}\"`)\n }\n\n const keyringFile = JSON.parse(envelope._data) as KeyringFile\n\n // — refuse to unwrap an expired slot. Check happens before any\n // KEK derivation so an expired slot doesn't leak timing on the\n // secret. Comparison uses Date.parse → ms-since-epoch; an\n // unparseable expires_at is treated as \"no expiry\" so a malformed\n // value can't silently lock users out (it'll surface in tests).\n if (keyringFile.expires_at !== undefined) {\n const cutoff = Date.parse(keyringFile.expires_at)\n if (Number.isFinite(cutoff) && Date.now() >= cutoff) {\n throw new KeyringExpiredError({ userId: keyringFile.user_id, expiresAt: keyringFile.expires_at })\n }\n }\n\n const salt = base64ToBuffer(keyringFile.salt)\n const kek = await deriveKey(secret, salt)\n\n // Verify the canary first when present. A canary success proves the\n // KEK is correct independent of any DEK byte — so subsequent DEK\n // unwrap failures are unambiguously corruption, not wrong-pass. A\n // canary failure with at least one DEK success indicates the KEK\n // is correct but the canary itself is corrupt.\n // `null` sentinel = legacy keyring without canary; falls back to the\n // multi-DEK heuristic.\n const canaryOk: boolean | null = keyringFile.canary !== undefined\n ? await verifyKeyringCanary(keyringFile.canary, kek)\n : null\n\n // Unwrap each DEK independently — collect successes and failures.\n const deks = new Map<string, EnclaveKey>()\n const failedCollections: string[] = []\n let firstUnwrapError: unknown = null\n for (const [collName, wrappedDek] of Object.entries(keyringFile.deks)) {\n try {\n const dek = await unwrapKey(wrappedDek, kek)\n deks.set(collName, dek)\n } catch (err) {\n failedCollections.push(collName)\n if (firstUnwrapError === null) firstUnwrapError = err\n }\n }\n\n if (canaryOk === true) {\n // KEK proven correct by the canary. Any DEK failure is corruption.\n if (failedCollections.length > 0) {\n throw new KeyringCorruptError({ failedCollections, intactCount: deks.size })\n }\n } else if (canaryOk === false) {\n // Canary failed. If any DEK unwrapped, KEK is correct → canary bytes\n // are corrupted (rare; reported under the '_canary' sentinel).\n if (deks.size > 0) {\n throw new KeyringCorruptError({\n failedCollections: [...failedCollections, '_canary'],\n intactCount: deks.size,\n })\n }\n // Canary failed AND no DEK unwrapped — wrong KEK (or whole-file\n // corruption). Surface the original InvalidKeyError so\n // onInvalidKey: 'reset' can fire its documented recovery path.\n throw firstUnwrapError instanceof Error ? firstUnwrapError : new InvalidKeyError()\n } else {\n // Legacy keyring (no canary). Fall back to the multi-DEK heuristic.\n if (failedCollections.length > 0) {\n if (deks.size > 0) {\n throw new KeyringCorruptError({ failedCollections, intactCount: deks.size })\n }\n throw firstUnwrapError instanceof Error ? firstUnwrapError : new InvalidKeyError()\n }\n }\n\n return {\n userId: keyringFile.user_id,\n displayName: keyringFile.display_name,\n role: keyringFile.role,\n permissions: keyringFile.permissions,\n deks,\n kek,\n salt,\n authenticators: keyringFile.authenticators ?? [],\n ...(keyringFile.export_capability !== undefined && { exportCapability: keyringFile.export_capability }),\n ...(keyringFile.import_capability !== undefined && { importCapability: keyringFile.import_capability }),\n ...(keyringFile.policy !== undefined && { policy: keyringFile.policy }),\n }\n}\n\n/**\n * Open-policy pre-gate: decide create-vs-fail-closed **before** any\n * vault write. `openVault` must not self-provision an owner keyring into a\n * vault held by other principals; create-on-open is allowed only for a\n * genuinely-new vault (no `_keyring/*` at all). Capability-free — one\n * `store.list`. Returns when the open may proceed (the caller is a member, or\n * the vault is genuinely-new and `create` is allowed, in which case the caller\n * falls through to the normal `createOwnerKeyring` path); throws `NoAccessError`\n * otherwise. Placed before managed-secret secret resolution (which persists\n * on first open), so a fail-closed open writes nothing.\n */\nexport async function assertKeyringOpenAllowed(\n store: NoydbStore,\n vault: string,\n userId: string,\n create: boolean,\n): Promise<void> {\n const keyringUsers = await store.list(vault, '_keyring')\n if (keyringUsers.includes(userId)) return // caller is a member → load existing\n if (!create) {\n throw new NoAccessError(`Vault \"${vault}\" not opened: create disabled and no keyring for \"${userId}\".`)\n }\n if (keyringUsers.length > 0) {\n throw new NoAccessError(\n `No keyring for user \"${userId}\" in vault \"${vault}\" (held by other principals) — refusing to self-provision.`,\n )\n }\n // empty → genuinely-new vault → caller proceeds to the create path\n}\n\n/**\n * Options for {@link createOwnerKeyring} (#846b). The `SecretPolicy` knobs are\n * flattened into the same bag rather than nested, so `assertStrongSecret` can\n * take `opts` directly.\n */\nexport interface CreateOwnerKeyringOptions extends SecretPolicy {\n /** The owner to create. */\n readonly userId: string\n /** The owner's secret. */\n readonly secret: string\n /** Gate creation on the phrase-format strength rules. Default false. */\n readonly validate?: boolean\n /** Escape hatch for fixtures and migrations — skips the strength gate. */\n readonly allowWeakSecret?: boolean\n}\n\n/**\n * Create the initial owner keyring for a new vault.\n *\n * Pass `{ validate: true }` (or any `SecretPolicy` knob) to gate creation\n * on the phrase-format strength rules — `Noydb` threads this from\n * `NoydbOptions.validateSecret`. Direct callers (CLI, scripts,\n * test fixtures) opt in explicitly.\n */\nexport async function createOwnerKeyring(\n store: NoydbStore,\n vault: string,\n opts: CreateOwnerKeyringOptions,\n): Promise<UnlockedKeyring> {\n const { userId, secret } = opts\n if (opts.validate && !opts.allowWeakSecret) {\n assertStrongSecret(secret, opts)\n }\n const salt = generateSalt()\n const kek = await deriveKey(secret, salt)\n\n // Eager-provision the _users DEK at owner creation. This guarantees\n // every subsequent grant inherits it via the existing\n // collName.startsWith('_') propagation in grant() — so multi-principal\n // user-envelope reads (alice reading bob's profile) work for new\n // vaults without any per-keyring DEK rotation. Pre-existing vaults\n // get the DEK lazily on first vault.user.* access (which only\n // materializes a single-principal DEK that won't propagate\n // retroactively — that's the documented \"lazy creation for\n // pre-existing keyrings\" rollout note in the spec).\n const userEnvelopeDek = await generateDEK()\n const wrappedUserEnvelopeDek = await wrapKey(userEnvelopeDek, kek)\n const canary = await mintKeyringCanary(kek)\n\n const keyringFile: KeyringFile = {\n _noydb_keyring: NOYDB_KEYRING_VERSION,\n user_id: userId,\n display_name: userId,\n role: 'owner',\n permissions: {},\n deks: { [USER_ENVELOPE_COLLECTION]: wrappedUserEnvelopeDek },\n salt: bufferToBase64(salt),\n created_at: new Date().toISOString(),\n granted_by: userId,\n canary,\n }\n\n await writeKeyringFile(store, vault, userId, keyringFile)\n\n return {\n userId,\n displayName: userId,\n role: 'owner',\n permissions: {},\n deks: new Map([[USER_ENVELOPE_COLLECTION, userEnvelopeDek]]),\n kek,\n salt,\n authenticators: [],\n }\n}\n\n// ─── Grant ─────────────────────────────────────────────────────────────\n\n/** Grant access to a new user. Caller must have grant privilege. */\nexport async function grant(\n store: NoydbStore,\n vault: string,\n callerKeyring: UnlockedKeyring,\n options: GrantOptions,\n): Promise<void> {\n if (!callerKeyring.kek) {\n throw new ValidationError(\n 'grant: caller keyring has no KEK — tier-2 wrap-DEKs and tier-3 PIN-resume ' +\n 'sessions cannot grant access to other users. Re-authenticate at tier 1 ' +\n '(secret) before granting.',\n )\n }\n\n if (!canGrant(callerKeyring.role, options.role)) {\n throw new PermissionDeniedError(\n `Role \"${callerKeyring.role}\" cannot grant role \"${options.role}\"`,\n )\n }\n\n // Optional strength validation — opt-in via grant({ validateSecret: true })\n // or via the calling Noydb's NoydbOptions.validateSecret flag.\n // The override `allowWeakSecret: true` skips even when validate is on.\n if (\n (options as { validateSecret?: boolean }).validateSecret &&\n !options.allowWeakSecret\n ) {\n assertStrongSecret(options.secret)\n }\n\n // Determine which collections the new user gets access to\n const permissions = resolvePermissions(options.role, options.permissions)\n\n // Derive the new user's KEK from their secret\n const newSalt = generateSalt()\n const newKek = await deriveKey(options.secret, newSalt)\n\n // Only owner and admin may ever hold a secret-bearing reserved DEK\n // (`_sync_credentials`, `_broker`) — they are the roles the dedicated\n // credential API admits. Every other grantee (custodian, viewer, operator,\n // client) is excluded, even from the \"wrap ALL\" branches below: a custodian\n // operates the DATA but must never read the firm's transport secrets (see\n // `requireAdminAccess` in sync-credentials.ts), and handing any sub-admin\n // one of these DEKs is a plaintext leak, not a metadata leak.\n const granteeMayHoldSecrets =\n options.role === 'owner' || options.role === 'admin'\n\n // Wrap the appropriate DEKs with the new user's KEK\n const wrappedDeks: Record<string, string> = {}\n for (const collName of Object.keys(permissions)) {\n // Never hand a secret-bearing reserved DEK to a sub-admin, even if the\n // grantor explicitly names it in `permissions` — that path is served\n // only by the owner/admin-gated credential API, not per-collection grants.\n if (isSecretBearingReservedCollection(collName) && !granteeMayHoldSecrets) continue\n const dek = callerKeyring.deks.get(collName)\n if (dek) {\n wrappedDeks[collName] = await wrapKey(dek, newKek)\n }\n }\n\n // For owner/admin/custodian/viewer roles, wrap ALL known DEKs.\n // FR-6: a custodian operates EVERY collection, so — like admin — it must\n // receive every collection DEK on grant. Without this branch a custodian\n // could neither read nor write and the role would be inert.\n if (\n options.role === 'owner' ||\n options.role === 'admin' ||\n options.role === 'custodian' ||\n options.role === 'viewer'\n ) {\n for (const [collName, dek] of callerKeyring.deks) {\n if (collName in wrappedDeks) continue\n if (isSecretBearingReservedCollection(collName) && !granteeMayHoldSecrets) continue\n wrappedDeks[collName] = await wrapKey(dek, newKek)\n }\n }\n\n // For ALL roles, propagate system-prefixed collection DEKs\n // (`_ledger`, `_history`, `_sync`, …). These are internal collections\n // that any user with access to the vault must be able to\n // read and write — for example, the hash-chained ledger writes\n // an entry on every put/delete, so operators and clients with write\n // access to a single data collection still need the `_ledger` DEK.\n //\n // Trade-off: a granted user can decrypt every system-collection\n // entry, including ones they would not otherwise have access to\n // (e.g., an operator on `invoices` can read ledger entries for\n // mutations in `salaries`). This is a metadata leak, not a\n // plaintext leak — the ledger entries record collection names,\n // record ids, and ciphertext hashes, but never plaintext records.\n // Per-collection ledger DEKs are tracked as a follow-up.\n //\n // EXCEPTION — secret-bearing reserved collections (`_sync_credentials`,\n // `_broker`) whose record CONTENTS are directly-usable secrets are NOT\n // propagated to sub-admin grantees. Unlike the operational collections\n // above, handing an operator/viewer/client/custodian one of these DEKs\n // IS a plaintext leak (the firm's transport OAuth tokens). Only owner and\n // admin — the roles the dedicated `getCredential`/`putCredential` API\n // admits — receive them, so the legit admin-reads-existing-credential\n // flow (which needs the DEK to decrypt, not regenerate) still works.\n for (const [collName, dek] of callerKeyring.deks) {\n if (!collName.startsWith('_') || collName in wrappedDeks) continue\n if (isSecretBearingReservedCollection(collName) && !granteeMayHoldSecrets) continue\n wrappedDeks[collName] = await wrapKey(dek, newKek)\n }\n\n // Anti-privilege-escalation check. Every DEK we just\n // wrapped into the new keyring must come from the caller's own DEK\n // set — the grantor cannot give the grantee access to a collection\n // they themselves can't read. Today this is structurally trivially\n // satisfied because every wrapped DEK was looked up in\n // `callerKeyring.deks` above, but the explicit check is wired in\n // so a future change (per-collection admin scoping, escrow-based\n // re-wrapping, etc.) cannot accidentally let a widening grant\n // through. See `PrivilegeEscalationError` for the rationale.\n for (const collName of Object.keys(wrappedDeks)) {\n if (!callerKeyring.deks.has(collName)) {\n throw new PrivilegeEscalationError(collName)\n }\n }\n\n const canary = await mintKeyringCanary(newKek)\n const keyringFile: KeyringFile = {\n _noydb_keyring: NOYDB_KEYRING_VERSION,\n user_id: options.userId,\n display_name: options.displayName,\n role: options.role,\n permissions,\n deks: wrappedDeks,\n salt: bufferToBase64(newSalt),\n created_at: new Date().toISOString(),\n granted_by: callerKeyring.userId,\n canary,\n ...(options.exportCapability !== undefined && { export_capability: options.exportCapability }),\n ...(options.importCapability !== undefined && { import_capability: options.importCapability }),\n }\n\n await writeKeyringFile(store, vault, options.userId, keyringFile)\n\n // User envelope bootstrap. Seeded with `options.initialProfile` if\n // provided, otherwise an empty `{}`. Encrypted with the caller's\n // _users DEK — which is the same DEK that was wrapped into the new\n // keyring's `wrappedDeks[USER_ENVELOPE_COLLECTION]` above (system-\n // collection propagation), so the new user can decrypt it on first\n // open. Skipped silently if the caller has no _users DEK (pre-feature\n // vault upgrade path — documented \"lazy creation for pre-existing\n // keyrings\" in the spec).\n const userEnvelopeDek = callerKeyring.deks.get(USER_ENVELOPE_COLLECTION)\n if (userEnvelopeDek) {\n const initialPayload = options.initialProfile ?? {}\n await saveUserEnvelope(\n store,\n vault,\n options.userId,\n initialPayload,\n userEnvelopeDek,\n )\n }\n}\n\n// ─── Revoke ────────────────────────────────────────────────────────────\n\n/**\n * Walk every keyring in the vault to find admins that the given\n * `rootUserId` (transitively) granted, via the `granted_by` parent\n * pointer recorded on each keyring file.\n *\n * Returns the set of descendant admin user-ids in DFS order, NOT\n * including the root itself. Non-admin descendants are excluded\n * because operators/viewers/clients cannot grant other users — they\n * are leaves in the delegation tree and cleaning them up is the\n * caller's job (or the next rotate, since they'd lose key access\n * anyway when the cascading admin's collections rotate).\n *\n * The walk uses a visited set keyed by user-id so cycles introduced\n * by re-grants (admin-A revoked, then re-granted later by admin-B who\n * was originally granted by A) terminate cleanly.\n */\nasync function findAdminDescendants(\n store: NoydbStore,\n vault: string,\n rootUserId: string,\n): Promise<string[]> {\n const allUserIds = await store.list(vault, '_keyring')\n\n // Build a map: parentUserId → child KeyringFiles. We only ever\n // descend into admins, so non-admin children are skipped at the\n // edge level rather than after a recursive call.\n const childrenByParent = new Map<string, string[]>()\n for (const userId of allUserIds) {\n const env = await store.get(vault, '_keyring', userId)\n if (!env) continue\n const kf = JSON.parse(env._data) as KeyringFile\n // Only admins can grant, so only admins have a delegation subtree to\n // cascade. FR-6: a custodian is intentionally EXCLUDED here — it cannot\n // grant (canGrant(custodian,*) === false), so it is never a cascade root\n // and never a descendant edge. Treating it as an admin-descendant would\n // wrongly sweep it into an admin's revoke cascade.\n if (kf.role !== 'admin') continue // only admins can grant — leaves are uninteresting\n if (kf.user_id === rootUserId) continue // self-edges are noise\n const list = childrenByParent.get(kf.granted_by) ?? []\n list.push(kf.user_id)\n childrenByParent.set(kf.granted_by, list)\n }\n\n const visited = new Set<string>()\n const order: string[] = []\n const stack: string[] = [...(childrenByParent.get(rootUserId) ?? [])]\n while (stack.length > 0) {\n const next = stack.pop()!\n if (visited.has(next)) continue\n visited.add(next)\n order.push(next)\n for (const grandchild of childrenByParent.get(next) ?? []) {\n if (!visited.has(grandchild)) stack.push(grandchild)\n }\n }\n return order\n}\n\n/** Revoke a user's access. Optionally rotate keys for affected collections. */\nexport async function revoke(\n store: NoydbStore,\n vault: string,\n callerKeyring: UnlockedKeyring,\n options: RevokeOptions,\n): Promise<void> {\n // Load the target's keyring to check their role\n const targetEnvelope = await store.get(vault, '_keyring', options.userId)\n if (!targetEnvelope) {\n throw new NoAccessError(`User \"${options.userId}\" has no keyring in vault \"${vault}\"`)\n }\n\n const targetKeyring = JSON.parse(targetEnvelope._data) as KeyringFile\n\n if (!canRevoke(callerKeyring.role, targetKeyring.role)) {\n throw new PermissionDeniedError(\n `Role \"${callerKeyring.role}\" cannot revoke role \"${targetKeyring.role}\"`,\n )\n }\n\n // Cascade-on-revoke. Only meaningful when the target is\n // an admin — operators/viewers/clients cannot grant other users so\n // they have no delegation subtree to walk.\n const cascadeMode = options.cascade ?? 'strict'\n const usersToRevoke: string[] = [options.userId]\n const affectedCollections = new Set(Object.keys(targetKeyring.deks))\n\n if (targetKeyring.role === 'admin') {\n const descendants = await findAdminDescendants(store, vault, options.userId)\n if (descendants.length > 0) {\n if (cascadeMode === 'warn') {\n // Diagnostic mode: leave the descendants in place but make\n // them visible. The owner / a different admin can clean up\n // manually. The single console.warn is intentionally noisy\n // (a list, not a count) so the operator sees exactly which\n // keyrings will become orphans.\n console.warn(\n `[noy-db] revoke(${options.userId}): cascade='warn' — leaving ` +\n `${descendants.length} descendant admin(s) in place: ` +\n `${descendants.join(', ')}. These admins were granted by the revoked user ` +\n `(transitively) and will become orphans in the delegation tree.`,\n )\n } else {\n // Strict mode (default): pull every descendant into the\n // revoke set. We collect their affected collections too so\n // the single rotation pass at the end covers everything.\n for (const userId of descendants) {\n const descEnv = await store.get(vault, '_keyring', userId)\n if (!descEnv) continue\n const descKf = JSON.parse(descEnv._data) as KeyringFile\n usersToRevoke.push(userId)\n for (const c of Object.keys(descKf.deks)) affectedCollections.add(c)\n }\n }\n }\n }\n\n // Delete every keyring in the revoke set. Order doesn't matter\n // because each keyring file is independent on disk; we don't have\n // referential integrity to maintain across deletes.\n for (const userId of usersToRevoke) {\n await store.delete(vault, '_keyring', userId)\n // Cascade-delete the principal's user envelope. Idempotent — no\n // error when the envelope was never written (e.g. the user was\n // granted but never authenticated to write their own profile).\n await deleteUserEnvelope(store, vault, userId)\n // Also drop the visibility sidecar at `_meta/visibility/<userId>`.\n // If the same `userId` is re-granted later (rare for humans,\n // possible for service accounts and test fixtures), the new\n // principal must start with a fresh visibility state instead of\n // silently inheriting the revoked user's `hidden` flag.\n await deleteUserVisibility(store, vault, userId)\n }\n\n // Single rotation pass at the end. The cost is O(records in\n // affected collections), NOT O(records × cascade depth) — every\n // descendant's collections were unioned into `affectedCollections`\n // before we got here, so the rotation re-encrypts each affected\n // record exactly once regardless of how deep the cascade went.\n if (options.rotateKeys !== false && affectedCollections.size > 0) {\n await rotateKeys(store, vault, callerKeyring, { collections: [...affectedCollections] })\n }\n}\n\n// ─── Update User ───────────────────────────────────────────────────────\n\n/**\n * Mutate `role`, `displayName`, and/or `permissions` on an existing\n * keyring. Pure plaintext-header rewrite — no DEK rewrap, no KEK\n * required, no authenticator slots touched. Tier-2 enrollments and\n * recovery codes survive the operation.\n *\n * Role-elevation guard: BOTH the old role AND the new role must\n * satisfy `canUpdateRole(callerRole, _)`. This blocks the two\n * privilege-escalation shapes:\n * - admin elevates someone (or themselves) to owner\n * - admin demotes an owner to a role they then control\n *\n * Owner is always allowed. Admin manages admin / operator / viewer /\n * client laterally.\n *\n * Identity preserved: same userId, same DEK wrappings. Last-write-wins\n * through the standard keyring put (same concurrency story as `grant`\n * and `revoke`).\n *\n * @throws `NoAccessError` when no keyring exists for the target.\n * @throws `PermissionDeniedError` when the role hierarchy rejects.\n * @throws `ValidationError` when the diff is empty (nothing to update).\n *\n */\nexport async function updateKeyringIdentity(\n store: NoydbStore,\n vault: string,\n callerKeyring: UnlockedKeyring,\n options: UpdateUserOptions,\n): Promise<void> {\n if (\n options.role === undefined &&\n options.displayName === undefined &&\n options.permissions === undefined\n ) {\n throw new ValidationError(\n `updateUser: at least one of role / displayName / permissions must be provided ` +\n `(userId: \"${options.userId}\").`,\n )\n }\n\n const env = await store.get(vault, '_keyring', options.userId)\n if (!env) {\n throw new NoAccessError(\n `updateUser: user \"${options.userId}\" has no keyring in vault \"${vault}\".`,\n )\n }\n const target = JSON.parse(env._data) as KeyringFile\n\n // Role-elevation guard. The OLD role must be one this caller is\n // allowed to manage, AND the NEW role (if changing) must be too.\n // Two-sided check: blocks admin→owner promotion (new side) and\n // demoting an owner (old side).\n if (!canUpdateRole(callerKeyring.role, target.role)) {\n throw new PermissionDeniedError(\n `Role \"${callerKeyring.role}\" cannot update a keyring with role \"${target.role}\"`,\n )\n }\n if (\n options.role !== undefined &&\n options.role !== target.role &&\n !canUpdateRole(callerKeyring.role, options.role)\n ) {\n throw new PermissionDeniedError(\n `Role \"${callerKeyring.role}\" cannot promote target to role \"${options.role}\"`,\n )\n }\n\n const next: KeyringFile = {\n ...target,\n ...(options.role !== undefined && { role: options.role }),\n ...(options.displayName !== undefined && {\n // null clears the field (stored as \"\"); a string sets it.\n display_name: options.displayName ?? '',\n }),\n ...(options.permissions !== undefined && { permissions: options.permissions }),\n }\n\n await writeKeyringFile(store, vault, options.userId, next)\n}\n\n// ─── Key Rotation ──────────────────────────────────────────────────────\n\n/**\n * Rotate DEKs for specified collections:\n * 1. Generate new DEKs\n * 2. Re-encrypt all records in affected collections\n * 3. Re-wrap new DEKs for all remaining users\n */\n/**\n * What a DEK rotation leaves behind (#854).\n *\n * A member's DEKs are wrapped under that member's KEK, and a KEK derives only\n * from that member's secret — so the caller cannot re-wrap a fresh DEK for\n * anyone else. That is the zero-knowledge property working as designed, but it\n * means rotation necessarily DROPS the rotated collections from every other\n * member's keyring rather than re-keying them.\n *\n * `needsRegrant` names exactly who lost what, so the caller can re-run\n * `grant()` instead of discovering the access loss later.\n */\nexport interface RotateResult {\n /** (member, collection) pairs whose access was dropped by the rotation. */\n readonly needsRegrant: ReadonlyArray<{ readonly userId: string; readonly collection: string }>\n}\n\n/** Options for {@link rotateKeys} (#846b — was a bare `string[]`). */\nexport interface RotateKeysOptions {\n /** Collections whose DEKs are re-minted. */\n readonly collections: readonly string[]\n}\n\nexport async function rotateKeys(\n store: NoydbStore,\n vault: string,\n callerKeyring: UnlockedKeyring,\n opts: RotateKeysOptions,\n): Promise<RotateResult> {\n const { collections } = opts\n // FR-6: re-keying is an owner-only meta-capability. A custodian operates the\n // vault fully but must NOT rotate — rotation would let it mint fresh DEKs and\n // strip the sealed owner's access, breaking the inalienability floor.\n if (callerKeyring.role === 'custodian') {\n throw new PermissionDeniedError(\n 'custodian cannot rotate keys (FR-6: re-key is an owner-only meta-capability; use the Deed owner)',\n )\n }\n // Generate new DEKs for each affected collection\n const newDeks = new Map<string, EnclaveKey>()\n for (const collName of collections) {\n newDeks.set(collName, await generateDEK())\n }\n\n // Re-encrypt all records in affected collections\n //\n // FORWARD REQUIREMENT (not implemented here — DEK rotation predates\n // perRecordKeys/classified fields): a future perRecordKeys-aware DEK\n // rotation MUST DROP `_bidx` here rather than carry it forward. `_bidx` is\n // rooted in the (soon-to-be-dead) old DEK; a stale tag surviving under the\n // new DEK is unreturnable garbage (no key can ever re-derive it to match a\n // query again) that still LEAKS the old equality partition. Unlike\n // `rotateRecordCek` (record-keys/sealing.ts), which carries `_bidx`\n // verbatim because it is CEK-rotation-only and the DEK is unchanged, a DEK\n // rotation changes the root the tag is derived from, so index coverage can\n // only regrow per-record, the next time each record is `put()` under the\n // new DEK.\n //\n // D-5 (pre-existing hazard, flagged not fixed): in a mixed collection, bare\n // (non-`_cek`) records below are re-encrypted and `put()` in this loop\n // *before* any `_cek` record is reached; if a later record in the same\n // collection throws (e.g. an unwrap failure), those already-rewritten bare\n // records are left persisted under the new DEK while `callerKeyring` is\n // never updated with it (the `deks.set` below hasn't run) — an unsaved-DEK\n // state with no rollback.\n for (const collName of collections) {\n const oldDek = callerKeyring.deks.get(collName)\n const newDek = newDeks.get(collName)!\n if (!oldDek) continue\n\n const ids = await store.list(vault, collName)\n for (const id of ids) {\n const envelope = await store.get(vault, collName, id)\n if (!envelope || !envelope._iv) continue\n\n // Decrypt with old DEK\n const plaintext = await decrypt(envelope._iv, envelope._data, oldDek)\n\n // Re-encrypt with new DEK\n const { iv, data } = await encrypt(plaintext, newDek)\n const newEnvelope: EncryptedEnvelope = {\n _noydb: NOYDB_FORMAT_VERSION,\n _v: envelope._v,\n _ts: new Date().toISOString(),\n _iv: iv,\n _data: data,\n }\n await store.put(vault, collName, id, newEnvelope)\n }\n }\n\n // Update caller's keyring with new DEKs\n for (const [collName, newDek] of newDeks) {\n callerKeyring.deks.set(collName, newDek)\n }\n await persistKeyring(store, vault, callerKeyring)\n\n // Drop the rotated collections from every OTHER member's keyring.\n //\n // The old comment here said these were \"re-wrapped\", which is what\n // `Noydb.rotate`'s jsdoc promised too — but re-wrapping is impossible and\n // always was (#854). See the note below.\n const needsRegrant: Array<{ userId: string; collection: string }> = []\n const userIds = await store.list(vault, '_keyring')\n for (const userId of userIds) {\n if (userId === callerKeyring.userId) continue\n\n const userEnvelope = await store.get(vault, '_keyring', userId)\n if (!userEnvelope) continue\n\n const userKeyringFile = JSON.parse(userEnvelope._data) as KeyringFile\n // A user's DEKs are wrapped with that user's KEK, and a KEK derives only\n // from the user's secret — the caller can never derive another user's\n // KEK, so re-wrapping the new DEKs for them is impossible. Rotation\n // therefore REMOVES the rotated collections' DEK entries (and permissions)\n // from each remaining user's keyring: secure (revoked keys are gone) but\n // the owner must re-run grant() for those users/collections.\n\n const updatedDeks = { ...userKeyringFile.deks }\n const updatedPermissions = { ...userKeyringFile.permissions }\n for (const collName of collections) {\n // Report only what the member actually held — a user who never had the\n // collection does not \"need a re-grant\".\n if (collName in updatedDeks || collName in updatedPermissions) {\n needsRegrant.push({ userId, collection: collName })\n }\n delete updatedDeks[collName]\n delete updatedPermissions[collName]\n }\n\n const updatedKeyring: KeyringFile = {\n ...userKeyringFile,\n deks: updatedDeks,\n permissions: updatedPermissions,\n }\n\n await writeKeyringFile(store, vault, userId, updatedKeyring)\n }\n\n return { needsRegrant }\n}\n\n// ─── Change Secret ─────────────────────────────────────────────────────\n\n/** Options for {@link changeSecret} (#846b). */\nexport interface ChangeSecretOptions extends SecretPolicy {\n /** The replacement secret. */\n readonly newSecret: string\n /** Escape hatch for fixtures and migrations — skips the strength gate. */\n readonly allowWeakSecret?: boolean\n}\n\n/**\n * Change the user's secret. Re-wraps every DEK under the new KEK.\n *\n * Validates the new secret against the strength rules unless\n * `allowWeakSecret: true` is passed. Mirrors `rotateSecret`'s\n * default-on validation contract.\n *\n * `db.team.rotateSecret()` adds a `checkGate('rotate-secret')` step\n * on top of this primitive and additionally requires the OLD secret\n * for re-derivation; `changeSecret` reuses the cached unlocked KEK so\n * the OLD secret is not retyped.\n */\nexport async function changeSecret(\n store: NoydbStore,\n vault: string,\n keyring: UnlockedKeyring,\n opts: ChangeSecretOptions,\n): Promise<UnlockedKeyring> {\n const { newSecret } = opts\n if (!opts.allowWeakSecret) {\n assertStrongSecret(newSecret, opts)\n }\n const newSalt = generateSalt()\n const newKek = await deriveKey(newSecret, newSalt)\n\n // Re-wrap all DEKs with the new KEK\n const wrappedDeks: Record<string, string> = {}\n for (const [collName, dek] of keyring.deks) {\n wrappedDeks[collName] = await wrapKey(dek, newKek)\n }\n\n const canary = await mintKeyringCanary(newKek)\n const keyringFile: KeyringFile = {\n _noydb_keyring: NOYDB_KEYRING_VERSION,\n user_id: keyring.userId,\n display_name: keyring.displayName,\n role: keyring.role,\n permissions: keyring.permissions,\n deks: wrappedDeks,\n salt: bufferToBase64(newSalt),\n created_at: new Date().toISOString(),\n granted_by: keyring.userId,\n canary,\n }\n\n await writeKeyringFile(store, vault, keyring.userId, keyringFile)\n\n return {\n userId: keyring.userId,\n displayName: keyring.displayName,\n role: keyring.role,\n permissions: keyring.permissions,\n deks: keyring.deks, // Same DEKs, different wrapping\n kek: newKek,\n salt: newSalt,\n // Tier-2 slots are NOT preserved through `changeSecret` —\n // each slot wraps the OLD KEK, so the new keyring has no\n // authenticator slots until the user re-enrolls. The higher-level\n // `db.team.rotateSecret()` preserves slots by rewrapping the\n // KEK reference, not the KEK itself.\n authenticators: [],\n ...(keyring.policy !== undefined && { policy: keyring.policy }),\n }\n}\n\n// ─── Bundle recipients ──────────────────────────────────────────\n\n/**\n * Recipient slot in a re-keyed `.noydb` bundle. Each slot becomes its\n * own keyring file inside the bundle, sealed with its own secret.\n * Same role/permission semantics as `db.grant()` but no store side\n * effect — the slot only exists inside the bundle bytes.\n *\n * @public\n */\nexport interface BundleRecipient {\n /** User id stamped onto the keyring file in the bundle. */\n readonly id: string\n /** Optional display name. Defaults to `id`. */\n readonly displayName?: string\n /** Secret the recipient will type to unlock. */\n readonly secret: string\n /** Role on the destination vault. Defaults to `'viewer'`. */\n readonly role?: Role\n /**\n * Per-collection permissions. When omitted, role defaults apply.\n * Restricting permissions here ALSO restricts which DEKs are wrapped\n * into the slot — a slot with `{ invoices: 'ro' }` cannot decrypt\n * other collections even though their ciphertext sits in the bundle.\n */\n readonly permissions?: Permissions\n /**\n * Optional `as-*` export grants on the destination vault.\n * Mirrors the `exportCapability` field on a live keyring.\n */\n readonly exportCapability?: ExportCapability\n /**\n * Optional `as-*` import grants on the destination vault.\n * Mirrors the `importCapability` field on a live keyring.\n * Default-closed: no plaintext format granted, no bundle import.\n */\n readonly importCapability?: ImportCapability\n /**\n * Optional bundle-slot expiry. ISO-8601 timestamp; past the\n * cutoff this slot's keyring refuses to load with\n * `KeyringExpiredError`. Time-boxed audit access pattern: \"this\n * slot works for 30 days then becomes opaque to its holder.\"\n */\n readonly expiresAt?: string\n}\n\n/**\n * Build a `KeyringFile` for one bundle recipient, given the source\n * vault's unwrapped DEKs. Mirrors `grant()` minus the store write —\n * the produced file is meant to be embedded in the bundle's\n * `keyrings` map, never persisted to the source vault.\n *\n * Privilege-escalation check still runs: every DEK wrapped into the\n * recipient's keyring must come from the source's own DEK set.\n *\n * @internal\n */\nexport async function buildRecipientKeyringFile(\n callerKeyring: UnlockedKeyring,\n recipient: BundleRecipient,\n): Promise<KeyringFile> {\n if (!callerKeyring.kek) {\n throw new ValidationError(\n 'buildRecipientKeyringFile: caller keyring has no KEK — tier-2 wrap-DEKs ' +\n 'and tier-3 PIN-resume sessions cannot create bundle recipients. ' +\n 'Re-authenticate at tier 1 (secret) before building a bundle.',\n )\n }\n\n const role: Role = recipient.role ?? 'viewer'\n const permissions = resolvePermissions(role, recipient.permissions)\n\n const newSalt = generateSalt()\n const newKek = await deriveKey(recipient.secret, newSalt)\n\n const wrappedDeks: Record<string, string> = {}\n\n // Collections the recipient was explicitly granted permission to.\n for (const collName of Object.keys(permissions)) {\n const dek = callerKeyring.deks.get(collName)\n if (dek) {\n wrappedDeks[collName] = await wrapKey(dek, newKek)\n }\n }\n\n // owner / admin / custodian / viewer: wrap every known DEK (matches grant).\n // FR-6: a custodian recipient operates every collection, so it receives all\n // DEKs just like admin (kept in lockstep with grant()'s all-DEKs branch).\n if (role === 'owner' || role === 'admin' || role === 'custodian' || role === 'viewer') {\n for (const [collName, dek] of callerKeyring.deks) {\n if (!(collName in wrappedDeks)) {\n wrappedDeks[collName] = await wrapKey(dek, newKek)\n }\n }\n }\n\n // Always propagate system-prefixed collection DEKs (`_ledger`, etc.) —\n // the recipient needs them to verify the bundle on import.\n for (const [collName, dek] of callerKeyring.deks) {\n if (collName.startsWith('_') && !(collName in wrappedDeks)) {\n wrappedDeks[collName] = await wrapKey(dek, newKek)\n }\n }\n\n // Anti-privilege-escalation: every wrapped DEK must come from the\n // caller's own DEK set. Belt-and-braces with the lookups above.\n for (const collName of Object.keys(wrappedDeks)) {\n if (!callerKeyring.deks.has(collName)) {\n throw new PrivilegeEscalationError(collName)\n }\n }\n\n const canary = await mintKeyringCanary(newKek)\n return {\n _noydb_keyring: NOYDB_KEYRING_VERSION,\n user_id: recipient.id,\n display_name: recipient.displayName ?? recipient.id,\n role,\n permissions,\n deks: wrappedDeks,\n salt: bufferToBase64(newSalt),\n created_at: new Date().toISOString(),\n granted_by: callerKeyring.userId,\n canary,\n ...(recipient.exportCapability !== undefined\n ? { export_capability: recipient.exportCapability }\n : {}),\n ...(recipient.importCapability !== undefined\n ? { import_capability: recipient.importCapability }\n : {}),\n ...(recipient.expiresAt !== undefined\n ? { expires_at: recipient.expiresAt }\n : {}),\n }\n}\n\n// ─── List Users ────────────────────────────────────────────────────────\n\n/** List all users with access to a vault. */\nexport async function listUsers(\n store: NoydbStore,\n vault: string,\n): Promise<UserInfo[]> {\n const userIds = await store.list(vault, '_keyring')\n const users: UserInfo[] = []\n\n for (const userId of userIds) {\n const envelope = await store.get(vault, '_keyring', userId)\n if (!envelope) continue\n const kf = JSON.parse(envelope._data) as KeyringFile\n users.push({\n userId: kf.user_id,\n displayName: kf.display_name,\n role: kf.role,\n permissions: kf.permissions,\n createdAt: kf.created_at,\n grantedBy: kf.granted_by,\n })\n }\n\n return users\n}\n\n/**\n * Optional filter knobs for {@link listUsersWithEnvelopes}.\n *\n * - `includeHidden` — when true, principals with `_meta/visibility/<id>`\n * set to `{ hidden: true }` are returned alongside everyone else.\n * Requires `owner` or `admin` callerRole; lower roles get\n * {@link import('../../kernel/errors.js').PermissionDeniedError}.\n */\nexport interface ListUsersOptions {\n readonly includeHidden?: boolean\n}\n\n/**\n * Joined enumeration: every keyring + its `_users/<keyringId>`\n * envelope side by side. Convenience for admin UIs that want to\n * render team-member lists with profile data (\"Bob — operator —\n * 'Bob the Auditor' avatar X locale fr-FR\") in a single pass.\n *\n * `userEnvelopeDek` is the vault's `_users` collection DEK\n * (`vault.getDEK('_users')`); used to decrypt every envelope.\n *\n * `callerRole` drives the directory-visibility checks:\n *\n * - When the vault's `_meta/directory` document has `enabled: false`,\n * only `owner` and `admin` callers may enumerate; anyone else gets\n * {@link import('../../kernel/errors.js').DirectoryDisabledError}.\n * - Principals with `_meta/visibility/<id>` set to `{ hidden: true }`\n * are filtered out by default. `owner`/`admin` callers can pass\n * `{ includeHidden: true }` to see them; lower roles passing that\n * option get `PermissionDeniedError`.\n *\n * Honest caveat: these filters are a UX hint, not a security\n * boundary. The keyring file is still listed at `_keyring/*` and the\n * envelope ciphertext at `_users/*`. A caller with direct store access\n * — or a caller that calls this function with `callerRole: 'owner'`\n * unconditionally — sees every principal. The protection is only as\n * strong as the role the calling layer passes in. The hub-level wrapper\n * on `Vault` sources `callerRole` from the unlocked keyring's `role`\n * field, which is signed-by-construction (it lives in the user's own\n * keyring file). See `https://github.com/vLannaAi/noy-db-docs/blob/main/content/docs/services/user-envelope.md` →\n * \"Directory visibility\".\n *\n * Principals without a persisted envelope (legacy keyrings predating\n * the user-envelope feature) come back with `envelope: null`. The\n * caller chooses how to render — usually \"fall back to keyring's\n * `displayName`\".\n *\n * Order matches `listUsers()` (store-defined; sort if you need a\n * stable display order).\n */\nexport async function listUsersWithEnvelopes<T = unknown>(\n store: NoydbStore,\n vault: string,\n userEnvelopeDek: EnclaveKey,\n callerRole: Role,\n options: ListUsersOptions = {},\n): Promise<Array<{ user: UserInfo; envelope: UserEnvelope<T> | null }>> {\n // FR-6: custodian is INTENTIONALLY treated as NON-privileged here (SAFER\n // default — review flag). Directory-privilege is a team-MANAGEMENT capability\n // (bypassing the visibility toggle + listing hidden principals); a custodian\n // is non-owning and cannot grant/revoke, so it should not see the hidden\n // team membership. It still gets the normal (non-hidden) directory view.\n const isPrivileged = callerRole === 'owner' || callerRole === 'admin'\n\n // 1. Vault-level directory toggle.\n const dirConfig = await readDirectoryConfig(store, vault)\n if (dirConfig?.enabled === false && !isPrivileged) {\n throw new DirectoryDisabledError(vault)\n }\n\n // 2. `includeHidden` requires admin/owner.\n if (options.includeHidden && !isPrivileged) {\n throw new PermissionDeniedError(\n 'Permission denied — listUsersWithEnvelopes({ includeHidden: true }) requires owner or admin role',\n )\n }\n\n const users = await listUsers(store, vault)\n const out: Array<{ user: UserInfo; envelope: UserEnvelope<T> | null }> = []\n for (const user of users) {\n if (!options.includeHidden) {\n const visibility = await readUserVisibility(store, vault, user.userId)\n if (visibility?.hidden) continue\n }\n const envelope = await loadUserEnvelopeFn<T>(\n store,\n vault,\n user.userId,\n userEnvelopeDek,\n )\n out.push({ user, envelope })\n }\n return out\n}\n\n\n// ─── DEK Management ────────────────────────────────────────────────────\n\n/** Ensure a DEK exists for a collection. Generates one if new. */\nexport async function ensureCollectionDEK(\n store: NoydbStore,\n vault: string,\n keyring: UnlockedKeyring,\n): Promise<(collectionName: string) => Promise<EnclaveKey>> {\n // Dedupe concurrent first-time DEK creates per collection. Without\n // this, two concurrent `getDEK('foo')` calls both pass the `existing`\n // check (the Map is empty), both generate fresh DEKs, and the second\n // `set` overwrites the first — making any envelope encrypted with\n // the discarded DEK fail to decrypt later (TamperedError on read).\n // Pre-existing race exposed by the multi-writer ledger work.\n const inFlight = new Map<string, Promise<EnclaveKey>>()\n return async (collectionName: string): Promise<EnclaveKey> => {\n const existing = keyring.deks.get(collectionName)\n if (existing) return existing\n const pending = inFlight.get(collectionName)\n if (pending) return pending\n\n const promise = (async () => {\n const dek = await generateDEK()\n keyring.deks.set(collectionName, dek)\n await persistKeyring(store, vault, keyring)\n return dek\n })()\n inFlight.set(collectionName, promise)\n try {\n return await promise\n } finally {\n inFlight.delete(collectionName)\n }\n }\n}\n\n// ─── Permission Checks ─────────────────────────────────────────────────\n\n/** Check if a user has write permission for a collection. */\nexport function hasWritePermission(keyring: UnlockedKeyring, collectionName: string): boolean {\n // FR-6: custodian writes every collection (admin-level operational rw).\n if (keyring.role === 'owner' || keyring.role === 'admin' || keyring.role === 'custodian') return true\n if (keyring.role === 'viewer' || keyring.role === 'client') return false\n return keyring.permissions[collectionName] === 'rw'\n}\n\n/** Check if a user has any access to a collection. */\nexport function hasAccess(keyring: UnlockedKeyring, collectionName: string): boolean {\n // FR-6: custodian reads every collection (admin-level operational access).\n if (\n keyring.role === 'owner' ||\n keyring.role === 'admin' ||\n keyring.role === 'custodian' ||\n keyring.role === 'viewer'\n )\n return true\n return collectionName in keyring.permissions\n}\n\n// ─── Helpers ───────────────────────────────────────────────────────────\n\n/** Persist a keyring file to the store. */\nexport async function persistKeyring(\n store: NoydbStore,\n vault: string,\n keyring: UnlockedKeyring,\n): Promise<void> {\n if (!keyring.kek) {\n throw new ValidationError(\n 'persistKeyring: keyring.kek is null — cannot wrap DEKs without the KEK. ' +\n 'This typically means the keyring was opened via tier-3 PIN resume, ' +\n 'session restore, or a wrap-DEKs tier-2 unlock. Re-authenticate at ' +\n 'tier 1 (secret) before persisting.',\n )\n }\n const wrappedDeks: Record<string, string> = {}\n for (const [collName, dek] of keyring.deks) {\n wrappedDeks[collName] = await wrapKey(dek, keyring.kek)\n }\n const canary = await mintKeyringCanary(keyring.kek)\n\n const keyringFile: KeyringFile = {\n _noydb_keyring: NOYDB_KEYRING_VERSION,\n user_id: keyring.userId,\n display_name: keyring.displayName,\n role: keyring.role,\n permissions: keyring.permissions,\n deks: wrappedDeks,\n salt: bufferToBase64(keyring.salt),\n created_at: new Date().toISOString(),\n granted_by: keyring.userId,\n canary,\n ...(keyring.exportCapability !== undefined && { export_capability: keyring.exportCapability }),\n ...(keyring.importCapability !== undefined && { import_capability: keyring.importCapability }),\n ...(keyring.authenticators.length > 0 && { authenticators: keyring.authenticators }),\n ...(keyring.policy !== undefined && { policy: keyring.policy }),\n }\n\n await writeKeyringFile(store, vault, keyring.userId, keyringFile)\n}\n\n// ─── Export capability ──────────────────────────────────────\n\n/**\n * Role-based default policy for the encrypted-bundle capability.\n *\n * Applied when `keyring.exportCapability` is absent or\n * `exportCapability.bundle` is undefined:\n *\n * - `owner` / `admin` → `true` (happy-path backup without friction)\n * - `operator` / `viewer` / `client` → `false` (explicit grant required)\n *\n * Rationale: a bundle is inert without the KEK, so an owner backing up\n * their own vault doesn't need friction; a non-admin role producing a\n * bundle for an external party does, because the bundle outlives\n * keyring revocation.\n */\nfunction defaultBundleCapability(role: Role): boolean {\n // FR-6: custodian is INTENTIONALLY not in the default-true set (SAFER\n // default — review flag). A bundle is an external artifact that outlives\n // keyring revocation; the owner can still grant a custodian\n // `exportCapability.bundle = true` explicitly when an offline backup is\n // part of the custody contract. Defaulting it off keeps a custodian from\n // silently minting a revocation-surviving copy of the whole vault.\n return role === 'owner' || role === 'admin'\n}\n\n/**\n * Check whether a keyring is authorised for a given `@noy-db/as-*`\n * export tier.\n *\n * - `tier: 'plaintext'` — returns true iff `exportCapability.plaintext`\n * contains the requested `format` or the `'*'` wildcard. Default for\n * every role is empty — no grant, no plaintext export.\n * - `tier: 'bundle'` — returns `exportCapability.bundle` if present, or\n * the role-based default otherwise (owner/admin → true, else false).\n *\n * `@noy-db/as-*` packages MUST call this before invoking the underlying\n * export primitive. Rogue forks that skip the check are caught by code\n * review — the single-entry-point contract is a convention, not a\n * runtime invariant. Vault-level gated wrappers\n * (`vault.exportRecords` / `exportBlobs` / `writeBundle`) will land in a\n * follow-up PR to enforce at the primitive level.\n */\nexport function hasExportCapability(\n keyring: UnlockedKeyring,\n tier: 'plaintext',\n format: ExportFormat,\n): boolean\nexport function hasExportCapability(\n keyring: UnlockedKeyring,\n tier: 'bundle',\n): boolean\nexport function hasExportCapability(\n keyring: UnlockedKeyring,\n tier: 'plaintext' | 'bundle',\n format?: ExportFormat,\n): boolean {\n const cap = keyring.exportCapability\n if (tier === 'plaintext') {\n const allowed = cap?.plaintext ?? []\n return allowed.includes('*') || (format !== undefined && allowed.includes(format))\n }\n // tier === 'bundle'\n return cap?.bundle ?? defaultBundleCapability(keyring.role)\n}\n\n/**\n * Same-shape inspector for an `ExportCapability` value that isn't yet\n * attached to a keyring (e.g. for previewing a grant before applying).\n * Role must be supplied separately so bundle defaults can be computed.\n */\nexport function evaluateExportCapability(\n capability: ExportCapability | undefined,\n role: Role,\n tier: 'plaintext',\n format: ExportFormat,\n): boolean\nexport function evaluateExportCapability(\n capability: ExportCapability | undefined,\n role: Role,\n tier: 'bundle',\n): boolean\nexport function evaluateExportCapability(\n capability: ExportCapability | undefined,\n role: Role,\n tier: 'plaintext' | 'bundle',\n format?: ExportFormat,\n): boolean {\n if (tier === 'plaintext') {\n const allowed = capability?.plaintext ?? []\n return allowed.includes('*') || (format !== undefined && allowed.includes(format))\n }\n return capability?.bundle ?? defaultBundleCapability(role)\n}\n\n// ─── Import capability (issue ) ────────────────────────────────────\n\n/**\n * Check whether a keyring is authorised for a given `@noy-db/as-*`\n * import tier (issue ).\n *\n * - `tier: 'plaintext'` — true iff `importCapability.plaintext`\n * contains the requested `format` or the `'*'` wildcard.\n * - `tier: 'bundle'` — true iff `importCapability.bundle === true`.\n *\n * **Default-closed for every role on every dimension** — including\n * owner. Import is more dangerous than export (corrupts vs leaks), so\n * the policy refuses to assume intent. Owners must positively grant\n * the capability via `vault.grant({ importCapability: ... })`.\n */\nexport function hasImportCapability(\n keyring: UnlockedKeyring,\n tier: 'plaintext',\n format: ExportFormat,\n): boolean\nexport function hasImportCapability(\n keyring: UnlockedKeyring,\n tier: 'bundle',\n): boolean\nexport function hasImportCapability(\n keyring: UnlockedKeyring,\n tier: 'plaintext' | 'bundle',\n format?: ExportFormat,\n): boolean {\n const cap = keyring.importCapability\n if (tier === 'plaintext') {\n const allowed = cap?.plaintext ?? []\n return allowed.includes('*') || (format !== undefined && allowed.includes(format))\n }\n // tier === 'bundle' — closed default for every role\n return cap?.bundle === true\n}\n\n/**\n * Same-shape inspector for an `ImportCapability` value that isn't yet\n * attached to a keyring (e.g. previewing a grant before applying).\n * `role` is accepted for symmetry with `evaluateExportCapability` even\n * though the import policy ignores it — bundle defaults are\n * role-agnostic and closed.\n */\nexport function evaluateImportCapability(\n capability: ImportCapability | undefined,\n role: Role,\n tier: 'plaintext',\n format: ExportFormat,\n): boolean\nexport function evaluateImportCapability(\n capability: ImportCapability | undefined,\n role: Role,\n tier: 'bundle',\n): boolean\nexport function evaluateImportCapability(\n capability: ImportCapability | undefined,\n _role: Role,\n tier: 'plaintext' | 'bundle',\n format?: ExportFormat,\n): boolean {\n if (tier === 'plaintext') {\n const allowed = capability?.plaintext ?? []\n return allowed.includes('*') || (format !== undefined && allowed.includes(format))\n }\n return capability?.bundle === true\n}\n\nfunction resolvePermissions(role: Role, explicit?: Permissions): Permissions {\n // FR-6: custodian is full-access-by-role (hasAccess/hasWritePermission\n // short-circuit on the role), so — like admin — its permissions map is\n // empty. Returning {} also prevents a caller-supplied `permissions` from\n // accidentally NARROWING a custodian below its role guarantee.\n if (role === 'owner' || role === 'admin' || role === 'custodian' || role === 'viewer') return {}\n return explicit ?? {}\n}\n\nasync function writeKeyringFile(\n store: NoydbStore,\n vault: string,\n userId: string,\n keyringFile: KeyringFile,\n): Promise<void> {\n const envelope = {\n _noydb: 1 as const,\n _v: 1,\n _ts: new Date().toISOString(),\n _iv: '',\n _data: JSON.stringify(keyringFile),\n }\n await store.put(vault, '_keyring', userId, envelope)\n}\n"],"mappings":"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","names":[]}