keri-ts 2.1.0 → 2.2.1

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 (300) hide show
  1. package/README.md +30 -2
  2. package/esm/keri/npm/src/acdc/authoring.js +195 -0
  3. package/esm/keri/npm/src/acdc/custody.js +187 -0
  4. package/esm/keri/npm/src/acdc/dhking.js +244 -32
  5. package/esm/keri/npm/src/acdc/regbasing.js +10 -0
  6. package/esm/keri/npm/src/acdc/salting.js +16 -1
  7. package/esm/keri/npm/src/acdc/scheming.js +93 -54
  8. package/esm/keri/npm/src/acdc/unblinding.js +118 -95
  9. package/esm/keri/npm/src/app/bootstrap/authenticating.js +4 -6
  10. package/esm/keri/npm/src/app/bootstrap/oobi-resolving.js +41 -0
  11. package/esm/keri/npm/src/app/bootstrap/oobiery.js +4 -6
  12. package/esm/keri/npm/src/app/credentials/credential-revoking.js +1 -0
  13. package/esm/keri/npm/src/app/credentials/credential-workflows.js +2 -0
  14. package/esm/keri/npm/src/app/credentials/registry-settling.js +44 -0
  15. package/esm/keri/npm/src/app/credentials/root-replicating.js +68 -60
  16. package/esm/keri/npm/src/app/credentials/root-replicator.js +198 -0
  17. package/esm/keri/npm/src/app/credentials/tel-completion.js +36 -0
  18. package/esm/keri/npm/src/app/delivery/forwarding.js +16 -3
  19. package/esm/keri/npm/src/app/exchanges/exchanging.js +14 -4
  20. package/esm/keri/npm/src/app/identity/challenging.js +50 -0
  21. package/esm/keri/npm/src/app/identity/delegating.js +77 -77
  22. package/esm/keri/npm/src/app/identity/delegation-approval.js +333 -0
  23. package/esm/keri/npm/src/app/identity/event-completion.js +154 -0
  24. package/esm/keri/npm/src/app/identity/habbing.js +129 -6
  25. package/esm/keri/npm/src/app/identity/keeping.js +77 -4
  26. package/esm/keri/npm/src/app/ipex/group-ipex.js +341 -0
  27. package/esm/keri/npm/src/app/ipex/group-issuance.js +905 -0
  28. package/esm/keri/npm/src/app/ipex/ipex-admitting.js +120 -0
  29. package/esm/keri/npm/src/app/ipex/ipex-conversation.js +197 -0
  30. package/esm/keri/npm/src/app/ipex/ipex-discloser.js +591 -0
  31. package/esm/keri/npm/src/app/ipex/ipex-granting.js +93 -0
  32. package/esm/keri/npm/src/app/ipex/ipex-grouping.js +50 -19
  33. package/esm/keri/npm/src/app/ipex/ipex-node-authentication.js +42 -18
  34. package/esm/keri/npm/src/app/ipex/issuance.js +896 -0
  35. package/esm/keri/npm/src/app/ipex/pending-presentations.js +46 -0
  36. package/esm/keri/npm/src/app/ipex/presentation-files.js +200 -0
  37. package/esm/keri/npm/src/app/ipex/presenting.js +408 -0
  38. package/esm/keri/npm/src/app/ipex/receive-issuance.js +86 -0
  39. package/esm/keri/npm/src/app/ipex/unblinding-workflow.js +667 -0
  40. package/esm/keri/npm/src/app/mailbox/ipex-arrival.js +103 -0
  41. package/esm/keri/npm/src/app/mailbox/kel-publisher.js +322 -0
  42. package/esm/keri/npm/src/app/mailbox/kel-publishing.js +65 -0
  43. package/esm/keri/npm/src/app/mailbox/kel-submission.js +24 -0
  44. package/esm/keri/npm/src/app/mailbox/mailbox-admin.js +208 -0
  45. package/esm/keri/npm/src/app/mailbox/mailbox-poller.js +212 -47
  46. package/esm/keri/npm/src/app/mailbox/mailboxing.js +10 -5
  47. package/esm/keri/npm/src/app/multisig/counseling.js +488 -0
  48. package/esm/keri/npm/src/app/multisig/group-members.js +18 -0
  49. package/esm/keri/npm/src/app/multisig/multisig-messaging.js +33 -6
  50. package/esm/keri/npm/src/app/multisig/multisig-workflows.js +98 -147
  51. package/esm/keri/npm/src/app/multisig/proposals.js +281 -0
  52. package/esm/keri/npm/src/app/multisig/proposing.js +122 -0
  53. package/esm/keri/npm/src/app/notifications/signaling.js +5 -5
  54. package/esm/keri/npm/src/app/protocol/parsering.js +26 -1
  55. package/esm/keri/npm/src/app/protocol/protocol-processing.js +9 -3
  56. package/esm/keri/npm/src/app/queries/query-workflows.js +98 -0
  57. package/esm/keri/npm/src/app/runtime/agent-runtime.js +106 -55
  58. package/esm/keri/npm/src/app/runtime/cue-runtime.js +3 -6
  59. package/esm/keri/npm/src/app/runtime/delivery-outbox.js +87 -0
  60. package/esm/keri/npm/src/app/runtime/doing.js +17 -0
  61. package/esm/keri/npm/src/app/runtime/ipex-auxiliaries.js +207 -0
  62. package/esm/keri/npm/src/app/runtime/operation-owners.js +151 -0
  63. package/esm/keri/npm/src/app/runtime/retry-backoff.js +93 -0
  64. package/esm/keri/npm/src/app/runtime/workflow-contract.js +80 -0
  65. package/esm/keri/npm/src/app/transport/http-client.js +25 -6
  66. package/esm/keri/npm/src/app/witness/http-endpoints.js +4 -4
  67. package/esm/keri/npm/src/cli/common/event-completion.js +49 -0
  68. package/esm/keri/npm/src/cli/common/runtime-workers.js +23 -0
  69. package/esm/keri/npm/src/cli/credentials/ipex.js +164 -191
  70. package/esm/keri/npm/src/cli/credentials/vc.js +53 -72
  71. package/esm/keri/npm/src/cli/endpoints/did.js +12 -3
  72. package/esm/keri/npm/src/cli/endpoints/oobi.js +10 -13
  73. package/esm/keri/npm/src/cli/identity/challenge.js +8 -34
  74. package/esm/keri/npm/src/cli/identity/delegate.js +81 -258
  75. package/esm/keri/npm/src/cli/identity/incept.js +15 -32
  76. package/esm/keri/npm/src/cli/identity/interact.js +5 -17
  77. package/esm/keri/npm/src/cli/identity/rotate.js +8 -36
  78. package/esm/keri/npm/src/cli/index.js +3 -1
  79. package/esm/keri/npm/src/cli/messaging/group-join.js +83 -0
  80. package/esm/keri/npm/src/cli/messaging/group-wait.js +88 -0
  81. package/esm/keri/npm/src/cli/messaging/multisig.js +190 -285
  82. package/esm/keri/npm/src/cli/messaging/root-copy-lines.js +31 -0
  83. package/esm/keri/npm/src/core/kel/eventing.js +66 -10
  84. package/esm/keri/npm/src/core/mailbox/kram-policy.js +39 -6
  85. package/esm/keri/npm/src/core/mailbox/kraming.js +2 -0
  86. package/esm/keri/npm/src/core/records.js +23 -0
  87. package/esm/keri/npm/src/core/routing.js +13 -0
  88. package/esm/keri/npm/src/db/basing.js +34 -2
  89. package/esm/keri/npm/src/db/core/indexeddb-persistence.js +221 -0
  90. package/esm/keri/npm/src/db/indexeddb.js +98 -0
  91. package/esm/keri/npm/src/db/keeping.js +11 -2
  92. package/esm/keri/npm/src/db/store-lock.js +150 -0
  93. package/esm/keri/npm/src/npm/db-indexeddb.browser.js +2 -0
  94. package/esm/keri/npm/src/npm/db-indexeddb.js +3 -0
  95. package/esm/keri/npm/src/npm/db.browser.js +2 -0
  96. package/esm/keri/npm/src/npm/index.browser.js +9 -0
  97. package/esm/keri/npm/src/npm/runtime-web.browser.js +2 -0
  98. package/esm/keri/npm/src/npm/runtime-web.js +3 -0
  99. package/esm/keri/npm/src/npm/runtime.browser.js +2 -0
  100. package/esm/keri/npm/src/runtime/index.js +51 -4
  101. package/esm/keri/npm/src/runtime/web/output-barrier.js +47 -0
  102. package/esm/keri/npm/src/runtime/web/persistent-storage.js +37 -0
  103. package/esm/keri/npm/src/runtime/web.js +10 -0
  104. package/esm/keri/npm/src/vdr/tel-publication.js +16 -0
  105. package/esm/keri/npm/src/version.js +2 -2
  106. package/package.json +15 -2
  107. package/types/acdc/authoring.d.ts +54 -0
  108. package/types/acdc/authoring.d.ts.map +1 -0
  109. package/types/acdc/custody.d.ts +84 -0
  110. package/types/acdc/custody.d.ts.map +1 -0
  111. package/types/acdc/dhking.d.ts +94 -3
  112. package/types/acdc/dhking.d.ts.map +1 -1
  113. package/types/acdc/regbasing.d.ts +4 -0
  114. package/types/acdc/regbasing.d.ts.map +1 -1
  115. package/types/acdc/salting.d.ts +6 -1
  116. package/types/acdc/salting.d.ts.map +1 -1
  117. package/types/acdc/scheming.d.ts +2 -0
  118. package/types/acdc/scheming.d.ts.map +1 -1
  119. package/types/acdc/unblinding.d.ts +58 -19
  120. package/types/acdc/unblinding.d.ts.map +1 -1
  121. package/types/app/bootstrap/authenticating.d.ts +3 -1
  122. package/types/app/bootstrap/authenticating.d.ts.map +1 -1
  123. package/types/app/bootstrap/oobi-resolving.d.ts +92 -0
  124. package/types/app/bootstrap/oobi-resolving.d.ts.map +1 -0
  125. package/types/app/bootstrap/oobiery.d.ts +3 -1
  126. package/types/app/bootstrap/oobiery.d.ts.map +1 -1
  127. package/types/app/credentials/credential-revoking.d.ts +3 -0
  128. package/types/app/credentials/credential-revoking.d.ts.map +1 -1
  129. package/types/app/credentials/credential-workflows.d.ts +5 -0
  130. package/types/app/credentials/credential-workflows.d.ts.map +1 -1
  131. package/types/app/credentials/registry-settling.d.ts +85 -0
  132. package/types/app/credentials/registry-settling.d.ts.map +1 -0
  133. package/types/app/credentials/root-replicating.d.ts +56 -44
  134. package/types/app/credentials/root-replicating.d.ts.map +1 -1
  135. package/types/app/credentials/root-replicator.d.ts +105 -0
  136. package/types/app/credentials/root-replicator.d.ts.map +1 -0
  137. package/types/app/credentials/tel-completion.d.ts +55 -0
  138. package/types/app/credentials/tel-completion.d.ts.map +1 -0
  139. package/types/app/delivery/forwarding.d.ts +6 -0
  140. package/types/app/delivery/forwarding.d.ts.map +1 -1
  141. package/types/app/exchanges/exchanging.d.ts.map +1 -1
  142. package/types/app/identity/challenging.d.ts +54 -0
  143. package/types/app/identity/challenging.d.ts.map +1 -1
  144. package/types/app/identity/delegating.d.ts +21 -4
  145. package/types/app/identity/delegating.d.ts.map +1 -1
  146. package/types/app/identity/delegation-approval.d.ts +228 -0
  147. package/types/app/identity/delegation-approval.d.ts.map +1 -0
  148. package/types/app/identity/event-completion.d.ts +151 -0
  149. package/types/app/identity/event-completion.d.ts.map +1 -0
  150. package/types/app/identity/habbing.d.ts +67 -2
  151. package/types/app/identity/habbing.d.ts.map +1 -1
  152. package/types/app/identity/keeping.d.ts +49 -3
  153. package/types/app/identity/keeping.d.ts.map +1 -1
  154. package/types/app/ipex/group-ipex.d.ts +254 -0
  155. package/types/app/ipex/group-ipex.d.ts.map +1 -0
  156. package/types/app/ipex/group-issuance.d.ts +281 -0
  157. package/types/app/ipex/group-issuance.d.ts.map +1 -0
  158. package/types/app/ipex/ipex-admitting.d.ts +47 -0
  159. package/types/app/ipex/ipex-admitting.d.ts.map +1 -0
  160. package/types/app/ipex/ipex-conversation.d.ts +195 -0
  161. package/types/app/ipex/ipex-conversation.d.ts.map +1 -0
  162. package/types/app/ipex/ipex-discloser.d.ts +156 -0
  163. package/types/app/ipex/ipex-discloser.d.ts.map +1 -0
  164. package/types/app/ipex/ipex-granting.d.ts +145 -0
  165. package/types/app/ipex/ipex-granting.d.ts.map +1 -0
  166. package/types/app/ipex/ipex-grouping.d.ts +18 -0
  167. package/types/app/ipex/ipex-grouping.d.ts.map +1 -1
  168. package/types/app/ipex/ipex-node-authentication.d.ts +9 -1
  169. package/types/app/ipex/ipex-node-authentication.d.ts.map +1 -1
  170. package/types/app/ipex/issuance.d.ts +445 -0
  171. package/types/app/ipex/issuance.d.ts.map +1 -0
  172. package/types/app/ipex/pending-presentations.d.ts +25 -0
  173. package/types/app/ipex/pending-presentations.d.ts.map +1 -0
  174. package/types/app/ipex/presentation-files.d.ts +47 -0
  175. package/types/app/ipex/presentation-files.d.ts.map +1 -0
  176. package/types/app/ipex/presenting.d.ts +300 -0
  177. package/types/app/ipex/presenting.d.ts.map +1 -0
  178. package/types/app/ipex/receive-issuance.d.ts +99 -0
  179. package/types/app/ipex/receive-issuance.d.ts.map +1 -0
  180. package/types/app/ipex/unblinding-workflow.d.ts +257 -0
  181. package/types/app/ipex/unblinding-workflow.d.ts.map +1 -0
  182. package/types/app/mailbox/ipex-arrival.d.ts +104 -0
  183. package/types/app/mailbox/ipex-arrival.d.ts.map +1 -0
  184. package/types/app/mailbox/kel-publisher.d.ts +141 -0
  185. package/types/app/mailbox/kel-publisher.d.ts.map +1 -0
  186. package/types/app/mailbox/kel-publishing.d.ts +70 -0
  187. package/types/app/mailbox/kel-publishing.d.ts.map +1 -0
  188. package/types/app/mailbox/kel-submission.d.ts +18 -0
  189. package/types/app/mailbox/kel-submission.d.ts.map +1 -0
  190. package/types/app/mailbox/mailbox-admin.d.ts +113 -0
  191. package/types/app/mailbox/mailbox-admin.d.ts.map +1 -0
  192. package/types/app/mailbox/mailbox-poller.d.ts +92 -3
  193. package/types/app/mailbox/mailbox-poller.d.ts.map +1 -1
  194. package/types/app/mailbox/mailboxing.d.ts +3 -0
  195. package/types/app/mailbox/mailboxing.d.ts.map +1 -1
  196. package/types/app/multisig/counseling.d.ts +196 -0
  197. package/types/app/multisig/counseling.d.ts.map +1 -0
  198. package/types/app/multisig/group-members.d.ts +11 -0
  199. package/types/app/multisig/group-members.d.ts.map +1 -1
  200. package/types/app/multisig/grouping.d.ts +39 -20
  201. package/types/app/multisig/grouping.d.ts.map +1 -1
  202. package/types/app/multisig/multisig-messaging.d.ts +23 -6
  203. package/types/app/multisig/multisig-messaging.d.ts.map +1 -1
  204. package/types/app/multisig/multisig-workflows.d.ts +91 -22
  205. package/types/app/multisig/multisig-workflows.d.ts.map +1 -1
  206. package/types/app/multisig/proposals.d.ts +129 -0
  207. package/types/app/multisig/proposals.d.ts.map +1 -0
  208. package/types/app/multisig/proposing.d.ts +125 -0
  209. package/types/app/multisig/proposing.d.ts.map +1 -0
  210. package/types/app/notifications/signaling.d.ts +3 -1
  211. package/types/app/notifications/signaling.d.ts.map +1 -1
  212. package/types/app/protocol/parsering.d.ts +13 -0
  213. package/types/app/protocol/parsering.d.ts.map +1 -1
  214. package/types/app/protocol/protocol-processing.d.ts +7 -1
  215. package/types/app/protocol/protocol-processing.d.ts.map +1 -1
  216. package/types/app/queries/query-workflows.d.ts +98 -0
  217. package/types/app/queries/query-workflows.d.ts.map +1 -0
  218. package/types/app/runtime/agent-runtime.d.ts +72 -25
  219. package/types/app/runtime/agent-runtime.d.ts.map +1 -1
  220. package/types/app/runtime/cue-runtime.d.ts +2 -1
  221. package/types/app/runtime/cue-runtime.d.ts.map +1 -1
  222. package/types/app/runtime/delivery-outbox.d.ts +68 -0
  223. package/types/app/runtime/delivery-outbox.d.ts.map +1 -0
  224. package/types/app/runtime/doing.d.ts +27 -0
  225. package/types/app/runtime/doing.d.ts.map +1 -0
  226. package/types/app/runtime/ipex-auxiliaries.d.ts +74 -0
  227. package/types/app/runtime/ipex-auxiliaries.d.ts.map +1 -0
  228. package/types/app/runtime/operation-owners.d.ts +137 -0
  229. package/types/app/runtime/operation-owners.d.ts.map +1 -0
  230. package/types/app/runtime/retry-backoff.d.ts +49 -0
  231. package/types/app/runtime/retry-backoff.d.ts.map +1 -0
  232. package/types/app/runtime/workflow-contract.d.ts +79 -0
  233. package/types/app/runtime/workflow-contract.d.ts.map +1 -0
  234. package/types/app/transport/http-client.d.ts.map +1 -1
  235. package/types/cli/common/event-completion.d.ts +26 -0
  236. package/types/cli/common/event-completion.d.ts.map +1 -0
  237. package/types/cli/common/runtime-workers.d.ts +23 -0
  238. package/types/cli/common/runtime-workers.d.ts.map +1 -0
  239. package/types/cli/credentials/ipex.d.ts.map +1 -1
  240. package/types/cli/credentials/vc.d.ts +1 -1
  241. package/types/cli/credentials/vc.d.ts.map +1 -1
  242. package/types/cli/endpoints/did.d.ts.map +1 -1
  243. package/types/cli/endpoints/oobi.d.ts +3 -1
  244. package/types/cli/endpoints/oobi.d.ts.map +1 -1
  245. package/types/cli/identity/challenge.d.ts +1 -1
  246. package/types/cli/identity/challenge.d.ts.map +1 -1
  247. package/types/cli/identity/delegate.d.ts +14 -1
  248. package/types/cli/identity/delegate.d.ts.map +1 -1
  249. package/types/cli/identity/incept.d.ts.map +1 -1
  250. package/types/cli/identity/interact.d.ts.map +1 -1
  251. package/types/cli/identity/rotate.d.ts.map +1 -1
  252. package/types/cli/index.d.ts +3 -1
  253. package/types/cli/index.d.ts.map +1 -1
  254. package/types/cli/messaging/group-join.d.ts +56 -0
  255. package/types/cli/messaging/group-join.d.ts.map +1 -0
  256. package/types/cli/messaging/group-wait.d.ts +34 -0
  257. package/types/cli/messaging/group-wait.d.ts.map +1 -0
  258. package/types/cli/messaging/multisig.d.ts +29 -5
  259. package/types/cli/messaging/multisig.d.ts.map +1 -1
  260. package/types/cli/messaging/root-copy-lines.d.ts +39 -0
  261. package/types/cli/messaging/root-copy-lines.d.ts.map +1 -0
  262. package/types/core/cues.d.ts +11 -0
  263. package/types/core/cues.d.ts.map +1 -1
  264. package/types/core/kel/eventing.d.ts.map +1 -1
  265. package/types/core/mailbox/kram-policy.d.ts +14 -0
  266. package/types/core/mailbox/kram-policy.d.ts.map +1 -1
  267. package/types/core/mailbox/kraming.d.ts +1 -1
  268. package/types/core/mailbox/kraming.d.ts.map +1 -1
  269. package/types/core/records.d.ts +61 -0
  270. package/types/core/records.d.ts.map +1 -1
  271. package/types/core/routing.d.ts +6 -0
  272. package/types/core/routing.d.ts.map +1 -1
  273. package/types/db/basing.d.ts +8 -2
  274. package/types/db/basing.d.ts.map +1 -1
  275. package/types/db/core/indexeddb-persistence.d.ts +59 -0
  276. package/types/db/core/indexeddb-persistence.d.ts.map +1 -0
  277. package/types/db/indexeddb.d.ts +37 -0
  278. package/types/db/indexeddb.d.ts.map +1 -0
  279. package/types/db/keeping.d.ts +5 -2
  280. package/types/db/keeping.d.ts.map +1 -1
  281. package/types/db/store-lock.d.ts +72 -0
  282. package/types/db/store-lock.d.ts.map +1 -0
  283. package/types/npm/db-indexeddb.d.ts +4 -0
  284. package/types/npm/db-indexeddb.d.ts.map +1 -0
  285. package/types/npm/runtime-web.d.ts +4 -0
  286. package/types/npm/runtime-web.d.ts.map +1 -0
  287. package/types/runtime/index.d.ts +42 -7
  288. package/types/runtime/index.d.ts.map +1 -1
  289. package/types/runtime/web/output-barrier.d.ts +30 -0
  290. package/types/runtime/web/output-barrier.d.ts.map +1 -0
  291. package/types/runtime/web/persistent-storage.d.ts +36 -0
  292. package/types/runtime/web/persistent-storage.d.ts.map +1 -0
  293. package/types/runtime/web.d.ts +11 -0
  294. package/types/runtime/web.d.ts.map +1 -0
  295. package/types/vdr/tel-publication.d.ts +6 -0
  296. package/types/vdr/tel-publication.d.ts.map +1 -1
  297. package/types/version.d.ts +2 -2
  298. package/esm/keri/npm/src/cli/common/tel-completion.js +0 -24
  299. package/types/cli/common/tel-completion.d.ts +0 -8
  300. package/types/cli/common/tel-completion.d.ts.map +0 -1
@@ -8,7 +8,7 @@ import { hkdf } from "@noble/hashes/hkdf";
8
8
  import { sha256 } from "@noble/hashes/sha2";
9
9
  import { Hab } from "../app/identity/habbing.js";
10
10
  import { groupWitnesserSelection, isLocalGroupHab, latestGroupContribution, localGroupMember, } from "../app/multisig/group-members.js";
11
- import { copyAuxiliaryQuery, envelopeFromFrame, } from "../app/protocol/parsering.js";
11
+ import { auxiliaryQueryWire, copyAuxiliaryQuery, envelopeFromFrame, restoreAuxiliaryQuery, } from "../app/protocol/parsering.js";
12
12
  import { Deck } from "../core/deck.js";
13
13
  import { ValidationError } from "../core/errors.js";
14
14
  import { verifyQueryAttachments } from "../core/exchange/exchange-proofing.js";
@@ -85,6 +85,72 @@ export function deriveRoot(privateKey, context) {
85
85
  shared.fill(0);
86
86
  }
87
87
  }
88
+ /**
89
+ * The DH transaction id of a Grant: its own SAID when it opens the exchange (`xip`), and otherwise the exchange it
90
+ * continues (`x`). The one definition, so the owner and any workflow that names the transaction agree.
91
+ */
92
+ export function grantTransactionId(grant) {
93
+ return grant.ilk === "xip" ? grant.said : grant.sad.x;
94
+ }
95
+ /** The parties and transaction of a Grant `hab` takes part in, or null when it is not a DH party of it. */
96
+ function grantParties(hab, input) {
97
+ const message = input.serder, issuer = message.pre, issuee = message.sad.ri;
98
+ const xid = grantTransactionId(message);
99
+ return typeof xid === "string" && typeof issuee === "string" && issuer !== issuee
100
+ && [issuer, issuee].includes(hab.pre)
101
+ ? { issuer, issuee, xid }
102
+ : null;
103
+ }
104
+ /** Resolve the pending Grant `hab` may start DH for, or the reason it cannot. Reads only; creates no approval. */
105
+ function freshScope(hab, exc, grant) {
106
+ const input = exc.retainedInput(grant, { pending: true });
107
+ if (!input)
108
+ return "DH requires an authenticated pending Grant";
109
+ const parties = grantParties(hab, input);
110
+ if (!parties)
111
+ return "Local habitat is not a DH party";
112
+ const context = exc.pendingGrant(grant, { ...parties, sender: parties.issuer, recipient: parties.issuee });
113
+ if (!context)
114
+ return "DH requires authenticated fresh registry scope";
115
+ return { input, ...parties, registries: [...context.registries] };
116
+ }
117
+ /**
118
+ * The scope of a completed exchange that can finish without new key exchange: the Grant is still pending
119
+ * inside its original deadline and `hab` holds a root for the registries it discloses. The scope is those
120
+ * registries; a registry that is still fresh must hold one. No fresh registry is required, because the
121
+ * issuer's update may already be accepted: {@link Dhke.finish} proves the held root against that update, so
122
+ * a root from any other exchange is rejected there. Pure read of stores. Null when nothing can resume.
123
+ */
124
+ export function resumableExchange({ hab, exc }, grant) {
125
+ const input = exc.retainedInput(grant, { pending: true });
126
+ const parties = input && grantParties(hab, input);
127
+ if (!input || !parties)
128
+ return null;
129
+ const disclosed = (input.nests ?? []).map((nest) => nest.body.ked?.rd);
130
+ const held = [...new Set(disclosed)].filter((registry) => typeof registry === "string" && hab.mgr.hasRegistryRoot(hab.pre, registry));
131
+ const fresh = exc.pendingGrant(grant, { ...parties, sender: parties.issuer, recipient: parties.issuee });
132
+ if (!held.length || fresh?.registries.some((registry) => !held.includes(registry)))
133
+ return null;
134
+ return { input, ...parties, registries: held };
135
+ }
136
+ /**
137
+ * Whether `registry` has no committed state: its head is still the inception and no update is escrowed.
138
+ * A staged update waits in the missing-anchor or out-of-order escrow under its registry until the issuer's
139
+ * anchor is accepted, and an accepted one moves the head, so two keyed reads bounded by this registry
140
+ * decide it. Only then can a retained root be unused, because every derivation from a root, and every copy
141
+ * of it to another member (which `RegistryRoot` authorizes only for an accepted, issuer-anchored update),
142
+ * starts from a staged or accepted update. A remote update escrowed for this registry also counts, which
143
+ * is the conservative side.
144
+ */
145
+ export function registryUncommitted(registryStore, registry) {
146
+ if (registryStore.headEvent(registry)?.ilk !== "rip")
147
+ return false;
148
+ for (const table of [registryStore.baser.maes, registryStore.baser.ooes]) {
149
+ for (const _ of table.getTopItemIter(registry))
150
+ return false;
151
+ }
152
+ return true;
153
+ }
88
154
  /**
89
155
  * Issuer/issuee DH for an explicitly approved pending Grant. Wire
90
156
  * intake cannot approve work. Inject as ProtocolProcessor.qvy and register on
@@ -95,7 +161,7 @@ export function deriveRoot(privateKey, context) {
95
161
  */
96
162
  export class Dhke {
97
163
  /** Bind concrete local owners; construction grants no consent and writes nothing. */
98
- constructor({ hab, exc, registryStore, rvy = hab.rvy, now = Date.now }) {
164
+ constructor({ hab, exc, registryStore, rvy = hab.rvy, now = Date.now, kram = true }) {
99
165
  Object.defineProperty(this, "resource", {
100
166
  enumerable: true,
101
167
  configurable: true,
@@ -108,6 +174,13 @@ export class Dhke {
108
174
  writable: true,
109
175
  value: new Deck()
110
176
  });
177
+ /** Completion cues for roots just retained by this group's custodian; feed `RootReplicator.replicate`. */
178
+ Object.defineProperty(this, "replicated", {
179
+ enumerable: true,
180
+ configurable: true,
181
+ writable: true,
182
+ value: new Deck()
183
+ });
111
184
  Object.defineProperty(this, "pendingQueries", {
112
185
  enumerable: true,
113
186
  configurable: true,
@@ -156,6 +229,13 @@ export class Dhke {
156
229
  writable: true,
157
230
  value: void 0
158
231
  });
232
+ Object.defineProperty(this, "kram", {
233
+ enumerable: true,
234
+ configurable: true,
235
+ writable: true,
236
+ value: void 0
237
+ });
238
+ this.kram = kram;
159
239
  if (!(hab instanceof Hab) || hab.db !== exc.hby.db || rvy.db !== hab.db
160
240
  || exc.hby.habs.get(hab.pre) !== hab)
161
241
  throw new ValidationError("DH requires a local signing habitat");
@@ -169,8 +249,20 @@ export class Dhke {
169
249
  throw new ValidationError("DH requires a local signing member");
170
250
  }
171
251
  }
172
- /** Select one live fresh issuance and freeze its exact bodies, parties and registry order. */
252
+ /**
253
+ * Select one live fresh issuance and freeze its exact bodies, parties and registry order.
254
+ *
255
+ * A registry that already holds a root refuses (`Registry root conflicts with this live DH operation`)
256
+ * unless it has no committed state ({@link registryUncommitted}): then the held root was never used and
257
+ * this exchange, whose Grant is from the registry's issuer, overwrites it when it retains its own root,
258
+ * never at approval, so an interruption before retention leaves the old root. Once any update is staged
259
+ * or accepted the refusal is unchanged. A KERIpy peer keeps the old rule and refuses such a second
260
+ * exchange: with a KERIpy holder the issuer needs a fresh registry. One process also refuses a second
261
+ * live operation on a registry, so two pending Grants cannot clobber each other's root.
262
+ */
173
263
  approve(grant, { coordinator, custodian } = {}) {
264
+ if (!this.kram)
265
+ throw new ValidationError("DH requires KRAM: enable it with the runtime's kram option");
174
266
  this.pruneQueries();
175
267
  this.hab.mgr.requireRegistryRootAccess();
176
268
  for (const op of this.operations.values()) {
@@ -184,27 +276,68 @@ export class Dhke {
184
276
  return op.xid;
185
277
  }
186
278
  }
187
- const input = this.exc.retainedInput(grant, { pending: true });
188
- if (!input)
189
- throw new ValidationError("DH requires an authenticated pending Grant");
190
- const message = input.serder, issuer = message.pre, issuee = message.sad.ri;
191
- const xid = message.ilk === "xip" ? message.said : message.sad.x;
192
- if (typeof xid !== "string" || typeof issuee !== "string" || issuer === issuee
193
- || ![issuer, issuee].includes(this.hab.pre))
194
- throw new ValidationError("Local habitat is not a DH party");
279
+ const scope = freshScope(this.hab, this.exc, grant);
280
+ if (typeof scope === "string")
281
+ throw new ValidationError(scope);
282
+ const op = this.frozen(scope, grant, { coordinator, custodian }, false);
283
+ for (const other of this.operations.values()) {
284
+ if (!other.abandoned && other.registries.some((registry) => op.registries.includes(registry))) {
285
+ throw new ValidationError("Another live DH operation uses this registry");
286
+ }
287
+ }
288
+ for (const registry of op.registries) {
289
+ if (this.hab.mgr.hasRegistryRoot(this.hab.pre, registry) && registryUncommitted(this.registryStore, registry)) {
290
+ op.replaces.add(registry);
291
+ }
292
+ }
293
+ this.roots(op);
294
+ this.operations.set(op.xid, op);
295
+ this.replayQuery(op.xid);
296
+ return op.xid;
297
+ }
298
+ /**
299
+ * Resume a completed exchange after a process restart, without new key exchange: rebuild the operation as
300
+ * completed from the roots held for the Grant's registries ({@link resumableExchange}). It holds no
301
+ * ephemeral key, so it cannot start, answer or renew a query; it can only {@link complete} and
302
+ * {@link finish}, which signs nothing and verifies the issuer's update against the held root. Needs an
303
+ * unlocked Keeper; wire intake never calls it. Repeating it returns the same transaction.
304
+ *
305
+ * @throws ValidationError unless the Grant is pending and holds a root for each registry in its scope.
306
+ */
307
+ resume(grant, { coordinator, custodian } = {}) {
308
+ this.pruneQueries();
309
+ this.hab.mgr.requireRegistryRootAccess();
310
+ for (const op of this.operations.values()) {
311
+ if (op.grant !== grant)
312
+ continue;
313
+ if (!op.completed)
314
+ throw new ValidationError("DH is still exchanging keys for this Grant; nothing to resume");
315
+ this.pending(op);
316
+ return op.xid;
317
+ }
318
+ const scope = resumableExchange(this, grant);
319
+ if (!scope)
320
+ throw new ValidationError("No root is held for this pending Grant's registries");
321
+ const op = this.frozen(scope, grant, { coordinator, custodian }, true);
322
+ this.roots(op);
323
+ this.operations.set(op.xid, op);
324
+ this.pendingQueries.delete(op.xid);
325
+ this.dropHeld(op.xid);
326
+ return op.xid;
327
+ }
328
+ /** Freeze the exact pending Grant, parties and registry order into an operation; registers nothing. */
329
+ frozen(scope, grant, { coordinator, custodian }, completed) {
330
+ const { input, issuer, issuee, xid, registries } = scope;
195
331
  if (this.operations.has(xid))
196
332
  throw new ValidationError("Another Grant is selected for this transaction");
197
- const context = this.exc.pendingGrant(grant, { xid, sender: issuer, recipient: issuee });
198
- if (!context)
199
- throw new ValidationError("DH requires authenticated fresh registry scope");
200
333
  const op = {
201
334
  xid,
202
335
  grant,
203
336
  issuer,
204
337
  issuee,
205
- raw: message.raw.slice(),
338
+ raw: input.serder.raw.slice(),
206
339
  bodies: (input.nests ?? []).map((nest) => nest.body.raw.slice()),
207
- registries: [...context.registries],
340
+ registries,
208
341
  privateKeys: new Map(),
209
342
  publicKeys: {},
210
343
  peer: null,
@@ -213,7 +346,8 @@ export class Dhke {
213
346
  proposal: null,
214
347
  contribution: null,
215
348
  custody: null,
216
- completed: false,
349
+ replaces: new Set(),
350
+ completed,
217
351
  abandoned: false,
218
352
  };
219
353
  if (this.group) {
@@ -235,10 +369,17 @@ export class Dhke {
235
369
  else if (coordinator !== undefined || custodian !== undefined) {
236
370
  throw new ValidationError("Individual DH has no group coordinator");
237
371
  }
238
- this.roots(op);
239
- this.operations.set(xid, op);
240
- this.replayQuery(xid);
241
- return xid;
372
+ return op;
373
+ }
374
+ /**
375
+ * Whether this owner already holds a live operation for `grant`. Two operation tables over one runtime
376
+ * (the host's and a command's own) use it so only the first to reach a Grant drives it.
377
+ */
378
+ holds(grant) {
379
+ for (const op of this.operations.values())
380
+ if (op.grant === grant && !op.abandoned)
381
+ return true;
382
+ return false;
242
383
  }
243
384
  /** Return the stable issuer proposal until own threshold intake supplies the complete query. */
244
385
  start(grant) {
@@ -506,12 +647,34 @@ export class Dhke {
506
647
  /** Drop local ephemeral key references and permanently refuse this operation. */
507
648
  abandon(xid) {
508
649
  const retained = this.pendingQueries.delete(xid);
650
+ this.dropHeld(xid);
509
651
  if (retained && !this.operations.has(xid))
510
652
  return;
511
653
  const op = this.operation(xid);
512
654
  this.clearPrivate(op);
513
655
  op.abandoned = true;
514
656
  }
657
+ /**
658
+ * Whether this local habitat is the party of DH transaction `xid` with `sender`.
659
+ *
660
+ * Messages carry no recipient, so a per-identifier registry selects the owner by transaction:
661
+ * an approved operation here, a stored Grant from `sender` addressed to this habitat, or, for a
662
+ * group, its own members' contributions, which KRAM must pool before this owner consents.
663
+ */
664
+ inTransaction(sender, xid) {
665
+ return this.operations.has(xid) || (this.group && sender === this.hab.pre)
666
+ || Dhke.recipients(this.hab.db, sender, xid).includes(this.hab.pre);
667
+ }
668
+ /** Recipients of the pending Grants `sender` opened in DH transaction `xid`, from KRAM-accepted store rows. */
669
+ static recipients(db, sender, xid) {
670
+ const recipients = [];
671
+ for (const [key] of db.tmsc.getTopItemIter([sender, xid])) {
672
+ const recipient = db.epse.get([key[2]])?.sad?.ri;
673
+ if (typeof recipient === "string")
674
+ recipients.push(recipient);
675
+ }
676
+ return recipients;
677
+ }
515
678
  /** Release all process-local approvals and retry bytes; zero owned ephemeral scalar arrays. */
516
679
  close() {
517
680
  for (const op of this.operations.values())
@@ -519,6 +682,7 @@ export class Dhke {
519
682
  this.operations.clear();
520
683
  this.pendingQueries.clear();
521
684
  this.outgoing.clear();
685
+ this.replicated.clear();
522
686
  }
523
687
  /** Retain public query evidence against one exact pending Grant, without creating keys or consent. */
524
688
  retainQuery(envelope) {
@@ -540,11 +704,22 @@ export class Dhke {
540
704
  for (const [, key] of entries)
541
705
  decodePublicKey(key);
542
706
  const expires = this.retainUntil(envelope, grant);
543
- const prior = this.pendingQueries.get(data.x);
544
- if (prior && !bytesEqual(prior.envelope.serder.raw, query.raw))
545
- throw new ValidationError("Conflicting DH query");
546
- if (!prior) {
707
+ // After a restart the persisted copy is the prior one, so a successor is judged as it is before one.
708
+ const prior = this.pendingQueries.get(data.x) ?? this.loadHeld(data.x, grant.said);
709
+ if (prior && !bytesEqual(prior.envelope.serder.raw, query.raw)) {
710
+ // The issuer re-dates a query that aged before this side consented (`requery`): same sender, keys and
711
+ // Grant under a new date. It replaces the held one; any other difference is a conflict.
712
+ const held = this.parameters(prior.envelope.serder, "q");
713
+ const same = prior.envelope.serder.pre === query.pre && prior.grant === grant.said
714
+ && JSON.stringify(Object.entries(held.pk).sort()) === JSON.stringify(entries.sort());
715
+ if (!same)
716
+ throw new ValidationError("Conflicting DH query");
717
+ }
718
+ if (!prior || !bytesEqual(prior.envelope.serder.raw, query.raw)) {
547
719
  this.pendingQueries.set(data.x, { envelope: copyAuxiliaryQuery(envelope), expires, grant: grant.said });
720
+ // KRAM now holds this query as seen, so a resend of it is a replay: the held copy must outlive this process.
721
+ if (!envelope.nests.length)
722
+ this.hab.db.heldq.pin([data.x], auxiliaryQueryWire(envelope));
548
723
  }
549
724
  }
550
725
  /** Expired evidence cannot survive another owner interaction. */
@@ -557,11 +732,32 @@ export class Dhke {
557
732
  }
558
733
  /** Consume the first authenticated request only after explicit approval; never renew either clock. */
559
734
  replayQuery(xid) {
560
- const pending = this.pendingQueries.get(xid);
735
+ const grant = this.operations.get(xid)?.grant;
736
+ const pending = this.pendingQueries.get(xid) ?? (grant ? this.loadHeld(xid, grant) : undefined);
561
737
  this.pendingQueries.delete(xid);
562
- if (pending && pending.grant === this.operations.get(xid)?.grant
563
- && BigInt(this.now()) * 1000n < pending.expires)
738
+ this.dropHeld(xid);
739
+ if (pending && pending.grant === grant && BigInt(this.now()) * 1000n < pending.expires) {
564
740
  this.processQuery(pending.envelope);
741
+ }
742
+ }
743
+ /** The persisted query for `xid` after a restart, with its lifetime recomputed from KRAM's durable records. */
744
+ loadHeld(xid, grant) {
745
+ const wire = this.hab.db.heldq.get([xid]);
746
+ const input = this.exc.retainedInput(grant, { pending: true });
747
+ if (!wire || !input)
748
+ return undefined;
749
+ try {
750
+ const envelope = restoreAuxiliaryQuery(wire);
751
+ return { envelope, expires: this.retainUntil(envelope, input.serder), grant };
752
+ }
753
+ catch (error) {
754
+ if (error instanceof ValidationError)
755
+ return undefined;
756
+ throw error;
757
+ }
758
+ }
759
+ dropHeld(xid) {
760
+ this.hab.db.heldq.rem([xid]);
565
761
  }
566
762
  /** Bound process-local public evidence by the original query and accepted Grant lifetimes. */
567
763
  retainUntil(envelope, grant) {
@@ -610,8 +806,12 @@ export class Dhke {
610
806
  }
611
807
  roots(op) {
612
808
  for (const registry of op.registries) {
613
- if ((this.hab.mgr.getRegistryRoot(this.hab.pre, registry) !== null) !== op.completed) {
614
- throw new ValidationError("Registry root conflicts with this live DH operation");
809
+ const held = this.hab.mgr.hasRegistryRoot(this.hab.pre, registry);
810
+ if (held !== op.completed && !(held && op.replaces.has(registry))) {
811
+ throw new ValidationError(held
812
+ ? "Registry root conflicts with this live DH operation: a DH-issued registry must not have a custody root, "
813
+ + "so do not call createCustody for it before issuance (use a registry without custody)"
814
+ : "Registry root conflicts with this live DH operation");
615
815
  }
616
816
  }
617
817
  }
@@ -648,6 +848,9 @@ export class Dhke {
648
848
  return { op, peer };
649
849
  }
650
850
  generate(op) {
851
+ // A completed operation, including a resumed one, never holds or makes ephemeral keys again.
852
+ if (op.completed)
853
+ throw new ValidationError("DH operation has already completed");
651
854
  this.custody(op, true);
652
855
  if (op.privateKeys.size)
653
856
  return;
@@ -675,11 +878,20 @@ export class Dhke {
675
878
  ]));
676
879
  }
677
880
  retain(op, peer, roots) {
678
- for (const [registry, root] of roots)
679
- this.hab.mgr.putRegistryRoot(this.hab.pre, registry, root);
881
+ for (const [registry, root] of roots) {
882
+ const replacing = op.replaces.has(registry);
883
+ // Recheck at commit time, from stores: the held root must still be unused.
884
+ if (replacing && !registryUncommitted(this.registryStore, registry)) {
885
+ throw new ValidationError("Registry root conflicts with this live DH operation");
886
+ }
887
+ this.hab.mgr.putRegistryRoot(this.hab.pre, registry, root, replacing);
888
+ }
680
889
  op.peer = { ...peer };
681
890
  op.completed = true;
682
891
  this.clearPrivate(op);
892
+ // Every current member must hold every root; the custodian replicates.
893
+ if (this.group)
894
+ this.replicated.push({ gid: this.hab.pre, registries: [...roots.keys()] });
683
895
  }
684
896
  clearPrivate(op) {
685
897
  for (const key of op.privateKeys.values())
@@ -1,6 +1,8 @@
1
1
  /** Public ACDC registry database layout and joined LMDB lifecycle. */
2
2
  import { Diger, NumberPrimitive, Saider, SerderACDC } from "cesr-ts";
3
+ import { DerivedUpdateRecord } from "../core/records.js";
3
4
  import { StoreOwner } from "../db/core/store-owner.js";
5
+ import { Komer } from "../db/koming.js";
4
6
  import { B64OnIoSetSuber, BytesSuber, CatCesrSuber, CesrOnSuber, CesrSuber, SerderSuber } from "../db/subing.js";
5
7
  /**
6
8
  * Own one environment for public registry TEL bodies, accepted ordinals and heads.
@@ -39,6 +41,13 @@ export class RegBaser extends StoreOwner {
39
41
  writable: true,
40
42
  value: void 0
41
43
  });
44
+ /** keri-ts only. BUP SAID -> public blind-derivation coordinates; never a salt or opening. */
45
+ Object.defineProperty(this, "dvus", {
46
+ enumerable: true,
47
+ configurable: true,
48
+ writable: true,
49
+ value: void 0
50
+ });
42
51
  /** Event-SAID -> earliest verified KEL source couple, in the current store. */
43
52
  Object.defineProperty(this, "ancs", {
44
53
  enumerable: true,
@@ -71,6 +80,7 @@ export class RegBaser extends StoreOwner {
71
80
  this.tels = new CesrOnSuber(this.store, { subkey: "tels.", ctor: Saider });
72
81
  this.heads = new CesrSuber(this.store, { subkey: "heds.", ctor: Saider });
73
82
  this.openings = new BytesSuber(this.store, { subkey: "openings." });
83
+ this.dvus = new Komer(this.store, { subkey: "dvus.", recordClass: DerivedUpdateRecord });
74
84
  this.ancs = new CatCesrSuber(this.store, { subkey: "ancs.", ctor: [NumberPrimitive, Diger] });
75
85
  this.maes = new B64OnIoSetSuber(this.store, { subkey: "maes." });
76
86
  this.ooes = new B64OnIoSetSuber(this.store, { subkey: "ooes." });
@@ -177,7 +177,7 @@ export function rootExn(hab, recipient, gid, registry, root, { event, td }) {
177
177
  * keeps the copy escrowed rather than refusing it.
178
178
  */
179
179
  export class RegistryRoot {
180
- constructor(hby, rgy, notifier = null) {
180
+ constructor(hby, rgy, notifier = null, changes) {
181
181
  Object.defineProperty(this, "resource", {
182
182
  enumerable: true,
183
183
  configurable: true,
@@ -208,11 +208,19 @@ export class RegistryRoot {
208
208
  writable: true,
209
209
  value: void 0
210
210
  });
211
+ /** Told `{kind: "root", id: registry}` after a copy is retained, so an embedder that waited for the root can act. */
212
+ Object.defineProperty(this, "changes", {
213
+ enumerable: true,
214
+ configurable: true,
215
+ writable: true,
216
+ value: void 0
217
+ });
211
218
  if (rgy.hby !== hby)
212
219
  throw new ValidationError("registry root replication requires the Habery's registry owner");
213
220
  this.hby = hby;
214
221
  this.rgy = rgy;
215
222
  this.notifier = notifier;
223
+ this.changes = changes;
216
224
  if (!hby.readonly)
217
225
  this.reconcile();
218
226
  }
@@ -371,6 +379,11 @@ export class RegistryRoot {
371
379
  }
372
380
  return found.sort((left, right) => left[0] - right[0]).map(([, aid]) => aid);
373
381
  }
382
+ /** Public key by which `member` currently contributes to `gid`, or null while not a known current signing member. */
383
+ contributor(gid, member) {
384
+ const gkever = this.hby.db.getKever(gid);
385
+ return gkever ? contribution(this.hby.db, member, gkever.verfers)?.key ?? null : null;
386
+ }
374
387
  /** Return the registry SAIDs with a root retained locally for `gid`. */
375
388
  held(gid) {
376
389
  const found = [];
@@ -691,6 +704,8 @@ export class RegistryRoot {
691
704
  rd: registry,
692
705
  });
693
706
  }
707
+ if (added)
708
+ this.changes?.push({ kind: "root", id: registry });
694
709
  return added;
695
710
  }
696
711
  /** Try one escrowed copy: true retained, null still pending, false dropped. */
@@ -286,72 +286,111 @@ class SchemaResources {
286
286
  return root;
287
287
  }
288
288
  }
289
+ /** Most closures kept per store; the oldest is evicted first. */
290
+ const COMPILED_SCHEMA_BOUND = 256;
291
+ /**
292
+ * Compiled closures by store, then root SAID. A closure is fully determined by its root SAID: the root and every
293
+ * bundled or `$ref` resource is verified against its SAID when admitted, so a cached closure cannot go stale.
294
+ * Per store, so a store that does not hold the schema still reports it missing; only a valid closure is kept.
295
+ */
296
+ const compiledSchemas = new WeakMap();
297
+ let compilations = 0;
298
+ /** How many schema closures this process has compiled; a diagnostic for tests of the cache. */
299
+ export function schemaCompilationCount() {
300
+ return compilations;
301
+ }
302
+ function isCompiled(value) {
303
+ return "validate" in value;
304
+ }
305
+ /** Compile an admitted closure once; regular-expression and duplicate-id refusals are normal invalid outcomes. */
306
+ function compileClosure(resources, root) {
307
+ const { roots, scopes } = resources;
308
+ // Compile only admitted private copies. Canonical local URNs remove URI-alias ambiguity without changing stored IDs.
309
+ const uri = (said) => `urn:said:${said}`;
310
+ const registered = new Set();
311
+ const copy = (value) => {
312
+ if (Array.isArray(value))
313
+ return value.map(copy);
314
+ if (!object(value))
315
+ return value;
316
+ const result = Object.fromEntries(Object.entries(value).map(([key, item]) => [key, copy(item)]));
317
+ const owner = scopes.get(value);
318
+ if (!owner)
319
+ return result;
320
+ if ("$id" in value) {
321
+ if (registered.has(owner.said))
322
+ delete result.$id;
323
+ else {
324
+ result.$id = uri(owner.said);
325
+ registered.add(owner.said);
326
+ }
327
+ }
328
+ if ("$ref" in value) {
329
+ const parsed = reference(value.$ref);
330
+ result.$ref = uri(parsed.said ?? owner.said) + parsed.fragment;
331
+ }
332
+ // Python treats these AJV extension/obsolete applicator keywords as annotations.
333
+ for (const key of ["$async", "nullable", "dependencies", "additionalItems"])
334
+ delete result[key];
335
+ return result;
336
+ };
337
+ const validator = new ValidatorCtor({ strict: false, validateFormats: false });
338
+ for (const schema of scopes.keys()) {
339
+ if (!validator.validateSchema(schema))
340
+ throw new InvalidSchema("Invalid Draft 2020-12 schema");
341
+ }
342
+ const copies = roots.map(copy);
343
+ let validate;
344
+ try {
345
+ for (const schema of copies)
346
+ validator.addSchema(schema);
347
+ validate = validator.getSchema(uri(root.said));
348
+ }
349
+ catch (error) {
350
+ if (error instanceof SyntaxError && error.message.includes("regular expression")) {
351
+ return { kind: "invalid", subject: "schema", reason: error.message };
352
+ }
353
+ if (error instanceof Error && /^reference "urn:said:[^"]+" resolves to more than one schema$/.test(error.message)) {
354
+ return { kind: "invalid", subject: "schema", reason: error.message };
355
+ }
356
+ throw error;
357
+ }
358
+ if (!validate)
359
+ throw new Error("Admitted schema was not registered");
360
+ compilations++;
361
+ return { validator, validate };
362
+ }
289
363
  /** Local static schema closure plus a synchronous Draft 2020-12 validator; legacy Schemer behavior is unchanged. */
290
364
  export function validateSchema(acdc, db, override) {
291
- const resources = new SchemaResources(db);
292
365
  try {
366
+ // The closure is always admitted: that verifies every SAID, and the verified root SAID names the cached validator.
367
+ const resources = new SchemaResources(db);
293
368
  const root = resources.admit(override ?? acdc.schema);
294
369
  if (!root) {
295
370
  return resources.missing
296
371
  ? { kind: "missing", said: resources.missing }
297
372
  : { kind: "invalid", subject: "schema", reason: "Invalid ACDC schema field" };
298
373
  }
299
- const { roots, scopes } = resources;
300
- // Compile only admitted private copies. Canonical local URNs remove URI-alias ambiguity without changing stored IDs.
301
- const uri = (said) => `urn:said:${said}`;
302
- const registered = new Set();
303
- const copy = (value) => {
304
- if (Array.isArray(value))
305
- return value.map(copy);
306
- if (!object(value))
307
- return value;
308
- const result = Object.fromEntries(Object.entries(value).map(([key, item]) => [key, copy(item)]));
309
- const owner = scopes.get(value);
310
- if (!owner)
311
- return result;
312
- if ("$id" in value) {
313
- if (registered.has(owner.said))
314
- delete result.$id;
315
- else {
316
- result.$id = uri(owner.said);
317
- registered.add(owner.said);
318
- }
374
+ // An override object is checked against the document it replaces each time and is never kept.
375
+ const keep = typeof override !== "object";
376
+ let store = compiledSchemas.get(db);
377
+ let compiled = keep ? store?.get(root.said) : undefined;
378
+ if (!compiled) {
379
+ const built = compileClosure(resources, root);
380
+ if (!isCompiled(built))
381
+ return built;
382
+ compiled = built;
383
+ if (keep) {
384
+ if (!store)
385
+ compiledSchemas.set(db, store = new Map());
386
+ if (store.size >= COMPILED_SCHEMA_BOUND)
387
+ store.delete(store.keys().next().value);
388
+ store.set(root.said, compiled);
319
389
  }
320
- if ("$ref" in value) {
321
- const parsed = reference(value.$ref);
322
- result.$ref = uri(parsed.said ?? owner.said) + parsed.fragment;
323
- }
324
- // Python treats these AJV extension/obsolete applicator keywords as annotations.
325
- for (const key of ["$async", "nullable", "dependencies", "additionalItems"])
326
- delete result[key];
327
- return result;
328
- };
329
- const validator = new ValidatorCtor({ strict: false, validateFormats: false });
330
- for (const schema of scopes.keys()) {
331
- if (!validator.validateSchema(schema))
332
- throw new InvalidSchema("Invalid Draft 2020-12 schema");
333
- }
334
- const copies = roots.map(copy);
335
- let validate;
336
- try {
337
- for (const schema of copies)
338
- validator.addSchema(schema);
339
- validate = validator.getSchema(uri(root.said));
340
- }
341
- catch (error) {
342
- if (error instanceof SyntaxError && error.message.includes("regular expression")) {
343
- return { kind: "invalid", subject: "schema", reason: error.message };
344
- }
345
- if (error instanceof Error && /^reference "urn:said:[^"]+" resolves to more than one schema$/.test(error.message)) {
346
- return { kind: "invalid", subject: "schema", reason: error.message };
347
- }
348
- throw error;
349
390
  }
350
- if (!validate)
351
- throw new Error("Admitted schema was not registered");
352
- return validate(acdc.sad)
391
+ return compiled.validate(acdc.sad)
353
392
  ? { kind: "valid" }
354
- : { kind: "invalid", subject: "credential", reason: validator.errorsText(validate.errors) };
393
+ : { kind: "invalid", subject: "credential", reason: compiled.validator.errorsText(compiled.validate.errors) };
355
394
  }
356
395
  catch (error) {
357
396
  if (error instanceof InvalidSchema)