@emilia-protocol/gate 0.11.0 → 0.14.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 (425) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +310 -0
  3. package/action-control-manifest.js +3 -268
  4. package/action-escrow-custodian.js +3 -394
  5. package/action-escrow-evidence.js +3 -858
  6. package/action-escrow-package.js +3 -1668
  7. package/action-escrow-postgres.js +3 -337
  8. package/action-escrow-state.js +3 -396
  9. package/action-escrow-verifiers.js +3 -331
  10. package/action-escrow.js +3 -2447
  11. package/action-packs.js +3 -140
  12. package/adapters/_kit.js +3 -104
  13. package/adapters/aws.js +3 -108
  14. package/adapters/cloudflare.js +3 -54
  15. package/adapters/gcp.js +3 -70
  16. package/adapters/github.js +3 -109
  17. package/adapters/jira.js +3 -55
  18. package/adapters/k8s.js +3 -70
  19. package/adapters/linear.js +3 -55
  20. package/adapters/salesforce.js +3 -55
  21. package/adapters/stripe.js +3 -81
  22. package/adapters/supabase.js +3 -121
  23. package/adapters/terraform.js +3 -59
  24. package/adapters/vercel.js +3 -84
  25. package/aec-execution.js +3 -312
  26. package/breakglass.js +3 -582
  27. package/capability-receipt.js +4 -0
  28. package/cf1-conformance.js +3 -106
  29. package/challenge-store.js +3 -69
  30. package/control-plane.js +3 -340
  31. package/coverage.js +3 -721
  32. package/demo.mjs +1 -1
  33. package/deployment-attestation.js +3 -247
  34. package/dist/action-control-manifest.d.ts +82 -0
  35. package/dist/action-control-manifest.d.ts.map +1 -0
  36. package/dist/action-control-manifest.js +422 -0
  37. package/dist/action-control-manifest.js.map +1 -0
  38. package/dist/action-escrow-custodian.d.ts +74 -0
  39. package/dist/action-escrow-custodian.d.ts.map +1 -0
  40. package/dist/action-escrow-custodian.js +363 -0
  41. package/dist/action-escrow-custodian.js.map +1 -0
  42. package/dist/action-escrow-evidence.d.ts +130 -0
  43. package/dist/action-escrow-evidence.d.ts.map +1 -0
  44. package/dist/action-escrow-evidence.js +1012 -0
  45. package/dist/action-escrow-evidence.js.map +1 -0
  46. package/dist/action-escrow-package.d.ts +10 -0
  47. package/dist/action-escrow-package.d.ts.map +1 -0
  48. package/dist/action-escrow-package.js +1583 -0
  49. package/dist/action-escrow-package.js.map +1 -0
  50. package/dist/action-escrow-postgres.d.ts +62 -0
  51. package/dist/action-escrow-postgres.d.ts.map +1 -0
  52. package/dist/action-escrow-postgres.js +339 -0
  53. package/dist/action-escrow-postgres.js.map +1 -0
  54. package/dist/action-escrow-state.d.ts +141 -0
  55. package/dist/action-escrow-state.d.ts.map +1 -0
  56. package/dist/action-escrow-state.js +413 -0
  57. package/dist/action-escrow-state.js.map +1 -0
  58. package/dist/action-escrow-verifiers.d.ts +65 -0
  59. package/dist/action-escrow-verifiers.d.ts.map +1 -0
  60. package/dist/action-escrow-verifiers.js +421 -0
  61. package/dist/action-escrow-verifiers.js.map +1 -0
  62. package/dist/action-escrow.d.ts +112 -0
  63. package/dist/action-escrow.d.ts.map +1 -0
  64. package/dist/action-escrow.js +2661 -0
  65. package/dist/action-escrow.js.map +1 -0
  66. package/dist/action-packs.d.ts +54 -0
  67. package/dist/action-packs.d.ts.map +1 -0
  68. package/dist/action-packs.js +143 -0
  69. package/dist/action-packs.js.map +1 -0
  70. package/dist/adapters/_kit.d.ts +50 -0
  71. package/dist/adapters/_kit.d.ts.map +1 -0
  72. package/dist/adapters/_kit.js +98 -0
  73. package/dist/adapters/_kit.js.map +1 -0
  74. package/dist/adapters/aws.d.ts +158 -0
  75. package/dist/adapters/aws.d.ts.map +1 -0
  76. package/dist/adapters/aws.js +136 -0
  77. package/dist/adapters/aws.js.map +1 -0
  78. package/dist/adapters/cloudflare.d.ts +116 -0
  79. package/dist/adapters/cloudflare.d.ts.map +1 -0
  80. package/dist/adapters/cloudflare.js +75 -0
  81. package/dist/adapters/cloudflare.js.map +1 -0
  82. package/dist/adapters/gcp.d.ts +139 -0
  83. package/dist/adapters/gcp.d.ts.map +1 -0
  84. package/dist/adapters/gcp.js +69 -0
  85. package/dist/adapters/gcp.js.map +1 -0
  86. package/dist/adapters/github.d.ts +123 -0
  87. package/dist/adapters/github.d.ts.map +1 -0
  88. package/dist/adapters/github.js +105 -0
  89. package/dist/adapters/github.js.map +1 -0
  90. package/dist/adapters/jira.d.ts +109 -0
  91. package/dist/adapters/jira.d.ts.map +1 -0
  92. package/dist/adapters/jira.js +54 -0
  93. package/dist/adapters/jira.js.map +1 -0
  94. package/dist/adapters/k8s.d.ts +139 -0
  95. package/dist/adapters/k8s.d.ts.map +1 -0
  96. package/dist/adapters/k8s.js +69 -0
  97. package/dist/adapters/k8s.js.map +1 -0
  98. package/dist/adapters/linear.d.ts +109 -0
  99. package/dist/adapters/linear.d.ts.map +1 -0
  100. package/dist/adapters/linear.js +54 -0
  101. package/dist/adapters/linear.js.map +1 -0
  102. package/dist/adapters/salesforce.d.ts +109 -0
  103. package/dist/adapters/salesforce.d.ts.map +1 -0
  104. package/dist/adapters/salesforce.js +54 -0
  105. package/dist/adapters/salesforce.js.map +1 -0
  106. package/dist/adapters/stripe.d.ts +116 -0
  107. package/dist/adapters/stripe.d.ts.map +1 -0
  108. package/dist/adapters/stripe.js +77 -0
  109. package/dist/adapters/stripe.js.map +1 -0
  110. package/dist/adapters/supabase.d.ts +127 -0
  111. package/dist/adapters/supabase.d.ts.map +1 -0
  112. package/dist/adapters/supabase.js +112 -0
  113. package/dist/adapters/supabase.js.map +1 -0
  114. package/dist/adapters/terraform.d.ts +109 -0
  115. package/dist/adapters/terraform.d.ts.map +1 -0
  116. package/dist/adapters/terraform.js +58 -0
  117. package/dist/adapters/terraform.js.map +1 -0
  118. package/dist/adapters/vercel.d.ts +114 -0
  119. package/dist/adapters/vercel.d.ts.map +1 -0
  120. package/dist/adapters/vercel.js +81 -0
  121. package/dist/adapters/vercel.js.map +1 -0
  122. package/dist/aec-execution.d.ts +107 -0
  123. package/dist/aec-execution.d.ts.map +1 -0
  124. package/dist/aec-execution.js +334 -0
  125. package/dist/aec-execution.js.map +1 -0
  126. package/dist/breakglass.d.ts +205 -0
  127. package/dist/breakglass.d.ts.map +1 -0
  128. package/dist/breakglass.js +627 -0
  129. package/dist/breakglass.js.map +1 -0
  130. package/dist/capability-receipt.d.ts +601 -0
  131. package/dist/capability-receipt.d.ts.map +1 -0
  132. package/dist/capability-receipt.js +1177 -0
  133. package/dist/capability-receipt.js.map +1 -0
  134. package/dist/cf1-conformance.d.ts +83 -0
  135. package/dist/cf1-conformance.d.ts.map +1 -0
  136. package/dist/cf1-conformance.js +94 -0
  137. package/dist/cf1-conformance.js.map +1 -0
  138. package/dist/challenge-store.d.ts +20 -0
  139. package/dist/challenge-store.d.ts.map +1 -0
  140. package/dist/challenge-store.js +62 -0
  141. package/dist/challenge-store.js.map +1 -0
  142. package/dist/control-plane.d.ts +136 -0
  143. package/dist/control-plane.d.ts.map +1 -0
  144. package/dist/control-plane.js +349 -0
  145. package/dist/control-plane.js.map +1 -0
  146. package/dist/coverage.d.ts +156 -0
  147. package/dist/coverage.d.ts.map +1 -0
  148. package/dist/coverage.js +768 -0
  149. package/dist/coverage.js.map +1 -0
  150. package/dist/deployment-attestation.d.ts +93 -0
  151. package/dist/deployment-attestation.d.ts.map +1 -0
  152. package/dist/deployment-attestation.js +262 -0
  153. package/dist/deployment-attestation.js.map +1 -0
  154. package/dist/eg1-conformance.d.ts +286 -0
  155. package/dist/eg1-conformance.d.ts.map +1 -0
  156. package/dist/eg1-conformance.js +379 -0
  157. package/dist/eg1-conformance.js.map +1 -0
  158. package/dist/enterprise.d.ts +88 -0
  159. package/dist/enterprise.d.ts.map +1 -0
  160. package/dist/enterprise.js +194 -0
  161. package/dist/enterprise.js.map +1 -0
  162. package/dist/evidence-postgres.d.ts +123 -0
  163. package/dist/evidence-postgres.d.ts.map +1 -0
  164. package/dist/evidence-postgres.js +343 -0
  165. package/dist/evidence-postgres.js.map +1 -0
  166. package/dist/evidence.d.ts +113 -0
  167. package/dist/evidence.d.ts.map +1 -0
  168. package/dist/evidence.js +369 -0
  169. package/dist/evidence.js.map +1 -0
  170. package/dist/execution-binding.d.ts +49 -0
  171. package/dist/execution-binding.d.ts.map +1 -0
  172. package/dist/execution-binding.js +243 -0
  173. package/dist/execution-binding.js.map +1 -0
  174. package/dist/formal-runtime-map.d.ts +42 -0
  175. package/dist/formal-runtime-map.d.ts.map +1 -0
  176. package/dist/formal-runtime-map.js +23 -0
  177. package/dist/formal-runtime-map.js.map +1 -0
  178. package/dist/index.d.ts +1003 -0
  179. package/dist/index.d.ts.map +1 -0
  180. package/dist/index.js +1737 -0
  181. package/dist/index.js.map +1 -0
  182. package/dist/key-registry.d.ts +60 -0
  183. package/dist/key-registry.d.ts.map +1 -0
  184. package/dist/key-registry.js +144 -0
  185. package/dist/key-registry.js.map +1 -0
  186. package/dist/mcp.d.ts +53 -0
  187. package/dist/mcp.d.ts.map +1 -0
  188. package/dist/mcp.js +112 -0
  189. package/dist/mcp.js.map +1 -0
  190. package/dist/metering.d.ts +80 -0
  191. package/dist/metering.d.ts.map +1 -0
  192. package/dist/metering.js +227 -0
  193. package/dist/metering.js.map +1 -0
  194. package/dist/metrics.d.ts +68 -0
  195. package/dist/metrics.d.ts.map +1 -0
  196. package/dist/metrics.js +242 -0
  197. package/dist/metrics.js.map +1 -0
  198. package/dist/network-witness.d.ts +167 -0
  199. package/dist/network-witness.d.ts.map +1 -0
  200. package/dist/network-witness.js +520 -0
  201. package/dist/network-witness.js.map +1 -0
  202. package/dist/proposal-to-effect.d.ts +146 -0
  203. package/dist/proposal-to-effect.d.ts.map +1 -0
  204. package/dist/proposal-to-effect.js +427 -0
  205. package/dist/proposal-to-effect.js.map +1 -0
  206. package/dist/protocol-state.d.ts +141 -0
  207. package/dist/protocol-state.d.ts.map +1 -0
  208. package/dist/protocol-state.js +106 -0
  209. package/dist/protocol-state.js.map +1 -0
  210. package/dist/receipt-program.d.ts +59 -0
  211. package/dist/receipt-program.d.ts.map +1 -0
  212. package/dist/receipt-program.js +1053 -0
  213. package/dist/receipt-program.js.map +1 -0
  214. package/dist/reliance-kernel.d.ts +66 -0
  215. package/dist/reliance-kernel.d.ts.map +1 -0
  216. package/dist/reliance-kernel.js +103 -0
  217. package/dist/reliance-kernel.js.map +1 -0
  218. package/dist/reliance-packet.d.ts +67 -0
  219. package/dist/reliance-packet.d.ts.map +1 -0
  220. package/dist/reliance-packet.js +176 -0
  221. package/dist/reliance-packet.js.map +1 -0
  222. package/dist/remedy-program-postgres.d.ts +37 -0
  223. package/dist/remedy-program-postgres.d.ts.map +1 -0
  224. package/dist/remedy-program-postgres.js +291 -0
  225. package/dist/remedy-program-postgres.js.map +1 -0
  226. package/dist/remedy-program-receipt.d.ts +97 -0
  227. package/dist/remedy-program-receipt.d.ts.map +1 -0
  228. package/dist/remedy-program-receipt.js +817 -0
  229. package/dist/remedy-program-receipt.js.map +1 -0
  230. package/dist/remedy-program.d.ts +76 -0
  231. package/dist/remedy-program.d.ts.map +1 -0
  232. package/dist/remedy-program.js +1346 -0
  233. package/dist/remedy-program.js.map +1 -0
  234. package/dist/reports/art14.d.ts +101 -0
  235. package/dist/reports/art14.d.ts.map +1 -0
  236. package/dist/reports/art14.js +340 -0
  237. package/dist/reports/art14.js.map +1 -0
  238. package/dist/reports/assurance-package.d.ts +197 -0
  239. package/dist/reports/assurance-package.d.ts.map +1 -0
  240. package/dist/reports/assurance-package.js +313 -0
  241. package/dist/reports/assurance-package.js.map +1 -0
  242. package/dist/reports/auditor-workpaper.d.ts +194 -0
  243. package/dist/reports/auditor-workpaper.d.ts.map +1 -0
  244. package/dist/reports/auditor-workpaper.js +545 -0
  245. package/dist/reports/auditor-workpaper.js.map +1 -0
  246. package/dist/reports/external-verification.d.ts +76 -0
  247. package/dist/reports/external-verification.d.ts.map +1 -0
  248. package/dist/reports/external-verification.js +209 -0
  249. package/dist/reports/external-verification.js.map +1 -0
  250. package/dist/reports/reperform.d.ts +122 -0
  251. package/dist/reports/reperform.d.ts.map +1 -0
  252. package/dist/reports/reperform.js +432 -0
  253. package/dist/reports/reperform.js.map +1 -0
  254. package/dist/reports/underwriter.d.ts +127 -0
  255. package/dist/reports/underwriter.d.ts.map +1 -0
  256. package/dist/reports/underwriter.js +348 -0
  257. package/dist/reports/underwriter.js.map +1 -0
  258. package/dist/retention.d.ts +88 -0
  259. package/dist/retention.d.ts.map +1 -0
  260. package/dist/retention.js +90 -0
  261. package/dist/retention.js.map +1 -0
  262. package/dist/roster.d.ts +113 -0
  263. package/dist/roster.d.ts.map +1 -0
  264. package/dist/roster.js +261 -0
  265. package/dist/roster.js.map +1 -0
  266. package/dist/runtime-monitor.d.ts +132 -0
  267. package/dist/runtime-monitor.d.ts.map +1 -0
  268. package/dist/runtime-monitor.js +197 -0
  269. package/dist/runtime-monitor.js.map +1 -0
  270. package/dist/settlement.d.ts +68 -0
  271. package/dist/settlement.d.ts.map +1 -0
  272. package/dist/settlement.js +306 -0
  273. package/dist/settlement.js.map +1 -0
  274. package/dist/siem.d.ts +261 -0
  275. package/dist/siem.d.ts.map +1 -0
  276. package/dist/siem.js +247 -0
  277. package/dist/siem.js.map +1 -0
  278. package/dist/store-postgres.d.ts +107 -0
  279. package/dist/store-postgres.d.ts.map +1 -0
  280. package/dist/store-postgres.js +152 -0
  281. package/dist/store-postgres.js.map +1 -0
  282. package/dist/store.d.ts +95 -0
  283. package/dist/store.d.ts.map +1 -0
  284. package/dist/store.js +202 -0
  285. package/dist/store.js.map +1 -0
  286. package/dist/strict-json.d.ts +14 -0
  287. package/dist/strict-json.d.ts.map +1 -0
  288. package/dist/strict-json.js +132 -0
  289. package/dist/strict-json.js.map +1 -0
  290. package/dist/trust-program-adapters.d.ts +122 -0
  291. package/dist/trust-program-adapters.d.ts.map +1 -0
  292. package/dist/trust-program-adapters.js +802 -0
  293. package/dist/trust-program-adapters.js.map +1 -0
  294. package/dist/trust-program-postgres.d.ts +41 -0
  295. package/dist/trust-program-postgres.d.ts.map +1 -0
  296. package/dist/trust-program-postgres.js +346 -0
  297. package/dist/trust-program-postgres.js.map +1 -0
  298. package/dist/trust-program-revocation.d.ts +206 -0
  299. package/dist/trust-program-revocation.d.ts.map +1 -0
  300. package/dist/trust-program-revocation.js +600 -0
  301. package/dist/trust-program-revocation.js.map +1 -0
  302. package/dist/trust-program.d.ts +197 -0
  303. package/dist/trust-program.d.ts.map +1 -0
  304. package/dist/trust-program.js +1351 -0
  305. package/dist/trust-program.js.map +1 -0
  306. package/dist/witness-postgres.d.ts +43 -0
  307. package/dist/witness-postgres.d.ts.map +1 -0
  308. package/dist/witness-postgres.js +92 -0
  309. package/dist/witness-postgres.js.map +1 -0
  310. package/dist/zk-range-proof.d.ts +78 -0
  311. package/dist/zk-range-proof.d.ts.map +1 -0
  312. package/dist/zk-range-proof.js +228 -0
  313. package/dist/zk-range-proof.js.map +1 -0
  314. package/eg1-conformance.js +3 -404
  315. package/enterprise.js +3 -180
  316. package/ep-assure.mjs +1 -0
  317. package/evidence-postgres.js +3 -356
  318. package/evidence.js +4 -360
  319. package/execution-binding.js +3 -212
  320. package/formal-runtime-map.js +4 -0
  321. package/index.js +4 -1249
  322. package/key-registry.js +3 -134
  323. package/mcp.js +3 -109
  324. package/metering.js +3 -226
  325. package/metrics.js +3 -231
  326. package/network-witness.js +3 -499
  327. package/package.json +35 -4
  328. package/proposal-to-effect.js +4 -0
  329. package/receipt-program.js +4 -0
  330. package/reliance-kernel.js +3 -112
  331. package/reliance-packet.js +3 -175
  332. package/remedy-program-postgres.js +4 -0
  333. package/remedy-program-receipt.js +4 -0
  334. package/remedy-program.js +4 -0
  335. package/reports/art14.js +0 -0
  336. package/reports/assurance-package.js +2 -312
  337. package/reports/auditor-workpaper.js +3 -537
  338. package/reports/external-verification.js +3 -222
  339. package/reports/reperform.js +3 -424
  340. package/reports/underwriter.js +3 -339
  341. package/retention.js +3 -84
  342. package/roster.js +3 -264
  343. package/runtime-monitor.js +4 -0
  344. package/settlement.js +3 -299
  345. package/siem.js +3 -235
  346. package/src/action-control-manifest.ts +424 -0
  347. package/src/action-escrow-custodian.ts +427 -0
  348. package/src/action-escrow-evidence.ts +1178 -0
  349. package/src/action-escrow-package.ts +1821 -0
  350. package/src/action-escrow-postgres.ts +366 -0
  351. package/src/action-escrow-state.ts +498 -0
  352. package/src/action-escrow-verifiers.ts +484 -0
  353. package/src/action-escrow.ts +3039 -0
  354. package/src/action-packs.ts +170 -0
  355. package/src/adapters/_kit.ts +117 -0
  356. package/src/adapters/aws.ts +142 -0
  357. package/src/adapters/cloudflare.ts +78 -0
  358. package/src/adapters/gcp.ts +71 -0
  359. package/src/adapters/github.ts +110 -0
  360. package/src/adapters/jira.ts +56 -0
  361. package/src/adapters/k8s.ts +71 -0
  362. package/src/adapters/linear.ts +56 -0
  363. package/src/adapters/salesforce.ts +56 -0
  364. package/src/adapters/stripe.ts +82 -0
  365. package/src/adapters/supabase.ts +122 -0
  366. package/src/adapters/terraform.ts +60 -0
  367. package/src/adapters/vercel.ts +85 -0
  368. package/src/aec-execution.ts +354 -0
  369. package/src/breakglass.ts +647 -0
  370. package/src/capability-receipt.ts +1258 -0
  371. package/src/cf1-conformance.ts +120 -0
  372. package/src/challenge-store.ts +70 -0
  373. package/src/control-plane.ts +374 -0
  374. package/src/coverage.ts +832 -0
  375. package/src/deployment-attestation.ts +259 -0
  376. package/src/eg1-conformance.ts +471 -0
  377. package/src/enterprise.ts +195 -0
  378. package/src/evidence-postgres.ts +374 -0
  379. package/src/evidence.ts +367 -0
  380. package/src/execution-binding.ts +236 -0
  381. package/src/formal-runtime-map.ts +24 -0
  382. package/src/index.ts +2127 -0
  383. package/src/key-registry.ts +150 -0
  384. package/src/mcp.ts +120 -0
  385. package/src/metering.ts +237 -0
  386. package/src/metrics.ts +232 -0
  387. package/src/network-witness.ts +585 -0
  388. package/src/proposal-to-effect.ts +571 -0
  389. package/src/protocol-state.ts +143 -0
  390. package/src/protocol-state.typecheck.ts +94 -0
  391. package/src/receipt-program.ts +1105 -0
  392. package/src/reliance-kernel.ts +119 -0
  393. package/src/reliance-packet.ts +206 -0
  394. package/src/remedy-program-postgres.ts +367 -0
  395. package/src/remedy-program-receipt.ts +891 -0
  396. package/src/remedy-program.ts +1388 -0
  397. package/src/reports/art14.ts +361 -0
  398. package/src/reports/assurance-package.ts +330 -0
  399. package/src/reports/auditor-workpaper.ts +558 -0
  400. package/src/reports/external-verification.ts +232 -0
  401. package/src/reports/reperform.ts +441 -0
  402. package/src/reports/underwriter.ts +356 -0
  403. package/src/retention.ts +100 -0
  404. package/src/roster.ts +273 -0
  405. package/src/runtime-monitor.ts +217 -0
  406. package/src/settlement.ts +313 -0
  407. package/src/siem.ts +247 -0
  408. package/src/store-postgres.ts +167 -0
  409. package/src/store.ts +214 -0
  410. package/src/strict-json.ts +115 -0
  411. package/src/trust-program-adapters.ts +847 -0
  412. package/src/trust-program-postgres.ts +429 -0
  413. package/src/trust-program-revocation.ts +751 -0
  414. package/src/trust-program.ts +1460 -0
  415. package/src/witness-postgres.ts +106 -0
  416. package/src/zk-range-proof.ts +250 -0
  417. package/store-postgres.js +3 -161
  418. package/store.js +4 -198
  419. package/strict-json.js +3 -102
  420. package/trust-program-adapters.js +4 -0
  421. package/trust-program-postgres.js +4 -0
  422. package/trust-program-revocation.js +4 -0
  423. package/trust-program.js +4 -0
  424. package/witness-postgres.js +3 -96
  425. package/zk-range-proof.js +4 -0
@@ -0,0 +1,3039 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Pure orchestration kernel for contractor milestone release.
4
+ *
5
+ * The kernel owns no keys, documents, funds, or provider credentials. It joins
6
+ * construction-pinned verification results under a durable expected-revision
7
+ * CAS and refuses every state or external-effect ambiguity.
8
+ */
9
+ import { canonicalize, hashCanonical } from './execution-binding.js';
10
+ import {
11
+ ACTION_ESCROW_CONTRACTOR_TEMPLATE_VERSION,
12
+ validateActionEscrowReleaseTemplate,
13
+ } from './action-escrow-verifiers.js';
14
+
15
+ export const ACTION_ESCROW_STATE_VERSION = 'EP-ACTION-ESCROW-STATE-v1';
16
+ export const ACTION_ESCROW_OUTCOME_VERSION = 'EP-ACTION-ESCROW-OUTCOME-v1';
17
+ export const ACTION_ESCROW_PROFILE_VERSION = 'EP-ACTION-ESCROW-PROFILE-v1';
18
+
19
+ export const ACTION_ESCROW_STATES = Object.freeze([
20
+ 'draft',
21
+ 'awaiting_acceptance',
22
+ 'effective',
23
+ 'awaiting_funding',
24
+ 'funded',
25
+ 'milestone_submitted',
26
+ 'release_reserved',
27
+ 'released',
28
+ 'disputed',
29
+ 'amendment_pending',
30
+ 'cancelled',
31
+ 'completed',
32
+ 'release_indeterminate',
33
+ ]);
34
+
35
+ export const ACTION_ESCROW_TRANSITIONS = Object.freeze({
36
+ draft: Object.freeze(['awaiting_acceptance', 'cancelled']),
37
+ awaiting_acceptance: Object.freeze(['effective', 'cancelled']),
38
+ effective: Object.freeze(['awaiting_funding', 'amendment_pending', 'cancelled']),
39
+ awaiting_funding: Object.freeze(['funded']),
40
+ funded: Object.freeze(['milestone_submitted', 'disputed']),
41
+ milestone_submitted: Object.freeze([
42
+ 'release_reserved',
43
+ 'disputed',
44
+ ]),
45
+ release_reserved: Object.freeze(['released', 'release_indeterminate', 'milestone_submitted']),
46
+ released: Object.freeze(['completed']),
47
+ disputed: Object.freeze([]),
48
+ amendment_pending: Object.freeze(['effective', 'cancelled']),
49
+ cancelled: Object.freeze([]),
50
+ completed: Object.freeze([]),
51
+ release_indeterminate: Object.freeze([
52
+ 'released',
53
+ 'milestone_submitted',
54
+ ]),
55
+ });
56
+
57
+ const RESOLUTION_VERSION = 'EP-RESOLUTION-v1';
58
+ const DIGEST_RE = /^sha256:[0-9a-f]{64}$/;
59
+ const MAX_CAS_ATTEMPTS = 4;
60
+ const MAX_OPERATIONS = 256;
61
+ const MAX_HISTORY = 512;
62
+ const MAX_SUPERSEDED_BINDINGS = 64;
63
+ const MAX_STATE_BYTES = 4 * 1024 * 1024;
64
+ const DEFAULT_PROVIDER_TIMEOUT_MS = 30_000;
65
+
66
+ const STORE_CAPABILITIES = Object.freeze([
67
+ 'durable',
68
+ 'atomicExpectedRevisionCas',
69
+ 'linearizableReads',
70
+ 'monotonicRevisions',
71
+ 'nonExpiring',
72
+ ]);
73
+
74
+ const COMMON_INPUT_KEYS = Object.freeze([
75
+ 'agreement_digest',
76
+ 'document_action_binding_digest',
77
+ 'milestone_id',
78
+ 'release_action_digest',
79
+ 'parties',
80
+ 'profile',
81
+ 'idempotency_key',
82
+ ]);
83
+
84
+ const RECORD_KEYS = new Set([
85
+ '@version',
86
+ 'escrow_key',
87
+ 'revision',
88
+ 'state',
89
+ 'agreement_digest',
90
+ 'document_action_binding_digest',
91
+ 'milestone_id',
92
+ 'release_action_digest',
93
+ 'parties',
94
+ 'parties_digest',
95
+ 'profile',
96
+ 'profile_digest',
97
+ 'document_action_binding',
98
+ 'agreement_acceptances',
99
+ 'funding',
100
+ 'milestone_evidence',
101
+ 'release_approvals',
102
+ 'release',
103
+ 'dispute',
104
+ 'cancellation',
105
+ 'completion',
106
+ 'pending_amendment',
107
+ 'superseded_bindings',
108
+ 'operations',
109
+ 'history',
110
+ 'created_at',
111
+ 'updated_at',
112
+ ]);
113
+ const CONTAINER_KEYS = new Set(['artifact', 'verification']);
114
+ const FUNDING_CONTAINER_KEYS = new Set(['statement', 'verification']);
115
+ const ACCEPTANCE_KEYS = new Set(['party_id', 'artifact', 'verification']);
116
+ const RELEASE_APPROVAL_KEYS = new Set(['party_id', 'resolution', 'verification']);
117
+ const CORE_VERIFICATION_KEYS = new Set([
118
+ 'valid',
119
+ 'agreement_digest',
120
+ 'document_action_binding_digest',
121
+ 'milestone_id',
122
+ 'release_action_digest',
123
+ 'parties_digest',
124
+ 'profile_digest',
125
+ ]);
126
+ const ACCEPTANCE_VERIFICATION_KEYS = new Set([
127
+ ...CORE_VERIFICATION_KEYS,
128
+ 'party_id',
129
+ 'principal_key_id',
130
+ 'acceptance_digest',
131
+ ]);
132
+ const FUNDING_VERIFICATION_KEYS = new Set([
133
+ ...CORE_VERIFICATION_KEYS,
134
+ 'authenticated',
135
+ 'provider_id',
136
+ 'statement_type',
137
+ 'status',
138
+ 'provider_transaction_id',
139
+ 'provider_milestone_id',
140
+ 'amount',
141
+ 'currency',
142
+ 'destination_id',
143
+ 'statement_digest',
144
+ ]);
145
+ const MILESTONE_VERIFICATION_KEYS = new Set([
146
+ ...CORE_VERIFICATION_KEYS,
147
+ 'evidence_digest',
148
+ 'submitter_party_id',
149
+ 'observed_at',
150
+ ]);
151
+ const RELEASE_APPROVAL_VERIFICATION_KEYS = new Set([
152
+ ...CORE_VERIFICATION_KEYS,
153
+ 'authorizes_action',
154
+ 'outcome',
155
+ 'party_role',
156
+ 'principal_key_id',
157
+ 'nonce',
158
+ 'issued_at',
159
+ 'expires_at',
160
+ 'resolution_digest',
161
+ 'evidence_digest',
162
+ ]);
163
+ const PROVIDER_VERIFICATION_KEYS = new Set([
164
+ ...CORE_VERIFICATION_KEYS,
165
+ 'authenticated',
166
+ 'provider_id',
167
+ 'provider_idempotency_key',
168
+ 'provider_request_digest',
169
+ 'provider_transaction_id',
170
+ 'provider_milestone_id',
171
+ 'amount',
172
+ 'currency',
173
+ 'destination_id',
174
+ 'statement_type',
175
+ 'status',
176
+ 'statement_digest',
177
+ ]);
178
+ const OPERATION_KEYS = new Set([
179
+ 'idempotency_key',
180
+ 'operation',
181
+ 'request_digest',
182
+ 'code',
183
+ 'ok',
184
+ 'outcome',
185
+ 'state',
186
+ 'at',
187
+ ]);
188
+ const HISTORY_KEYS = new Set(['from', 'to', 'operation', 'idempotency_key', 'at']);
189
+ const KERNEL_OPERATIONS = new Set([
190
+ 'create',
191
+ 'begin_acceptance',
192
+ 'accept_agreement',
193
+ 'request_funding',
194
+ 'record_funding',
195
+ 'submit_milestone',
196
+ 'approve_release',
197
+ 'release',
198
+ 'reconcile_release',
199
+ 'open_dispute',
200
+ 'propose_amendment',
201
+ 'accept_amendment',
202
+ 'cancel',
203
+ 'complete',
204
+ ]);
205
+
206
+ function isPlainObject(value) {
207
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
208
+ const prototype = Object.getPrototypeOf(value);
209
+ return prototype === Object.prototype || prototype === null;
210
+ }
211
+
212
+ function exactKeys(value, allowed, required = allowed) {
213
+ if (!isPlainObject(value)) return false;
214
+ const keys = Object.keys(value);
215
+ return keys.every((key) => allowed.has(key))
216
+ && [...required].every((key) => Object.hasOwn(value, key));
217
+ }
218
+
219
+ function validString(value, max = 512) {
220
+ return typeof value === 'string'
221
+ && value.length > 0
222
+ && value.length <= max
223
+ && !/[\u0000-\u001f\u007f]/.test(value);
224
+ }
225
+
226
+ function validDigest(value) {
227
+ return typeof value === 'string' && DIGEST_RE.test(value);
228
+ }
229
+
230
+ function validInstant(value) {
231
+ if (!validString(value, 64)) return false;
232
+ const parsed = Date.parse(value);
233
+ return Number.isFinite(parsed) && new Date(parsed).toISOString() === value;
234
+ }
235
+
236
+ function canonicalSnapshot(value) {
237
+ return JSON.parse(canonicalize(value));
238
+ }
239
+
240
+ function canonicalDigest(value) {
241
+ return `sha256:${hashCanonical(value)}`;
242
+ }
243
+
244
+ function resolutionBindingInput(record) {
245
+ return {
246
+ agreement_digest: record.agreement_digest,
247
+ document_action_binding_digest: record.document_action_binding_digest,
248
+ milestone_id: record.milestone_id,
249
+ release_action_digest: record.release_action_digest,
250
+ profile_digest: record.profile_digest,
251
+ evidence_digest: record.milestone_evidence?.verification?.evidence_digest,
252
+ release_action_template:
253
+ record.document_action_binding?.verification?.release_action_template,
254
+ };
255
+ }
256
+
257
+ /**
258
+ * Build the exact human-facing envelope signed by an Action Escrow approver.
259
+ *
260
+ * The release action digest remains the normative machine-action binding. This
261
+ * envelope binds the document, evidence, and material release fields that the
262
+ * approver reviews before selecting the approval option.
263
+ */
264
+ export function createActionEscrowReleaseBindingMoment(input) {
265
+ try {
266
+ if (!isPlainObject(input)
267
+ || !validDigest(input.agreement_digest)
268
+ || !validDigest(input.document_action_binding_digest)
269
+ || !validString(input.milestone_id, 256)
270
+ || !validDigest(input.release_action_digest)
271
+ || !validDigest(input.profile_digest)
272
+ || !validDigest(input.evidence_digest)
273
+ || !isPlainObject(input.release_action_template)) {
274
+ return null;
275
+ }
276
+ const template = input.release_action_template;
277
+ if (template.action_type !== 'escrow.milestone.release'
278
+ || !validString(template.amount, 128)
279
+ || !validString(template.currency, 16)
280
+ || !validString(template.payee_id, 512)
281
+ || !validString(template.destination_id, 512)
282
+ || !validDigest(template.document_sha256)
283
+ || !validDigest(template.material_terms_sha256)
284
+ || template.completion_evidence_sha256 !== input.evidence_digest
285
+ || !Number.isSafeInteger(template.amendment_version)
286
+ || template.amendment_version < 1) {
287
+ return null;
288
+ }
289
+ return deepFreeze(canonicalSnapshot({
290
+ synopsis: `Authorize one ${template.amount} ${template.currency} milestone release.`,
291
+ findings: [
292
+ `Agreement digest: ${input.agreement_digest}`,
293
+ `Document-action binding digest: ${input.document_action_binding_digest}`,
294
+ `Release action digest: ${input.release_action_digest}`,
295
+ `Milestone evidence digest: ${input.evidence_digest}`,
296
+ `Milestone: ${input.milestone_id}`,
297
+ `Payee: ${template.payee_id}`,
298
+ `Destination: ${template.destination_id}`,
299
+ `Amendment version: ${template.amendment_version}`,
300
+ ],
301
+ recommendations: [
302
+ 'Verify the exact document, amount, destination, and completion evidence before approving.',
303
+ ],
304
+ offer: 'Decline if any material field is unexpected; amendments require a new binding and fresh approvals.',
305
+ question: {
306
+ stem: `Authorize release of ${template.amount} ${template.currency} for milestone ${input.milestone_id} to ${template.payee_id} at ${template.destination_id}?`,
307
+ options: [
308
+ {
309
+ label: 'Approve exact release',
310
+ reasoning: 'Authorize only the action identified by the signed action digest.',
311
+ },
312
+ {
313
+ label: 'Decline release',
314
+ reasoning: 'Do not authorize this action or any custodian effect.',
315
+ },
316
+ ],
317
+ recommended_idx: 1,
318
+ hatches: {
319
+ free_text: false,
320
+ dialogue: false,
321
+ },
322
+ },
323
+ meta: {
324
+ decision_class: 'escrow.milestone.release',
325
+ calibration_note: 'No approval recommendation; verify every material field.',
326
+ },
327
+ }));
328
+ } catch {
329
+ return null;
330
+ }
331
+ }
332
+
333
+ export function computeActionEscrowReleaseBindingMomentDigest(input) {
334
+ const bindingMoment = createActionEscrowReleaseBindingMoment(input);
335
+ return bindingMoment === null ? null : canonicalDigest(bindingMoment);
336
+ }
337
+
338
+ /**
339
+ * Return the relying-party-pinned nonce for one party and one exact release.
340
+ *
341
+ * It is stable while both parties approve, changes with any material action or
342
+ * evidence change, and is consumed by the durable approval CAS.
343
+ */
344
+ export function computeActionEscrowResolutionNonce(input, partyId) {
345
+ if (!validString(partyId, 256)) return null;
346
+ const bindingMomentDigest = computeActionEscrowReleaseBindingMomentDigest(input);
347
+ if (bindingMomentDigest === null) return null;
348
+ try {
349
+ return `ep-ae-resolution:${hashCanonical({
350
+ '@version': 'EP-ACTION-ESCROW-RESOLUTION-NONCE-v1',
351
+ party_id: partyId,
352
+ binding_moment_digest: bindingMomentDigest,
353
+ release_action_digest: input.release_action_digest,
354
+ evidence_digest: input.evidence_digest,
355
+ })}`;
356
+ } catch {
357
+ return null;
358
+ }
359
+ }
360
+
361
+ function deepFreeze(value) {
362
+ if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
363
+ Object.freeze(value);
364
+ for (const child of Object.values(value)) deepFreeze(child);
365
+ return value;
366
+ }
367
+
368
+ function sameStringSet(left, right) {
369
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
370
+ const values = new Set(left);
371
+ return values.size === left.length && right.every((value) => values.has(value));
372
+ }
373
+
374
+ function validateParties(parties) {
375
+ if (!Array.isArray(parties) || parties.length < 2 || parties.length > 16) {
376
+ return 'parties_invalid';
377
+ }
378
+ const ids: string[] = [];
379
+ for (const party of parties) {
380
+ if (!exactKeys(party, new Set(['party_id', 'role']))
381
+ || !validString(party.party_id, 256)
382
+ || !validString(party.role, 128)) {
383
+ return 'party_invalid';
384
+ }
385
+ ids.push(party.party_id);
386
+ }
387
+ return new Set(ids).size === ids.length ? null : 'party_duplicate';
388
+ }
389
+
390
+ function validateProfileShape(profile) {
391
+ if (!exactKeys(profile, new Set([
392
+ '@version',
393
+ 'profile_id',
394
+ 'provider_id',
395
+ 'required_acceptance_party_ids',
396
+ 'required_release_approver_party_ids',
397
+ 'prohibit_self_approval',
398
+ ]))) {
399
+ return 'profile_shape_invalid';
400
+ }
401
+ if (profile['@version'] !== ACTION_ESCROW_PROFILE_VERSION
402
+ || !validString(profile.profile_id, 256)
403
+ || !validString(profile.provider_id, 256)) {
404
+ return 'profile_identity_invalid';
405
+ }
406
+ for (const field of [
407
+ 'required_acceptance_party_ids',
408
+ 'required_release_approver_party_ids',
409
+ ]) {
410
+ const roster = profile[field];
411
+ if (!Array.isArray(roster) || roster.length === 0 || roster.length > 16
412
+ || roster.some((partyId) => !validString(partyId, 256))
413
+ || new Set(roster).size !== roster.length) {
414
+ return `${field}_invalid`;
415
+ }
416
+ }
417
+ if (profile.prohibit_self_approval !== true
418
+ && profile.prohibit_self_approval !== false) {
419
+ return 'profile_self_approval_policy_invalid';
420
+ }
421
+ return null;
422
+ }
423
+
424
+ function validateProfileForParties(profile, parties) {
425
+ const shapeError = validateProfileShape(profile);
426
+ if (shapeError) return shapeError;
427
+ const partyIds = parties.map((party) => party.party_id);
428
+ if (!sameStringSet(profile.required_acceptance_party_ids, partyIds)) {
429
+ return 'mutual_acceptance_roster_invalid';
430
+ }
431
+ if (!profile.required_release_approver_party_ids.every((partyId) => partyIds.includes(partyId))) {
432
+ return 'release_approval_roster_invalid';
433
+ }
434
+ return null;
435
+ }
436
+
437
+ function escrowKey(context) {
438
+ return `ep-action-escrow:${hashCanonical({
439
+ '@version': 'EP-ACTION-ESCROW-KEY-v1',
440
+ agreement_digest: context.agreement_digest,
441
+ milestone_id: context.milestone_id,
442
+ })}`;
443
+ }
444
+
445
+ function releaseReservationKey(context) {
446
+ return `ep-ae-reservation:${hashCanonical({
447
+ '@version': 'EP-ACTION-ESCROW-RELEASE-KEY-v1',
448
+ agreement_digest: context.agreement_digest,
449
+ document_action_binding_digest: context.document_action_binding_digest,
450
+ milestone_id: context.milestone_id,
451
+ release_action_digest: context.release_action_digest,
452
+ profile_digest: context.profile_digest,
453
+ })}`;
454
+ }
455
+
456
+ function providerIdempotencyKey(context) {
457
+ return `ep-ae-release:${hashCanonical({
458
+ '@version': 'EP-ACTION-ESCROW-PROVIDER-IDEMPOTENCY-v1',
459
+ agreement_digest: context.agreement_digest,
460
+ document_action_binding_digest: context.document_action_binding_digest,
461
+ milestone_id: context.milestone_id,
462
+ release_action_digest: context.release_action_digest,
463
+ profile_digest: context.profile_digest,
464
+ })}`;
465
+ }
466
+
467
+ function expectedBindings(context) {
468
+ return {
469
+ agreement_digest: context.agreement_digest,
470
+ document_action_binding_digest: context.document_action_binding_digest,
471
+ milestone_id: context.milestone_id,
472
+ release_action_digest: context.release_action_digest,
473
+ parties_digest: context.parties_digest,
474
+ profile_digest: context.profile_digest,
475
+ };
476
+ }
477
+
478
+ function normalizedInput(
479
+ operation,
480
+ input,
481
+ extraAllowed: string[] = [],
482
+ extraRequired: string[] = extraAllowed,
483
+ ) {
484
+ try {
485
+ const snapshot = canonicalSnapshot(input);
486
+ const allowed = new Set([...COMMON_INPUT_KEYS, ...extraAllowed]);
487
+ const required = new Set([...COMMON_INPUT_KEYS, ...extraRequired]);
488
+ if (!exactKeys(snapshot, allowed, required)) {
489
+ return { error: 'invalid_operation_input' };
490
+ }
491
+ if (!validDigest(snapshot.agreement_digest)
492
+ || !validDigest(snapshot.document_action_binding_digest)
493
+ || !validDigest(snapshot.release_action_digest)
494
+ || !validString(snapshot.milestone_id, 256)
495
+ || !validString(snapshot.idempotency_key, 256)) {
496
+ return { error: 'invalid_operation_binding' };
497
+ }
498
+ const partiesError = validateParties(snapshot.parties);
499
+ if (partiesError) return { error: partiesError };
500
+ const profileError = validateProfileForParties(snapshot.profile, snapshot.parties);
501
+ if (profileError) return { error: profileError };
502
+
503
+ const context = {
504
+ agreement_digest: snapshot.agreement_digest,
505
+ document_action_binding_digest: snapshot.document_action_binding_digest,
506
+ milestone_id: snapshot.milestone_id,
507
+ release_action_digest: snapshot.release_action_digest,
508
+ parties: snapshot.parties,
509
+ parties_digest: canonicalDigest(snapshot.parties),
510
+ profile: snapshot.profile,
511
+ profile_digest: canonicalDigest(snapshot.profile),
512
+ };
513
+ return {
514
+ snapshot,
515
+ context,
516
+ escrowKey: escrowKey(context),
517
+ requestDigest: canonicalDigest({ operation, input: snapshot }),
518
+ };
519
+ } catch {
520
+ return { error: 'invalid_operation_input' };
521
+ }
522
+ }
523
+
524
+ function outcome({
525
+ ok = false,
526
+ type = 'refused',
527
+ code,
528
+ operation = null,
529
+ record = null,
530
+ details = null,
531
+ }: {
532
+ ok?: boolean;
533
+ type?: string;
534
+ code?: string;
535
+ operation?: string | null;
536
+ record?: Record<string, any> | null;
537
+ details?: any;
538
+ }) {
539
+ try {
540
+ return deepFreeze(canonicalSnapshot({
541
+ '@version': ACTION_ESCROW_OUTCOME_VERSION,
542
+ ok,
543
+ outcome: type,
544
+ code,
545
+ operation,
546
+ state: record?.state ?? null,
547
+ revision: record?.revision ?? null,
548
+ escrow_key: record?.escrow_key ?? null,
549
+ details,
550
+ record,
551
+ }));
552
+ } catch {
553
+ return Object.freeze({
554
+ '@version': ACTION_ESCROW_OUTCOME_VERSION,
555
+ ok: false,
556
+ outcome: 'refused',
557
+ code: 'closed_outcome_encoding_failed',
558
+ operation,
559
+ state: null,
560
+ revision: null,
561
+ escrow_key: null,
562
+ details: null,
563
+ record: null,
564
+ });
565
+ }
566
+ }
567
+
568
+ function operationBindingMatches(record, normalized, mode = 'active') {
569
+ const binding = mode === 'pending'
570
+ ? record.pending_amendment
571
+ : record;
572
+ if (!binding) return false;
573
+ return record.agreement_digest === normalized.context.agreement_digest
574
+ && record.milestone_id === normalized.context.milestone_id
575
+ && binding.document_action_binding_digest
576
+ === normalized.context.document_action_binding_digest
577
+ && binding.release_action_digest === normalized.context.release_action_digest
578
+ && record.parties_digest === normalized.context.parties_digest
579
+ && record.profile_digest === normalized.context.profile_digest;
580
+ }
581
+
582
+ function boundVerificationMatches(result, expected) {
583
+ return isPlainObject(result)
584
+ && result.valid === true
585
+ && result.agreement_digest === expected.agreement_digest
586
+ && result.document_action_binding_digest === expected.document_action_binding_digest
587
+ && result.milestone_id === expected.milestone_id
588
+ && result.release_action_digest === expected.release_action_digest
589
+ && result.parties_digest === expected.parties_digest
590
+ && result.profile_digest === expected.profile_digest;
591
+ }
592
+
593
+ function boundVerificationSummary(
594
+ result: any,
595
+ expected: any,
596
+ extras: Record<string, any> = {},
597
+ ): Record<string, any> {
598
+ return {
599
+ valid: true,
600
+ ...expectedBindings(expected),
601
+ ...extras,
602
+ };
603
+ }
604
+
605
+ function bindingVerificationDetails(result, expected) {
606
+ try {
607
+ if (!boundVerificationMatches(result, expectedBindings(expected))
608
+ || !validDigest(result.verification_digest)
609
+ || !validDigest(result.document_digest)
610
+ || !validString(result.agreement_id, 256)
611
+ || !validString(result.binding_id, 256)
612
+ || !isPlainObject(result.release_action_template)) {
613
+ return null;
614
+ }
615
+ // Passed through an explicitly Record<string, any>-typed local (rather than
616
+ // as an inline object literal) so this call isn't subject to TypeScript's
617
+ // excess-property check against validateActionEscrowReleaseTemplate's
618
+ // options type, which (in action-escrow-verifiers.ts, out of scope here)
619
+ // is currently under-typed relative to the properties it actually reads.
620
+ const releaseActionTemplateOptions: Record<string, any> = {
621
+ profileDigest: expected.profile_digest,
622
+ agreementId: result.agreement_id,
623
+ agreementDigest: expected.agreement_digest,
624
+ milestoneId: expected.milestone_id,
625
+ documentDigest: result.document_digest,
626
+ contractorProjectSource:
627
+ result.release_action_template.action_escrow_template_profile
628
+ === ACTION_ESCROW_CONTRACTOR_TEMPLATE_VERSION,
629
+ };
630
+ const releaseActionTemplate = validateActionEscrowReleaseTemplate(
631
+ result.release_action_template,
632
+ releaseActionTemplateOptions,
633
+ );
634
+ if (!releaseActionTemplate) return null;
635
+ return {
636
+ verification_digest: result.verification_digest,
637
+ document_digest: result.document_digest,
638
+ agreement_id: result.agreement_id,
639
+ binding_id: result.binding_id,
640
+ release_action_template: releaseActionTemplate,
641
+ };
642
+ } catch {
643
+ return null;
644
+ }
645
+ }
646
+
647
+ function storedBindingVerificationValid(container, expected) {
648
+ if (!isPlainObject(container)
649
+ || !isPlainObject(container.artifact)
650
+ || !isPlainObject(container.verification)
651
+ || container.verification.valid !== true) {
652
+ return false;
653
+ }
654
+ return bindingVerificationDetails(container.verification, expected) !== null;
655
+ }
656
+
657
+ function coreVerificationValid(value, record, allowedKeys, overrides = {}) {
658
+ const expected = {
659
+ agreement_digest: record.agreement_digest,
660
+ document_action_binding_digest: record.document_action_binding_digest,
661
+ milestone_id: record.milestone_id,
662
+ release_action_digest: record.release_action_digest,
663
+ parties_digest: record.parties_digest,
664
+ profile_digest: record.profile_digest,
665
+ ...overrides,
666
+ };
667
+ return exactKeys(value, allowedKeys, CORE_VERIFICATION_KEYS)
668
+ && value.valid === true
669
+ && Object.entries(expected).every(([key, entry]) => value[key] === entry);
670
+ }
671
+
672
+ function storedAcceptancesValid(entries, record, {
673
+ documentActionBindingDigest = record.document_action_binding_digest,
674
+ releaseActionDigest = record.release_action_digest,
675
+ } = {}) {
676
+ if (!Array.isArray(entries) || entries.length > record.profile.required_acceptance_party_ids.length) {
677
+ return false;
678
+ }
679
+ const required = new Set(record.profile.required_acceptance_party_ids);
680
+ const parties = new Set(record.parties.map((party) => party.party_id));
681
+ const seenParties = new Set();
682
+ const seenKeys = new Set();
683
+ for (const entry of entries) {
684
+ const verification = entry?.verification;
685
+ if (!exactKeys(entry, ACCEPTANCE_KEYS)
686
+ || !isPlainObject(entry.artifact)
687
+ || !isPlainObject(verification)
688
+ || !coreVerificationValid(verification, record, ACCEPTANCE_VERIFICATION_KEYS, {
689
+ document_action_binding_digest: documentActionBindingDigest,
690
+ release_action_digest: releaseActionDigest,
691
+ })
692
+ || !parties.has(entry.party_id)
693
+ || !required.has(entry.party_id)
694
+ || seenParties.has(entry.party_id)
695
+ || verification.party_id !== entry.party_id
696
+ || !validString(verification.principal_key_id, 512)
697
+ || seenKeys.has(verification.principal_key_id)
698
+ || !validDigest(verification.acceptance_digest)) {
699
+ return false;
700
+ }
701
+ seenParties.add(entry.party_id);
702
+ seenKeys.add(verification.principal_key_id);
703
+ }
704
+ return true;
705
+ }
706
+
707
+ function storedFundingValid(record) {
708
+ if (record.funding === null) return true;
709
+ const template = record.document_action_binding.verification.release_action_template;
710
+ const verification = record.funding?.verification;
711
+ return exactKeys(record.funding, FUNDING_CONTAINER_KEYS)
712
+ && isPlainObject(record.funding.statement)
713
+ && coreVerificationValid(verification, record, FUNDING_VERIFICATION_KEYS)
714
+ && verification.authenticated === true
715
+ && verification.provider_id === record.profile.provider_id
716
+ && verification.statement_type === 'funding'
717
+ && verification.status === 'funded'
718
+ && verification.provider_transaction_id === template.custodian_transaction_id
719
+ && verification.provider_milestone_id === template.custodian_milestone_id
720
+ && verification.amount === template.amount
721
+ && verification.currency === template.currency
722
+ && verification.destination_id === template.destination_id
723
+ && validDigest(verification.statement_digest);
724
+ }
725
+
726
+ function storedMilestoneEvidenceValid(record) {
727
+ if (record.milestone_evidence === null) return true;
728
+ const verification = record.milestone_evidence?.verification;
729
+ return exactKeys(record.milestone_evidence, CONTAINER_KEYS)
730
+ && isPlainObject(record.milestone_evidence.artifact)
731
+ && coreVerificationValid(verification, record, MILESTONE_VERIFICATION_KEYS)
732
+ && validDigest(verification.evidence_digest)
733
+ && record.parties.some((party) => (
734
+ party.party_id === verification.submitter_party_id
735
+ ))
736
+ && validInstant(verification.observed_at);
737
+ }
738
+
739
+ function storedReleaseApprovalsValid(record) {
740
+ if (!Array.isArray(record.release_approvals)
741
+ || record.release_approvals.length > record.profile.required_release_approver_party_ids.length
742
+ || (record.release_approvals.length > 0 && record.milestone_evidence === null)) {
743
+ return false;
744
+ }
745
+ const required = new Set(record.profile.required_release_approver_party_ids);
746
+ const parties = new Map<string, any>(record.parties.map((party) => [party.party_id, party]));
747
+ const seenParties = new Set();
748
+ const seenKeys = new Set();
749
+ const evidenceDigest = record.milestone_evidence?.verification?.evidence_digest;
750
+ const bindingInput = resolutionBindingInput(record);
751
+ const bindingMomentDigest = computeActionEscrowReleaseBindingMomentDigest(bindingInput);
752
+ if (record.release_approvals.length > 0
753
+ && (!validDigest(evidenceDigest) || !validDigest(bindingMomentDigest))) {
754
+ return false;
755
+ }
756
+ for (const entry of record.release_approvals) {
757
+ const party = parties.get(entry?.party_id);
758
+ const verification = entry?.verification;
759
+ const context = entry?.resolution?.signoff?.context;
760
+ if (!exactKeys(entry, RELEASE_APPROVAL_KEYS)
761
+ || !party
762
+ || !required.has(entry.party_id)
763
+ || seenParties.has(entry.party_id)
764
+ || !isPlainObject(entry.resolution)
765
+ || entry.resolution.profile !== RESOLUTION_VERSION
766
+ || !isPlainObject(context)
767
+ || !isPlainObject(context.resolution)
768
+ || !coreVerificationValid(verification, record, RELEASE_APPROVAL_VERIFICATION_KEYS)
769
+ || verification.authorizes_action !== true
770
+ || verification.outcome !== 'approved'
771
+ || verification.party_role !== party.role
772
+ || !validString(verification.principal_key_id, 512)
773
+ || seenKeys.has(verification.principal_key_id)
774
+ || !validString(verification.nonce, 512)
775
+ || !validInstant(verification.issued_at)
776
+ || !validInstant(verification.expires_at)
777
+ || verification.resolution_digest !== canonicalDigest(entry.resolution)
778
+ || verification.evidence_digest !== evidenceDigest
779
+ || context.principal !== entry.party_id
780
+ || context.principal_key_id !== verification.principal_key_id
781
+ || context.envelope_hash !== bindingMomentDigest
782
+ || context.action_hash !== record.release_action_digest
783
+ || context.initiator !== record.milestone_evidence.verification.submitter_party_id
784
+ || context.nonce !== computeActionEscrowResolutionNonce(bindingInput, entry.party_id)
785
+ || context.nonce !== verification.nonce
786
+ || context.issued_at !== verification.issued_at
787
+ || context.expires_at !== verification.expires_at
788
+ || context.resolution.outcome !== 'approved'
789
+ || context.resolution.selected_option !== 0) {
790
+ return false;
791
+ }
792
+ seenParties.add(entry.party_id);
793
+ seenKeys.add(verification.principal_key_id);
794
+ }
795
+ return true;
796
+ }
797
+
798
+ function storedHistoryValid(record) {
799
+ if (!Array.isArray(record.operations)
800
+ || record.operations.length === 0
801
+ || !Array.isArray(record.history)
802
+ || record.history.length === 0
803
+ || record.operations.length > MAX_OPERATIONS
804
+ || record.history.length > MAX_HISTORY) {
805
+ return false;
806
+ }
807
+ const operations = new Map();
808
+ for (const entry of record.operations) {
809
+ if (!exactKeys(entry, OPERATION_KEYS)
810
+ || !validString(entry.idempotency_key, 512)
811
+ || operations.has(entry.idempotency_key)
812
+ || !validString(entry.operation, 128)
813
+ || !KERNEL_OPERATIONS.has(entry.operation)
814
+ || !validDigest(entry.request_digest)
815
+ || !validString(entry.code, 256)
816
+ || typeof entry.ok !== 'boolean'
817
+ || !validString(entry.outcome, 128)
818
+ || !ACTION_ESCROW_STATES.includes(entry.state)
819
+ || !validInstant(entry.at)) {
820
+ return false;
821
+ }
822
+ operations.set(entry.idempotency_key, entry);
823
+ }
824
+ let previousState = null;
825
+ for (const [index, entry] of record.history.entries()) {
826
+ const operation = operations.get(entry?.idempotency_key);
827
+ if (!exactKeys(entry, HISTORY_KEYS)
828
+ || !ACTION_ESCROW_STATES.includes(entry.to)
829
+ || (entry.from !== null && !ACTION_ESCROW_STATES.includes(entry.from))
830
+ || !validString(entry.operation, 128)
831
+ || !validString(entry.idempotency_key, 512)
832
+ || !validInstant(entry.at)
833
+ || !operation
834
+ || operation.operation !== entry.operation
835
+ || entry.from !== previousState
836
+ || (index > 0 && !ACTION_ESCROW_TRANSITIONS[entry.from]?.includes(entry.to))) {
837
+ return false;
838
+ }
839
+ if (index === 0 && (entry.from !== null || entry.to !== 'draft' || entry.operation !== 'create')) {
840
+ return false;
841
+ }
842
+ previousState = entry.to;
843
+ }
844
+ return previousState === record.state;
845
+ }
846
+
847
+ function recordShapeValid(record, revision) {
848
+ if (!exactKeys(record, RECORD_KEYS)
849
+ || record['@version'] !== ACTION_ESCROW_STATE_VERSION
850
+ || record.revision !== revision
851
+ || !Number.isSafeInteger(record.revision)
852
+ || record.revision < 0
853
+ || !ACTION_ESCROW_STATES.includes(record.state)
854
+ || !validString(record.escrow_key, 256)
855
+ || !validDigest(record.agreement_digest)
856
+ || !validDigest(record.document_action_binding_digest)
857
+ || !validDigest(record.release_action_digest)
858
+ || !validString(record.milestone_id, 256)
859
+ || !validDigest(record.parties_digest)
860
+ || !validDigest(record.profile_digest)
861
+ || validateParties(record.parties)
862
+ || validateProfileForParties(record.profile, record.parties)
863
+ || record.parties_digest !== canonicalDigest(record.parties)
864
+ || record.profile_digest !== canonicalDigest(record.profile)
865
+ || record.escrow_key !== escrowKey(record)
866
+ || !storedBindingVerificationValid(record.document_action_binding, record)
867
+ || !Array.isArray(record.agreement_acceptances)
868
+ || !Array.isArray(record.release_approvals)
869
+ || !Array.isArray(record.superseded_bindings)
870
+ || !Array.isArray(record.operations)
871
+ || !Array.isArray(record.history)
872
+ || record.operations.length > MAX_OPERATIONS
873
+ || record.history.length > MAX_HISTORY
874
+ || record.superseded_bindings.length > MAX_SUPERSEDED_BINDINGS
875
+ || !validInstant(record.created_at)
876
+ || !validInstant(record.updated_at)
877
+ || !storedHistoryValid(record)
878
+ || !storedAcceptancesValid(record.agreement_acceptances, record)
879
+ || !storedFundingValid(record)
880
+ || !storedMilestoneEvidenceValid(record)
881
+ || !storedReleaseApprovalsValid(record)) {
882
+ return false;
883
+ }
884
+ if (['release_reserved', 'released', 'release_indeterminate'].includes(record.state)
885
+ && !isPlainObject(record.release)) {
886
+ return false;
887
+ }
888
+ return true;
889
+ }
890
+
891
+ function existingOperation(record, normalized, operation) {
892
+ const found = record.operations.find(
893
+ (entry) => entry.idempotency_key === normalized.snapshot.idempotency_key,
894
+ );
895
+ if (!found) return null;
896
+ if (found.operation !== operation || found.request_digest !== normalized.requestDigest) {
897
+ return { conflict: true };
898
+ }
899
+ return { conflict: false, entry: found };
900
+ }
901
+
902
+ function idempotentResult(record, normalized, operation) {
903
+ const existing = existingOperation(record, normalized, operation);
904
+ if (!existing) return null;
905
+ if (existing.conflict) {
906
+ return outcome({
907
+ code: 'idempotency_key_conflict',
908
+ operation,
909
+ record,
910
+ });
911
+ }
912
+ if (operation === 'release'
913
+ && ['release_reserved', 'release_indeterminate'].includes(record.state)) {
914
+ return outcome({
915
+ code: 'release_reconciliation_required',
916
+ operation,
917
+ record,
918
+ type: 'indeterminate',
919
+ });
920
+ }
921
+ return outcome({
922
+ ok: foundOperationOk(existing.entry),
923
+ type: foundOperationOk(existing.entry)
924
+ ? 'idempotent'
925
+ : existing.entry.outcome ?? 'refused',
926
+ code: existing.entry.code,
927
+ operation,
928
+ record,
929
+ });
930
+ }
931
+
932
+ function foundOperationOk(entry) {
933
+ return entry.ok === undefined ? true : entry.ok === true;
934
+ }
935
+
936
+ function appendOperation(
937
+ next: Record<string, any>,
938
+ record: Record<string, any>,
939
+ normalized: Record<string, any>,
940
+ operation: string,
941
+ code: string | undefined,
942
+ at: string,
943
+ result: Record<string, any> = {},
944
+ ) {
945
+ if (record.operations.length >= MAX_OPERATIONS) {
946
+ return 'operation_history_limit_reached';
947
+ }
948
+ next.operations.push({
949
+ idempotency_key: normalized.snapshot.idempotency_key,
950
+ operation,
951
+ request_digest: normalized.requestDigest,
952
+ code,
953
+ ok: result.ok ?? true,
954
+ outcome: result.type ?? 'applied',
955
+ state: next.state,
956
+ at,
957
+ });
958
+ return null;
959
+ }
960
+
961
+ /**
962
+ * @param {Record<string, any>} record
963
+ * @param {Record<string, any>} normalized
964
+ * @param {string} operation
965
+ * @param {string | undefined} code
966
+ * @param {string} at
967
+ * @param {(next: Record<string, any>) => void} mutate
968
+ * @param {Record<string, any>} [result]
969
+ */
970
+ function finalizeMutation(record, normalized, operation, code, at, mutate, result = {}) {
971
+ const next = canonicalSnapshot(record);
972
+ const fromState = next.state;
973
+ mutate(next);
974
+ next.revision = record.revision + 1;
975
+ next.updated_at = at;
976
+ const operationError = appendOperation(
977
+ next,
978
+ record,
979
+ normalized,
980
+ operation,
981
+ code,
982
+ at,
983
+ result,
984
+ );
985
+ if (operationError) return { error: operationError };
986
+ if (fromState !== next.state) {
987
+ if (next.history.length >= MAX_HISTORY) return { error: 'state_history_limit_reached' };
988
+ next.history.push({
989
+ from: fromState,
990
+ to: next.state,
991
+ operation,
992
+ idempotency_key: normalized.snapshot.idempotency_key,
993
+ at,
994
+ });
995
+ }
996
+ return { next };
997
+ }
998
+
999
+ /**
1000
+ * @param {Record<string, any>} record
1001
+ */
1002
+ function providerExpected(record) {
1003
+ const template = record.document_action_binding.verification.release_action_template;
1004
+ return {
1005
+ ...expectedBindings(record),
1006
+ parties: record.parties,
1007
+ profile: record.profile,
1008
+ provider_id: record.profile.provider_id,
1009
+ provider_idempotency_key: record.release?.provider_idempotency_key ?? null,
1010
+ provider_request_digest: record.release?.provider_request?.request_digest ?? null,
1011
+ provider_transaction_id: template.custodian_transaction_id,
1012
+ provider_milestone_id: template.custodian_milestone_id,
1013
+ amount: template.amount,
1014
+ currency: template.currency,
1015
+ destination_id: template.destination_id,
1016
+ };
1017
+ }
1018
+
1019
+ type ActionEscrowVerifierFn = (artifact: any, expected: any) => any;
1020
+
1021
+ type ActionEscrowKernelOptions = {
1022
+ store?: Record<string, any>;
1023
+ provider?: Record<string, any>;
1024
+ verifyDocumentActionBinding?: ActionEscrowVerifierFn;
1025
+ verifyAgreementAcceptance?: ActionEscrowVerifierFn;
1026
+ verifyMilestoneEvidence?: ActionEscrowVerifierFn;
1027
+ verifyResolutionReceipt?: ActionEscrowVerifierFn;
1028
+ verifyProviderStatement?: ActionEscrowVerifierFn;
1029
+ verifyStateCommand?: ActionEscrowVerifierFn;
1030
+ profilesById?: Record<string, any>;
1031
+ resolveProfile?: (profileId: string, context: Record<string, any>) => any;
1032
+ now?: () => (Date | number | string);
1033
+ providerTimeoutMs?: number;
1034
+ };
1035
+
1036
+ /**
1037
+ * Create a fail-closed Action Escrow kernel.
1038
+ *
1039
+ * Store contract:
1040
+ * durable === true
1041
+ * atomicExpectedRevisionCas === true
1042
+ * linearizableReads === true
1043
+ * monotonicRevisions === true
1044
+ * nonExpiring === true
1045
+ * read(key) -> null | { revision, value: canonicalJsonText }
1046
+ * compareAndSwap(key, expectedRevision|null, nextValue)
1047
+ * -> { applied, revision }
1048
+ */
1049
+ export function createActionEscrowKernel(options: ActionEscrowKernelOptions = {}) {
1050
+ let configurationError: string | null = null;
1051
+ let readStore:
1052
+ | ((key: string) => Promise<{ revision: number; value: string } | null>)
1053
+ | null
1054
+ | undefined = null;
1055
+ let compareAndSwap:
1056
+ | ((
1057
+ key: string,
1058
+ expectedRevision: number | null,
1059
+ nextValue: string,
1060
+ ) => Promise<{ applied: boolean; revision: number | null }>)
1061
+ | null
1062
+ | undefined = null;
1063
+ let releaseProvider: ((request: Record<string, any>) => any) | null | undefined = null;
1064
+ let getProviderRelease: ((request: Record<string, any>) => any) | null | undefined = null;
1065
+ let verifyDocumentActionBinding: ActionEscrowVerifierFn | null | undefined = null;
1066
+ let verifyAgreementAcceptance: ActionEscrowVerifierFn | null | undefined = null;
1067
+ let verifyMilestoneEvidence: ActionEscrowVerifierFn | null | undefined = null;
1068
+ let verifyResolutionReceipt: ActionEscrowVerifierFn | null | undefined = null;
1069
+ let verifyProviderStatement: ActionEscrowVerifierFn | null | undefined = null;
1070
+ let verifyStateCommand: ActionEscrowVerifierFn | null | undefined = null;
1071
+ let resolveProfile:
1072
+ | ((profileId: string, context: Record<string, any>) => any)
1073
+ | null
1074
+ | undefined = null;
1075
+ let pinnedProfiles: Record<string, any> | null = null;
1076
+ let now: (() => (Date | number | string)) | null | undefined = null;
1077
+ let providerTimeoutMs = DEFAULT_PROVIDER_TIMEOUT_MS;
1078
+
1079
+ try {
1080
+ if (!isPlainObject(options)) throw new Error('invalid options');
1081
+ const store = options.store;
1082
+ if (!STORE_CAPABILITIES.every((capability) => store?.[capability] === true)
1083
+ || typeof store?.read !== 'function'
1084
+ || typeof store?.compareAndSwap !== 'function') {
1085
+ configurationError = 'durable_cas_store_required';
1086
+ } else {
1087
+ readStore = store.read.bind(store);
1088
+ compareAndSwap = store.compareAndSwap.bind(store);
1089
+ }
1090
+
1091
+ const provider = options.provider;
1092
+ if (!configurationError
1093
+ && (typeof provider?.release !== 'function'
1094
+ || typeof provider?.getRelease !== 'function')) {
1095
+ configurationError = 'release_provider_required';
1096
+ } else if (!configurationError) {
1097
+ // The `if` above only takes this `else` when both provider?.release and
1098
+ // provider?.getRelease are functions, which is impossible if provider
1099
+ // itself were undefined (optional chaining would yield undefined, not
1100
+ // 'function', for typeof).
1101
+ releaseProvider = provider!.release.bind(provider);
1102
+ getProviderRelease = provider!.getRelease.bind(provider);
1103
+ }
1104
+
1105
+ const verifierEntries = [
1106
+ ['verifyDocumentActionBinding', options.verifyDocumentActionBinding],
1107
+ ['verifyAgreementAcceptance', options.verifyAgreementAcceptance],
1108
+ ['verifyMilestoneEvidence', options.verifyMilestoneEvidence],
1109
+ ['verifyResolutionReceipt', options.verifyResolutionReceipt],
1110
+ ['verifyProviderStatement', options.verifyProviderStatement],
1111
+ ['verifyStateCommand', options.verifyStateCommand],
1112
+ ];
1113
+ if (!configurationError && verifierEntries.some(([, verifier]) => typeof verifier !== 'function')) {
1114
+ configurationError = 'pinned_verifiers_required';
1115
+ } else if (!configurationError) {
1116
+ verifyDocumentActionBinding = options.verifyDocumentActionBinding;
1117
+ verifyAgreementAcceptance = options.verifyAgreementAcceptance;
1118
+ verifyMilestoneEvidence = options.verifyMilestoneEvidence;
1119
+ verifyResolutionReceipt = options.verifyResolutionReceipt;
1120
+ verifyProviderStatement = options.verifyProviderStatement;
1121
+ verifyStateCommand = options.verifyStateCommand;
1122
+ }
1123
+
1124
+ if (!configurationError && options.profilesById !== undefined) {
1125
+ const snapshot: Record<string, any> = canonicalSnapshot(options.profilesById);
1126
+ if (!isPlainObject(snapshot) || Object.keys(snapshot).length === 0) {
1127
+ configurationError = 'pinned_profile_configuration_invalid';
1128
+ } else {
1129
+ for (const [profileId, profile] of Object.entries(snapshot)) {
1130
+ if (profileId !== profile.profile_id || validateProfileShape(profile)) {
1131
+ configurationError = 'pinned_profile_configuration_invalid';
1132
+ break;
1133
+ }
1134
+ }
1135
+ if (!configurationError) pinnedProfiles = deepFreeze(snapshot);
1136
+ }
1137
+ } else if (!configurationError && typeof options.resolveProfile === 'function') {
1138
+ resolveProfile = options.resolveProfile;
1139
+ } else if (!configurationError) {
1140
+ configurationError = 'pinned_profile_resolver_required';
1141
+ }
1142
+
1143
+ if (!configurationError && options.now !== undefined && typeof options.now !== 'function') {
1144
+ configurationError = 'clock_required';
1145
+ }
1146
+ now = typeof options.now === 'function' ? options.now : Date.now;
1147
+
1148
+ if (options.providerTimeoutMs !== undefined) {
1149
+ if (!Number.isSafeInteger(options.providerTimeoutMs)
1150
+ || options.providerTimeoutMs <= 0
1151
+ || options.providerTimeoutMs > 300_000) {
1152
+ configurationError ??= 'provider_timeout_invalid';
1153
+ } else {
1154
+ providerTimeoutMs = options.providerTimeoutMs;
1155
+ }
1156
+ }
1157
+ } catch {
1158
+ configurationError ??= 'invalid_kernel_configuration';
1159
+ }
1160
+
1161
+ /**
1162
+ * @param {string} operation
1163
+ * @param {() => Promise<any>} task
1164
+ */
1165
+ async function safe(operation, task) {
1166
+ try {
1167
+ return await task();
1168
+ } catch {
1169
+ return outcome({
1170
+ code: 'kernel_internal_refusal',
1171
+ operation,
1172
+ });
1173
+ }
1174
+ }
1175
+
1176
+ /**
1177
+ * @param {string} operation
1178
+ */
1179
+ function configurationRefusal(operation) {
1180
+ return configurationError
1181
+ ? outcome({ code: configurationError, operation })
1182
+ : null;
1183
+ }
1184
+
1185
+ function instant() {
1186
+ try {
1187
+ // now is always assigned a real function during construction (either
1188
+ // options.now or the Date.now fallback) unless construction threw
1189
+ // first, in which case configurationError is set and callers refuse
1190
+ // via configurationRefusal() before reaching here.
1191
+ const candidate = now!();
1192
+ const date = candidate instanceof Date ? candidate : new Date(candidate);
1193
+ return Number.isFinite(date.getTime()) ? date.toISOString() : null;
1194
+ } catch {
1195
+ return null;
1196
+ }
1197
+ }
1198
+
1199
+ function operationInstant(
1200
+ record: Record<string, any> | null = null,
1201
+ ): { error: string; at?: undefined } | { error?: undefined; at: string } {
1202
+ const at = instant();
1203
+ if (!at) return { error: 'invalid_clock' };
1204
+ if (record !== null && Date.parse(at) < Date.parse(record.updated_at)) {
1205
+ return { error: 'clock_regression' };
1206
+ }
1207
+ return { at };
1208
+ }
1209
+
1210
+ async function withProviderTimeout(task) {
1211
+ let timer;
1212
+ try {
1213
+ return await Promise.race([
1214
+ Promise.resolve().then(task),
1215
+ new Promise((_, reject) => {
1216
+ timer = setTimeout(
1217
+ () => reject(new Error('provider_timeout')),
1218
+ providerTimeoutMs,
1219
+ );
1220
+ }),
1221
+ ]);
1222
+ } finally {
1223
+ clearTimeout(timer);
1224
+ }
1225
+ }
1226
+
1227
+ async function readRecord(key) {
1228
+ let envelope;
1229
+ try {
1230
+ // readStore is always assigned once configurationError is null (store
1231
+ // validation above either sets configurationError or assigns
1232
+ // readStore); callers refuse via configurationRefusal() otherwise.
1233
+ envelope = await readStore!(key);
1234
+ } catch {
1235
+ return { error: 'store_unavailable' };
1236
+ }
1237
+ if (envelope === null) return { record: null };
1238
+ try {
1239
+ if (!exactKeys(envelope, new Set(['revision', 'value']))
1240
+ || !Number.isSafeInteger(envelope.revision)
1241
+ || envelope.revision < 0
1242
+ || typeof envelope.value !== 'string'
1243
+ || Buffer.byteLength(envelope.value, 'utf8') > MAX_STATE_BYTES) {
1244
+ return { error: 'store_read_invalid' };
1245
+ }
1246
+ const record = JSON.parse(envelope.value);
1247
+ if (canonicalize(record) !== envelope.value
1248
+ || !recordShapeValid(record, envelope.revision)) {
1249
+ return { error: 'store_record_invalid' };
1250
+ }
1251
+ return { record: deepFreeze(record) };
1252
+ } catch {
1253
+ return { error: 'store_record_invalid' };
1254
+ }
1255
+ }
1256
+
1257
+ async function writeRecord(key, expectedRevision, next) {
1258
+ let value;
1259
+ try {
1260
+ value = canonicalize(next);
1261
+ } catch {
1262
+ return { error: 'state_encoding_failed' };
1263
+ }
1264
+ let acknowledgement;
1265
+ try {
1266
+ // compareAndSwap is always assigned once configurationError is null,
1267
+ // for the same reason readStore is (see readRecord above).
1268
+ acknowledgement = await compareAndSwap!(key, expectedRevision, value);
1269
+ } catch {
1270
+ return { error: 'store_unavailable' };
1271
+ }
1272
+ try {
1273
+ const expectedNext = expectedRevision === null ? 0 : expectedRevision + 1;
1274
+ if (!exactKeys(acknowledgement, new Set(['applied', 'revision']))
1275
+ || (acknowledgement.applied !== true && acknowledgement.applied !== false)
1276
+ || (acknowledgement.revision !== null
1277
+ && (!Number.isSafeInteger(acknowledgement.revision)
1278
+ || acknowledgement.revision < 0))) {
1279
+ return { error: 'store_acknowledgement_invalid' };
1280
+ }
1281
+ if (acknowledgement.applied === true) {
1282
+ if (acknowledgement.revision !== expectedNext || next.revision !== expectedNext) {
1283
+ return { error: 'store_revision_invalid' };
1284
+ }
1285
+ return { applied: true };
1286
+ }
1287
+ return { applied: false };
1288
+ } catch {
1289
+ return { error: 'store_acknowledgement_invalid' };
1290
+ }
1291
+ }
1292
+
1293
+ async function invokeVerifier(verifier, artifact, expected) {
1294
+ try {
1295
+ const artifactCopy = deepFreeze(canonicalSnapshot(artifact));
1296
+ const expectedCopy = deepFreeze(canonicalSnapshot(expected));
1297
+ const result = canonicalSnapshot(await verifier(artifactCopy, expectedCopy));
1298
+ if (!isPlainObject(result)) return { error: 'verifier_result_invalid' };
1299
+ return { artifact: artifactCopy, result };
1300
+ } catch {
1301
+ return { error: 'verifier_failed' };
1302
+ }
1303
+ }
1304
+
1305
+ async function verifyStoredReleaseInputs(record, at) {
1306
+ const expectedCore = {
1307
+ ...expectedBindings(record),
1308
+ parties: record.parties,
1309
+ profile: record.profile,
1310
+ };
1311
+ const lastSupersession = record.superseded_bindings.at(-1);
1312
+ const expectedSupersedes = lastSupersession?.superseded_by_binding_digest
1313
+ === record.document_action_binding_digest
1314
+ ? lastSupersession.document_action_binding_digest
1315
+ : undefined;
1316
+ const bindingExpected = expectedSupersedes === undefined
1317
+ ? expectedCore
1318
+ : {
1319
+ ...expectedCore,
1320
+ supersedes_document_action_binding_digest: expectedSupersedes,
1321
+ };
1322
+ const binding = await invokeVerifier(
1323
+ verifyDocumentActionBinding,
1324
+ record.document_action_binding.artifact,
1325
+ bindingExpected,
1326
+ );
1327
+ const bindingDetails = binding.error
1328
+ ? null
1329
+ : bindingVerificationDetails(binding.result, bindingExpected);
1330
+ const storedBindingDetails = bindingVerificationDetails(
1331
+ record.document_action_binding.verification,
1332
+ record,
1333
+ );
1334
+ if (!bindingDetails
1335
+ || !storedBindingDetails
1336
+ || canonicalize(bindingDetails) !== canonicalize(storedBindingDetails)
1337
+ || (binding.result.supersedes_document_action_binding_digest ?? null)
1338
+ !== (record.document_action_binding.verification
1339
+ .supersedes_document_action_binding_digest ?? null)) {
1340
+ return { code: binding.error ?? 'document_action_binding_stale' };
1341
+ }
1342
+
1343
+ const template = record.document_action_binding.verification.release_action_template;
1344
+ const fundingExpected = {
1345
+ ...expectedCore,
1346
+ provider_id: record.profile.provider_id,
1347
+ statement_type: 'funding',
1348
+ expected_status: 'funded',
1349
+ provider_transaction_id: template.custodian_transaction_id,
1350
+ provider_milestone_id: template.custodian_milestone_id,
1351
+ amount: template.amount,
1352
+ currency: template.currency,
1353
+ destination_id: template.destination_id,
1354
+ };
1355
+ const funding = await invokeVerifier(
1356
+ verifyProviderStatement,
1357
+ record.funding.statement,
1358
+ fundingExpected,
1359
+ );
1360
+ if (funding.error
1361
+ || !boundVerificationMatches(funding.result, expectedBindings(fundingExpected))
1362
+ || funding.result.authenticated !== true
1363
+ || funding.result.provider_id !== fundingExpected.provider_id
1364
+ || funding.result.statement_type !== 'funding'
1365
+ || funding.result.status !== 'funded'
1366
+ || funding.result.provider_transaction_id !== fundingExpected.provider_transaction_id
1367
+ || funding.result.provider_milestone_id !== fundingExpected.provider_milestone_id
1368
+ || funding.result.amount !== fundingExpected.amount
1369
+ || funding.result.currency !== fundingExpected.currency
1370
+ || funding.result.destination_id !== fundingExpected.destination_id
1371
+ || funding.result.statement_digest !== record.funding.verification.statement_digest) {
1372
+ return { code: funding.error ?? 'funding_statement_stale' };
1373
+ }
1374
+
1375
+ const milestone = await invokeVerifier(
1376
+ verifyMilestoneEvidence,
1377
+ record.milestone_evidence.artifact,
1378
+ expectedCore,
1379
+ );
1380
+ const expectedEvidenceDigest = template.completion_evidence_sha256;
1381
+ if (milestone.error
1382
+ || !boundVerificationMatches(milestone.result, expectedBindings(expectedCore))
1383
+ || !validDigest(milestone.result.evidence_digest)
1384
+ || milestone.result.evidence_digest !== expectedEvidenceDigest
1385
+ || milestone.result.evidence_digest
1386
+ !== record.milestone_evidence.verification.evidence_digest
1387
+ || !record.parties.some((party) => (
1388
+ party.party_id === milestone.result.submitter_party_id
1389
+ ))
1390
+ || !validInstant(milestone.result.observed_at)
1391
+ || Date.parse(milestone.result.observed_at) > Date.parse(at)) {
1392
+ return { code: milestone.error ?? 'milestone_evidence_stale' };
1393
+ }
1394
+
1395
+ const bindingInput = resolutionBindingInput(record);
1396
+ const bindingMoment = createActionEscrowReleaseBindingMoment(bindingInput);
1397
+ const bindingMomentDigest = bindingMoment === null
1398
+ ? null
1399
+ : canonicalDigest(bindingMoment);
1400
+ const expectedInitiator = record.milestone_evidence.verification.submitter_party_id;
1401
+ if (bindingMoment === null
1402
+ || bindingMomentDigest === null
1403
+ || !validString(expectedInitiator, 256)) {
1404
+ return { code: 'release_approval_context_invalid' };
1405
+ }
1406
+ for (const entry of record.release_approvals) {
1407
+ const context = entry.resolution.signoff.context;
1408
+ const expectedNonce = computeActionEscrowResolutionNonce(
1409
+ bindingInput,
1410
+ entry.party_id,
1411
+ );
1412
+ const expected = {
1413
+ ...expectedCore,
1414
+ party_id: entry.party_id,
1415
+ evidence_digest: record.milestone_evidence.verification.evidence_digest,
1416
+ binding_moment: bindingMoment,
1417
+ binding_moment_digest: bindingMomentDigest,
1418
+ expected_selected_option: 0,
1419
+ expected_initiator: expectedInitiator,
1420
+ expected_nonce: expectedNonce,
1421
+ evaluation_time: at,
1422
+ };
1423
+ const approval = await invokeVerifier(
1424
+ verifyResolutionReceipt,
1425
+ entry.resolution,
1426
+ expected,
1427
+ );
1428
+ if (approval.error
1429
+ || approval.result.valid !== true
1430
+ || approval.result.authorizes_action !== true
1431
+ || approval.result.outcome !== 'approved'
1432
+ || !boundVerificationMatches(approval.result, expectedBindings(expected))
1433
+ || approval.result.party_id !== entry.party_id
1434
+ || approval.result.party_role
1435
+ !== record.parties.find((party) => party.party_id === entry.party_id)?.role
1436
+ || approval.result.principal_key_id !== context.principal_key_id
1437
+ || approval.result.nonce !== context.nonce
1438
+ || approval.result.issued_at !== context.issued_at
1439
+ || approval.result.expires_at !== context.expires_at
1440
+ || approval.result.evidence_digest !== expected.evidence_digest
1441
+ || approval.result.principal_key_id !== entry.verification.principal_key_id
1442
+ || approval.result.nonce !== entry.verification.nonce
1443
+ || approval.result.issued_at !== entry.verification.issued_at
1444
+ || approval.result.expires_at !== entry.verification.expires_at) {
1445
+ return {
1446
+ code: approval.error ?? 'release_approval_stale',
1447
+ details: { party_id: entry.party_id },
1448
+ };
1449
+ }
1450
+ }
1451
+ return null;
1452
+ }
1453
+
1454
+ async function verifyCommandAuthorization(
1455
+ artifact,
1456
+ record,
1457
+ command,
1458
+ partyId,
1459
+ details,
1460
+ ) {
1461
+ if (!record.parties.some((party) => party.party_id === partyId)) {
1462
+ return { error: 'command_party_invalid' };
1463
+ }
1464
+ const detailsDigest = canonicalDigest(details);
1465
+ const expected: Record<string, any> = {
1466
+ ...expectedBindings(record),
1467
+ profile: record.profile,
1468
+ parties: record.parties,
1469
+ command,
1470
+ party_id: partyId,
1471
+ details_digest: detailsDigest,
1472
+ };
1473
+ expected.command_digest = canonicalDigest({
1474
+ '@version': 'EP-ACTION-ESCROW-COMMAND-v1',
1475
+ ...expectedBindings(record),
1476
+ command,
1477
+ party_id: partyId,
1478
+ details_digest: detailsDigest,
1479
+ });
1480
+ const verified = await invokeVerifier(verifyStateCommand, artifact, expected);
1481
+ if (verified.error
1482
+ || verified.result.valid !== true
1483
+ || verified.result.authorizes_command !== true
1484
+ || verified.result.command !== command
1485
+ || verified.result.party_id !== partyId
1486
+ || verified.result.details_digest !== detailsDigest
1487
+ || verified.result.command_digest !== expected.command_digest
1488
+ || !boundVerificationMatches(verified.result, expectedBindings(record))) {
1489
+ return { error: verified.error ?? 'command_authorization_refused' };
1490
+ }
1491
+ return {
1492
+ artifact: verified.artifact,
1493
+ verification: {
1494
+ valid: true,
1495
+ authorizes_command: true,
1496
+ ...expectedBindings(record),
1497
+ command,
1498
+ party_id: partyId,
1499
+ details_digest: detailsDigest,
1500
+ command_digest: expected.command_digest,
1501
+ },
1502
+ };
1503
+ }
1504
+
1505
+ async function selectedProfile(normalized) {
1506
+ try {
1507
+ let selected;
1508
+ if (pinnedProfiles) {
1509
+ if (!Object.hasOwn(pinnedProfiles, normalized.snapshot.profile.profile_id)) {
1510
+ return { error: 'profile_not_pinned' };
1511
+ }
1512
+ selected = pinnedProfiles[normalized.snapshot.profile.profile_id];
1513
+ } else {
1514
+ // Construction guarantees exactly one of pinnedProfiles/resolveProfile
1515
+ // is set once configurationError is null (see the profilesById /
1516
+ // resolveProfile branch above); pinnedProfiles is falsy here, so
1517
+ // resolveProfile must be the one that was assigned.
1518
+ selected = canonicalSnapshot(await resolveProfile!(
1519
+ normalized.snapshot.profile.profile_id,
1520
+ deepFreeze(canonicalSnapshot({
1521
+ agreement_digest: normalized.context.agreement_digest,
1522
+ milestone_id: normalized.context.milestone_id,
1523
+ parties: normalized.context.parties,
1524
+ })),
1525
+ ));
1526
+ }
1527
+ if (validateProfileForParties(selected, normalized.context.parties)
1528
+ || canonicalDigest(selected) !== normalized.context.profile_digest) {
1529
+ return { error: 'profile_not_pinned' };
1530
+ }
1531
+ return { profile: deepFreeze(canonicalSnapshot(selected)) };
1532
+ } catch {
1533
+ return { error: 'profile_resolution_failed' };
1534
+ }
1535
+ }
1536
+
1537
+ async function create(input = {}) {
1538
+ const operation = 'create';
1539
+ return safe(operation, async () => {
1540
+ const normalized = normalizedInput(
1541
+ operation,
1542
+ input,
1543
+ ['document_action_binding'],
1544
+ );
1545
+ if (normalized.error) return outcome({ code: normalized.error, operation });
1546
+ const config = configurationRefusal(operation);
1547
+ if (config) return config;
1548
+
1549
+ const pinned = await selectedProfile(normalized);
1550
+ if (pinned.error) return outcome({ code: pinned.error, operation });
1551
+
1552
+ for (let attempt = 0; attempt < MAX_CAS_ATTEMPTS; attempt++) {
1553
+ const loaded = await readRecord(normalized.escrowKey);
1554
+ if (loaded.error) return outcome({ code: loaded.error, operation });
1555
+ if (loaded.record) {
1556
+ if (!operationBindingMatches(loaded.record, normalized)) {
1557
+ return outcome({
1558
+ code: 'operation_binding_mismatch',
1559
+ operation,
1560
+ record: loaded.record,
1561
+ });
1562
+ }
1563
+ return idempotentResult(loaded.record, normalized, operation)
1564
+ ?? outcome({ code: 'escrow_already_exists', operation, record: loaded.record });
1565
+ }
1566
+
1567
+ const at = instant();
1568
+ if (!at) return outcome({ code: 'invalid_clock', operation });
1569
+ const expected = {
1570
+ ...normalized.context,
1571
+ profile: pinned.profile,
1572
+ };
1573
+ const verification = await invokeVerifier(
1574
+ verifyDocumentActionBinding,
1575
+ normalized.snapshot.document_action_binding,
1576
+ expected,
1577
+ );
1578
+ const bindingDetails = verification.error
1579
+ ? null
1580
+ : bindingVerificationDetails(verification.result, expected);
1581
+ if (!bindingDetails) {
1582
+ return outcome({
1583
+ code: verification.error ?? 'document_action_binding_invalid',
1584
+ operation,
1585
+ });
1586
+ }
1587
+
1588
+ // normalizedInput() only omits `context` on its early `{ error }`
1589
+ // returns, all of which are handled above via the `normalized.error`
1590
+ // checks; once past those, `context` is always populated.
1591
+ const normalizedContext = normalized.context as Record<string, any>;
1592
+ const record = {
1593
+ '@version': ACTION_ESCROW_STATE_VERSION,
1594
+ escrow_key: normalized.escrowKey,
1595
+ revision: 0,
1596
+ state: 'draft',
1597
+ agreement_digest: normalizedContext.agreement_digest,
1598
+ document_action_binding_digest:
1599
+ normalizedContext.document_action_binding_digest,
1600
+ milestone_id: normalizedContext.milestone_id,
1601
+ release_action_digest: normalizedContext.release_action_digest,
1602
+ parties: normalizedContext.parties,
1603
+ parties_digest: normalizedContext.parties_digest,
1604
+ profile: pinned.profile,
1605
+ profile_digest: normalizedContext.profile_digest,
1606
+ document_action_binding: {
1607
+ artifact: verification.artifact,
1608
+ verification: boundVerificationSummary(
1609
+ verification.result,
1610
+ expected,
1611
+ bindingDetails,
1612
+ ),
1613
+ },
1614
+ agreement_acceptances: [],
1615
+ funding: null,
1616
+ milestone_evidence: null,
1617
+ release_approvals: [],
1618
+ release: null,
1619
+ dispute: null,
1620
+ cancellation: null,
1621
+ completion: null,
1622
+ pending_amendment: null,
1623
+ superseded_bindings: [],
1624
+ operations: [{
1625
+ idempotency_key: normalized.snapshot.idempotency_key,
1626
+ operation,
1627
+ request_digest: normalized.requestDigest,
1628
+ code: 'escrow_created',
1629
+ ok: true,
1630
+ outcome: 'applied',
1631
+ state: 'draft',
1632
+ at,
1633
+ }],
1634
+ history: [{
1635
+ from: null,
1636
+ to: 'draft',
1637
+ operation,
1638
+ idempotency_key: normalized.snapshot.idempotency_key,
1639
+ at,
1640
+ }],
1641
+ created_at: at,
1642
+ updated_at: at,
1643
+ };
1644
+ const written = await writeRecord(normalized.escrowKey, null, record);
1645
+ if (written.error) return outcome({ code: written.error, operation });
1646
+ if (written.applied) {
1647
+ return outcome({
1648
+ ok: true,
1649
+ type: 'applied',
1650
+ code: 'escrow_created',
1651
+ operation,
1652
+ record,
1653
+ });
1654
+ }
1655
+ }
1656
+ return outcome({ code: 'store_conflict', operation });
1657
+ });
1658
+ }
1659
+
1660
+ async function mutate(
1661
+ operation: string,
1662
+ input: Record<string, any>,
1663
+ {
1664
+ extraAllowed = [],
1665
+ extraRequired = extraAllowed,
1666
+ bindingMode = 'active',
1667
+ transform,
1668
+ }: {
1669
+ extraAllowed?: string[];
1670
+ extraRequired?: string[];
1671
+ bindingMode?: string;
1672
+ transform: (draft: Record<string, any>, normalized: Record<string, any>, at: string) => any;
1673
+ },
1674
+ ) {
1675
+ return safe(operation, async () => {
1676
+ const normalized = normalizedInput(
1677
+ operation,
1678
+ input,
1679
+ extraAllowed,
1680
+ extraRequired,
1681
+ );
1682
+ if (normalized.error) return outcome({ code: normalized.error, operation });
1683
+ const config = configurationRefusal(operation);
1684
+ if (config) return config;
1685
+
1686
+ for (let attempt = 0; attempt < MAX_CAS_ATTEMPTS; attempt++) {
1687
+ const loaded = await readRecord(normalized.escrowKey);
1688
+ if (loaded.error) return outcome({ code: loaded.error, operation });
1689
+ if (!loaded.record) return outcome({ code: 'escrow_not_found', operation });
1690
+ const record = loaded.record;
1691
+ if (!operationBindingMatches(record, normalized, bindingMode)) {
1692
+ return outcome({
1693
+ code: 'operation_binding_mismatch',
1694
+ operation,
1695
+ record,
1696
+ });
1697
+ }
1698
+ const repeated = idempotentResult(record, normalized, operation);
1699
+ if (repeated) return repeated;
1700
+ const operationTime = operationInstant(record);
1701
+ if (operationTime.error) {
1702
+ return outcome({ code: operationTime.error, operation, record });
1703
+ }
1704
+ // operationInstant() only omits `at` on its `{ error }` return,
1705
+ // already handled above, so `at` is guaranteed present here.
1706
+ const { at } = operationTime as { at: string };
1707
+
1708
+ const draft = canonicalSnapshot(record);
1709
+ const decision = await transform(draft, normalized, at);
1710
+ if (decision?.refusal) {
1711
+ return outcome({
1712
+ code: decision.refusal,
1713
+ operation,
1714
+ record,
1715
+ details: decision.details ?? null,
1716
+ type: decision.type ?? 'refused',
1717
+ });
1718
+ }
1719
+ const code = decision?.code ?? `${operation}_applied`;
1720
+ const finalized = finalizeMutation(
1721
+ record,
1722
+ normalized,
1723
+ operation,
1724
+ code,
1725
+ at,
1726
+ (next) => {
1727
+ Object.assign(next, draft);
1728
+ },
1729
+ {
1730
+ ok: decision?.ok ?? true,
1731
+ type: decision?.type ?? 'applied',
1732
+ },
1733
+ );
1734
+ if (finalized.error) {
1735
+ return outcome({ code: finalized.error, operation, record });
1736
+ }
1737
+ const written = await writeRecord(
1738
+ normalized.escrowKey,
1739
+ record.revision,
1740
+ finalized.next,
1741
+ );
1742
+ if (written.error) return outcome({ code: written.error, operation, record });
1743
+ if (written.applied) {
1744
+ return outcome({
1745
+ ok: decision?.ok ?? true,
1746
+ type: decision?.type ?? 'applied',
1747
+ code,
1748
+ operation,
1749
+ record: finalized.next,
1750
+ details: decision?.details ?? null,
1751
+ });
1752
+ }
1753
+ }
1754
+ return outcome({ code: 'store_conflict', operation });
1755
+ });
1756
+ }
1757
+
1758
+ async function beginAcceptance(input = {}) {
1759
+ return mutate('begin_acceptance', input, {
1760
+ transform(draft) {
1761
+ if (draft.state !== 'draft') {
1762
+ return { refusal: 'invalid_state_transition' };
1763
+ }
1764
+ draft.state = 'awaiting_acceptance';
1765
+ return { code: 'acceptance_requested' };
1766
+ },
1767
+ });
1768
+ }
1769
+
1770
+ async function acceptAgreement(input = {}) {
1771
+ return mutate('accept_agreement', input, {
1772
+ extraAllowed: ['party_id', 'agreement_acceptance'],
1773
+ async transform(draft, normalized, at) {
1774
+ if (draft.state !== 'awaiting_acceptance') {
1775
+ return { refusal: 'invalid_state_transition' };
1776
+ }
1777
+ const partyId = normalized.snapshot.party_id;
1778
+ if (!draft.profile.required_acceptance_party_ids.includes(partyId)) {
1779
+ return { refusal: 'acceptance_party_not_required' };
1780
+ }
1781
+ if (draft.agreement_acceptances.some((entry) => entry.party_id === partyId)) {
1782
+ return { refusal: 'agreement_already_accepted' };
1783
+ }
1784
+ const expected = {
1785
+ ...normalized.context,
1786
+ party_id: partyId,
1787
+ };
1788
+ const verified = await invokeVerifier(
1789
+ verifyAgreementAcceptance,
1790
+ normalized.snapshot.agreement_acceptance,
1791
+ expected,
1792
+ );
1793
+ if (verified.error
1794
+ || !boundVerificationMatches(verified.result, expectedBindings(expected))
1795
+ || verified.result.party_id !== partyId
1796
+ || !validString(verified.result.principal_key_id, 512)
1797
+ || draft.agreement_acceptances.some(
1798
+ (entry) => entry.verification?.principal_key_id
1799
+ === verified.result.principal_key_id,
1800
+ )
1801
+ || !validDigest(verified.result.acceptance_digest)) {
1802
+ return { refusal: verified.error ?? 'agreement_acceptance_invalid' };
1803
+ }
1804
+ draft.agreement_acceptances.push({
1805
+ party_id: partyId,
1806
+ artifact: verified.artifact,
1807
+ verification: boundVerificationSummary(
1808
+ verified.result,
1809
+ expected,
1810
+ {
1811
+ party_id: partyId,
1812
+ principal_key_id: verified.result.principal_key_id,
1813
+ acceptance_digest: verified.result.acceptance_digest,
1814
+ },
1815
+ ),
1816
+ });
1817
+ const accepted = new Set(draft.agreement_acceptances.map((entry) => entry.party_id));
1818
+ if (draft.profile.required_acceptance_party_ids.every((id) => accepted.has(id))) {
1819
+ draft.state = 'effective';
1820
+ return { code: 'agreement_effective' };
1821
+ }
1822
+ return { code: 'agreement_acceptance_recorded' };
1823
+ },
1824
+ });
1825
+ }
1826
+
1827
+ async function requestFunding(input = {}) {
1828
+ return mutate('request_funding', input, {
1829
+ transform(draft) {
1830
+ if (draft.state !== 'effective') {
1831
+ return { refusal: 'invalid_state_transition' };
1832
+ }
1833
+ draft.state = 'awaiting_funding';
1834
+ return { code: 'funding_requested' };
1835
+ },
1836
+ });
1837
+ }
1838
+
1839
+ async function recordFunding(input = {}) {
1840
+ return mutate('record_funding', input, {
1841
+ extraAllowed: ['provider_statement'],
1842
+ async transform(draft, normalized, at) {
1843
+ if (draft.state !== 'awaiting_funding') {
1844
+ return { refusal: 'invalid_state_transition' };
1845
+ }
1846
+ const template = draft.document_action_binding.verification.release_action_template;
1847
+ const expected = {
1848
+ ...normalized.context,
1849
+ provider_id: draft.profile.provider_id,
1850
+ statement_type: 'funding',
1851
+ expected_status: 'funded',
1852
+ provider_transaction_id: template.custodian_transaction_id,
1853
+ provider_milestone_id: template.custodian_milestone_id,
1854
+ amount: template.amount,
1855
+ currency: template.currency,
1856
+ destination_id: template.destination_id,
1857
+ };
1858
+ const verified = await invokeVerifier(
1859
+ verifyProviderStatement,
1860
+ normalized.snapshot.provider_statement,
1861
+ expected,
1862
+ );
1863
+ if (verified.error
1864
+ || !boundVerificationMatches(verified.result, expectedBindings(expected))
1865
+ || verified.result.authenticated !== true
1866
+ || verified.result.provider_id !== draft.profile.provider_id
1867
+ || verified.result.statement_type !== 'funding'
1868
+ || verified.result.status !== 'funded'
1869
+ || verified.result.provider_transaction_id !== expected.provider_transaction_id
1870
+ || verified.result.provider_milestone_id !== expected.provider_milestone_id
1871
+ || verified.result.amount !== expected.amount
1872
+ || verified.result.currency !== expected.currency
1873
+ || verified.result.destination_id !== expected.destination_id
1874
+ || !validDigest(verified.result.statement_digest)) {
1875
+ return { refusal: verified.error ?? 'funding_statement_invalid' };
1876
+ }
1877
+ draft.funding = {
1878
+ statement: verified.artifact,
1879
+ verification: boundVerificationSummary(
1880
+ verified.result,
1881
+ expected,
1882
+ {
1883
+ authenticated: true,
1884
+ provider_id: draft.profile.provider_id,
1885
+ statement_type: 'funding',
1886
+ status: 'funded',
1887
+ provider_transaction_id: expected.provider_transaction_id,
1888
+ provider_milestone_id: expected.provider_milestone_id,
1889
+ amount: expected.amount,
1890
+ currency: expected.currency,
1891
+ destination_id: expected.destination_id,
1892
+ statement_digest: verified.result.statement_digest,
1893
+ },
1894
+ ),
1895
+ };
1896
+ draft.state = 'funded';
1897
+ return { code: 'funding_confirmed' };
1898
+ },
1899
+ });
1900
+ }
1901
+
1902
+ async function submitMilestone(input = {}) {
1903
+ return mutate('submit_milestone', input, {
1904
+ extraAllowed: ['milestone_evidence'],
1905
+ async transform(draft, normalized, at) {
1906
+ if (draft.state !== 'funded') {
1907
+ return { refusal: 'invalid_state_transition' };
1908
+ }
1909
+ const expected = normalized.context;
1910
+ const verified = await invokeVerifier(
1911
+ verifyMilestoneEvidence,
1912
+ normalized.snapshot.milestone_evidence,
1913
+ expected,
1914
+ );
1915
+ const partyIds = draft.parties.map((party) => party.party_id);
1916
+ const expectedEvidenceDigest = draft.document_action_binding
1917
+ .verification.release_action_template.completion_evidence_sha256;
1918
+ if (verified.error
1919
+ || !boundVerificationMatches(verified.result, expectedBindings(expected))
1920
+ || !validDigest(verified.result.evidence_digest)
1921
+ || verified.result.evidence_digest !== expectedEvidenceDigest
1922
+ || !partyIds.includes(verified.result.submitter_party_id)
1923
+ || !validInstant(verified.result.observed_at)
1924
+ || Date.parse(verified.result.observed_at) > Date.parse(at)) {
1925
+ return { refusal: verified.error ?? 'milestone_evidence_invalid' };
1926
+ }
1927
+ draft.milestone_evidence = {
1928
+ artifact: verified.artifact,
1929
+ verification: boundVerificationSummary(
1930
+ verified.result,
1931
+ expected,
1932
+ {
1933
+ evidence_digest: verified.result.evidence_digest,
1934
+ submitter_party_id: verified.result.submitter_party_id,
1935
+ observed_at: verified.result.observed_at,
1936
+ },
1937
+ ),
1938
+ };
1939
+ draft.release_approvals = [];
1940
+ draft.state = 'milestone_submitted';
1941
+ return { code: 'milestone_evidence_recorded' };
1942
+ },
1943
+ });
1944
+ }
1945
+
1946
+ async function approveRelease(input = {}) {
1947
+ return mutate('approve_release', input, {
1948
+ extraAllowed: ['party_id', 'resolution'],
1949
+ async transform(draft, normalized, at) {
1950
+ if (draft.state !== 'milestone_submitted') {
1951
+ return { refusal: 'invalid_state_transition' };
1952
+ }
1953
+ const partyId = normalized.snapshot.party_id;
1954
+ if (!draft.profile.required_release_approver_party_ids.includes(partyId)) {
1955
+ return { refusal: 'release_approval_party_not_required' };
1956
+ }
1957
+ const party = draft.parties.find((entry) => entry.party_id === partyId);
1958
+ if (draft.profile.prohibit_self_approval
1959
+ && partyId === draft.milestone_evidence?.verification?.submitter_party_id) {
1960
+ return { refusal: 'self_approval_refused' };
1961
+ }
1962
+ if (draft.release_approvals.some((entry) => entry.party_id === partyId)) {
1963
+ return { refusal: 'release_approval_already_recorded' };
1964
+ }
1965
+
1966
+ const artifact = normalized.snapshot.resolution;
1967
+ const bindingInput = resolutionBindingInput(draft);
1968
+ const bindingMoment = createActionEscrowReleaseBindingMoment(bindingInput);
1969
+ const bindingMomentDigest = bindingMoment === null
1970
+ ? null
1971
+ : canonicalDigest(bindingMoment);
1972
+ const expectedNonce = computeActionEscrowResolutionNonce(bindingInput, partyId);
1973
+ const expectedInitiator = draft.milestone_evidence?.verification?.submitter_party_id;
1974
+ if (!isPlainObject(artifact)
1975
+ || artifact.profile !== RESOLUTION_VERSION
1976
+ || !isPlainObject(artifact.signoff)
1977
+ || !isPlainObject(artifact.signoff.context)
1978
+ || bindingMoment === null
1979
+ || bindingMomentDigest === null
1980
+ || expectedNonce === null
1981
+ || !validString(expectedInitiator, 256)) {
1982
+ return { refusal: 'resolution_profile_invalid' };
1983
+ }
1984
+ const resolutionContext = artifact.signoff.context;
1985
+ if (resolutionContext.principal !== partyId) {
1986
+ return { refusal: 'resolution_party_mismatch' };
1987
+ }
1988
+ if (resolutionContext.envelope_hash !== bindingMomentDigest) {
1989
+ return { refusal: 'resolution_binding_mismatch' };
1990
+ }
1991
+ if (resolutionContext.action_hash !== draft.release_action_digest) {
1992
+ return { refusal: 'resolution_action_mismatch' };
1993
+ }
1994
+ if (resolutionContext.initiator !== expectedInitiator) {
1995
+ return { refusal: 'resolution_initiator_mismatch' };
1996
+ }
1997
+ if (resolutionContext.nonce !== expectedNonce) {
1998
+ return { refusal: 'resolution_nonce_mismatch' };
1999
+ }
2000
+ if (resolutionContext.resolution?.outcome !== 'approved') {
2001
+ return { refusal: 'resolution_not_approved' };
2002
+ }
2003
+ if (!validString(resolutionContext.principal_key_id, 512)
2004
+ || !validString(resolutionContext.nonce, 512)
2005
+ || !validInstant(resolutionContext.issued_at)
2006
+ || !validInstant(resolutionContext.expires_at)
2007
+ || Date.parse(resolutionContext.issued_at)
2008
+ < Date.parse(draft.milestone_evidence.verification.observed_at)
2009
+ || Date.parse(resolutionContext.issued_at) > Date.parse(at)
2010
+ || Date.parse(resolutionContext.expires_at) <= Date.parse(at)
2011
+ || Date.parse(resolutionContext.expires_at)
2012
+ <= Date.parse(resolutionContext.issued_at)) {
2013
+ return { refusal: 'resolution_freshness_invalid' };
2014
+ }
2015
+ if (draft.release_approvals.some(
2016
+ (entry) => entry.verification?.principal_key_id
2017
+ === resolutionContext.principal_key_id,
2018
+ )) {
2019
+ return { refusal: 'resolution_key_already_counted' };
2020
+ }
2021
+ const expected = {
2022
+ ...normalized.context,
2023
+ party_id: partyId,
2024
+ evidence_digest: draft.milestone_evidence.verification.evidence_digest,
2025
+ binding_moment: bindingMoment,
2026
+ binding_moment_digest: bindingMomentDigest,
2027
+ expected_selected_option: 0,
2028
+ expected_initiator: expectedInitiator,
2029
+ expected_nonce: expectedNonce,
2030
+ evaluation_time: at,
2031
+ };
2032
+ const verified = await invokeVerifier(
2033
+ verifyResolutionReceipt,
2034
+ artifact,
2035
+ expected,
2036
+ );
2037
+ if (verified.error
2038
+ || verified.result.valid !== true
2039
+ || verified.result.authorizes_action !== true
2040
+ || verified.result.outcome !== 'approved'
2041
+ || !boundVerificationMatches(verified.result, expectedBindings(expected))
2042
+ || verified.result.party_id !== partyId
2043
+ || verified.result.party_role !== party.role
2044
+ || verified.result.principal_key_id !== resolutionContext.principal_key_id
2045
+ || verified.result.nonce !== resolutionContext.nonce
2046
+ || verified.result.issued_at !== resolutionContext.issued_at
2047
+ || verified.result.expires_at !== resolutionContext.expires_at
2048
+ || verified.result.evidence_digest !== expected.evidence_digest) {
2049
+ return { refusal: verified.error ?? 'resolution_verification_refused' };
2050
+ }
2051
+ draft.release_approvals.push({
2052
+ party_id: partyId,
2053
+ resolution: verified.artifact,
2054
+ verification: {
2055
+ valid: true,
2056
+ authorizes_action: true,
2057
+ outcome: 'approved',
2058
+ party_role: party.role,
2059
+ principal_key_id: resolutionContext.principal_key_id,
2060
+ nonce: resolutionContext.nonce,
2061
+ issued_at: resolutionContext.issued_at,
2062
+ expires_at: resolutionContext.expires_at,
2063
+ resolution_digest: canonicalDigest(verified.artifact),
2064
+ agreement_digest: draft.agreement_digest,
2065
+ document_action_binding_digest: draft.document_action_binding_digest,
2066
+ milestone_id: draft.milestone_id,
2067
+ release_action_digest: draft.release_action_digest,
2068
+ parties_digest: draft.parties_digest,
2069
+ profile_digest: draft.profile_digest,
2070
+ evidence_digest: draft.milestone_evidence.verification.evidence_digest,
2071
+ },
2072
+ });
2073
+ return { code: 'release_approval_recorded' };
2074
+ },
2075
+ });
2076
+ }
2077
+
2078
+ async function releasePreconditions(record, at) {
2079
+ if (record.funding?.verification?.status !== 'funded') {
2080
+ return { code: 'funding_not_verified' };
2081
+ }
2082
+ if (!validDigest(record.milestone_evidence?.verification?.evidence_digest)) {
2083
+ return { code: 'milestone_evidence_not_verified' };
2084
+ }
2085
+ const approved = new Set(record.release_approvals
2086
+ .filter((entry) => entry.verification?.authorizes_action === true
2087
+ && entry.verification?.document_action_binding_digest
2088
+ === record.document_action_binding_digest
2089
+ && entry.verification?.release_action_digest === record.release_action_digest
2090
+ && entry.verification?.evidence_digest
2091
+ === record.milestone_evidence.verification.evidence_digest)
2092
+ .map((entry) => entry.party_id));
2093
+ const missing = record.profile.required_release_approver_party_ids
2094
+ .filter((partyId) => !approved.has(partyId));
2095
+ if (missing.length > 0) {
2096
+ return { code: 'release_approval_missing', details: { missing_party_ids: missing } };
2097
+ }
2098
+ const evaluationTime = Date.parse(at);
2099
+ const stale = record.release_approvals
2100
+ .filter((entry) => (
2101
+ !validInstant(entry.verification?.issued_at)
2102
+ || !validInstant(entry.verification?.expires_at)
2103
+ || Date.parse(entry.verification.issued_at) > evaluationTime
2104
+ || Date.parse(entry.verification.expires_at) <= evaluationTime
2105
+ ))
2106
+ .map((entry) => entry.party_id);
2107
+ if (stale.length > 0) {
2108
+ return { code: 'release_approval_expired', details: { party_ids: stale } };
2109
+ }
2110
+ return verifyStoredReleaseInputs(record, at);
2111
+ }
2112
+
2113
+ function providerRequestFor(record) {
2114
+ const bindingVerification = record.document_action_binding.verification;
2115
+ const request = {
2116
+ method: 'POST',
2117
+ provider_id: record.profile.provider_id,
2118
+ agreement_digest: record.agreement_digest,
2119
+ document_action_binding_digest: record.document_action_binding_digest,
2120
+ milestone_id: record.milestone_id,
2121
+ release_action_digest: record.release_action_digest,
2122
+ parties: record.parties,
2123
+ parties_digest: record.parties_digest,
2124
+ profile: record.profile,
2125
+ profile_digest: record.profile_digest,
2126
+ agreement_id: bindingVerification.agreement_id,
2127
+ binding_id: bindingVerification.binding_id,
2128
+ document_digest: bindingVerification.document_digest,
2129
+ release_action_template: bindingVerification.release_action_template,
2130
+ release_key: releaseReservationKey(record),
2131
+ idempotency_key: providerIdempotencyKey(record),
2132
+ };
2133
+ return {
2134
+ ...request,
2135
+ request_digest: canonicalDigest({
2136
+ '@version': 'EP-ACTION-ESCROW-PROVIDER-REQUEST-v1',
2137
+ ...request,
2138
+ }),
2139
+ };
2140
+ }
2141
+
2142
+ async function authoritativeProviderRelease(record: Record<string, any>): Promise<
2143
+ | { error: string; artifact?: undefined; verification?: undefined }
2144
+ | { error?: undefined; artifact: any; verification: Record<string, any> }
2145
+ > {
2146
+ const request = {
2147
+ ...record.release.provider_request,
2148
+ method: 'GET',
2149
+ };
2150
+ let response;
2151
+ try {
2152
+ // getProviderRelease is always assigned once configurationError is
2153
+ // null (see the provider validation above); callers refuse via
2154
+ // configurationRefusal() otherwise.
2155
+ response = canonicalSnapshot(await withProviderTimeout(
2156
+ () => getProviderRelease!(deepFreeze(canonicalSnapshot(request))),
2157
+ ));
2158
+ } catch {
2159
+ return { error: 'provider_reconciliation_failed' };
2160
+ }
2161
+ if (!exactKeys(response, new Set(['authenticated', 'statement']))
2162
+ || response.authenticated !== true) {
2163
+ return { error: 'provider_reconciliation_unauthenticated' };
2164
+ }
2165
+ const expected = providerExpected(record);
2166
+ const verified = await invokeVerifier(
2167
+ verifyProviderStatement,
2168
+ response.statement,
2169
+ expected,
2170
+ );
2171
+ if (verified.error
2172
+ || !boundVerificationMatches(verified.result, expectedBindings(expected))
2173
+ || verified.result.authenticated !== true
2174
+ || verified.result.provider_id !== expected.provider_id
2175
+ || verified.result.statement_type !== 'release'
2176
+ || !['released', 'not_released', 'pending'].includes(verified.result.status)
2177
+ || !validDigest(verified.result.statement_digest)
2178
+ || verified.result.provider_idempotency_key !== expected.provider_idempotency_key
2179
+ || verified.result.provider_request_digest !== expected.provider_request_digest
2180
+ || verified.result.provider_transaction_id !== expected.provider_transaction_id
2181
+ || verified.result.provider_milestone_id !== expected.provider_milestone_id
2182
+ || verified.result.amount !== expected.amount
2183
+ || verified.result.currency !== expected.currency
2184
+ || verified.result.destination_id !== expected.destination_id) {
2185
+ return { error: verified.error ?? 'provider_release_statement_invalid' };
2186
+ }
2187
+ return {
2188
+ artifact: verified.artifact,
2189
+ verification: boundVerificationSummary(
2190
+ verified.result,
2191
+ expected,
2192
+ {
2193
+ authenticated: true,
2194
+ provider_id: expected.provider_id,
2195
+ provider_idempotency_key: expected.provider_idempotency_key,
2196
+ provider_request_digest: expected.provider_request_digest,
2197
+ provider_transaction_id: expected.provider_transaction_id,
2198
+ provider_milestone_id: expected.provider_milestone_id,
2199
+ amount: expected.amount,
2200
+ currency: expected.currency,
2201
+ destination_id: expected.destination_id,
2202
+ statement_type: 'release',
2203
+ status: verified.result.status,
2204
+ statement_digest: verified.result.statement_digest,
2205
+ },
2206
+ ),
2207
+ };
2208
+ }
2209
+
2210
+ function updateReleaseOperation(next, idempotencyKey, code, state) {
2211
+ const entry = next.operations.find(
2212
+ (operation) => operation.operation === 'release'
2213
+ && operation.idempotency_key === idempotencyKey,
2214
+ );
2215
+ if (entry) {
2216
+ entry.code = code;
2217
+ entry.ok = state === 'released';
2218
+ entry.outcome = state === 'released'
2219
+ ? 'applied'
2220
+ : state === 'release_indeterminate'
2221
+ ? 'indeterminate'
2222
+ : 'refused';
2223
+ entry.state = state;
2224
+ }
2225
+ }
2226
+
2227
+ function internalReleaseTransition(
2228
+ record: Record<string, any>,
2229
+ targetState: string,
2230
+ code: string | undefined,
2231
+ at: string,
2232
+ providerResult: { artifact: any; verification: any } | null = null,
2233
+ ) {
2234
+ const next = canonicalSnapshot(record);
2235
+ const from = next.state;
2236
+ next.state = targetState;
2237
+ next.revision = record.revision + 1;
2238
+ next.updated_at = at;
2239
+ next.release.status = targetState === 'released'
2240
+ ? 'released'
2241
+ : targetState === 'milestone_submitted'
2242
+ ? 'not_released'
2243
+ : 'indeterminate';
2244
+ if (providerResult) {
2245
+ next.release.reconciled_at = at;
2246
+ next.release.provider_statement = providerResult.artifact;
2247
+ next.release.provider_verification = providerResult.verification;
2248
+ }
2249
+ updateReleaseOperation(
2250
+ next,
2251
+ next.release.operation_idempotency_key,
2252
+ code,
2253
+ targetState,
2254
+ );
2255
+ if (from !== targetState) {
2256
+ if (next.history.length >= MAX_HISTORY) return { error: 'state_history_limit_reached' };
2257
+ next.history.push({
2258
+ from,
2259
+ to: targetState,
2260
+ operation: 'release',
2261
+ idempotency_key: next.release.operation_idempotency_key,
2262
+ at,
2263
+ });
2264
+ }
2265
+ return { next };
2266
+ }
2267
+
2268
+ /**
2269
+ * @param {Record<string, any>} record
2270
+ */
2271
+ async function readLatestRelease(record) {
2272
+ const loaded = await readRecord(record.escrow_key);
2273
+ return loaded.error ? { error: loaded.error } : { record: loaded.record };
2274
+ }
2275
+
2276
+ /**
2277
+ * @param {Record<string, any>} record
2278
+ * @param {string} operation
2279
+ * @param {string | undefined} code
2280
+ * @param {{ artifact: any, verification: any } | null} providerResult
2281
+ */
2282
+ async function freezeIndeterminate(record, operation, code, providerResult = null) {
2283
+ let current = record;
2284
+ for (let attempt = 0; attempt < MAX_CAS_ATTEMPTS; attempt++) {
2285
+ if (current?.state === 'released') {
2286
+ return outcome({
2287
+ ok: true,
2288
+ type: 'reconciled',
2289
+ code: 'release_committed',
2290
+ operation,
2291
+ record: current,
2292
+ });
2293
+ }
2294
+ if (current?.state === 'release_indeterminate') {
2295
+ return outcome({
2296
+ type: 'indeterminate',
2297
+ code,
2298
+ operation,
2299
+ record: current,
2300
+ });
2301
+ }
2302
+ if (current?.state !== 'release_reserved') {
2303
+ return outcome({
2304
+ type: 'indeterminate',
2305
+ code,
2306
+ operation,
2307
+ record: current,
2308
+ });
2309
+ }
2310
+ const operationTime = operationInstant(current);
2311
+ if (operationTime.error) {
2312
+ return outcome({
2313
+ type: 'indeterminate',
2314
+ code: operationTime.error,
2315
+ operation,
2316
+ record: current,
2317
+ });
2318
+ }
2319
+ const { at } = operationTime as { at: string };
2320
+ const transition = internalReleaseTransition(
2321
+ current,
2322
+ 'release_indeterminate',
2323
+ code,
2324
+ at,
2325
+ providerResult,
2326
+ );
2327
+ if (transition.error) {
2328
+ return outcome({
2329
+ type: 'indeterminate',
2330
+ code: transition.error,
2331
+ operation,
2332
+ record: current,
2333
+ });
2334
+ }
2335
+ const written = await writeRecord(
2336
+ current.escrow_key,
2337
+ current.revision,
2338
+ transition.next,
2339
+ );
2340
+ if (written.applied) {
2341
+ return outcome({
2342
+ type: 'indeterminate',
2343
+ code,
2344
+ operation,
2345
+ record: transition.next,
2346
+ });
2347
+ }
2348
+ const latest = await readLatestRelease(current);
2349
+ if (latest.error) {
2350
+ return outcome({
2351
+ type: 'indeterminate',
2352
+ code,
2353
+ operation,
2354
+ record: current,
2355
+ });
2356
+ }
2357
+ current = latest.record;
2358
+ }
2359
+ return outcome({
2360
+ type: 'indeterminate',
2361
+ code,
2362
+ operation,
2363
+ record: current,
2364
+ });
2365
+ }
2366
+
2367
+ /**
2368
+ * @param {Record<string, any>} record
2369
+ * @param {{ artifact: any, verification: any }} providerResult
2370
+ * @param {string} operation
2371
+ */
2372
+ async function commitReleaseResult(record, providerResult, operation) {
2373
+ const status = providerResult.verification.status;
2374
+ const operationTime = operationInstant(record);
2375
+ if (operationTime.error) {
2376
+ return freezeIndeterminate(
2377
+ record,
2378
+ operation,
2379
+ operationTime.error,
2380
+ providerResult,
2381
+ );
2382
+ }
2383
+ const { at } = operationTime as { at: string };
2384
+ const targetState = status === 'released'
2385
+ ? 'released'
2386
+ : status === 'not_released'
2387
+ ? 'milestone_submitted'
2388
+ : 'release_indeterminate';
2389
+ const code = status === 'released'
2390
+ ? 'release_committed'
2391
+ : status === 'not_released'
2392
+ ? 'provider_release_not_released'
2393
+ : 'release_effect_indeterminate';
2394
+ const transition = internalReleaseTransition(
2395
+ record,
2396
+ targetState,
2397
+ code,
2398
+ at,
2399
+ providerResult,
2400
+ );
2401
+ if (transition.error) {
2402
+ return freezeIndeterminate(
2403
+ record,
2404
+ operation,
2405
+ 'release_commit_indeterminate',
2406
+ providerResult,
2407
+ );
2408
+ }
2409
+ const written = await writeRecord(record.escrow_key, record.revision, transition.next);
2410
+ if (written.applied) {
2411
+ return outcome({
2412
+ ok: status === 'released',
2413
+ type: status === 'released' ? 'applied' : status === 'pending' ? 'indeterminate' : 'refused',
2414
+ code,
2415
+ operation,
2416
+ record: transition.next,
2417
+ });
2418
+ }
2419
+
2420
+ const latest = await readLatestRelease(record);
2421
+ if (!latest.error
2422
+ && latest.record?.state === 'released'
2423
+ && latest.record.release?.release_key === record.release.release_key) {
2424
+ return outcome({
2425
+ ok: true,
2426
+ type: 'idempotent',
2427
+ code: 'release_committed',
2428
+ operation,
2429
+ record: latest.record,
2430
+ });
2431
+ }
2432
+ return freezeIndeterminate(
2433
+ latest.record ?? record,
2434
+ operation,
2435
+ 'release_commit_indeterminate',
2436
+ providerResult,
2437
+ );
2438
+ }
2439
+
2440
+ async function release(input = {}) {
2441
+ const operation = 'release';
2442
+ return safe(operation, async () => {
2443
+ const normalized = normalizedInput(operation, input);
2444
+ if (normalized.error) return outcome({ code: normalized.error, operation });
2445
+ const config = configurationRefusal(operation);
2446
+ if (config) return config;
2447
+
2448
+ let reserved: Record<string, any> | null = null;
2449
+ for (let attempt = 0; attempt < MAX_CAS_ATTEMPTS; attempt++) {
2450
+ const loaded = await readRecord(normalized.escrowKey);
2451
+ if (loaded.error) return outcome({ code: loaded.error, operation });
2452
+ if (!loaded.record) return outcome({ code: 'escrow_not_found', operation });
2453
+ const record = loaded.record;
2454
+ if (!operationBindingMatches(record, normalized)) {
2455
+ return outcome({ code: 'operation_binding_mismatch', operation, record });
2456
+ }
2457
+ const repeated = idempotentResult(record, normalized, operation);
2458
+ if (repeated) return repeated;
2459
+ if (['released', 'completed'].includes(record.state)) {
2460
+ return outcome({ code: 'release_already_applied', operation, record });
2461
+ }
2462
+ if (['release_reserved', 'release_indeterminate'].includes(record.state)) {
2463
+ return outcome({
2464
+ code: 'release_reconciliation_required',
2465
+ operation,
2466
+ record,
2467
+ type: 'indeterminate',
2468
+ });
2469
+ }
2470
+ if (record.state !== 'milestone_submitted') {
2471
+ return outcome({ code: 'invalid_state_transition', operation, record });
2472
+ }
2473
+ const operationTime = operationInstant(record);
2474
+ if (operationTime.error) {
2475
+ return outcome({ code: operationTime.error, operation, record });
2476
+ }
2477
+ const { at } = operationTime as { at: string };
2478
+ const precondition = await releasePreconditions(record, at);
2479
+ if (precondition) {
2480
+ return outcome({
2481
+ code: precondition.code,
2482
+ operation,
2483
+ record,
2484
+ details: precondition.details ?? null,
2485
+ });
2486
+ }
2487
+ const request = providerRequestFor(record);
2488
+ const finalized = finalizeMutation(
2489
+ record,
2490
+ normalized,
2491
+ operation,
2492
+ 'release_reserved',
2493
+ at,
2494
+ (next) => {
2495
+ next.state = 'release_reserved';
2496
+ next.release = {
2497
+ release_key: request.release_key,
2498
+ provider_idempotency_key: request.idempotency_key,
2499
+ operation_idempotency_key: normalized.snapshot.idempotency_key,
2500
+ status: 'reserved',
2501
+ reserved_at: at,
2502
+ reconciled_at: null,
2503
+ provider_request: request,
2504
+ provider_statement: null,
2505
+ provider_verification: null,
2506
+ };
2507
+ },
2508
+ { ok: false, type: 'reserved' },
2509
+ );
2510
+ if (finalized.error) {
2511
+ return outcome({ code: finalized.error, operation, record });
2512
+ }
2513
+ const written = await writeRecord(
2514
+ normalized.escrowKey,
2515
+ record.revision,
2516
+ finalized.next,
2517
+ );
2518
+ if (written.error) return outcome({ code: written.error, operation, record });
2519
+ if (written.applied) {
2520
+ reserved = deepFreeze(finalized.next);
2521
+ break;
2522
+ }
2523
+ }
2524
+ if (!reserved) return outcome({ code: 'store_conflict', operation });
2525
+
2526
+ try {
2527
+ // reserved was just checked non-null above; releaseProvider is
2528
+ // always assigned once configurationError is null (see the provider
2529
+ // validation above). Both checks precede this closure, which (being
2530
+ // a plain arrow function, not a hoisted declaration) still needs
2531
+ // explicit assertions since `reserved` is a mutable `let`.
2532
+ await withProviderTimeout(
2533
+ () => releaseProvider!(
2534
+ deepFreeze(canonicalSnapshot(reserved!.release.provider_request)),
2535
+ ),
2536
+ );
2537
+ } catch {
2538
+ return freezeIndeterminate(
2539
+ reserved,
2540
+ operation,
2541
+ 'release_effect_indeterminate',
2542
+ );
2543
+ }
2544
+
2545
+ const reconciled = await authoritativeProviderRelease(reserved);
2546
+ if (reconciled.error) {
2547
+ return freezeIndeterminate(
2548
+ reserved,
2549
+ operation,
2550
+ 'release_effect_indeterminate',
2551
+ );
2552
+ }
2553
+ return commitReleaseResult(
2554
+ reserved,
2555
+ /** @type {{artifact: any, verification: any}} */ (reconciled),
2556
+ operation,
2557
+ );
2558
+ });
2559
+ }
2560
+
2561
+ async function reconcileRelease(input = {}) {
2562
+ const operation = 'reconcile_release';
2563
+ return safe(operation, async () => {
2564
+ const normalized = normalizedInput(operation, input);
2565
+ if (normalized.error) return outcome({ code: normalized.error, operation });
2566
+ const config = configurationRefusal(operation);
2567
+ if (config) return config;
2568
+
2569
+ for (let attempt = 0; attempt < MAX_CAS_ATTEMPTS; attempt++) {
2570
+ const loaded = await readRecord(normalized.escrowKey);
2571
+ if (loaded.error) return outcome({ code: loaded.error, operation });
2572
+ if (!loaded.record) return outcome({ code: 'escrow_not_found', operation });
2573
+ const record = loaded.record;
2574
+ if (!operationBindingMatches(record, normalized)) {
2575
+ return outcome({ code: 'operation_binding_mismatch', operation, record });
2576
+ }
2577
+ const repeated = idempotentResult(record, normalized, operation);
2578
+ if (repeated) return repeated;
2579
+ if (record.state === 'released') {
2580
+ return outcome({
2581
+ ok: true,
2582
+ type: 'idempotent',
2583
+ code: 'release_already_applied',
2584
+ operation,
2585
+ record,
2586
+ });
2587
+ }
2588
+ if (!['release_reserved', 'release_indeterminate'].includes(record.state)) {
2589
+ return outcome({ code: 'invalid_state_transition', operation, record });
2590
+ }
2591
+ if (record.release?.provider_idempotency_key !== providerIdempotencyKey(record)
2592
+ || record.release?.release_key !== releaseReservationKey(record)) {
2593
+ return outcome({ code: 'release_binding_corrupt', operation, record });
2594
+ }
2595
+
2596
+ const providerResult = await authoritativeProviderRelease(record);
2597
+ if (providerResult.error) {
2598
+ return outcome({
2599
+ code: providerResult.error,
2600
+ operation,
2601
+ record,
2602
+ type: 'indeterminate',
2603
+ });
2604
+ }
2605
+ const operationTime = operationInstant(record);
2606
+ if (operationTime.error) {
2607
+ return outcome({
2608
+ code: operationTime.error,
2609
+ operation,
2610
+ record,
2611
+ type: 'indeterminate',
2612
+ });
2613
+ }
2614
+ const { at } = operationTime as { at: string };
2615
+ const providerVerification = providerResult.verification as { status: string };
2616
+ const status = providerVerification.status;
2617
+ const targetState = status === 'released'
2618
+ ? 'released'
2619
+ : status === 'not_released'
2620
+ ? record.pending_amendment ? 'amendment_pending' : 'milestone_submitted'
2621
+ : 'release_indeterminate';
2622
+ const code = status === 'released'
2623
+ ? 'release_reconciled_released'
2624
+ : status === 'not_released'
2625
+ ? 'release_reconciled_not_released'
2626
+ : 'release_still_indeterminate';
2627
+ const draft = canonicalSnapshot(record);
2628
+ draft.state = targetState;
2629
+ draft.release.status = status === 'pending' ? 'indeterminate' : status;
2630
+ draft.release.reconciled_at = at;
2631
+ draft.release.provider_statement = providerResult.artifact;
2632
+ draft.release.provider_verification = providerVerification;
2633
+ if (status === 'not_released' && draft.pending_amendment) {
2634
+ draft.funding = null;
2635
+ draft.milestone_evidence = null;
2636
+ draft.release_approvals = [];
2637
+ }
2638
+ updateReleaseOperation(
2639
+ draft,
2640
+ draft.release.operation_idempotency_key,
2641
+ code,
2642
+ targetState,
2643
+ );
2644
+ const finalized = finalizeMutation(
2645
+ record,
2646
+ normalized,
2647
+ operation,
2648
+ code,
2649
+ at,
2650
+ (next) => Object.assign(next, draft),
2651
+ {
2652
+ ok: status !== 'pending',
2653
+ type: status === 'pending' ? 'indeterminate' : 'reconciled',
2654
+ },
2655
+ );
2656
+ if (finalized.error) {
2657
+ return outcome({
2658
+ code: finalized.error,
2659
+ operation,
2660
+ record,
2661
+ type: 'indeterminate',
2662
+ });
2663
+ }
2664
+ const written = await writeRecord(
2665
+ normalized.escrowKey,
2666
+ record.revision,
2667
+ finalized.next,
2668
+ );
2669
+ if (written.error) {
2670
+ return outcome({
2671
+ code: written.error,
2672
+ operation,
2673
+ record,
2674
+ type: 'indeterminate',
2675
+ });
2676
+ }
2677
+ if (written.applied) {
2678
+ return outcome({
2679
+ ok: status !== 'pending',
2680
+ type: status === 'pending' ? 'indeterminate' : 'reconciled',
2681
+ code,
2682
+ operation,
2683
+ record: finalized.next,
2684
+ });
2685
+ }
2686
+ }
2687
+ return outcome({
2688
+ code: 'store_conflict',
2689
+ operation,
2690
+ type: 'indeterminate',
2691
+ });
2692
+ });
2693
+ }
2694
+
2695
+ async function openDispute(input = {}) {
2696
+ return mutate('open_dispute', input, {
2697
+ extraAllowed: ['party_id', 'reason', 'command_authorization'],
2698
+ async transform(draft, normalized, at) {
2699
+ if (!['funded', 'milestone_submitted'].includes(draft.state)) {
2700
+ return { refusal: 'invalid_state_transition' };
2701
+ }
2702
+ const partyId = normalized.snapshot.party_id;
2703
+ if (!draft.parties.some(
2704
+ /** @param {{ party_id: string, role: string }} party */
2705
+ (party) => party.party_id === partyId,
2706
+ )
2707
+ || !validString(normalized.snapshot.reason, 2048)) {
2708
+ return { refusal: 'dispute_input_invalid' };
2709
+ }
2710
+ const authorization = await verifyCommandAuthorization(
2711
+ normalized.snapshot.command_authorization,
2712
+ draft,
2713
+ 'open_dispute',
2714
+ partyId,
2715
+ { reason: normalized.snapshot.reason },
2716
+ );
2717
+ if (authorization.error) return { refusal: authorization.error };
2718
+ draft.dispute = {
2719
+ party_id: partyId,
2720
+ reason: normalized.snapshot.reason,
2721
+ authorization,
2722
+ opened_at: at,
2723
+ };
2724
+ draft.state = 'disputed';
2725
+ return { code: 'dispute_opened' };
2726
+ },
2727
+ });
2728
+ }
2729
+
2730
+ async function proposeAmendment(input = {}) {
2731
+ return mutate('propose_amendment', input, {
2732
+ extraAllowed: [
2733
+ 'party_id',
2734
+ 'command_authorization',
2735
+ 'next_document_action_binding_digest',
2736
+ 'next_release_action_digest',
2737
+ 'next_document_action_binding',
2738
+ ],
2739
+ async transform(draft, normalized, at) {
2740
+ if (draft.state === 'awaiting_funding') {
2741
+ return { refusal: 'amendment_requires_funding_reconciliation' };
2742
+ }
2743
+ if (draft.funding !== null
2744
+ || draft.release !== null
2745
+ || ['funded', 'milestone_submitted', 'disputed'].includes(draft.state)) {
2746
+ return { refusal: 'amendment_requires_custodian_unwind' };
2747
+ }
2748
+ if (draft.state !== 'effective'
2749
+ || draft.pending_amendment) {
2750
+ return { refusal: 'invalid_state_transition' };
2751
+ }
2752
+ const nextBindingDigest =
2753
+ normalized.snapshot.next_document_action_binding_digest;
2754
+ const nextActionDigest = normalized.snapshot.next_release_action_digest;
2755
+ if (!validDigest(nextBindingDigest)
2756
+ || !validDigest(nextActionDigest)
2757
+ || nextBindingDigest === draft.document_action_binding_digest) {
2758
+ return { refusal: 'amendment_binding_invalid' };
2759
+ }
2760
+ const partyId = normalized.snapshot.party_id;
2761
+ const authorization = await verifyCommandAuthorization(
2762
+ normalized.snapshot.command_authorization,
2763
+ draft,
2764
+ 'propose_amendment',
2765
+ partyId,
2766
+ {
2767
+ next_document_action_binding_digest: nextBindingDigest,
2768
+ next_release_action_digest: nextActionDigest,
2769
+ },
2770
+ );
2771
+ if (authorization.error) return { refusal: authorization.error };
2772
+ const nextContext = {
2773
+ ...normalized.context,
2774
+ document_action_binding_digest: nextBindingDigest,
2775
+ release_action_digest: nextActionDigest,
2776
+ supersedes_document_action_binding_digest:
2777
+ draft.document_action_binding_digest,
2778
+ };
2779
+ const verified = await invokeVerifier(
2780
+ verifyDocumentActionBinding,
2781
+ normalized.snapshot.next_document_action_binding,
2782
+ nextContext,
2783
+ );
2784
+ const bindingDetails = verified.error
2785
+ ? null
2786
+ : bindingVerificationDetails(verified.result, nextContext);
2787
+ if (!bindingDetails
2788
+ || verified.result.supersedes_document_action_binding_digest
2789
+ !== draft.document_action_binding_digest
2790
+ ) {
2791
+ return { refusal: verified.error ?? 'amendment_document_binding_invalid' };
2792
+ }
2793
+
2794
+ draft.pending_amendment = {
2795
+ from_state: draft.state,
2796
+ document_action_binding_digest: nextBindingDigest,
2797
+ release_action_digest: nextActionDigest,
2798
+ document_action_binding: {
2799
+ artifact: verified.artifact,
2800
+ verification: boundVerificationSummary(
2801
+ verified.result,
2802
+ nextContext,
2803
+ {
2804
+ ...bindingDetails,
2805
+ supersedes_document_action_binding_digest:
2806
+ draft.document_action_binding_digest,
2807
+ },
2808
+ ),
2809
+ },
2810
+ agreement_acceptances: [],
2811
+ proposer_party_id: partyId,
2812
+ proposal_authorization: authorization,
2813
+ proposed_at: at,
2814
+ };
2815
+ draft.funding = null;
2816
+ draft.milestone_evidence = null;
2817
+ draft.release_approvals = [];
2818
+ draft.release = null;
2819
+ draft.dispute = null;
2820
+ draft.cancellation = null;
2821
+ draft.state = 'amendment_pending';
2822
+ return { code: 'amendment_pending' };
2823
+ },
2824
+ });
2825
+ }
2826
+
2827
+ async function acceptAmendment(input = {}) {
2828
+ return mutate('accept_amendment', input, {
2829
+ extraAllowed: ['party_id', 'agreement_acceptance'],
2830
+ bindingMode: 'pending',
2831
+ async transform(draft, normalized, at) {
2832
+ if (draft.state !== 'amendment_pending' || !draft.pending_amendment) {
2833
+ return { refusal: 'invalid_state_transition' };
2834
+ }
2835
+ const partyId = normalized.snapshot.party_id;
2836
+ if (!draft.profile.required_acceptance_party_ids.includes(partyId)) {
2837
+ return { refusal: 'acceptance_party_not_required' };
2838
+ }
2839
+ if (draft.pending_amendment.agreement_acceptances
2840
+ .some((entry) => entry.party_id === partyId)) {
2841
+ return { refusal: 'amendment_already_accepted' };
2842
+ }
2843
+ const expected = {
2844
+ ...normalized.context,
2845
+ party_id: partyId,
2846
+ };
2847
+ const verified = await invokeVerifier(
2848
+ verifyAgreementAcceptance,
2849
+ normalized.snapshot.agreement_acceptance,
2850
+ expected,
2851
+ );
2852
+ if (verified.error
2853
+ || !boundVerificationMatches(verified.result, expectedBindings(expected))
2854
+ || verified.result.party_id !== partyId
2855
+ || !validString(verified.result.principal_key_id, 512)
2856
+ || draft.pending_amendment.agreement_acceptances.some(
2857
+ (entry) => entry.verification?.principal_key_id
2858
+ === verified.result.principal_key_id,
2859
+ )
2860
+ || !validDigest(verified.result.acceptance_digest)) {
2861
+ return { refusal: verified.error ?? 'amendment_acceptance_invalid' };
2862
+ }
2863
+ draft.pending_amendment.agreement_acceptances.push({
2864
+ party_id: partyId,
2865
+ artifact: verified.artifact,
2866
+ verification: boundVerificationSummary(
2867
+ verified.result,
2868
+ expected,
2869
+ {
2870
+ party_id: partyId,
2871
+ principal_key_id: verified.result.principal_key_id,
2872
+ acceptance_digest: verified.result.acceptance_digest,
2873
+ },
2874
+ ),
2875
+ });
2876
+ const accepted = new Set(
2877
+ draft.pending_amendment.agreement_acceptances
2878
+ .map((entry) => entry.party_id),
2879
+ );
2880
+ if (!draft.profile.required_acceptance_party_ids
2881
+ .every((id) => accepted.has(id))) {
2882
+ return { code: 'amendment_acceptance_recorded' };
2883
+ }
2884
+ if (draft.superseded_bindings.length >= MAX_SUPERSEDED_BINDINGS) {
2885
+ return { refusal: 'supersession_history_limit_reached' };
2886
+ }
2887
+ const pending = draft.pending_amendment;
2888
+ draft.superseded_bindings.push({
2889
+ document_action_binding_digest: draft.document_action_binding_digest,
2890
+ release_action_digest: draft.release_action_digest,
2891
+ superseded_by_binding_digest: pending.document_action_binding_digest,
2892
+ superseded_at: at,
2893
+ });
2894
+ draft.document_action_binding_digest =
2895
+ pending.document_action_binding_digest;
2896
+ draft.release_action_digest = pending.release_action_digest;
2897
+ draft.document_action_binding = pending.document_action_binding;
2898
+ draft.agreement_acceptances = pending.agreement_acceptances;
2899
+ draft.funding = null;
2900
+ draft.milestone_evidence = null;
2901
+ draft.release_approvals = [];
2902
+ draft.release = null;
2903
+ draft.dispute = null;
2904
+ draft.cancellation = null;
2905
+ draft.pending_amendment = null;
2906
+ draft.state = 'effective';
2907
+ return { code: 'amendment_effective' };
2908
+ },
2909
+ });
2910
+ }
2911
+
2912
+ async function cancel(input = {}) {
2913
+ return mutate('cancel', input, {
2914
+ extraAllowed: ['party_id', 'reason', 'command_authorization'],
2915
+ extraRequired: ['party_id', 'command_authorization'],
2916
+ async transform(draft, normalized, at) {
2917
+ if (draft.state === 'awaiting_funding') {
2918
+ return { refusal: 'cancellation_requires_funding_reconciliation' };
2919
+ }
2920
+ if (draft.funding !== null || draft.release !== null) {
2921
+ return { refusal: 'cancellation_requires_custodian_unwind' };
2922
+ }
2923
+ if (![
2924
+ 'draft',
2925
+ 'awaiting_acceptance',
2926
+ 'effective',
2927
+ 'amendment_pending',
2928
+ ].includes(draft.state)) {
2929
+ return { refusal: 'invalid_state_transition' };
2930
+ }
2931
+ if (normalized.snapshot.reason !== undefined
2932
+ && !validString(normalized.snapshot.reason, 2048)) {
2933
+ return { refusal: 'cancellation_reason_invalid' };
2934
+ }
2935
+ const authorization = await verifyCommandAuthorization(
2936
+ normalized.snapshot.command_authorization,
2937
+ draft,
2938
+ 'cancel',
2939
+ normalized.snapshot.party_id,
2940
+ { reason: normalized.snapshot.reason ?? null },
2941
+ );
2942
+ if (authorization.error) return { refusal: authorization.error };
2943
+ draft.state = 'cancelled';
2944
+ draft.cancellation = {
2945
+ party_id: normalized.snapshot.party_id,
2946
+ reason: normalized.snapshot.reason ?? null,
2947
+ authorization,
2948
+ cancelled_at: at,
2949
+ };
2950
+ return { code: 'escrow_cancelled' };
2951
+ },
2952
+ });
2953
+ }
2954
+
2955
+ async function complete(input = {}) {
2956
+ return mutate('complete', input, {
2957
+ extraAllowed: ['party_id', 'command_authorization'],
2958
+ extraRequired: ['party_id', 'command_authorization'],
2959
+ async transform(draft, normalized, at) {
2960
+ if (draft.state !== 'released') {
2961
+ return { refusal: 'invalid_state_transition' };
2962
+ }
2963
+ const authorization = await verifyCommandAuthorization(
2964
+ normalized.snapshot.command_authorization,
2965
+ draft,
2966
+ 'complete',
2967
+ normalized.snapshot.party_id,
2968
+ { meaning: 'administrative_archive_only' },
2969
+ );
2970
+ if (authorization.error) return { refusal: authorization.error };
2971
+ draft.state = 'completed';
2972
+ draft.completion = {
2973
+ party_id: normalized.snapshot.party_id,
2974
+ meaning: 'administrative_archive_only',
2975
+ authorization,
2976
+ completed_at: at,
2977
+ };
2978
+ return { code: 'escrow_completed' };
2979
+ },
2980
+ });
2981
+ }
2982
+
2983
+ const methods = {
2984
+ create,
2985
+ beginAcceptance,
2986
+ acceptAgreement,
2987
+ requestFunding,
2988
+ recordFunding,
2989
+ submitMilestone,
2990
+ approveRelease,
2991
+ release,
2992
+ reconcileRelease,
2993
+ openDispute,
2994
+ proposeAmendment,
2995
+ acceptAmendment,
2996
+ cancel,
2997
+ complete,
2998
+ };
2999
+
3000
+ async function apply(operation, input = {}) {
3001
+ try {
3002
+ const selected = methods[operation];
3003
+ if (typeof selected !== 'function') {
3004
+ return outcome({
3005
+ code: 'unknown_operation',
3006
+ operation: validString(operation, 128) ? operation : null,
3007
+ });
3008
+ }
3009
+ return selected(input);
3010
+ } catch {
3011
+ return outcome({
3012
+ code: 'invalid_operation_input',
3013
+ operation: null,
3014
+ });
3015
+ }
3016
+ }
3017
+
3018
+ return Object.freeze({
3019
+ ready: configurationError === null,
3020
+ configuration: Object.freeze({
3021
+ ok: configurationError === null,
3022
+ reason: configurationError,
3023
+ }),
3024
+ apply,
3025
+ ...methods,
3026
+ });
3027
+ }
3028
+
3029
+ export default Object.freeze({
3030
+ ACTION_ESCROW_STATE_VERSION,
3031
+ ACTION_ESCROW_OUTCOME_VERSION,
3032
+ ACTION_ESCROW_PROFILE_VERSION,
3033
+ ACTION_ESCROW_STATES,
3034
+ ACTION_ESCROW_TRANSITIONS,
3035
+ createActionEscrowReleaseBindingMoment,
3036
+ computeActionEscrowReleaseBindingMomentDigest,
3037
+ computeActionEscrowResolutionNonce,
3038
+ createActionEscrowKernel,
3039
+ });