@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
package/action-escrow.js CHANGED
@@ -1,2448 +1,4 @@
1
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 { validateActionEscrowReleaseTemplate } from './action-escrow-verifiers.js';
11
-
12
- export const ACTION_ESCROW_STATE_VERSION = 'EP-ACTION-ESCROW-STATE-v1';
13
- export const ACTION_ESCROW_OUTCOME_VERSION = 'EP-ACTION-ESCROW-OUTCOME-v1';
14
- export const ACTION_ESCROW_PROFILE_VERSION = 'EP-ACTION-ESCROW-PROFILE-v1';
15
-
16
- export const ACTION_ESCROW_STATES = Object.freeze([
17
- 'draft',
18
- 'awaiting_acceptance',
19
- 'effective',
20
- 'awaiting_funding',
21
- 'funded',
22
- 'milestone_submitted',
23
- 'release_reserved',
24
- 'released',
25
- 'disputed',
26
- 'amendment_pending',
27
- 'cancelled',
28
- 'completed',
29
- 'release_indeterminate',
30
- ]);
31
-
32
- export const ACTION_ESCROW_TRANSITIONS = Object.freeze({
33
- draft: Object.freeze(['awaiting_acceptance', 'cancelled']),
34
- awaiting_acceptance: Object.freeze(['effective', 'cancelled']),
35
- effective: Object.freeze(['awaiting_funding', 'amendment_pending', 'cancelled']),
36
- awaiting_funding: Object.freeze(['funded']),
37
- funded: Object.freeze(['milestone_submitted', 'disputed']),
38
- milestone_submitted: Object.freeze([
39
- 'release_reserved',
40
- 'disputed',
41
- ]),
42
- release_reserved: Object.freeze(['released', 'release_indeterminate', 'milestone_submitted']),
43
- released: Object.freeze(['completed']),
44
- disputed: Object.freeze([]),
45
- amendment_pending: Object.freeze(['effective', 'cancelled']),
46
- cancelled: Object.freeze([]),
47
- completed: Object.freeze([]),
48
- release_indeterminate: Object.freeze([
49
- 'released',
50
- 'milestone_submitted',
51
- ]),
52
- });
53
-
54
- const RESOLUTION_VERSION = 'EP-RESOLUTION-v1';
55
- const DIGEST_RE = /^sha256:[0-9a-f]{64}$/;
56
- const MAX_CAS_ATTEMPTS = 4;
57
- const MAX_OPERATIONS = 256;
58
- const MAX_HISTORY = 512;
59
- const MAX_SUPERSEDED_BINDINGS = 64;
60
- const MAX_STATE_BYTES = 4 * 1024 * 1024;
61
- const DEFAULT_PROVIDER_TIMEOUT_MS = 30_000;
62
-
63
- const STORE_CAPABILITIES = Object.freeze([
64
- 'durable',
65
- 'atomicExpectedRevisionCas',
66
- 'linearizableReads',
67
- 'monotonicRevisions',
68
- 'nonExpiring',
69
- ]);
70
-
71
- const COMMON_INPUT_KEYS = Object.freeze([
72
- 'agreement_digest',
73
- 'document_action_binding_digest',
74
- 'milestone_id',
75
- 'release_action_digest',
76
- 'parties',
77
- 'profile',
78
- 'idempotency_key',
79
- ]);
80
-
81
- const RECORD_KEYS = new Set([
82
- '@version',
83
- 'escrow_key',
84
- 'revision',
85
- 'state',
86
- 'agreement_digest',
87
- 'document_action_binding_digest',
88
- 'milestone_id',
89
- 'release_action_digest',
90
- 'parties',
91
- 'parties_digest',
92
- 'profile',
93
- 'profile_digest',
94
- 'document_action_binding',
95
- 'agreement_acceptances',
96
- 'funding',
97
- 'milestone_evidence',
98
- 'release_approvals',
99
- 'release',
100
- 'dispute',
101
- 'cancellation',
102
- 'completion',
103
- 'pending_amendment',
104
- 'superseded_bindings',
105
- 'operations',
106
- 'history',
107
- 'created_at',
108
- 'updated_at',
109
- ]);
110
-
111
- function isPlainObject(value) {
112
- if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
113
- const prototype = Object.getPrototypeOf(value);
114
- return prototype === Object.prototype || prototype === null;
115
- }
116
-
117
- function exactKeys(value, allowed, required = allowed) {
118
- if (!isPlainObject(value)) return false;
119
- const keys = Object.keys(value);
120
- return keys.every((key) => allowed.has(key))
121
- && [...required].every((key) => Object.hasOwn(value, key));
122
- }
123
-
124
- function validString(value, max = 512) {
125
- return typeof value === 'string'
126
- && value.length > 0
127
- && value.length <= max
128
- && !/[\u0000-\u001f\u007f]/.test(value);
129
- }
130
-
131
- function validDigest(value) {
132
- return typeof value === 'string' && DIGEST_RE.test(value);
133
- }
134
-
135
- function validInstant(value) {
136
- if (!validString(value, 64)) return false;
137
- const parsed = Date.parse(value);
138
- return Number.isFinite(parsed) && new Date(parsed).toISOString() === value;
139
- }
140
-
141
- function canonicalSnapshot(value) {
142
- return JSON.parse(canonicalize(value));
143
- }
144
-
145
- function canonicalDigest(value) {
146
- return `sha256:${hashCanonical(value)}`;
147
- }
148
-
149
- function resolutionBindingInput(record) {
150
- return {
151
- agreement_digest: record.agreement_digest,
152
- document_action_binding_digest: record.document_action_binding_digest,
153
- milestone_id: record.milestone_id,
154
- release_action_digest: record.release_action_digest,
155
- profile_digest: record.profile_digest,
156
- evidence_digest: record.milestone_evidence?.verification?.evidence_digest,
157
- release_action_template:
158
- record.document_action_binding?.verification?.release_action_template,
159
- };
160
- }
161
-
162
- /**
163
- * Build the exact human-facing envelope signed by an Action Escrow approver.
164
- *
165
- * The release action digest remains the normative machine-action binding. This
166
- * envelope binds the document, evidence, and material release fields that the
167
- * approver reviews before selecting the approval option.
168
- */
169
- export function createActionEscrowReleaseBindingMoment(input) {
170
- try {
171
- if (!isPlainObject(input)
172
- || !validDigest(input.agreement_digest)
173
- || !validDigest(input.document_action_binding_digest)
174
- || !validString(input.milestone_id, 256)
175
- || !validDigest(input.release_action_digest)
176
- || !validDigest(input.profile_digest)
177
- || !validDigest(input.evidence_digest)
178
- || !isPlainObject(input.release_action_template)) {
179
- return null;
180
- }
181
- const template = input.release_action_template;
182
- if (template.action_type !== 'escrow.milestone.release'
183
- || !validString(template.amount, 128)
184
- || !validString(template.currency, 16)
185
- || !validString(template.payee_id, 512)
186
- || !validString(template.destination_id, 512)
187
- || !validDigest(template.document_sha256)
188
- || !validDigest(template.material_terms_sha256)
189
- || template.completion_evidence_sha256 !== input.evidence_digest
190
- || !Number.isSafeInteger(template.amendment_version)
191
- || template.amendment_version < 1) {
192
- return null;
193
- }
194
- return deepFreeze(canonicalSnapshot({
195
- synopsis: `Authorize one ${template.amount} ${template.currency} milestone release.`,
196
- findings: [
197
- `Agreement digest: ${input.agreement_digest}`,
198
- `Document-action binding digest: ${input.document_action_binding_digest}`,
199
- `Release action digest: ${input.release_action_digest}`,
200
- `Milestone evidence digest: ${input.evidence_digest}`,
201
- `Milestone: ${input.milestone_id}`,
202
- `Payee: ${template.payee_id}`,
203
- `Destination: ${template.destination_id}`,
204
- `Amendment version: ${template.amendment_version}`,
205
- ],
206
- recommendations: [
207
- 'Verify the exact document, amount, destination, and completion evidence before approving.',
208
- ],
209
- offer: 'Decline if any material field is unexpected; amendments require a new binding and fresh approvals.',
210
- question: {
211
- stem: `Authorize release of ${template.amount} ${template.currency} for milestone ${input.milestone_id} to ${template.payee_id} at ${template.destination_id}?`,
212
- options: [
213
- {
214
- label: 'Approve exact release',
215
- reasoning: 'Authorize only the action identified by the signed action digest.',
216
- },
217
- {
218
- label: 'Decline release',
219
- reasoning: 'Do not authorize this action or any custodian effect.',
220
- },
221
- ],
222
- recommended_idx: 1,
223
- hatches: {
224
- free_text: false,
225
- dialogue: false,
226
- },
227
- },
228
- meta: {
229
- decision_class: 'escrow.milestone.release',
230
- calibration_note: 'No approval recommendation; verify every material field.',
231
- },
232
- }));
233
- } catch {
234
- return null;
235
- }
236
- }
237
-
238
- export function computeActionEscrowReleaseBindingMomentDigest(input) {
239
- const bindingMoment = createActionEscrowReleaseBindingMoment(input);
240
- return bindingMoment === null ? null : canonicalDigest(bindingMoment);
241
- }
242
-
243
- /**
244
- * Return the relying-party-pinned nonce for one party and one exact release.
245
- *
246
- * It is stable while both parties approve, changes with any material action or
247
- * evidence change, and is consumed by the durable approval CAS.
248
- */
249
- export function computeActionEscrowResolutionNonce(input, partyId) {
250
- if (!validString(partyId, 256)) return null;
251
- const bindingMomentDigest = computeActionEscrowReleaseBindingMomentDigest(input);
252
- if (bindingMomentDigest === null) return null;
253
- try {
254
- return `ep-ae-resolution:${hashCanonical({
255
- '@version': 'EP-ACTION-ESCROW-RESOLUTION-NONCE-v1',
256
- party_id: partyId,
257
- binding_moment_digest: bindingMomentDigest,
258
- release_action_digest: input.release_action_digest,
259
- evidence_digest: input.evidence_digest,
260
- })}`;
261
- } catch {
262
- return null;
263
- }
264
- }
265
-
266
- function deepFreeze(value) {
267
- if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
268
- Object.freeze(value);
269
- for (const child of Object.values(value)) deepFreeze(child);
270
- return value;
271
- }
272
-
273
- function sameStringSet(left, right) {
274
- if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
275
- const values = new Set(left);
276
- return values.size === left.length && right.every((value) => values.has(value));
277
- }
278
-
279
- function validateParties(parties) {
280
- if (!Array.isArray(parties) || parties.length < 2 || parties.length > 16) {
281
- return 'parties_invalid';
282
- }
283
- const ids = [];
284
- for (const party of parties) {
285
- if (!exactKeys(party, new Set(['party_id', 'role']))
286
- || !validString(party.party_id, 256)
287
- || !validString(party.role, 128)) {
288
- return 'party_invalid';
289
- }
290
- ids.push(party.party_id);
291
- }
292
- return new Set(ids).size === ids.length ? null : 'party_duplicate';
293
- }
294
-
295
- function validateProfileShape(profile) {
296
- if (!exactKeys(profile, new Set([
297
- '@version',
298
- 'profile_id',
299
- 'provider_id',
300
- 'required_acceptance_party_ids',
301
- 'required_release_approver_party_ids',
302
- 'prohibit_self_approval',
303
- ]))) {
304
- return 'profile_shape_invalid';
305
- }
306
- if (profile['@version'] !== ACTION_ESCROW_PROFILE_VERSION
307
- || !validString(profile.profile_id, 256)
308
- || !validString(profile.provider_id, 256)) {
309
- return 'profile_identity_invalid';
310
- }
311
- for (const field of [
312
- 'required_acceptance_party_ids',
313
- 'required_release_approver_party_ids',
314
- ]) {
315
- const roster = profile[field];
316
- if (!Array.isArray(roster) || roster.length === 0 || roster.length > 16
317
- || roster.some((partyId) => !validString(partyId, 256))
318
- || new Set(roster).size !== roster.length) {
319
- return `${field}_invalid`;
320
- }
321
- }
322
- if (profile.prohibit_self_approval !== true
323
- && profile.prohibit_self_approval !== false) {
324
- return 'profile_self_approval_policy_invalid';
325
- }
326
- return null;
327
- }
328
-
329
- function validateProfileForParties(profile, parties) {
330
- const shapeError = validateProfileShape(profile);
331
- if (shapeError) return shapeError;
332
- const partyIds = parties.map((party) => party.party_id);
333
- if (!sameStringSet(profile.required_acceptance_party_ids, partyIds)) {
334
- return 'mutual_acceptance_roster_invalid';
335
- }
336
- if (!profile.required_release_approver_party_ids.every((partyId) => partyIds.includes(partyId))) {
337
- return 'release_approval_roster_invalid';
338
- }
339
- return null;
340
- }
341
-
342
- function escrowKey(context) {
343
- return `ep-action-escrow:${hashCanonical({
344
- '@version': 'EP-ACTION-ESCROW-KEY-v1',
345
- agreement_digest: context.agreement_digest,
346
- milestone_id: context.milestone_id,
347
- })}`;
348
- }
349
-
350
- function releaseReservationKey(context) {
351
- return `ep-ae-reservation:${hashCanonical({
352
- '@version': 'EP-ACTION-ESCROW-RELEASE-KEY-v1',
353
- agreement_digest: context.agreement_digest,
354
- document_action_binding_digest: context.document_action_binding_digest,
355
- milestone_id: context.milestone_id,
356
- release_action_digest: context.release_action_digest,
357
- profile_digest: context.profile_digest,
358
- })}`;
359
- }
360
-
361
- function providerIdempotencyKey(context) {
362
- return `ep-ae-release:${hashCanonical({
363
- '@version': 'EP-ACTION-ESCROW-PROVIDER-IDEMPOTENCY-v1',
364
- agreement_digest: context.agreement_digest,
365
- document_action_binding_digest: context.document_action_binding_digest,
366
- milestone_id: context.milestone_id,
367
- release_action_digest: context.release_action_digest,
368
- profile_digest: context.profile_digest,
369
- })}`;
370
- }
371
-
372
- function expectedBindings(context) {
373
- return {
374
- agreement_digest: context.agreement_digest,
375
- document_action_binding_digest: context.document_action_binding_digest,
376
- milestone_id: context.milestone_id,
377
- release_action_digest: context.release_action_digest,
378
- parties_digest: context.parties_digest,
379
- profile_digest: context.profile_digest,
380
- };
381
- }
382
-
383
- function normalizedInput(operation, input, extraAllowed = [], extraRequired = extraAllowed) {
384
- try {
385
- const snapshot = canonicalSnapshot(input);
386
- const allowed = new Set([...COMMON_INPUT_KEYS, ...extraAllowed]);
387
- const required = new Set([...COMMON_INPUT_KEYS, ...extraRequired]);
388
- if (!exactKeys(snapshot, allowed, required)) {
389
- return { error: 'invalid_operation_input' };
390
- }
391
- if (!validDigest(snapshot.agreement_digest)
392
- || !validDigest(snapshot.document_action_binding_digest)
393
- || !validDigest(snapshot.release_action_digest)
394
- || !validString(snapshot.milestone_id, 256)
395
- || !validString(snapshot.idempotency_key, 256)) {
396
- return { error: 'invalid_operation_binding' };
397
- }
398
- const partiesError = validateParties(snapshot.parties);
399
- if (partiesError) return { error: partiesError };
400
- const profileError = validateProfileForParties(snapshot.profile, snapshot.parties);
401
- if (profileError) return { error: profileError };
402
-
403
- const context = {
404
- agreement_digest: snapshot.agreement_digest,
405
- document_action_binding_digest: snapshot.document_action_binding_digest,
406
- milestone_id: snapshot.milestone_id,
407
- release_action_digest: snapshot.release_action_digest,
408
- parties: snapshot.parties,
409
- parties_digest: canonicalDigest(snapshot.parties),
410
- profile: snapshot.profile,
411
- profile_digest: canonicalDigest(snapshot.profile),
412
- };
413
- return {
414
- snapshot,
415
- context,
416
- escrowKey: escrowKey(context),
417
- requestDigest: canonicalDigest({ operation, input: snapshot }),
418
- };
419
- } catch {
420
- return { error: 'invalid_operation_input' };
421
- }
422
- }
423
-
424
- function outcome({
425
- ok = false,
426
- type = 'refused',
427
- code,
428
- operation = null,
429
- record = null,
430
- details = null,
431
- }) {
432
- try {
433
- return deepFreeze(canonicalSnapshot({
434
- '@version': ACTION_ESCROW_OUTCOME_VERSION,
435
- ok,
436
- outcome: type,
437
- code,
438
- operation,
439
- state: record?.state ?? null,
440
- revision: record?.revision ?? null,
441
- escrow_key: record?.escrow_key ?? null,
442
- details,
443
- record,
444
- }));
445
- } catch {
446
- return Object.freeze({
447
- '@version': ACTION_ESCROW_OUTCOME_VERSION,
448
- ok: false,
449
- outcome: 'refused',
450
- code: 'closed_outcome_encoding_failed',
451
- operation,
452
- state: null,
453
- revision: null,
454
- escrow_key: null,
455
- details: null,
456
- record: null,
457
- });
458
- }
459
- }
460
-
461
- function operationBindingMatches(record, normalized, mode = 'active') {
462
- const binding = mode === 'pending'
463
- ? record.pending_amendment
464
- : record;
465
- if (!binding) return false;
466
- return record.agreement_digest === normalized.context.agreement_digest
467
- && record.milestone_id === normalized.context.milestone_id
468
- && binding.document_action_binding_digest
469
- === normalized.context.document_action_binding_digest
470
- && binding.release_action_digest === normalized.context.release_action_digest
471
- && record.parties_digest === normalized.context.parties_digest
472
- && record.profile_digest === normalized.context.profile_digest;
473
- }
474
-
475
- function boundVerificationMatches(result, expected) {
476
- return isPlainObject(result)
477
- && result.valid === true
478
- && result.agreement_digest === expected.agreement_digest
479
- && result.document_action_binding_digest === expected.document_action_binding_digest
480
- && result.milestone_id === expected.milestone_id
481
- && result.release_action_digest === expected.release_action_digest
482
- && result.parties_digest === expected.parties_digest
483
- && result.profile_digest === expected.profile_digest;
484
- }
485
-
486
- function boundVerificationSummary(result, expected, extras = {}) {
487
- return {
488
- valid: true,
489
- ...expectedBindings(expected),
490
- ...extras,
491
- };
492
- }
493
-
494
- function bindingVerificationDetails(result, expected) {
495
- try {
496
- if (!boundVerificationMatches(result, expectedBindings(expected))
497
- || !validDigest(result.verification_digest)
498
- || !validDigest(result.document_digest)
499
- || !validString(result.agreement_id, 256)
500
- || !validString(result.binding_id, 256)
501
- || !isPlainObject(result.release_action_template)) {
502
- return null;
503
- }
504
- const releaseActionTemplate = validateActionEscrowReleaseTemplate(
505
- result.release_action_template,
506
- {
507
- profileDigest: expected.profile_digest,
508
- agreementId: result.agreement_id,
509
- agreementDigest: expected.agreement_digest,
510
- milestoneId: expected.milestone_id,
511
- documentDigest: result.document_digest,
512
- },
513
- );
514
- if (!releaseActionTemplate) return null;
515
- return {
516
- verification_digest: result.verification_digest,
517
- document_digest: result.document_digest,
518
- agreement_id: result.agreement_id,
519
- binding_id: result.binding_id,
520
- release_action_template: releaseActionTemplate,
521
- };
522
- } catch {
523
- return null;
524
- }
525
- }
526
-
527
- function storedBindingVerificationValid(container, expected) {
528
- if (!isPlainObject(container)
529
- || !isPlainObject(container.artifact)
530
- || !isPlainObject(container.verification)
531
- || container.verification.valid !== true) {
532
- return false;
533
- }
534
- return bindingVerificationDetails(container.verification, expected) !== null;
535
- }
536
-
537
- function recordShapeValid(record, revision) {
538
- if (!exactKeys(record, RECORD_KEYS)
539
- || record['@version'] !== ACTION_ESCROW_STATE_VERSION
540
- || record.revision !== revision
541
- || !Number.isSafeInteger(record.revision)
542
- || record.revision < 0
543
- || !ACTION_ESCROW_STATES.includes(record.state)
544
- || !validString(record.escrow_key, 256)
545
- || !validDigest(record.agreement_digest)
546
- || !validDigest(record.document_action_binding_digest)
547
- || !validDigest(record.release_action_digest)
548
- || !validString(record.milestone_id, 256)
549
- || !validDigest(record.parties_digest)
550
- || !validDigest(record.profile_digest)
551
- || validateParties(record.parties)
552
- || validateProfileForParties(record.profile, record.parties)
553
- || record.parties_digest !== canonicalDigest(record.parties)
554
- || record.profile_digest !== canonicalDigest(record.profile)
555
- || record.escrow_key !== escrowKey(record)
556
- || !storedBindingVerificationValid(record.document_action_binding, record)
557
- || !Array.isArray(record.agreement_acceptances)
558
- || !Array.isArray(record.release_approvals)
559
- || !Array.isArray(record.superseded_bindings)
560
- || !Array.isArray(record.operations)
561
- || !Array.isArray(record.history)
562
- || record.operations.length > MAX_OPERATIONS
563
- || record.history.length > MAX_HISTORY
564
- || record.superseded_bindings.length > MAX_SUPERSEDED_BINDINGS
565
- || !validInstant(record.created_at)
566
- || !validInstant(record.updated_at)) {
567
- return false;
568
- }
569
- if (['release_reserved', 'released', 'release_indeterminate'].includes(record.state)
570
- && !isPlainObject(record.release)) {
571
- return false;
572
- }
573
- return true;
574
- }
575
-
576
- function existingOperation(record, normalized, operation) {
577
- const found = record.operations.find(
578
- (entry) => entry.idempotency_key === normalized.snapshot.idempotency_key,
579
- );
580
- if (!found) return null;
581
- if (found.operation !== operation || found.request_digest !== normalized.requestDigest) {
582
- return { conflict: true };
583
- }
584
- return { conflict: false, entry: found };
585
- }
586
-
587
- function idempotentResult(record, normalized, operation) {
588
- const existing = existingOperation(record, normalized, operation);
589
- if (!existing) return null;
590
- if (existing.conflict) {
591
- return outcome({
592
- code: 'idempotency_key_conflict',
593
- operation,
594
- record,
595
- });
596
- }
597
- if (operation === 'release'
598
- && ['release_reserved', 'release_indeterminate'].includes(record.state)) {
599
- return outcome({
600
- code: 'release_reconciliation_required',
601
- operation,
602
- record,
603
- type: 'indeterminate',
604
- });
605
- }
606
- return outcome({
607
- ok: foundOperationOk(existing.entry),
608
- type: foundOperationOk(existing.entry)
609
- ? 'idempotent'
610
- : existing.entry.outcome ?? 'refused',
611
- code: existing.entry.code,
612
- operation,
613
- record,
614
- });
615
- }
616
-
617
- function foundOperationOk(entry) {
618
- return entry.ok === undefined ? true : entry.ok === true;
619
- }
620
-
621
- function appendOperation(next, record, normalized, operation, code, at, result = {}) {
622
- if (record.operations.length >= MAX_OPERATIONS) {
623
- return 'operation_history_limit_reached';
624
- }
625
- next.operations.push({
626
- idempotency_key: normalized.snapshot.idempotency_key,
627
- operation,
628
- request_digest: normalized.requestDigest,
629
- code,
630
- ok: result.ok ?? true,
631
- outcome: result.type ?? 'applied',
632
- state: next.state,
633
- at,
634
- });
635
- return null;
636
- }
637
-
638
- function finalizeMutation(record, normalized, operation, code, at, mutate, result = {}) {
639
- const next = canonicalSnapshot(record);
640
- const fromState = next.state;
641
- mutate(next);
642
- next.revision = record.revision + 1;
643
- next.updated_at = at;
644
- const operationError = appendOperation(
645
- next,
646
- record,
647
- normalized,
648
- operation,
649
- code,
650
- at,
651
- result,
652
- );
653
- if (operationError) return { error: operationError };
654
- if (fromState !== next.state) {
655
- if (next.history.length >= MAX_HISTORY) return { error: 'state_history_limit_reached' };
656
- next.history.push({
657
- from: fromState,
658
- to: next.state,
659
- operation,
660
- idempotency_key: normalized.snapshot.idempotency_key,
661
- at,
662
- });
663
- }
664
- return { next };
665
- }
666
-
667
- function providerExpected(record) {
668
- const template = record.document_action_binding.verification.release_action_template;
669
- return {
670
- ...expectedBindings(record),
671
- parties: record.parties,
672
- profile: record.profile,
673
- provider_id: record.profile.provider_id,
674
- provider_idempotency_key: record.release?.provider_idempotency_key ?? null,
675
- provider_request_digest: record.release?.provider_request?.request_digest ?? null,
676
- provider_transaction_id: template.custodian_transaction_id,
677
- provider_milestone_id: template.custodian_milestone_id,
678
- amount: template.amount,
679
- currency: template.currency,
680
- destination_id: template.destination_id,
681
- };
682
- }
683
-
684
- /**
685
- * Create a fail-closed Action Escrow kernel.
686
- *
687
- * Store contract:
688
- * durable === true
689
- * atomicExpectedRevisionCas === true
690
- * linearizableReads === true
691
- * monotonicRevisions === true
692
- * nonExpiring === true
693
- * read(key) -> null | { revision, value: canonicalJsonText }
694
- * compareAndSwap(key, expectedRevision|null, nextValue)
695
- * -> { applied, revision }
696
- */
697
- export function createActionEscrowKernel(options = {}) {
698
- let configurationError = null;
699
- let readStore = null;
700
- let compareAndSwap = null;
701
- let releaseProvider = null;
702
- let getProviderRelease = null;
703
- let verifyDocumentActionBinding = null;
704
- let verifyAgreementAcceptance = null;
705
- let verifyMilestoneEvidence = null;
706
- let verifyResolutionReceipt = null;
707
- let verifyProviderStatement = null;
708
- let verifyStateCommand = null;
709
- let resolveProfile = null;
710
- let pinnedProfiles = null;
711
- let now = null;
712
- let providerTimeoutMs = DEFAULT_PROVIDER_TIMEOUT_MS;
713
-
714
- try {
715
- if (!isPlainObject(options)) throw new Error('invalid options');
716
- const store = options.store;
717
- if (!STORE_CAPABILITIES.every((capability) => store?.[capability] === true)
718
- || typeof store?.read !== 'function'
719
- || typeof store?.compareAndSwap !== 'function') {
720
- configurationError = 'durable_cas_store_required';
721
- } else {
722
- readStore = store.read.bind(store);
723
- compareAndSwap = store.compareAndSwap.bind(store);
724
- }
725
-
726
- const provider = options.provider;
727
- if (!configurationError
728
- && (typeof provider?.release !== 'function'
729
- || typeof provider?.getRelease !== 'function')) {
730
- configurationError = 'release_provider_required';
731
- } else if (!configurationError) {
732
- releaseProvider = provider.release.bind(provider);
733
- getProviderRelease = provider.getRelease.bind(provider);
734
- }
735
-
736
- const verifierEntries = [
737
- ['verifyDocumentActionBinding', options.verifyDocumentActionBinding],
738
- ['verifyAgreementAcceptance', options.verifyAgreementAcceptance],
739
- ['verifyMilestoneEvidence', options.verifyMilestoneEvidence],
740
- ['verifyResolutionReceipt', options.verifyResolutionReceipt],
741
- ['verifyProviderStatement', options.verifyProviderStatement],
742
- ['verifyStateCommand', options.verifyStateCommand],
743
- ];
744
- if (!configurationError && verifierEntries.some(([, verifier]) => typeof verifier !== 'function')) {
745
- configurationError = 'pinned_verifiers_required';
746
- } else if (!configurationError) {
747
- verifyDocumentActionBinding = options.verifyDocumentActionBinding;
748
- verifyAgreementAcceptance = options.verifyAgreementAcceptance;
749
- verifyMilestoneEvidence = options.verifyMilestoneEvidence;
750
- verifyResolutionReceipt = options.verifyResolutionReceipt;
751
- verifyProviderStatement = options.verifyProviderStatement;
752
- verifyStateCommand = options.verifyStateCommand;
753
- }
754
-
755
- if (!configurationError && options.profilesById !== undefined) {
756
- const snapshot = canonicalSnapshot(options.profilesById);
757
- if (!isPlainObject(snapshot) || Object.keys(snapshot).length === 0) {
758
- configurationError = 'pinned_profile_configuration_invalid';
759
- } else {
760
- for (const [profileId, profile] of Object.entries(snapshot)) {
761
- if (profileId !== profile.profile_id || validateProfileShape(profile)) {
762
- configurationError = 'pinned_profile_configuration_invalid';
763
- break;
764
- }
765
- }
766
- if (!configurationError) pinnedProfiles = deepFreeze(snapshot);
767
- }
768
- } else if (!configurationError && typeof options.resolveProfile === 'function') {
769
- resolveProfile = options.resolveProfile;
770
- } else if (!configurationError) {
771
- configurationError = 'pinned_profile_resolver_required';
772
- }
773
-
774
- if (!configurationError && options.now !== undefined && typeof options.now !== 'function') {
775
- configurationError = 'clock_required';
776
- }
777
- now = typeof options.now === 'function' ? options.now : Date.now;
778
-
779
- if (options.providerTimeoutMs !== undefined) {
780
- if (!Number.isSafeInteger(options.providerTimeoutMs)
781
- || options.providerTimeoutMs <= 0
782
- || options.providerTimeoutMs > 300_000) {
783
- configurationError ??= 'provider_timeout_invalid';
784
- } else {
785
- providerTimeoutMs = options.providerTimeoutMs;
786
- }
787
- }
788
- } catch {
789
- configurationError ??= 'invalid_kernel_configuration';
790
- }
791
-
792
- async function safe(operation, task) {
793
- try {
794
- return await task();
795
- } catch {
796
- return outcome({
797
- code: 'kernel_internal_refusal',
798
- operation,
799
- });
800
- }
801
- }
802
-
803
- function configurationRefusal(operation) {
804
- return configurationError
805
- ? outcome({ code: configurationError, operation })
806
- : null;
807
- }
808
-
809
- function instant() {
810
- try {
811
- const candidate = now();
812
- const date = candidate instanceof Date ? candidate : new Date(candidate);
813
- return Number.isFinite(date.getTime()) ? date.toISOString() : null;
814
- } catch {
815
- return null;
816
- }
817
- }
818
-
819
- function operationInstant(record = null) {
820
- const at = instant();
821
- if (!at) return { error: 'invalid_clock' };
822
- if (record !== null && Date.parse(at) < Date.parse(record.updated_at)) {
823
- return { error: 'clock_regression' };
824
- }
825
- return { at };
826
- }
827
-
828
- async function withProviderTimeout(task) {
829
- let timer;
830
- try {
831
- return await Promise.race([
832
- Promise.resolve().then(task),
833
- new Promise((_, reject) => {
834
- timer = setTimeout(
835
- () => reject(new Error('provider_timeout')),
836
- providerTimeoutMs,
837
- );
838
- }),
839
- ]);
840
- } finally {
841
- clearTimeout(timer);
842
- }
843
- }
844
-
845
- async function readRecord(key) {
846
- let envelope;
847
- try {
848
- envelope = await readStore(key);
849
- } catch {
850
- return { error: 'store_unavailable' };
851
- }
852
- if (envelope === null) return { record: null };
853
- try {
854
- if (!exactKeys(envelope, new Set(['revision', 'value']))
855
- || !Number.isSafeInteger(envelope.revision)
856
- || envelope.revision < 0
857
- || typeof envelope.value !== 'string'
858
- || Buffer.byteLength(envelope.value, 'utf8') > MAX_STATE_BYTES) {
859
- return { error: 'store_read_invalid' };
860
- }
861
- const record = JSON.parse(envelope.value);
862
- if (canonicalize(record) !== envelope.value
863
- || !recordShapeValid(record, envelope.revision)) {
864
- return { error: 'store_record_invalid' };
865
- }
866
- return { record: deepFreeze(record) };
867
- } catch {
868
- return { error: 'store_record_invalid' };
869
- }
870
- }
871
-
872
- async function writeRecord(key, expectedRevision, next) {
873
- let value;
874
- try {
875
- value = canonicalize(next);
876
- } catch {
877
- return { error: 'state_encoding_failed' };
878
- }
879
- let acknowledgement;
880
- try {
881
- acknowledgement = await compareAndSwap(key, expectedRevision, value);
882
- } catch {
883
- return { error: 'store_unavailable' };
884
- }
885
- try {
886
- const expectedNext = expectedRevision === null ? 0 : expectedRevision + 1;
887
- if (!exactKeys(acknowledgement, new Set(['applied', 'revision']))
888
- || (acknowledgement.applied !== true && acknowledgement.applied !== false)
889
- || (acknowledgement.revision !== null
890
- && (!Number.isSafeInteger(acknowledgement.revision)
891
- || acknowledgement.revision < 0))) {
892
- return { error: 'store_acknowledgement_invalid' };
893
- }
894
- if (acknowledgement.applied === true) {
895
- if (acknowledgement.revision !== expectedNext || next.revision !== expectedNext) {
896
- return { error: 'store_revision_invalid' };
897
- }
898
- return { applied: true };
899
- }
900
- return { applied: false };
901
- } catch {
902
- return { error: 'store_acknowledgement_invalid' };
903
- }
904
- }
905
-
906
- async function invokeVerifier(verifier, artifact, expected) {
907
- try {
908
- const artifactCopy = deepFreeze(canonicalSnapshot(artifact));
909
- const expectedCopy = deepFreeze(canonicalSnapshot(expected));
910
- const result = canonicalSnapshot(await verifier(artifactCopy, expectedCopy));
911
- if (!isPlainObject(result)) return { error: 'verifier_result_invalid' };
912
- return { artifact: artifactCopy, result };
913
- } catch {
914
- return { error: 'verifier_failed' };
915
- }
916
- }
917
-
918
- async function verifyCommandAuthorization(
919
- artifact,
920
- record,
921
- command,
922
- partyId,
923
- details,
924
- ) {
925
- if (!record.parties.some((party) => party.party_id === partyId)) {
926
- return { error: 'command_party_invalid' };
927
- }
928
- const detailsDigest = canonicalDigest(details);
929
- const expected = {
930
- ...expectedBindings(record),
931
- profile: record.profile,
932
- parties: record.parties,
933
- command,
934
- party_id: partyId,
935
- details_digest: detailsDigest,
936
- };
937
- expected.command_digest = canonicalDigest({
938
- '@version': 'EP-ACTION-ESCROW-COMMAND-v1',
939
- ...expectedBindings(record),
940
- command,
941
- party_id: partyId,
942
- details_digest: detailsDigest,
943
- });
944
- const verified = await invokeVerifier(verifyStateCommand, artifact, expected);
945
- if (verified.error
946
- || verified.result.valid !== true
947
- || verified.result.authorizes_command !== true
948
- || verified.result.command !== command
949
- || verified.result.party_id !== partyId
950
- || verified.result.details_digest !== detailsDigest
951
- || verified.result.command_digest !== expected.command_digest
952
- || !boundVerificationMatches(verified.result, expectedBindings(record))) {
953
- return { error: verified.error ?? 'command_authorization_refused' };
954
- }
955
- return {
956
- artifact: verified.artifact,
957
- verification: {
958
- valid: true,
959
- authorizes_command: true,
960
- ...expectedBindings(record),
961
- command,
962
- party_id: partyId,
963
- details_digest: detailsDigest,
964
- command_digest: expected.command_digest,
965
- },
966
- };
967
- }
968
-
969
- async function selectedProfile(normalized) {
970
- try {
971
- let selected;
972
- if (pinnedProfiles) {
973
- if (!Object.hasOwn(pinnedProfiles, normalized.snapshot.profile.profile_id)) {
974
- return { error: 'profile_not_pinned' };
975
- }
976
- selected = pinnedProfiles[normalized.snapshot.profile.profile_id];
977
- } else {
978
- selected = canonicalSnapshot(await resolveProfile(
979
- normalized.snapshot.profile.profile_id,
980
- deepFreeze(canonicalSnapshot({
981
- agreement_digest: normalized.context.agreement_digest,
982
- milestone_id: normalized.context.milestone_id,
983
- parties: normalized.context.parties,
984
- })),
985
- ));
986
- }
987
- if (validateProfileForParties(selected, normalized.context.parties)
988
- || canonicalDigest(selected) !== normalized.context.profile_digest) {
989
- return { error: 'profile_not_pinned' };
990
- }
991
- return { profile: deepFreeze(canonicalSnapshot(selected)) };
992
- } catch {
993
- return { error: 'profile_resolution_failed' };
994
- }
995
- }
996
-
997
- async function create(input = {}) {
998
- const operation = 'create';
999
- return safe(operation, async () => {
1000
- const normalized = normalizedInput(
1001
- operation,
1002
- input,
1003
- ['document_action_binding'],
1004
- );
1005
- if (normalized.error) return outcome({ code: normalized.error, operation });
1006
- const config = configurationRefusal(operation);
1007
- if (config) return config;
1008
-
1009
- const pinned = await selectedProfile(normalized);
1010
- if (pinned.error) return outcome({ code: pinned.error, operation });
1011
-
1012
- for (let attempt = 0; attempt < MAX_CAS_ATTEMPTS; attempt++) {
1013
- const loaded = await readRecord(normalized.escrowKey);
1014
- if (loaded.error) return outcome({ code: loaded.error, operation });
1015
- if (loaded.record) {
1016
- if (!operationBindingMatches(loaded.record, normalized)) {
1017
- return outcome({
1018
- code: 'operation_binding_mismatch',
1019
- operation,
1020
- record: loaded.record,
1021
- });
1022
- }
1023
- return idempotentResult(loaded.record, normalized, operation)
1024
- ?? outcome({ code: 'escrow_already_exists', operation, record: loaded.record });
1025
- }
1026
-
1027
- const at = instant();
1028
- if (!at) return outcome({ code: 'invalid_clock', operation });
1029
- const expected = {
1030
- ...normalized.context,
1031
- profile: pinned.profile,
1032
- };
1033
- const verification = await invokeVerifier(
1034
- verifyDocumentActionBinding,
1035
- normalized.snapshot.document_action_binding,
1036
- expected,
1037
- );
1038
- const bindingDetails = verification.error
1039
- ? null
1040
- : bindingVerificationDetails(verification.result, expected);
1041
- if (!bindingDetails) {
1042
- return outcome({
1043
- code: verification.error ?? 'document_action_binding_invalid',
1044
- operation,
1045
- });
1046
- }
1047
-
1048
- const record = {
1049
- '@version': ACTION_ESCROW_STATE_VERSION,
1050
- escrow_key: normalized.escrowKey,
1051
- revision: 0,
1052
- state: 'draft',
1053
- agreement_digest: normalized.context.agreement_digest,
1054
- document_action_binding_digest:
1055
- normalized.context.document_action_binding_digest,
1056
- milestone_id: normalized.context.milestone_id,
1057
- release_action_digest: normalized.context.release_action_digest,
1058
- parties: normalized.context.parties,
1059
- parties_digest: normalized.context.parties_digest,
1060
- profile: pinned.profile,
1061
- profile_digest: normalized.context.profile_digest,
1062
- document_action_binding: {
1063
- artifact: verification.artifact,
1064
- verification: boundVerificationSummary(
1065
- verification.result,
1066
- expected,
1067
- bindingDetails,
1068
- ),
1069
- },
1070
- agreement_acceptances: [],
1071
- funding: null,
1072
- milestone_evidence: null,
1073
- release_approvals: [],
1074
- release: null,
1075
- dispute: null,
1076
- cancellation: null,
1077
- completion: null,
1078
- pending_amendment: null,
1079
- superseded_bindings: [],
1080
- operations: [{
1081
- idempotency_key: normalized.snapshot.idempotency_key,
1082
- operation,
1083
- request_digest: normalized.requestDigest,
1084
- code: 'escrow_created',
1085
- ok: true,
1086
- outcome: 'applied',
1087
- state: 'draft',
1088
- at,
1089
- }],
1090
- history: [{
1091
- from: null,
1092
- to: 'draft',
1093
- operation,
1094
- idempotency_key: normalized.snapshot.idempotency_key,
1095
- at,
1096
- }],
1097
- created_at: at,
1098
- updated_at: at,
1099
- };
1100
- const written = await writeRecord(normalized.escrowKey, null, record);
1101
- if (written.error) return outcome({ code: written.error, operation });
1102
- if (written.applied) {
1103
- return outcome({
1104
- ok: true,
1105
- type: 'applied',
1106
- code: 'escrow_created',
1107
- operation,
1108
- record,
1109
- });
1110
- }
1111
- }
1112
- return outcome({ code: 'store_conflict', operation });
1113
- });
1114
- }
1115
-
1116
- async function mutate(
1117
- operation,
1118
- input,
1119
- {
1120
- extraAllowed = [],
1121
- extraRequired = extraAllowed,
1122
- bindingMode = 'active',
1123
- transform,
1124
- },
1125
- ) {
1126
- return safe(operation, async () => {
1127
- const normalized = normalizedInput(
1128
- operation,
1129
- input,
1130
- extraAllowed,
1131
- extraRequired,
1132
- );
1133
- if (normalized.error) return outcome({ code: normalized.error, operation });
1134
- const config = configurationRefusal(operation);
1135
- if (config) return config;
1136
-
1137
- for (let attempt = 0; attempt < MAX_CAS_ATTEMPTS; attempt++) {
1138
- const loaded = await readRecord(normalized.escrowKey);
1139
- if (loaded.error) return outcome({ code: loaded.error, operation });
1140
- if (!loaded.record) return outcome({ code: 'escrow_not_found', operation });
1141
- const record = loaded.record;
1142
- if (!operationBindingMatches(record, normalized, bindingMode)) {
1143
- return outcome({
1144
- code: 'operation_binding_mismatch',
1145
- operation,
1146
- record,
1147
- });
1148
- }
1149
- const repeated = idempotentResult(record, normalized, operation);
1150
- if (repeated) return repeated;
1151
- const operationTime = operationInstant(record);
1152
- if (operationTime.error) {
1153
- return outcome({ code: operationTime.error, operation, record });
1154
- }
1155
- const { at } = operationTime;
1156
-
1157
- const draft = canonicalSnapshot(record);
1158
- const decision = await transform(draft, normalized, at);
1159
- if (decision?.refusal) {
1160
- return outcome({
1161
- code: decision.refusal,
1162
- operation,
1163
- record,
1164
- details: decision.details ?? null,
1165
- type: decision.type ?? 'refused',
1166
- });
1167
- }
1168
- const code = decision?.code ?? `${operation}_applied`;
1169
- const finalized = finalizeMutation(
1170
- record,
1171
- normalized,
1172
- operation,
1173
- code,
1174
- at,
1175
- (next) => {
1176
- Object.assign(next, draft);
1177
- },
1178
- {
1179
- ok: decision?.ok ?? true,
1180
- type: decision?.type ?? 'applied',
1181
- },
1182
- );
1183
- if (finalized.error) {
1184
- return outcome({ code: finalized.error, operation, record });
1185
- }
1186
- const written = await writeRecord(
1187
- normalized.escrowKey,
1188
- record.revision,
1189
- finalized.next,
1190
- );
1191
- if (written.error) return outcome({ code: written.error, operation, record });
1192
- if (written.applied) {
1193
- return outcome({
1194
- ok: decision?.ok ?? true,
1195
- type: decision?.type ?? 'applied',
1196
- code,
1197
- operation,
1198
- record: finalized.next,
1199
- details: decision?.details ?? null,
1200
- });
1201
- }
1202
- }
1203
- return outcome({ code: 'store_conflict', operation });
1204
- });
1205
- }
1206
-
1207
- async function beginAcceptance(input = {}) {
1208
- return mutate('begin_acceptance', input, {
1209
- transform(draft) {
1210
- if (draft.state !== 'draft') {
1211
- return { refusal: 'invalid_state_transition' };
1212
- }
1213
- draft.state = 'awaiting_acceptance';
1214
- return { code: 'acceptance_requested' };
1215
- },
1216
- });
1217
- }
1218
-
1219
- async function acceptAgreement(input = {}) {
1220
- return mutate('accept_agreement', input, {
1221
- extraAllowed: ['party_id', 'agreement_acceptance'],
1222
- async transform(draft, normalized, at) {
1223
- if (draft.state !== 'awaiting_acceptance') {
1224
- return { refusal: 'invalid_state_transition' };
1225
- }
1226
- const partyId = normalized.snapshot.party_id;
1227
- if (!draft.profile.required_acceptance_party_ids.includes(partyId)) {
1228
- return { refusal: 'acceptance_party_not_required' };
1229
- }
1230
- if (draft.agreement_acceptances.some((entry) => entry.party_id === partyId)) {
1231
- return { refusal: 'agreement_already_accepted' };
1232
- }
1233
- const expected = {
1234
- ...normalized.context,
1235
- party_id: partyId,
1236
- };
1237
- const verified = await invokeVerifier(
1238
- verifyAgreementAcceptance,
1239
- normalized.snapshot.agreement_acceptance,
1240
- expected,
1241
- );
1242
- if (verified.error
1243
- || !boundVerificationMatches(verified.result, expectedBindings(expected))
1244
- || verified.result.party_id !== partyId
1245
- || !validString(verified.result.principal_key_id, 512)
1246
- || draft.agreement_acceptances.some(
1247
- (entry) => entry.verification?.principal_key_id
1248
- === verified.result.principal_key_id,
1249
- )
1250
- || !validDigest(verified.result.acceptance_digest)) {
1251
- return { refusal: verified.error ?? 'agreement_acceptance_invalid' };
1252
- }
1253
- draft.agreement_acceptances.push({
1254
- party_id: partyId,
1255
- artifact: verified.artifact,
1256
- verification: boundVerificationSummary(
1257
- verified.result,
1258
- expected,
1259
- {
1260
- party_id: partyId,
1261
- principal_key_id: verified.result.principal_key_id,
1262
- acceptance_digest: verified.result.acceptance_digest,
1263
- },
1264
- ),
1265
- });
1266
- const accepted = new Set(draft.agreement_acceptances.map((entry) => entry.party_id));
1267
- if (draft.profile.required_acceptance_party_ids.every((id) => accepted.has(id))) {
1268
- draft.state = 'effective';
1269
- return { code: 'agreement_effective' };
1270
- }
1271
- return { code: 'agreement_acceptance_recorded' };
1272
- },
1273
- });
1274
- }
1275
-
1276
- async function requestFunding(input = {}) {
1277
- return mutate('request_funding', input, {
1278
- transform(draft) {
1279
- if (draft.state !== 'effective') {
1280
- return { refusal: 'invalid_state_transition' };
1281
- }
1282
- draft.state = 'awaiting_funding';
1283
- return { code: 'funding_requested' };
1284
- },
1285
- });
1286
- }
1287
-
1288
- async function recordFunding(input = {}) {
1289
- return mutate('record_funding', input, {
1290
- extraAllowed: ['provider_statement'],
1291
- async transform(draft, normalized, at) {
1292
- if (draft.state !== 'awaiting_funding') {
1293
- return { refusal: 'invalid_state_transition' };
1294
- }
1295
- const template = draft.document_action_binding.verification.release_action_template;
1296
- const expected = {
1297
- ...normalized.context,
1298
- provider_id: draft.profile.provider_id,
1299
- statement_type: 'funding',
1300
- expected_status: 'funded',
1301
- provider_transaction_id: template.custodian_transaction_id,
1302
- provider_milestone_id: template.custodian_milestone_id,
1303
- amount: template.amount,
1304
- currency: template.currency,
1305
- destination_id: template.destination_id,
1306
- };
1307
- const verified = await invokeVerifier(
1308
- verifyProviderStatement,
1309
- normalized.snapshot.provider_statement,
1310
- expected,
1311
- );
1312
- if (verified.error
1313
- || !boundVerificationMatches(verified.result, expectedBindings(expected))
1314
- || verified.result.authenticated !== true
1315
- || verified.result.provider_id !== draft.profile.provider_id
1316
- || verified.result.statement_type !== 'funding'
1317
- || verified.result.status !== 'funded'
1318
- || verified.result.provider_transaction_id !== expected.provider_transaction_id
1319
- || verified.result.provider_milestone_id !== expected.provider_milestone_id
1320
- || verified.result.amount !== expected.amount
1321
- || verified.result.currency !== expected.currency
1322
- || verified.result.destination_id !== expected.destination_id
1323
- || !validDigest(verified.result.statement_digest)) {
1324
- return { refusal: verified.error ?? 'funding_statement_invalid' };
1325
- }
1326
- draft.funding = {
1327
- statement: verified.artifact,
1328
- verification: boundVerificationSummary(
1329
- verified.result,
1330
- expected,
1331
- {
1332
- authenticated: true,
1333
- provider_id: draft.profile.provider_id,
1334
- statement_type: 'funding',
1335
- status: 'funded',
1336
- provider_transaction_id: expected.provider_transaction_id,
1337
- provider_milestone_id: expected.provider_milestone_id,
1338
- amount: expected.amount,
1339
- currency: expected.currency,
1340
- destination_id: expected.destination_id,
1341
- statement_digest: verified.result.statement_digest,
1342
- },
1343
- ),
1344
- };
1345
- draft.state = 'funded';
1346
- return { code: 'funding_confirmed' };
1347
- },
1348
- });
1349
- }
1350
-
1351
- async function submitMilestone(input = {}) {
1352
- return mutate('submit_milestone', input, {
1353
- extraAllowed: ['milestone_evidence'],
1354
- async transform(draft, normalized, at) {
1355
- if (draft.state !== 'funded') {
1356
- return { refusal: 'invalid_state_transition' };
1357
- }
1358
- const expected = normalized.context;
1359
- const verified = await invokeVerifier(
1360
- verifyMilestoneEvidence,
1361
- normalized.snapshot.milestone_evidence,
1362
- expected,
1363
- );
1364
- const partyIds = draft.parties.map((party) => party.party_id);
1365
- const expectedEvidenceDigest = draft.document_action_binding
1366
- .verification.release_action_template.completion_evidence_sha256;
1367
- if (verified.error
1368
- || !boundVerificationMatches(verified.result, expectedBindings(expected))
1369
- || !validDigest(verified.result.evidence_digest)
1370
- || verified.result.evidence_digest !== expectedEvidenceDigest
1371
- || !partyIds.includes(verified.result.submitter_party_id)
1372
- || !validInstant(verified.result.observed_at)
1373
- || Date.parse(verified.result.observed_at) > Date.parse(at)) {
1374
- return { refusal: verified.error ?? 'milestone_evidence_invalid' };
1375
- }
1376
- draft.milestone_evidence = {
1377
- artifact: verified.artifact,
1378
- verification: boundVerificationSummary(
1379
- verified.result,
1380
- expected,
1381
- {
1382
- evidence_digest: verified.result.evidence_digest,
1383
- submitter_party_id: verified.result.submitter_party_id,
1384
- observed_at: verified.result.observed_at,
1385
- },
1386
- ),
1387
- };
1388
- draft.release_approvals = [];
1389
- draft.state = 'milestone_submitted';
1390
- return { code: 'milestone_evidence_recorded' };
1391
- },
1392
- });
1393
- }
1394
-
1395
- async function approveRelease(input = {}) {
1396
- return mutate('approve_release', input, {
1397
- extraAllowed: ['party_id', 'resolution'],
1398
- async transform(draft, normalized, at) {
1399
- if (draft.state !== 'milestone_submitted') {
1400
- return { refusal: 'invalid_state_transition' };
1401
- }
1402
- const partyId = normalized.snapshot.party_id;
1403
- if (!draft.profile.required_release_approver_party_ids.includes(partyId)) {
1404
- return { refusal: 'release_approval_party_not_required' };
1405
- }
1406
- const party = draft.parties.find((entry) => entry.party_id === partyId);
1407
- if (draft.profile.prohibit_self_approval
1408
- && partyId === draft.milestone_evidence?.verification?.submitter_party_id) {
1409
- return { refusal: 'self_approval_refused' };
1410
- }
1411
- if (draft.release_approvals.some((entry) => entry.party_id === partyId)) {
1412
- return { refusal: 'release_approval_already_recorded' };
1413
- }
1414
-
1415
- const artifact = normalized.snapshot.resolution;
1416
- const bindingInput = resolutionBindingInput(draft);
1417
- const bindingMoment = createActionEscrowReleaseBindingMoment(bindingInput);
1418
- const bindingMomentDigest = bindingMoment === null
1419
- ? null
1420
- : canonicalDigest(bindingMoment);
1421
- const expectedNonce = computeActionEscrowResolutionNonce(bindingInput, partyId);
1422
- const expectedInitiator = draft.milestone_evidence?.verification?.submitter_party_id;
1423
- if (!isPlainObject(artifact)
1424
- || artifact.profile !== RESOLUTION_VERSION
1425
- || !isPlainObject(artifact.signoff)
1426
- || !isPlainObject(artifact.signoff.context)
1427
- || bindingMoment === null
1428
- || bindingMomentDigest === null
1429
- || expectedNonce === null
1430
- || !validString(expectedInitiator, 256)) {
1431
- return { refusal: 'resolution_profile_invalid' };
1432
- }
1433
- const resolutionContext = artifact.signoff.context;
1434
- if (resolutionContext.principal !== partyId) {
1435
- return { refusal: 'resolution_party_mismatch' };
1436
- }
1437
- if (resolutionContext.envelope_hash !== bindingMomentDigest) {
1438
- return { refusal: 'resolution_binding_mismatch' };
1439
- }
1440
- if (resolutionContext.action_hash !== draft.release_action_digest) {
1441
- return { refusal: 'resolution_action_mismatch' };
1442
- }
1443
- if (resolutionContext.initiator !== expectedInitiator) {
1444
- return { refusal: 'resolution_initiator_mismatch' };
1445
- }
1446
- if (resolutionContext.nonce !== expectedNonce) {
1447
- return { refusal: 'resolution_nonce_mismatch' };
1448
- }
1449
- if (resolutionContext.resolution?.outcome !== 'approved') {
1450
- return { refusal: 'resolution_not_approved' };
1451
- }
1452
- if (!validString(resolutionContext.principal_key_id, 512)
1453
- || !validString(resolutionContext.nonce, 512)
1454
- || !validInstant(resolutionContext.issued_at)
1455
- || !validInstant(resolutionContext.expires_at)
1456
- || Date.parse(resolutionContext.issued_at)
1457
- < Date.parse(draft.milestone_evidence.verification.observed_at)
1458
- || Date.parse(resolutionContext.issued_at) > Date.parse(at)
1459
- || Date.parse(resolutionContext.expires_at) <= Date.parse(at)
1460
- || Date.parse(resolutionContext.expires_at)
1461
- <= Date.parse(resolutionContext.issued_at)) {
1462
- return { refusal: 'resolution_freshness_invalid' };
1463
- }
1464
- if (draft.release_approvals.some(
1465
- (entry) => entry.verification?.principal_key_id
1466
- === resolutionContext.principal_key_id,
1467
- )) {
1468
- return { refusal: 'resolution_key_already_counted' };
1469
- }
1470
- const expected = {
1471
- ...normalized.context,
1472
- party_id: partyId,
1473
- evidence_digest: draft.milestone_evidence.verification.evidence_digest,
1474
- binding_moment: bindingMoment,
1475
- binding_moment_digest: bindingMomentDigest,
1476
- expected_selected_option: 0,
1477
- expected_initiator: expectedInitiator,
1478
- expected_nonce: expectedNonce,
1479
- evaluation_time: at,
1480
- };
1481
- const verified = await invokeVerifier(
1482
- verifyResolutionReceipt,
1483
- artifact,
1484
- expected,
1485
- );
1486
- if (verified.error
1487
- || verified.result.valid !== true
1488
- || verified.result.authorizes_action !== true
1489
- || verified.result.outcome !== 'approved'
1490
- || !boundVerificationMatches(verified.result, expectedBindings(expected))
1491
- || verified.result.party_id !== partyId
1492
- || verified.result.party_role !== party.role
1493
- || verified.result.principal_key_id !== resolutionContext.principal_key_id
1494
- || verified.result.nonce !== resolutionContext.nonce
1495
- || verified.result.issued_at !== resolutionContext.issued_at
1496
- || verified.result.expires_at !== resolutionContext.expires_at
1497
- || verified.result.evidence_digest !== expected.evidence_digest) {
1498
- return { refusal: verified.error ?? 'resolution_verification_refused' };
1499
- }
1500
- draft.release_approvals.push({
1501
- party_id: partyId,
1502
- resolution: verified.artifact,
1503
- verification: {
1504
- valid: true,
1505
- authorizes_action: true,
1506
- outcome: 'approved',
1507
- party_role: party.role,
1508
- principal_key_id: resolutionContext.principal_key_id,
1509
- nonce: resolutionContext.nonce,
1510
- issued_at: resolutionContext.issued_at,
1511
- expires_at: resolutionContext.expires_at,
1512
- resolution_digest: canonicalDigest(verified.artifact),
1513
- agreement_digest: draft.agreement_digest,
1514
- document_action_binding_digest: draft.document_action_binding_digest,
1515
- milestone_id: draft.milestone_id,
1516
- release_action_digest: draft.release_action_digest,
1517
- parties_digest: draft.parties_digest,
1518
- profile_digest: draft.profile_digest,
1519
- evidence_digest: draft.milestone_evidence.verification.evidence_digest,
1520
- },
1521
- });
1522
- return { code: 'release_approval_recorded' };
1523
- },
1524
- });
1525
- }
1526
-
1527
- function releasePreconditions(record, at) {
1528
- if (record.funding?.verification?.status !== 'funded') {
1529
- return { code: 'funding_not_verified' };
1530
- }
1531
- if (!validDigest(record.milestone_evidence?.verification?.evidence_digest)) {
1532
- return { code: 'milestone_evidence_not_verified' };
1533
- }
1534
- const approved = new Set(record.release_approvals
1535
- .filter((entry) => entry.verification?.authorizes_action === true
1536
- && entry.verification?.document_action_binding_digest
1537
- === record.document_action_binding_digest
1538
- && entry.verification?.release_action_digest === record.release_action_digest
1539
- && entry.verification?.evidence_digest
1540
- === record.milestone_evidence.verification.evidence_digest)
1541
- .map((entry) => entry.party_id));
1542
- const missing = record.profile.required_release_approver_party_ids
1543
- .filter((partyId) => !approved.has(partyId));
1544
- if (missing.length > 0) {
1545
- return { code: 'release_approval_missing', details: { missing_party_ids: missing } };
1546
- }
1547
- const evaluationTime = Date.parse(at);
1548
- const stale = record.release_approvals
1549
- .filter((entry) => (
1550
- !validInstant(entry.verification?.issued_at)
1551
- || !validInstant(entry.verification?.expires_at)
1552
- || Date.parse(entry.verification.issued_at) > evaluationTime
1553
- || Date.parse(entry.verification.expires_at) <= evaluationTime
1554
- ))
1555
- .map((entry) => entry.party_id);
1556
- return stale.length > 0
1557
- ? { code: 'release_approval_expired', details: { party_ids: stale } }
1558
- : null;
1559
- }
1560
-
1561
- function providerRequestFor(record) {
1562
- const bindingVerification = record.document_action_binding.verification;
1563
- const request = {
1564
- method: 'POST',
1565
- provider_id: record.profile.provider_id,
1566
- agreement_digest: record.agreement_digest,
1567
- document_action_binding_digest: record.document_action_binding_digest,
1568
- milestone_id: record.milestone_id,
1569
- release_action_digest: record.release_action_digest,
1570
- parties: record.parties,
1571
- parties_digest: record.parties_digest,
1572
- profile: record.profile,
1573
- profile_digest: record.profile_digest,
1574
- agreement_id: bindingVerification.agreement_id,
1575
- binding_id: bindingVerification.binding_id,
1576
- document_digest: bindingVerification.document_digest,
1577
- release_action_template: bindingVerification.release_action_template,
1578
- release_key: releaseReservationKey(record),
1579
- idempotency_key: providerIdempotencyKey(record),
1580
- };
1581
- return {
1582
- ...request,
1583
- request_digest: canonicalDigest({
1584
- '@version': 'EP-ACTION-ESCROW-PROVIDER-REQUEST-v1',
1585
- ...request,
1586
- }),
1587
- };
1588
- }
1589
-
1590
- async function authoritativeProviderRelease(record) {
1591
- const request = {
1592
- ...record.release.provider_request,
1593
- method: 'GET',
1594
- };
1595
- let response;
1596
- try {
1597
- response = canonicalSnapshot(await withProviderTimeout(
1598
- () => getProviderRelease(deepFreeze(canonicalSnapshot(request))),
1599
- ));
1600
- } catch {
1601
- return { error: 'provider_reconciliation_failed' };
1602
- }
1603
- if (!exactKeys(response, new Set(['authenticated', 'statement']))
1604
- || response.authenticated !== true) {
1605
- return { error: 'provider_reconciliation_unauthenticated' };
1606
- }
1607
- const expected = providerExpected(record);
1608
- const verified = await invokeVerifier(
1609
- verifyProviderStatement,
1610
- response.statement,
1611
- expected,
1612
- );
1613
- if (verified.error
1614
- || !boundVerificationMatches(verified.result, expectedBindings(expected))
1615
- || verified.result.authenticated !== true
1616
- || verified.result.provider_id !== expected.provider_id
1617
- || verified.result.statement_type !== 'release'
1618
- || !['released', 'not_released', 'pending'].includes(verified.result.status)
1619
- || !validDigest(verified.result.statement_digest)
1620
- || verified.result.provider_idempotency_key !== expected.provider_idempotency_key
1621
- || verified.result.provider_request_digest !== expected.provider_request_digest
1622
- || verified.result.provider_transaction_id !== expected.provider_transaction_id
1623
- || verified.result.provider_milestone_id !== expected.provider_milestone_id
1624
- || verified.result.amount !== expected.amount
1625
- || verified.result.currency !== expected.currency
1626
- || verified.result.destination_id !== expected.destination_id) {
1627
- return { error: verified.error ?? 'provider_release_statement_invalid' };
1628
- }
1629
- return {
1630
- artifact: verified.artifact,
1631
- verification: boundVerificationSummary(
1632
- verified.result,
1633
- expected,
1634
- {
1635
- authenticated: true,
1636
- provider_id: expected.provider_id,
1637
- provider_idempotency_key: expected.provider_idempotency_key,
1638
- provider_request_digest: expected.provider_request_digest,
1639
- provider_transaction_id: expected.provider_transaction_id,
1640
- provider_milestone_id: expected.provider_milestone_id,
1641
- amount: expected.amount,
1642
- currency: expected.currency,
1643
- destination_id: expected.destination_id,
1644
- statement_type: 'release',
1645
- status: verified.result.status,
1646
- statement_digest: verified.result.statement_digest,
1647
- },
1648
- ),
1649
- };
1650
- }
1651
-
1652
- function updateReleaseOperation(next, idempotencyKey, code, state) {
1653
- const entry = next.operations.find(
1654
- (operation) => operation.operation === 'release'
1655
- && operation.idempotency_key === idempotencyKey,
1656
- );
1657
- if (entry) {
1658
- entry.code = code;
1659
- entry.ok = state === 'released';
1660
- entry.outcome = state === 'released'
1661
- ? 'applied'
1662
- : state === 'release_indeterminate'
1663
- ? 'indeterminate'
1664
- : 'refused';
1665
- entry.state = state;
1666
- }
1667
- }
1668
-
1669
- function internalReleaseTransition(record, targetState, code, at, providerResult = null) {
1670
- const next = canonicalSnapshot(record);
1671
- const from = next.state;
1672
- next.state = targetState;
1673
- next.revision = record.revision + 1;
1674
- next.updated_at = at;
1675
- next.release.status = targetState === 'released'
1676
- ? 'released'
1677
- : targetState === 'milestone_submitted'
1678
- ? 'not_released'
1679
- : 'indeterminate';
1680
- next.release.reconciled_at = at;
1681
- if (providerResult) {
1682
- next.release.provider_statement = providerResult.artifact;
1683
- next.release.provider_verification = providerResult.verification;
1684
- }
1685
- updateReleaseOperation(
1686
- next,
1687
- next.release.operation_idempotency_key,
1688
- code,
1689
- targetState,
1690
- );
1691
- if (from !== targetState) {
1692
- if (next.history.length >= MAX_HISTORY) return { error: 'state_history_limit_reached' };
1693
- next.history.push({
1694
- from,
1695
- to: targetState,
1696
- operation: 'release',
1697
- idempotency_key: next.release.operation_idempotency_key,
1698
- at,
1699
- });
1700
- }
1701
- return { next };
1702
- }
1703
-
1704
- async function readLatestRelease(record) {
1705
- const loaded = await readRecord(record.escrow_key);
1706
- return loaded.error ? { error: loaded.error } : { record: loaded.record };
1707
- }
1708
-
1709
- async function freezeIndeterminate(record, operation, code, providerResult = null) {
1710
- let current = record;
1711
- for (let attempt = 0; attempt < MAX_CAS_ATTEMPTS; attempt++) {
1712
- if (current?.state === 'released') {
1713
- return outcome({
1714
- ok: true,
1715
- type: 'reconciled',
1716
- code: 'release_committed',
1717
- operation,
1718
- record: current,
1719
- });
1720
- }
1721
- if (current?.state === 'release_indeterminate') {
1722
- return outcome({
1723
- type: 'indeterminate',
1724
- code,
1725
- operation,
1726
- record: current,
1727
- });
1728
- }
1729
- if (current?.state !== 'release_reserved') {
1730
- return outcome({
1731
- type: 'indeterminate',
1732
- code,
1733
- operation,
1734
- record: current,
1735
- });
1736
- }
1737
- const operationTime = operationInstant(current);
1738
- if (operationTime.error) {
1739
- return outcome({
1740
- type: 'indeterminate',
1741
- code: operationTime.error,
1742
- operation,
1743
- record: current,
1744
- });
1745
- }
1746
- const { at } = operationTime;
1747
- const transition = internalReleaseTransition(
1748
- current,
1749
- 'release_indeterminate',
1750
- code,
1751
- at,
1752
- providerResult,
1753
- );
1754
- if (transition.error) {
1755
- return outcome({
1756
- type: 'indeterminate',
1757
- code: transition.error,
1758
- operation,
1759
- record: current,
1760
- });
1761
- }
1762
- const written = await writeRecord(
1763
- current.escrow_key,
1764
- current.revision,
1765
- transition.next,
1766
- );
1767
- if (written.applied) {
1768
- return outcome({
1769
- type: 'indeterminate',
1770
- code,
1771
- operation,
1772
- record: transition.next,
1773
- });
1774
- }
1775
- const latest = await readLatestRelease(current);
1776
- if (latest.error) {
1777
- return outcome({
1778
- type: 'indeterminate',
1779
- code,
1780
- operation,
1781
- record: current,
1782
- });
1783
- }
1784
- current = latest.record;
1785
- }
1786
- return outcome({
1787
- type: 'indeterminate',
1788
- code,
1789
- operation,
1790
- record: current,
1791
- });
1792
- }
1793
-
1794
- async function commitReleaseResult(record, providerResult, operation) {
1795
- const status = providerResult.verification.status;
1796
- const operationTime = operationInstant(record);
1797
- if (operationTime.error) {
1798
- return freezeIndeterminate(
1799
- record,
1800
- operation,
1801
- operationTime.error,
1802
- providerResult,
1803
- );
1804
- }
1805
- const { at } = operationTime;
1806
- const targetState = status === 'released'
1807
- ? 'released'
1808
- : status === 'not_released'
1809
- ? 'milestone_submitted'
1810
- : 'release_indeterminate';
1811
- const code = status === 'released'
1812
- ? 'release_committed'
1813
- : status === 'not_released'
1814
- ? 'provider_release_not_released'
1815
- : 'release_effect_indeterminate';
1816
- const transition = internalReleaseTransition(
1817
- record,
1818
- targetState,
1819
- code,
1820
- at,
1821
- providerResult,
1822
- );
1823
- if (transition.error) {
1824
- return freezeIndeterminate(
1825
- record,
1826
- operation,
1827
- 'release_commit_indeterminate',
1828
- providerResult,
1829
- );
1830
- }
1831
- const written = await writeRecord(record.escrow_key, record.revision, transition.next);
1832
- if (written.applied) {
1833
- return outcome({
1834
- ok: status === 'released',
1835
- type: status === 'released' ? 'applied' : status === 'pending' ? 'indeterminate' : 'refused',
1836
- code,
1837
- operation,
1838
- record: transition.next,
1839
- });
1840
- }
1841
-
1842
- const latest = await readLatestRelease(record);
1843
- if (!latest.error
1844
- && latest.record?.state === 'released'
1845
- && latest.record.release?.release_key === record.release.release_key) {
1846
- return outcome({
1847
- ok: true,
1848
- type: 'idempotent',
1849
- code: 'release_committed',
1850
- operation,
1851
- record: latest.record,
1852
- });
1853
- }
1854
- return freezeIndeterminate(
1855
- latest.record ?? record,
1856
- operation,
1857
- 'release_commit_indeterminate',
1858
- providerResult,
1859
- );
1860
- }
1861
-
1862
- async function release(input = {}) {
1863
- const operation = 'release';
1864
- return safe(operation, async () => {
1865
- const normalized = normalizedInput(operation, input);
1866
- if (normalized.error) return outcome({ code: normalized.error, operation });
1867
- const config = configurationRefusal(operation);
1868
- if (config) return config;
1869
-
1870
- let reserved = null;
1871
- for (let attempt = 0; attempt < MAX_CAS_ATTEMPTS; attempt++) {
1872
- const loaded = await readRecord(normalized.escrowKey);
1873
- if (loaded.error) return outcome({ code: loaded.error, operation });
1874
- if (!loaded.record) return outcome({ code: 'escrow_not_found', operation });
1875
- const record = loaded.record;
1876
- if (!operationBindingMatches(record, normalized)) {
1877
- return outcome({ code: 'operation_binding_mismatch', operation, record });
1878
- }
1879
- const repeated = idempotentResult(record, normalized, operation);
1880
- if (repeated) return repeated;
1881
- if (['released', 'completed'].includes(record.state)) {
1882
- return outcome({ code: 'release_already_applied', operation, record });
1883
- }
1884
- if (['release_reserved', 'release_indeterminate'].includes(record.state)) {
1885
- return outcome({
1886
- code: 'release_reconciliation_required',
1887
- operation,
1888
- record,
1889
- type: 'indeterminate',
1890
- });
1891
- }
1892
- if (record.state !== 'milestone_submitted') {
1893
- return outcome({ code: 'invalid_state_transition', operation, record });
1894
- }
1895
- const operationTime = operationInstant(record);
1896
- if (operationTime.error) {
1897
- return outcome({ code: operationTime.error, operation, record });
1898
- }
1899
- const { at } = operationTime;
1900
- const precondition = releasePreconditions(record, at);
1901
- if (precondition) {
1902
- return outcome({
1903
- code: precondition.code,
1904
- operation,
1905
- record,
1906
- details: precondition.details ?? null,
1907
- });
1908
- }
1909
- const request = providerRequestFor(record);
1910
- const finalized = finalizeMutation(
1911
- record,
1912
- normalized,
1913
- operation,
1914
- 'release_reserved',
1915
- at,
1916
- (next) => {
1917
- next.state = 'release_reserved';
1918
- next.release = {
1919
- release_key: request.release_key,
1920
- provider_idempotency_key: request.idempotency_key,
1921
- operation_idempotency_key: normalized.snapshot.idempotency_key,
1922
- status: 'reserved',
1923
- reserved_at: at,
1924
- reconciled_at: null,
1925
- provider_request: request,
1926
- provider_statement: null,
1927
- provider_verification: null,
1928
- };
1929
- },
1930
- { ok: false, type: 'reserved' },
1931
- );
1932
- if (finalized.error) {
1933
- return outcome({ code: finalized.error, operation, record });
1934
- }
1935
- const written = await writeRecord(
1936
- normalized.escrowKey,
1937
- record.revision,
1938
- finalized.next,
1939
- );
1940
- if (written.error) return outcome({ code: written.error, operation, record });
1941
- if (written.applied) {
1942
- reserved = deepFreeze(finalized.next);
1943
- break;
1944
- }
1945
- }
1946
- if (!reserved) return outcome({ code: 'store_conflict', operation });
1947
-
1948
- try {
1949
- await withProviderTimeout(
1950
- () => releaseProvider(
1951
- deepFreeze(canonicalSnapshot(reserved.release.provider_request)),
1952
- ),
1953
- );
1954
- } catch {
1955
- return freezeIndeterminate(
1956
- reserved,
1957
- operation,
1958
- 'release_effect_indeterminate',
1959
- );
1960
- }
1961
-
1962
- const reconciled = await authoritativeProviderRelease(reserved);
1963
- if (reconciled.error) {
1964
- return freezeIndeterminate(
1965
- reserved,
1966
- operation,
1967
- 'release_effect_indeterminate',
1968
- );
1969
- }
1970
- return commitReleaseResult(reserved, reconciled, operation);
1971
- });
1972
- }
1973
-
1974
- async function reconcileRelease(input = {}) {
1975
- const operation = 'reconcile_release';
1976
- return safe(operation, async () => {
1977
- const normalized = normalizedInput(operation, input);
1978
- if (normalized.error) return outcome({ code: normalized.error, operation });
1979
- const config = configurationRefusal(operation);
1980
- if (config) return config;
1981
-
1982
- for (let attempt = 0; attempt < MAX_CAS_ATTEMPTS; attempt++) {
1983
- const loaded = await readRecord(normalized.escrowKey);
1984
- if (loaded.error) return outcome({ code: loaded.error, operation });
1985
- if (!loaded.record) return outcome({ code: 'escrow_not_found', operation });
1986
- const record = loaded.record;
1987
- if (!operationBindingMatches(record, normalized)) {
1988
- return outcome({ code: 'operation_binding_mismatch', operation, record });
1989
- }
1990
- const repeated = idempotentResult(record, normalized, operation);
1991
- if (repeated) return repeated;
1992
- if (record.state === 'released') {
1993
- return outcome({
1994
- ok: true,
1995
- type: 'idempotent',
1996
- code: 'release_already_applied',
1997
- operation,
1998
- record,
1999
- });
2000
- }
2001
- if (!['release_reserved', 'release_indeterminate'].includes(record.state)) {
2002
- return outcome({ code: 'invalid_state_transition', operation, record });
2003
- }
2004
- if (record.release?.provider_idempotency_key !== providerIdempotencyKey(record)
2005
- || record.release?.release_key !== releaseReservationKey(record)) {
2006
- return outcome({ code: 'release_binding_corrupt', operation, record });
2007
- }
2008
-
2009
- const providerResult = await authoritativeProviderRelease(record);
2010
- if (providerResult.error) {
2011
- return outcome({
2012
- code: providerResult.error,
2013
- operation,
2014
- record,
2015
- type: 'indeterminate',
2016
- });
2017
- }
2018
- const operationTime = operationInstant(record);
2019
- if (operationTime.error) {
2020
- return outcome({
2021
- code: operationTime.error,
2022
- operation,
2023
- record,
2024
- type: 'indeterminate',
2025
- });
2026
- }
2027
- const { at } = operationTime;
2028
- const status = providerResult.verification.status;
2029
- const targetState = status === 'released'
2030
- ? 'released'
2031
- : status === 'not_released'
2032
- ? record.pending_amendment ? 'amendment_pending' : 'milestone_submitted'
2033
- : 'release_indeterminate';
2034
- const code = status === 'released'
2035
- ? 'release_reconciled_released'
2036
- : status === 'not_released'
2037
- ? 'release_reconciled_not_released'
2038
- : 'release_still_indeterminate';
2039
- const draft = canonicalSnapshot(record);
2040
- draft.state = targetState;
2041
- draft.release.status = status === 'pending' ? 'indeterminate' : status;
2042
- draft.release.reconciled_at = at;
2043
- draft.release.provider_statement = providerResult.artifact;
2044
- draft.release.provider_verification = providerResult.verification;
2045
- if (status === 'not_released' && draft.pending_amendment) {
2046
- draft.funding = null;
2047
- draft.milestone_evidence = null;
2048
- draft.release_approvals = [];
2049
- }
2050
- updateReleaseOperation(
2051
- draft,
2052
- draft.release.operation_idempotency_key,
2053
- code,
2054
- targetState,
2055
- );
2056
- const finalized = finalizeMutation(
2057
- record,
2058
- normalized,
2059
- operation,
2060
- code,
2061
- at,
2062
- (next) => Object.assign(next, draft),
2063
- {
2064
- ok: status !== 'pending',
2065
- type: status === 'pending' ? 'indeterminate' : 'reconciled',
2066
- },
2067
- );
2068
- if (finalized.error) {
2069
- return outcome({
2070
- code: finalized.error,
2071
- operation,
2072
- record,
2073
- type: 'indeterminate',
2074
- });
2075
- }
2076
- const written = await writeRecord(
2077
- normalized.escrowKey,
2078
- record.revision,
2079
- finalized.next,
2080
- );
2081
- if (written.error) {
2082
- return outcome({
2083
- code: written.error,
2084
- operation,
2085
- record,
2086
- type: 'indeterminate',
2087
- });
2088
- }
2089
- if (written.applied) {
2090
- return outcome({
2091
- ok: status !== 'pending',
2092
- type: status === 'pending' ? 'indeterminate' : 'reconciled',
2093
- code,
2094
- operation,
2095
- record: finalized.next,
2096
- });
2097
- }
2098
- }
2099
- return outcome({
2100
- code: 'store_conflict',
2101
- operation,
2102
- type: 'indeterminate',
2103
- });
2104
- });
2105
- }
2106
-
2107
- async function openDispute(input = {}) {
2108
- return mutate('open_dispute', input, {
2109
- extraAllowed: ['party_id', 'reason', 'command_authorization'],
2110
- async transform(draft, normalized, at) {
2111
- if (!['funded', 'milestone_submitted'].includes(draft.state)) {
2112
- return { refusal: 'invalid_state_transition' };
2113
- }
2114
- const partyId = normalized.snapshot.party_id;
2115
- if (!draft.parties.some((party) => party.party_id === partyId)
2116
- || !validString(normalized.snapshot.reason, 2048)) {
2117
- return { refusal: 'dispute_input_invalid' };
2118
- }
2119
- const authorization = await verifyCommandAuthorization(
2120
- normalized.snapshot.command_authorization,
2121
- draft,
2122
- 'open_dispute',
2123
- partyId,
2124
- { reason: normalized.snapshot.reason },
2125
- );
2126
- if (authorization.error) return { refusal: authorization.error };
2127
- draft.dispute = {
2128
- party_id: partyId,
2129
- reason: normalized.snapshot.reason,
2130
- authorization,
2131
- opened_at: at,
2132
- };
2133
- draft.state = 'disputed';
2134
- return { code: 'dispute_opened' };
2135
- },
2136
- });
2137
- }
2138
-
2139
- async function proposeAmendment(input = {}) {
2140
- return mutate('propose_amendment', input, {
2141
- extraAllowed: [
2142
- 'party_id',
2143
- 'command_authorization',
2144
- 'next_document_action_binding_digest',
2145
- 'next_release_action_digest',
2146
- 'next_document_action_binding',
2147
- ],
2148
- async transform(draft, normalized, at) {
2149
- if (draft.state === 'awaiting_funding') {
2150
- return { refusal: 'amendment_requires_funding_reconciliation' };
2151
- }
2152
- if (draft.funding !== null
2153
- || draft.release !== null
2154
- || ['funded', 'milestone_submitted', 'disputed'].includes(draft.state)) {
2155
- return { refusal: 'amendment_requires_custodian_unwind' };
2156
- }
2157
- if (draft.state !== 'effective'
2158
- || draft.pending_amendment) {
2159
- return { refusal: 'invalid_state_transition' };
2160
- }
2161
- const nextBindingDigest =
2162
- normalized.snapshot.next_document_action_binding_digest;
2163
- const nextActionDigest = normalized.snapshot.next_release_action_digest;
2164
- if (!validDigest(nextBindingDigest)
2165
- || !validDigest(nextActionDigest)
2166
- || nextBindingDigest === draft.document_action_binding_digest) {
2167
- return { refusal: 'amendment_binding_invalid' };
2168
- }
2169
- const partyId = normalized.snapshot.party_id;
2170
- const authorization = await verifyCommandAuthorization(
2171
- normalized.snapshot.command_authorization,
2172
- draft,
2173
- 'propose_amendment',
2174
- partyId,
2175
- {
2176
- next_document_action_binding_digest: nextBindingDigest,
2177
- next_release_action_digest: nextActionDigest,
2178
- },
2179
- );
2180
- if (authorization.error) return { refusal: authorization.error };
2181
- const nextContext = {
2182
- ...normalized.context,
2183
- document_action_binding_digest: nextBindingDigest,
2184
- release_action_digest: nextActionDigest,
2185
- supersedes_document_action_binding_digest:
2186
- draft.document_action_binding_digest,
2187
- };
2188
- const verified = await invokeVerifier(
2189
- verifyDocumentActionBinding,
2190
- normalized.snapshot.next_document_action_binding,
2191
- nextContext,
2192
- );
2193
- const bindingDetails = verified.error
2194
- ? null
2195
- : bindingVerificationDetails(verified.result, nextContext);
2196
- if (!bindingDetails
2197
- || verified.result.supersedes_document_action_binding_digest
2198
- !== draft.document_action_binding_digest
2199
- ) {
2200
- return { refusal: verified.error ?? 'amendment_document_binding_invalid' };
2201
- }
2202
-
2203
- draft.pending_amendment = {
2204
- from_state: draft.state,
2205
- document_action_binding_digest: nextBindingDigest,
2206
- release_action_digest: nextActionDigest,
2207
- document_action_binding: {
2208
- artifact: verified.artifact,
2209
- verification: boundVerificationSummary(
2210
- verified.result,
2211
- nextContext,
2212
- {
2213
- ...bindingDetails,
2214
- supersedes_document_action_binding_digest:
2215
- draft.document_action_binding_digest,
2216
- },
2217
- ),
2218
- },
2219
- agreement_acceptances: [],
2220
- proposer_party_id: partyId,
2221
- proposal_authorization: authorization,
2222
- proposed_at: at,
2223
- };
2224
- draft.funding = null;
2225
- draft.milestone_evidence = null;
2226
- draft.release_approvals = [];
2227
- draft.release = null;
2228
- draft.dispute = null;
2229
- draft.cancellation = null;
2230
- draft.state = 'amendment_pending';
2231
- return { code: 'amendment_pending' };
2232
- },
2233
- });
2234
- }
2235
-
2236
- async function acceptAmendment(input = {}) {
2237
- return mutate('accept_amendment', input, {
2238
- extraAllowed: ['party_id', 'agreement_acceptance'],
2239
- bindingMode: 'pending',
2240
- async transform(draft, normalized, at) {
2241
- if (draft.state !== 'amendment_pending' || !draft.pending_amendment) {
2242
- return { refusal: 'invalid_state_transition' };
2243
- }
2244
- const partyId = normalized.snapshot.party_id;
2245
- if (!draft.profile.required_acceptance_party_ids.includes(partyId)) {
2246
- return { refusal: 'acceptance_party_not_required' };
2247
- }
2248
- if (draft.pending_amendment.agreement_acceptances
2249
- .some((entry) => entry.party_id === partyId)) {
2250
- return { refusal: 'amendment_already_accepted' };
2251
- }
2252
- const expected = {
2253
- ...normalized.context,
2254
- party_id: partyId,
2255
- };
2256
- const verified = await invokeVerifier(
2257
- verifyAgreementAcceptance,
2258
- normalized.snapshot.agreement_acceptance,
2259
- expected,
2260
- );
2261
- if (verified.error
2262
- || !boundVerificationMatches(verified.result, expectedBindings(expected))
2263
- || verified.result.party_id !== partyId
2264
- || !validString(verified.result.principal_key_id, 512)
2265
- || draft.pending_amendment.agreement_acceptances.some(
2266
- (entry) => entry.verification?.principal_key_id
2267
- === verified.result.principal_key_id,
2268
- )
2269
- || !validDigest(verified.result.acceptance_digest)) {
2270
- return { refusal: verified.error ?? 'amendment_acceptance_invalid' };
2271
- }
2272
- draft.pending_amendment.agreement_acceptances.push({
2273
- party_id: partyId,
2274
- artifact: verified.artifact,
2275
- verification: boundVerificationSummary(
2276
- verified.result,
2277
- expected,
2278
- {
2279
- party_id: partyId,
2280
- principal_key_id: verified.result.principal_key_id,
2281
- acceptance_digest: verified.result.acceptance_digest,
2282
- },
2283
- ),
2284
- });
2285
- const accepted = new Set(
2286
- draft.pending_amendment.agreement_acceptances
2287
- .map((entry) => entry.party_id),
2288
- );
2289
- if (!draft.profile.required_acceptance_party_ids
2290
- .every((id) => accepted.has(id))) {
2291
- return { code: 'amendment_acceptance_recorded' };
2292
- }
2293
- if (draft.superseded_bindings.length >= MAX_SUPERSEDED_BINDINGS) {
2294
- return { refusal: 'supersession_history_limit_reached' };
2295
- }
2296
- const pending = draft.pending_amendment;
2297
- draft.superseded_bindings.push({
2298
- document_action_binding_digest: draft.document_action_binding_digest,
2299
- release_action_digest: draft.release_action_digest,
2300
- superseded_by_binding_digest: pending.document_action_binding_digest,
2301
- superseded_at: at,
2302
- });
2303
- draft.document_action_binding_digest =
2304
- pending.document_action_binding_digest;
2305
- draft.release_action_digest = pending.release_action_digest;
2306
- draft.document_action_binding = pending.document_action_binding;
2307
- draft.agreement_acceptances = pending.agreement_acceptances;
2308
- draft.funding = null;
2309
- draft.milestone_evidence = null;
2310
- draft.release_approvals = [];
2311
- draft.release = null;
2312
- draft.dispute = null;
2313
- draft.cancellation = null;
2314
- draft.pending_amendment = null;
2315
- draft.state = 'effective';
2316
- return { code: 'amendment_effective' };
2317
- },
2318
- });
2319
- }
2320
-
2321
- async function cancel(input = {}) {
2322
- return mutate('cancel', input, {
2323
- extraAllowed: ['party_id', 'reason', 'command_authorization'],
2324
- extraRequired: ['party_id', 'command_authorization'],
2325
- async transform(draft, normalized, at) {
2326
- if (draft.state === 'awaiting_funding') {
2327
- return { refusal: 'cancellation_requires_funding_reconciliation' };
2328
- }
2329
- if (draft.funding !== null || draft.release !== null) {
2330
- return { refusal: 'cancellation_requires_custodian_unwind' };
2331
- }
2332
- if (![
2333
- 'draft',
2334
- 'awaiting_acceptance',
2335
- 'effective',
2336
- 'amendment_pending',
2337
- ].includes(draft.state)) {
2338
- return { refusal: 'invalid_state_transition' };
2339
- }
2340
- if (normalized.snapshot.reason !== undefined
2341
- && !validString(normalized.snapshot.reason, 2048)) {
2342
- return { refusal: 'cancellation_reason_invalid' };
2343
- }
2344
- const authorization = await verifyCommandAuthorization(
2345
- normalized.snapshot.command_authorization,
2346
- draft,
2347
- 'cancel',
2348
- normalized.snapshot.party_id,
2349
- { reason: normalized.snapshot.reason ?? null },
2350
- );
2351
- if (authorization.error) return { refusal: authorization.error };
2352
- draft.state = 'cancelled';
2353
- draft.cancellation = {
2354
- party_id: normalized.snapshot.party_id,
2355
- reason: normalized.snapshot.reason ?? null,
2356
- authorization,
2357
- cancelled_at: at,
2358
- };
2359
- return { code: 'escrow_cancelled' };
2360
- },
2361
- });
2362
- }
2363
-
2364
- async function complete(input = {}) {
2365
- return mutate('complete', input, {
2366
- extraAllowed: ['party_id', 'command_authorization'],
2367
- extraRequired: ['party_id', 'command_authorization'],
2368
- async transform(draft, normalized, at) {
2369
- if (draft.state !== 'released') {
2370
- return { refusal: 'invalid_state_transition' };
2371
- }
2372
- const authorization = await verifyCommandAuthorization(
2373
- normalized.snapshot.command_authorization,
2374
- draft,
2375
- 'complete',
2376
- normalized.snapshot.party_id,
2377
- { meaning: 'administrative_archive_only' },
2378
- );
2379
- if (authorization.error) return { refusal: authorization.error };
2380
- draft.state = 'completed';
2381
- draft.completion = {
2382
- party_id: normalized.snapshot.party_id,
2383
- meaning: 'administrative_archive_only',
2384
- authorization,
2385
- completed_at: at,
2386
- };
2387
- return { code: 'escrow_completed' };
2388
- },
2389
- });
2390
- }
2391
-
2392
- const methods = {
2393
- create,
2394
- beginAcceptance,
2395
- acceptAgreement,
2396
- requestFunding,
2397
- recordFunding,
2398
- submitMilestone,
2399
- approveRelease,
2400
- release,
2401
- reconcileRelease,
2402
- openDispute,
2403
- proposeAmendment,
2404
- acceptAmendment,
2405
- cancel,
2406
- complete,
2407
- };
2408
-
2409
- async function apply(operation, input = {}) {
2410
- try {
2411
- const selected = methods[operation];
2412
- if (typeof selected !== 'function') {
2413
- return outcome({
2414
- code: 'unknown_operation',
2415
- operation: validString(operation, 128) ? operation : null,
2416
- });
2417
- }
2418
- return selected(input);
2419
- } catch {
2420
- return outcome({
2421
- code: 'invalid_operation_input',
2422
- operation: null,
2423
- });
2424
- }
2425
- }
2426
-
2427
- return Object.freeze({
2428
- ready: configurationError === null,
2429
- configuration: Object.freeze({
2430
- ok: configurationError === null,
2431
- reason: configurationError,
2432
- }),
2433
- apply,
2434
- ...methods,
2435
- });
2436
- }
2437
-
2438
- export default Object.freeze({
2439
- ACTION_ESCROW_STATE_VERSION,
2440
- ACTION_ESCROW_OUTCOME_VERSION,
2441
- ACTION_ESCROW_PROFILE_VERSION,
2442
- ACTION_ESCROW_STATES,
2443
- ACTION_ESCROW_TRANSITIONS,
2444
- createActionEscrowReleaseBindingMoment,
2445
- computeActionEscrowReleaseBindingMomentDigest,
2446
- computeActionEscrowResolutionNonce,
2447
- createActionEscrowKernel,
2448
- });
2
+ // Compatibility entry point for the TypeScript action-escrow module.
3
+ export * from './dist/action-escrow.js';
4
+ export { default } from './dist/action-escrow.js';