@emilia-protocol/gate 0.11.0 → 0.13.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 (419) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +252 -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 +1002 -0
  179. package/dist/index.d.ts.map +1 -0
  180. package/dist/index.js +1736 -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/protocol-state.d.ts +141 -0
  203. package/dist/protocol-state.d.ts.map +1 -0
  204. package/dist/protocol-state.js +106 -0
  205. package/dist/protocol-state.js.map +1 -0
  206. package/dist/receipt-program.d.ts +59 -0
  207. package/dist/receipt-program.d.ts.map +1 -0
  208. package/dist/receipt-program.js +1053 -0
  209. package/dist/receipt-program.js.map +1 -0
  210. package/dist/reliance-kernel.d.ts +66 -0
  211. package/dist/reliance-kernel.d.ts.map +1 -0
  212. package/dist/reliance-kernel.js +103 -0
  213. package/dist/reliance-kernel.js.map +1 -0
  214. package/dist/reliance-packet.d.ts +67 -0
  215. package/dist/reliance-packet.d.ts.map +1 -0
  216. package/dist/reliance-packet.js +176 -0
  217. package/dist/reliance-packet.js.map +1 -0
  218. package/dist/remedy-program-postgres.d.ts +37 -0
  219. package/dist/remedy-program-postgres.d.ts.map +1 -0
  220. package/dist/remedy-program-postgres.js +291 -0
  221. package/dist/remedy-program-postgres.js.map +1 -0
  222. package/dist/remedy-program-receipt.d.ts +97 -0
  223. package/dist/remedy-program-receipt.d.ts.map +1 -0
  224. package/dist/remedy-program-receipt.js +817 -0
  225. package/dist/remedy-program-receipt.js.map +1 -0
  226. package/dist/remedy-program.d.ts +76 -0
  227. package/dist/remedy-program.d.ts.map +1 -0
  228. package/dist/remedy-program.js +1346 -0
  229. package/dist/remedy-program.js.map +1 -0
  230. package/dist/reports/art14.d.ts +101 -0
  231. package/dist/reports/art14.d.ts.map +1 -0
  232. package/dist/reports/art14.js +340 -0
  233. package/dist/reports/art14.js.map +1 -0
  234. package/dist/reports/assurance-package.d.ts +197 -0
  235. package/dist/reports/assurance-package.d.ts.map +1 -0
  236. package/dist/reports/assurance-package.js +313 -0
  237. package/dist/reports/assurance-package.js.map +1 -0
  238. package/dist/reports/auditor-workpaper.d.ts +194 -0
  239. package/dist/reports/auditor-workpaper.d.ts.map +1 -0
  240. package/dist/reports/auditor-workpaper.js +545 -0
  241. package/dist/reports/auditor-workpaper.js.map +1 -0
  242. package/dist/reports/external-verification.d.ts +76 -0
  243. package/dist/reports/external-verification.d.ts.map +1 -0
  244. package/dist/reports/external-verification.js +209 -0
  245. package/dist/reports/external-verification.js.map +1 -0
  246. package/dist/reports/reperform.d.ts +122 -0
  247. package/dist/reports/reperform.d.ts.map +1 -0
  248. package/dist/reports/reperform.js +432 -0
  249. package/dist/reports/reperform.js.map +1 -0
  250. package/dist/reports/underwriter.d.ts +127 -0
  251. package/dist/reports/underwriter.d.ts.map +1 -0
  252. package/dist/reports/underwriter.js +348 -0
  253. package/dist/reports/underwriter.js.map +1 -0
  254. package/dist/retention.d.ts +88 -0
  255. package/dist/retention.d.ts.map +1 -0
  256. package/dist/retention.js +90 -0
  257. package/dist/retention.js.map +1 -0
  258. package/dist/roster.d.ts +113 -0
  259. package/dist/roster.d.ts.map +1 -0
  260. package/dist/roster.js +261 -0
  261. package/dist/roster.js.map +1 -0
  262. package/dist/runtime-monitor.d.ts +132 -0
  263. package/dist/runtime-monitor.d.ts.map +1 -0
  264. package/dist/runtime-monitor.js +197 -0
  265. package/dist/runtime-monitor.js.map +1 -0
  266. package/dist/settlement.d.ts +68 -0
  267. package/dist/settlement.d.ts.map +1 -0
  268. package/dist/settlement.js +306 -0
  269. package/dist/settlement.js.map +1 -0
  270. package/dist/siem.d.ts +261 -0
  271. package/dist/siem.d.ts.map +1 -0
  272. package/dist/siem.js +247 -0
  273. package/dist/siem.js.map +1 -0
  274. package/dist/store-postgres.d.ts +107 -0
  275. package/dist/store-postgres.d.ts.map +1 -0
  276. package/dist/store-postgres.js +152 -0
  277. package/dist/store-postgres.js.map +1 -0
  278. package/dist/store.d.ts +95 -0
  279. package/dist/store.d.ts.map +1 -0
  280. package/dist/store.js +202 -0
  281. package/dist/store.js.map +1 -0
  282. package/dist/strict-json.d.ts +14 -0
  283. package/dist/strict-json.d.ts.map +1 -0
  284. package/dist/strict-json.js +132 -0
  285. package/dist/strict-json.js.map +1 -0
  286. package/dist/trust-program-adapters.d.ts +122 -0
  287. package/dist/trust-program-adapters.d.ts.map +1 -0
  288. package/dist/trust-program-adapters.js +802 -0
  289. package/dist/trust-program-adapters.js.map +1 -0
  290. package/dist/trust-program-postgres.d.ts +41 -0
  291. package/dist/trust-program-postgres.d.ts.map +1 -0
  292. package/dist/trust-program-postgres.js +346 -0
  293. package/dist/trust-program-postgres.js.map +1 -0
  294. package/dist/trust-program-revocation.d.ts +206 -0
  295. package/dist/trust-program-revocation.d.ts.map +1 -0
  296. package/dist/trust-program-revocation.js +600 -0
  297. package/dist/trust-program-revocation.js.map +1 -0
  298. package/dist/trust-program.d.ts +197 -0
  299. package/dist/trust-program.d.ts.map +1 -0
  300. package/dist/trust-program.js +1351 -0
  301. package/dist/trust-program.js.map +1 -0
  302. package/dist/witness-postgres.d.ts +43 -0
  303. package/dist/witness-postgres.d.ts.map +1 -0
  304. package/dist/witness-postgres.js +92 -0
  305. package/dist/witness-postgres.js.map +1 -0
  306. package/dist/zk-range-proof.d.ts +78 -0
  307. package/dist/zk-range-proof.d.ts.map +1 -0
  308. package/dist/zk-range-proof.js +228 -0
  309. package/dist/zk-range-proof.js.map +1 -0
  310. package/eg1-conformance.js +3 -404
  311. package/enterprise.js +3 -180
  312. package/ep-assure.mjs +1 -0
  313. package/evidence-postgres.js +3 -356
  314. package/evidence.js +4 -360
  315. package/execution-binding.js +3 -212
  316. package/formal-runtime-map.js +4 -0
  317. package/index.js +4 -1249
  318. package/key-registry.js +3 -134
  319. package/mcp.js +3 -109
  320. package/metering.js +3 -226
  321. package/metrics.js +3 -231
  322. package/network-witness.js +3 -499
  323. package/package.json +31 -2
  324. package/receipt-program.js +4 -0
  325. package/reliance-kernel.js +3 -112
  326. package/reliance-packet.js +3 -175
  327. package/remedy-program-postgres.js +4 -0
  328. package/remedy-program-receipt.js +4 -0
  329. package/remedy-program.js +4 -0
  330. package/reports/art14.js +0 -0
  331. package/reports/assurance-package.js +2 -312
  332. package/reports/auditor-workpaper.js +3 -537
  333. package/reports/external-verification.js +3 -222
  334. package/reports/reperform.js +3 -424
  335. package/reports/underwriter.js +3 -339
  336. package/retention.js +3 -84
  337. package/roster.js +3 -264
  338. package/runtime-monitor.js +4 -0
  339. package/settlement.js +3 -299
  340. package/siem.js +3 -235
  341. package/src/action-control-manifest.ts +424 -0
  342. package/src/action-escrow-custodian.ts +427 -0
  343. package/src/action-escrow-evidence.ts +1178 -0
  344. package/src/action-escrow-package.ts +1821 -0
  345. package/src/action-escrow-postgres.ts +366 -0
  346. package/src/action-escrow-state.ts +498 -0
  347. package/src/action-escrow-verifiers.ts +484 -0
  348. package/src/action-escrow.ts +3039 -0
  349. package/src/action-packs.ts +170 -0
  350. package/src/adapters/_kit.ts +117 -0
  351. package/src/adapters/aws.ts +142 -0
  352. package/src/adapters/cloudflare.ts +78 -0
  353. package/src/adapters/gcp.ts +71 -0
  354. package/src/adapters/github.ts +110 -0
  355. package/src/adapters/jira.ts +56 -0
  356. package/src/adapters/k8s.ts +71 -0
  357. package/src/adapters/linear.ts +56 -0
  358. package/src/adapters/salesforce.ts +56 -0
  359. package/src/adapters/stripe.ts +82 -0
  360. package/src/adapters/supabase.ts +122 -0
  361. package/src/adapters/terraform.ts +60 -0
  362. package/src/adapters/vercel.ts +85 -0
  363. package/src/aec-execution.ts +354 -0
  364. package/src/breakglass.ts +647 -0
  365. package/src/capability-receipt.ts +1258 -0
  366. package/src/cf1-conformance.ts +120 -0
  367. package/src/challenge-store.ts +70 -0
  368. package/src/control-plane.ts +374 -0
  369. package/src/coverage.ts +832 -0
  370. package/src/deployment-attestation.ts +259 -0
  371. package/src/eg1-conformance.ts +471 -0
  372. package/src/enterprise.ts +195 -0
  373. package/src/evidence-postgres.ts +374 -0
  374. package/src/evidence.ts +367 -0
  375. package/src/execution-binding.ts +236 -0
  376. package/src/formal-runtime-map.ts +24 -0
  377. package/src/index.ts +2122 -0
  378. package/src/key-registry.ts +150 -0
  379. package/src/mcp.ts +120 -0
  380. package/src/metering.ts +237 -0
  381. package/src/metrics.ts +232 -0
  382. package/src/network-witness.ts +585 -0
  383. package/src/protocol-state.ts +143 -0
  384. package/src/protocol-state.typecheck.ts +94 -0
  385. package/src/receipt-program.ts +1105 -0
  386. package/src/reliance-kernel.ts +119 -0
  387. package/src/reliance-packet.ts +206 -0
  388. package/src/remedy-program-postgres.ts +367 -0
  389. package/src/remedy-program-receipt.ts +891 -0
  390. package/src/remedy-program.ts +1388 -0
  391. package/src/reports/art14.ts +361 -0
  392. package/src/reports/assurance-package.ts +330 -0
  393. package/src/reports/auditor-workpaper.ts +558 -0
  394. package/src/reports/external-verification.ts +232 -0
  395. package/src/reports/reperform.ts +441 -0
  396. package/src/reports/underwriter.ts +356 -0
  397. package/src/retention.ts +100 -0
  398. package/src/roster.ts +273 -0
  399. package/src/runtime-monitor.ts +217 -0
  400. package/src/settlement.ts +313 -0
  401. package/src/siem.ts +247 -0
  402. package/src/store-postgres.ts +167 -0
  403. package/src/store.ts +214 -0
  404. package/src/strict-json.ts +115 -0
  405. package/src/trust-program-adapters.ts +847 -0
  406. package/src/trust-program-postgres.ts +429 -0
  407. package/src/trust-program-revocation.ts +751 -0
  408. package/src/trust-program.ts +1460 -0
  409. package/src/witness-postgres.ts +106 -0
  410. package/src/zk-range-proof.ts +250 -0
  411. package/store-postgres.js +3 -161
  412. package/store.js +4 -198
  413. package/strict-json.js +3 -102
  414. package/trust-program-adapters.js +4 -0
  415. package/trust-program-postgres.js +4 -0
  416. package/trust-program-revocation.js +4 -0
  417. package/trust-program.js +4 -0
  418. package/witness-postgres.js +3 -96
  419. package/zk-range-proof.js +4 -0
@@ -0,0 +1,1388 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * EMILIA Gate Remedy Program Profile v1.
4
+ *
5
+ * A fail-closed, post-effect compensation state machine. The kernel never
6
+ * rewrites an already-observed effect: it verifies the original effect,
7
+ * records disputes and late revocations, and authorizes separately bound
8
+ * compensating operations through atomic compare-and-swap transitions.
9
+ */
10
+
11
+ import { createHash } from 'node:crypto';
12
+ import { canonicalize } from '../execution-binding.js';
13
+
14
+ export const REMEDY_PROGRAM_VERSION = 'EP-GATE-REMEDY-PROGRAM-PROFILE-v1';
15
+
16
+ const DIGEST = /^sha256:[0-9a-f]{64}$/;
17
+ const CAID = /^caid:1:[a-z][a-z0-9.-]*\.[1-9][0-9]*:jcs-sha256:[A-Za-z0-9_-]{43}$/;
18
+ const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
19
+ const MAX_CONTEXT_BYTES = 512;
20
+ const MAX_REMEDY_ATTEMPTS = 1024;
21
+ const DEFAULT_MAX_DISPUTE_AGE_MS = 30 * 24 * 60 * 60 * 1000;
22
+
23
+ const CONFIG_KEYS = new Set([
24
+ 'store', 'verifyOriginalEffect', 'verifyRevocation', 'verifyDispute',
25
+ 'verifyRemedyAuthorization', 'verifyRemedyOutcome', 'verifyOriginalReconciliation',
26
+ 'verifyResolution',
27
+ 'now', 'maxDisputeAgeMs', 'allowEphemeralState', 'production',
28
+ ]);
29
+ const CREATE_KEYS = new Set([
30
+ 'instanceId', 'tenantId', 'environment', 'audience', 'original',
31
+ 'remedyProfileDigest', 'destinationBindingDigest', 'maxRemedyUnits',
32
+ 'unit', 'evidence',
33
+ ]);
34
+ const ORIGINAL_KEYS = new Set([
35
+ 'caid', 'action_digest', 'operation_id', 'consequence_mode',
36
+ 'consequence_digest', 'terminal_evidence_digest', 'outcome', 'occurred_at',
37
+ ]);
38
+ const VERIFIED_ORIGINAL_KEYS = new Set(['ok', ...ORIGINAL_KEYS, 'evidence_digest']);
39
+ const ORIGINAL_RECONCILIATION_INPUT_KEYS = new Set([
40
+ 'tenantId', 'instanceId', 'outcome', 'evidence',
41
+ ]);
42
+ const ORIGINAL_RECONCILIATION_EVIDENCE_KEYS = new Set([
43
+ 'evidence_id', 'evidence_digest', 'observed_at',
44
+ ]);
45
+ const VERIFIED_ORIGINAL_RECONCILIATION_KEYS = new Set([
46
+ 'ok', ...ORIGINAL_RECONCILIATION_EVIDENCE_KEYS, 'original_operation_id',
47
+ 'original_action_digest', 'terminal_evidence_digest', 'outcome',
48
+ ]);
49
+ const REVOCATION_INPUT_KEYS = new Set(['tenantId', 'instanceId', 'evidence']);
50
+ const REVOCATION_EVIDENCE_KEYS = new Set(['id', 'digest']);
51
+ const VERIFIED_REVOCATION_KEYS = new Set([
52
+ 'ok', 'evidence_id', 'evidence_digest', 'target_operation_id',
53
+ 'action_digest', 'authority_id', 'revoked_at',
54
+ ]);
55
+ const DISPUTE_INPUT_KEYS = new Set(['tenantId', 'instanceId', 'dispute']);
56
+ const DISPUTE_KEYS = new Set([
57
+ 'dispute_id', 'evidence_id', 'evidence_digest', 'challenger_id',
58
+ 'requested_units', 'opened_at',
59
+ ]);
60
+ const VERIFIED_DISPUTE_KEYS = new Set([
61
+ 'ok', ...DISPUTE_KEYS, 'original_operation_id', 'original_action_digest',
62
+ ]);
63
+ const AUTHORIZATION_INPUT_KEYS = new Set(['tenantId', 'instanceId', 'authorization']);
64
+ const AUTHORIZATION_KEYS = new Set([
65
+ 'evidence_id', 'evidence_digest', 'remedy_operation_id', 'remedy_caid',
66
+ 'remedy_action_digest', 'consequence_mode', 'capability_template_digest',
67
+ 'escrow_profile_digest', 'units', 'authorized_at',
68
+ ]);
69
+ const VERIFIED_AUTHORIZATION_KEYS = new Set([
70
+ 'ok', ...AUTHORIZATION_KEYS, 'dispute_id', 'original_operation_id',
71
+ 'destination_binding_digest', 'unit',
72
+ ]);
73
+ const CLAIM_KEYS = new Set(['tenantId', 'instanceId', 'remedyOperationId', 'claimToken']);
74
+ const FINALIZE_KEYS = new Set([
75
+ 'tenantId', 'instanceId', 'remedyOperationId', 'claimToken', 'outcome', 'evidence',
76
+ ]);
77
+ const RECONCILE_KEYS = new Set(['tenantId', 'instanceId', 'remedyOperationId', 'outcome', 'evidence']);
78
+ const OUTCOME_EVIDENCE_KEYS = new Set(['evidence_id', 'evidence_digest', 'observed_at']);
79
+ const VERIFIED_OUTCOME_KEYS = new Set([
80
+ 'ok', ...OUTCOME_EVIDENCE_KEYS, 'remedy_operation_id',
81
+ 'remedy_action_digest', 'destination_binding_digest', 'units', 'unit',
82
+ 'outcome',
83
+ ]);
84
+ const RESOLUTION_INPUT_KEYS = new Set(['tenantId', 'instanceId', 'resolution']);
85
+ const RESOLUTION_KEYS = new Set([
86
+ 'evidence_id', 'evidence_digest', 'outcome', 'resolved_at',
87
+ ]);
88
+ const VERIFIED_RESOLUTION_KEYS = new Set(['ok', ...RESOLUTION_KEYS, 'dispute_id']);
89
+ const STATE_KEYS = new Set([
90
+ 'version', 'instance_id', 'tenant_id', 'environment', 'audience', 'status',
91
+ 'revision', 'created_at', 'updated_at', 'original', 'remedy_profile_digest',
92
+ 'destination_binding_digest', 'max_remedy_units', 'unit', 'remedied_units',
93
+ 'remaining_units', 'used_evidence_ids', 'used_evidence_digests',
94
+ 'original_reconciliation', 'revocation', 'dispute', 'active_remedy',
95
+ 'remedies', 'resolution',
96
+ 'create_request_digest',
97
+ ]);
98
+ const STORED_ORIGINAL_KEYS = new Set([...ORIGINAL_KEYS, 'evidence_digest']);
99
+ const STORED_ORIGINAL_RECONCILIATION_KEYS = new Set([
100
+ ...VERIFIED_ORIGINAL_RECONCILIATION_KEYS,
101
+ 'request_digest',
102
+ ]);
103
+ STORED_ORIGINAL_RECONCILIATION_KEYS.delete('ok');
104
+ const STORED_REVOCATION_KEYS = new Set([
105
+ 'evidence_id', 'evidence_digest', 'target_operation_id', 'action_digest',
106
+ 'authority_id', 'revoked_at', 'effect', 'request_digest',
107
+ ]);
108
+ const STORED_DISPUTE_KEYS = new Set([
109
+ ...DISPUTE_KEYS, 'original_operation_id', 'original_action_digest',
110
+ 'request_digest',
111
+ ]);
112
+ const ATTEMPT_KEYS = new Set([
113
+ 'evidence_id', 'evidence_digest', 'dispute_id', 'original_operation_id',
114
+ 'remedy_operation_id', 'remedy_caid', 'remedy_action_digest',
115
+ 'consequence_mode', 'capability_template_digest', 'escrow_profile_digest',
116
+ 'destination_binding_digest', 'units', 'unit', 'authorized_at',
117
+ 'request_digest', 'status', 'claim_token_digest', 'claimed_at',
118
+ 'claim_request_digest', 'outcome', 'outcome_evidence',
119
+ 'finalize_request_digest', 'reconciliation', 'reconcile_request_digest',
120
+ ]);
121
+ const STORED_OUTCOME_KEYS = new Set([
122
+ ...OUTCOME_EVIDENCE_KEYS, 'remedy_operation_id', 'remedy_action_digest',
123
+ 'destination_binding_digest', 'units', 'unit', 'outcome',
124
+ ]);
125
+ const STORED_RESOLUTION_KEYS = new Set([
126
+ ...RESOLUTION_KEYS, 'dispute_id', 'request_digest',
127
+ ]);
128
+
129
+ type DataRecord = Record<string, any>;
130
+ type Failure = Readonly<{ ok: false; reason: string }>;
131
+
132
+ export interface RemedyProgramState extends Record<string, unknown> {
133
+ version: string;
134
+ instance_id: string;
135
+ status: string;
136
+ revision: number;
137
+ remedied_units: number;
138
+ remaining_units: number;
139
+ }
140
+
141
+ export interface RemedyProgramResult extends Record<string, unknown> {
142
+ ok: boolean;
143
+ reason?: string;
144
+ state?: RemedyProgramState;
145
+ }
146
+
147
+ export interface RemedyProgramStore {
148
+ readonly durable: boolean;
149
+ create(state: RemedyProgramState): Promise<RemedyProgramResult>;
150
+ get(input: { tenantId: string; instanceId: string }): Promise<RemedyProgramResult>;
151
+ compareAndSwap(input: {
152
+ tenantId: string;
153
+ instanceId: string;
154
+ expectedRevision: number;
155
+ state: RemedyProgramState;
156
+ }): Promise<RemedyProgramResult>;
157
+ }
158
+
159
+ export interface RemedyProgramKernelConfig extends Record<string, unknown> {
160
+ store: RemedyProgramStore;
161
+ verifyOriginalEffect: (input: Readonly<DataRecord>) => unknown | Promise<unknown>;
162
+ verifyRevocation: (input: Readonly<DataRecord>) => unknown | Promise<unknown>;
163
+ verifyDispute: (input: Readonly<DataRecord>) => unknown | Promise<unknown>;
164
+ verifyRemedyAuthorization: (input: Readonly<DataRecord>) => unknown | Promise<unknown>;
165
+ verifyRemedyOutcome: (input: Readonly<DataRecord>) => unknown | Promise<unknown>;
166
+ verifyOriginalReconciliation: (input: Readonly<DataRecord>) => unknown | Promise<unknown>;
167
+ verifyResolution?: (input: Readonly<DataRecord>) => unknown | Promise<unknown>;
168
+ now?: () => number;
169
+ maxDisputeAgeMs?: number;
170
+ allowEphemeralState?: boolean;
171
+ production?: boolean;
172
+ }
173
+
174
+ function deepFreeze<T>(value: T): Readonly<T> {
175
+ if (value === null || typeof value !== 'object') return value;
176
+ const stack: object[] = [value as object];
177
+ const seen = new WeakSet<object>();
178
+ while (stack.length > 0) {
179
+ const current = stack.pop()!;
180
+ if (seen.has(current)) continue;
181
+ seen.add(current);
182
+ for (const child of Object.values(current)) {
183
+ if (child !== null && typeof child === 'object') stack.push(child);
184
+ }
185
+ Object.freeze(current);
186
+ }
187
+ return value;
188
+ }
189
+
190
+ function snapshot<T>(value: T, label: string): Readonly<T> {
191
+ try {
192
+ canonicalize(value);
193
+ return deepFreeze(structuredClone(value));
194
+ } catch {
195
+ throw new TypeError(`${label} must be bounded canonical JSON`);
196
+ }
197
+ }
198
+
199
+ function clone<T>(value: T): T {
200
+ return structuredClone(value);
201
+ }
202
+
203
+ function isRecord(value: unknown): value is DataRecord {
204
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
205
+ const prototype = Object.getPrototypeOf(value);
206
+ return prototype === Object.prototype || prototype === null;
207
+ }
208
+
209
+ function isDataRecord(value: unknown): value is DataRecord {
210
+ return isRecord(value) && Reflect.ownKeys(value).every((key) => {
211
+ if (typeof key !== 'string') return false;
212
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
213
+ return descriptor?.enumerable === true && Object.hasOwn(descriptor, 'value');
214
+ });
215
+ }
216
+
217
+ function exactKeys(value: unknown, keys: Set<string>): value is DataRecord {
218
+ return isDataRecord(value)
219
+ && Reflect.ownKeys(value).length === keys.size
220
+ && Object.keys(value).every((key) => keys.has(key));
221
+ }
222
+
223
+ function validId(value: unknown): value is string {
224
+ return typeof value === 'string' && ID.test(value);
225
+ }
226
+
227
+ function validContext(value: unknown): value is string {
228
+ return typeof value === 'string' && value.length > 0
229
+ && Buffer.byteLength(value, 'utf8') <= MAX_CONTEXT_BYTES
230
+ && !/[\u0000-\u001f\u007f]/.test(value);
231
+ }
232
+
233
+ function instant(value: unknown): number {
234
+ if (typeof value !== 'string'
235
+ || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)) return NaN;
236
+ const parsed = Date.parse(value);
237
+ return Number.isFinite(parsed) && new Date(parsed).toISOString() === value ? parsed : NaN;
238
+ }
239
+
240
+ function digest(value: unknown): string {
241
+ return `sha256:${createHash('sha256').update(canonicalize(value)).digest('hex')}`;
242
+ }
243
+
244
+ function same(left: unknown, right: unknown): boolean {
245
+ try {
246
+ return canonicalize(left) === canonicalize(right);
247
+ } catch {
248
+ return false;
249
+ }
250
+ }
251
+
252
+ function fail(reason: string): Failure {
253
+ return Object.freeze({ ok: false, reason });
254
+ }
255
+
256
+ function isFailure(value: Readonly<DataRecord> | Failure): value is Failure {
257
+ return value.ok === false && typeof value.reason === 'string';
258
+ }
259
+
260
+ function pass(fields: DataRecord): Readonly<DataRecord> {
261
+ return deepFreeze(fields);
262
+ }
263
+
264
+ function withoutOk(projection: DataRecord): DataRecord {
265
+ const result: DataRecord = {};
266
+ for (const [key, value] of Object.entries(projection)) {
267
+ if (key !== 'ok') result[key] = clone(value);
268
+ }
269
+ return result;
270
+ }
271
+
272
+ function validOriginal(value: unknown, stored = false): value is DataRecord {
273
+ const keys = stored ? STORED_ORIGINAL_KEYS : ORIGINAL_KEYS;
274
+ return exactKeys(value, keys)
275
+ && typeof value.caid === 'string' && CAID.test(value.caid)
276
+ && typeof value.action_digest === 'string' && DIGEST.test(value.action_digest)
277
+ && validId(value.operation_id)
278
+ && ['receipt-program', 'action-escrow'].includes(value.consequence_mode)
279
+ && typeof value.consequence_digest === 'string' && DIGEST.test(value.consequence_digest)
280
+ && typeof value.terminal_evidence_digest === 'string' && DIGEST.test(value.terminal_evidence_digest)
281
+ && ['executed', 'indeterminate'].includes(value.outcome)
282
+ && Number.isFinite(instant(value.occurred_at))
283
+ && (!stored || (typeof value.evidence_digest === 'string' && DIGEST.test(value.evidence_digest)));
284
+ }
285
+
286
+ function validOriginalReconciliation(value: unknown): value is DataRecord {
287
+ return exactKeys(value, STORED_ORIGINAL_RECONCILIATION_KEYS)
288
+ && validEvidencePair(value.evidence_id, value.evidence_digest)
289
+ && validId(value.original_operation_id)
290
+ && typeof value.original_action_digest === 'string' && DIGEST.test(value.original_action_digest)
291
+ && typeof value.terminal_evidence_digest === 'string' && DIGEST.test(value.terminal_evidence_digest)
292
+ && ['executed', 'proved_no_effect'].includes(value.outcome)
293
+ && Number.isFinite(instant(value.observed_at))
294
+ && typeof value.request_digest === 'string' && DIGEST.test(value.request_digest);
295
+ }
296
+
297
+ function validDispute(value: unknown): value is DataRecord {
298
+ return exactKeys(value, STORED_DISPUTE_KEYS)
299
+ && validId(value.dispute_id) && validId(value.evidence_id)
300
+ && typeof value.evidence_digest === 'string' && DIGEST.test(value.evidence_digest)
301
+ && validContext(value.challenger_id)
302
+ && validId(value.original_operation_id)
303
+ && typeof value.original_action_digest === 'string' && DIGEST.test(value.original_action_digest)
304
+ && Number.isSafeInteger(value.requested_units) && value.requested_units > 0
305
+ && Number.isFinite(instant(value.opened_at))
306
+ && typeof value.request_digest === 'string' && DIGEST.test(value.request_digest);
307
+ }
308
+
309
+ function validStoredOutcome(value: unknown, reconciliation: boolean): value is DataRecord {
310
+ return exactKeys(value, STORED_OUTCOME_KEYS)
311
+ && validId(value.evidence_id)
312
+ && typeof value.evidence_digest === 'string' && DIGEST.test(value.evidence_digest)
313
+ && validId(value.remedy_operation_id)
314
+ && typeof value.remedy_action_digest === 'string' && DIGEST.test(value.remedy_action_digest)
315
+ && typeof value.destination_binding_digest === 'string' && DIGEST.test(value.destination_binding_digest)
316
+ && Number.isSafeInteger(value.units) && value.units > 0
317
+ && validContext(value.unit)
318
+ && (reconciliation
319
+ ? ['executed', 'proved_no_effect'].includes(value.outcome)
320
+ : ['executed', 'proved_no_effect', 'indeterminate'].includes(value.outcome))
321
+ && Number.isFinite(instant(value.observed_at));
322
+ }
323
+
324
+ function validRemedyOwner(value: DataRecord): boolean {
325
+ return (value.consequence_mode === 'receipt-program'
326
+ && typeof value.capability_template_digest === 'string'
327
+ && DIGEST.test(value.capability_template_digest)
328
+ && value.escrow_profile_digest === null)
329
+ || (value.consequence_mode === 'action-escrow'
330
+ && value.capability_template_digest === null
331
+ && typeof value.escrow_profile_digest === 'string'
332
+ && DIGEST.test(value.escrow_profile_digest));
333
+ }
334
+
335
+ function validAttempt(value: unknown): value is DataRecord {
336
+ if (!exactKeys(value, ATTEMPT_KEYS)
337
+ || !validId(value.evidence_id)
338
+ || typeof value.evidence_digest !== 'string' || !DIGEST.test(value.evidence_digest)
339
+ || !validId(value.dispute_id) || !validId(value.original_operation_id)
340
+ || !validId(value.remedy_operation_id)
341
+ || typeof value.remedy_caid !== 'string' || !CAID.test(value.remedy_caid)
342
+ || typeof value.remedy_action_digest !== 'string' || !DIGEST.test(value.remedy_action_digest)
343
+ || !validRemedyOwner(value)
344
+ || typeof value.destination_binding_digest !== 'string' || !DIGEST.test(value.destination_binding_digest)
345
+ || !Number.isSafeInteger(value.units) || value.units < 1
346
+ || !validContext(value.unit) || !Number.isFinite(instant(value.authorized_at))
347
+ || typeof value.request_digest !== 'string' || !DIGEST.test(value.request_digest)
348
+ || !['authorized', 'claimed', 'indeterminate', 'executed', 'proved_no_effect'].includes(value.status)) {
349
+ return false;
350
+ }
351
+ const claimed = value.status !== 'authorized';
352
+ if (claimed !== (typeof value.claim_token_digest === 'string' && DIGEST.test(value.claim_token_digest))
353
+ || claimed !== Number.isFinite(instant(value.claimed_at))
354
+ || claimed !== (typeof value.claim_request_digest === 'string' && DIGEST.test(value.claim_request_digest))) {
355
+ return false;
356
+ }
357
+ const finalized = ['indeterminate', 'executed', 'proved_no_effect'].includes(value.status);
358
+ if (finalized !== (typeof value.finalize_request_digest === 'string' && DIGEST.test(value.finalize_request_digest))
359
+ || finalized !== validStoredOutcome(value.outcome_evidence, false)
360
+ || finalized !== (typeof value.outcome === 'string')) return false;
361
+ const reconciled = value.reconciliation !== null;
362
+ if (reconciled !== (typeof value.reconcile_request_digest === 'string' && DIGEST.test(value.reconcile_request_digest))) {
363
+ return false;
364
+ }
365
+ if (reconciled && !validStoredOutcome(value.reconciliation, true)) return false;
366
+ return true;
367
+ }
368
+
369
+ function validState(value: unknown, tenantId?: string, instanceId?: string): value is DataRecord {
370
+ if (!exactKeys(value, STATE_KEYS)
371
+ || value.version !== REMEDY_PROGRAM_VERSION
372
+ || !validId(value.instance_id) || (instanceId !== undefined && value.instance_id !== instanceId)
373
+ || !validContext(value.tenant_id) || (tenantId !== undefined && value.tenant_id !== tenantId)
374
+ || !validContext(value.environment) || !validContext(value.audience)
375
+ || ![
376
+ 'effect_executed', 'effect_indeterminate', 'disputed', 'remedy_authorized',
377
+ 'remedy_claimed', 'remedy_indeterminate', 'partially_remedied',
378
+ 'remedied', 'resolved_no_remedy', 'original_proved_no_effect',
379
+ ].includes(value.status)
380
+ || !Number.isSafeInteger(value.revision) || value.revision < 0
381
+ || !Number.isFinite(instant(value.created_at)) || !Number.isFinite(instant(value.updated_at))
382
+ || instant(value.updated_at) < instant(value.created_at)
383
+ || !validOriginal(value.original, true)
384
+ || typeof value.remedy_profile_digest !== 'string' || !DIGEST.test(value.remedy_profile_digest)
385
+ || typeof value.destination_binding_digest !== 'string' || !DIGEST.test(value.destination_binding_digest)
386
+ || !Number.isSafeInteger(value.max_remedy_units) || value.max_remedy_units < 1
387
+ || !validContext(value.unit)
388
+ || !Number.isSafeInteger(value.remedied_units) || value.remedied_units < 0
389
+ || !Number.isSafeInteger(value.remaining_units) || value.remaining_units < 0
390
+ || value.remedied_units + value.remaining_units !== value.max_remedy_units
391
+ || !Array.isArray(value.used_evidence_ids) || !value.used_evidence_ids.every(validId)
392
+ || new Set(value.used_evidence_ids).size !== value.used_evidence_ids.length
393
+ || !Array.isArray(value.used_evidence_digests)
394
+ || !value.used_evidence_digests.every((entry: unknown) => typeof entry === 'string' && DIGEST.test(entry))
395
+ || new Set(value.used_evidence_digests).size !== value.used_evidence_digests.length
396
+ || !Array.isArray(value.remedies) || value.remedies.length > MAX_REMEDY_ATTEMPTS
397
+ || !value.remedies.every(validAttempt)
398
+ || (value.dispute !== null && !validDispute(value.dispute))
399
+ || (value.active_remedy !== null && !validAttempt(value.active_remedy))
400
+ || typeof value.create_request_digest !== 'string' || !DIGEST.test(value.create_request_digest)) {
401
+ return false;
402
+ }
403
+ if (value.revocation !== null && (!exactKeys(value.revocation, STORED_REVOCATION_KEYS)
404
+ || !validId(value.revocation.evidence_id)
405
+ || typeof value.revocation.evidence_digest !== 'string' || !DIGEST.test(value.revocation.evidence_digest)
406
+ || !validId(value.revocation.target_operation_id)
407
+ || typeof value.revocation.action_digest !== 'string' || !DIGEST.test(value.revocation.action_digest)
408
+ || !validContext(value.revocation.authority_id)
409
+ || !Number.isFinite(instant(value.revocation.revoked_at))
410
+ || value.revocation.effect !== 'future_authority_only'
411
+ || typeof value.revocation.request_digest !== 'string' || !DIGEST.test(value.revocation.request_digest))) {
412
+ return false;
413
+ }
414
+ if (value.original_reconciliation !== null
415
+ && !validOriginalReconciliation(value.original_reconciliation)) return false;
416
+ if (value.original_reconciliation !== null
417
+ && (value.original.outcome !== 'indeterminate'
418
+ || value.original_reconciliation.original_operation_id !== value.original.operation_id
419
+ || value.original_reconciliation.original_action_digest !== value.original.action_digest
420
+ || value.original_reconciliation.terminal_evidence_digest !== value.original.terminal_evidence_digest
421
+ || instant(value.original_reconciliation.observed_at) < instant(value.original.occurred_at))) {
422
+ return false;
423
+ }
424
+ if (value.resolution !== null && (!exactKeys(value.resolution, STORED_RESOLUTION_KEYS)
425
+ || !validId(value.resolution.evidence_id)
426
+ || typeof value.resolution.evidence_digest !== 'string' || !DIGEST.test(value.resolution.evidence_digest)
427
+ || value.resolution.outcome !== 'no_remedy'
428
+ || !Number.isFinite(instant(value.resolution.resolved_at))
429
+ || !validId(value.resolution.dispute_id)
430
+ || typeof value.resolution.request_digest !== 'string' || !DIGEST.test(value.resolution.request_digest))) {
431
+ return false;
432
+ }
433
+ const committed = value.remedies
434
+ .filter((attempt: DataRecord) => attempt.status === 'executed')
435
+ .reduce((total: number, attempt: DataRecord) => total + attempt.units, 0);
436
+ if (committed !== value.remedied_units || committed > value.max_remedy_units) return false;
437
+ const effectiveOriginalOutcome = value.original.outcome === 'executed'
438
+ ? 'executed' : value.original_reconciliation?.outcome ?? 'indeterminate';
439
+ if (value.status === 'effect_indeterminate' && effectiveOriginalOutcome !== 'indeterminate') return false;
440
+ if (value.status === 'original_proved_no_effect') {
441
+ if (effectiveOriginalOutcome !== 'proved_no_effect'
442
+ || value.active_remedy !== null || value.remedies.length !== 0
443
+ || value.remedied_units !== 0) return false;
444
+ } else if (effectiveOriginalOutcome === 'proved_no_effect') return false;
445
+ if (!['effect_indeterminate', 'original_proved_no_effect', 'disputed', 'resolved_no_remedy'].includes(value.status)
446
+ && effectiveOriginalOutcome !== 'executed') return false;
447
+ if (value.status === 'disputed' && effectiveOriginalOutcome === 'proved_no_effect') return false;
448
+ if (['remedy_authorized', 'remedy_claimed', 'remedy_indeterminate'].includes(value.status)
449
+ !== (value.active_remedy !== null)) return false;
450
+ return true;
451
+ }
452
+
453
+ function tokenDigest(token: string): string {
454
+ return digest({ claim_token: token });
455
+ }
456
+
457
+ function validEvidencePair(id: unknown, evidenceDigest: unknown): boolean {
458
+ return validId(id) && typeof evidenceDigest === 'string' && DIGEST.test(evidenceDigest);
459
+ }
460
+
461
+ function evidenceUsed(state: DataRecord, id: string, evidenceDigest: string): boolean {
462
+ return state.used_evidence_ids.includes(id) || state.used_evidence_digests.includes(evidenceDigest);
463
+ }
464
+
465
+ function consumeEvidence(state: DataRecord, id: string, evidenceDigest: string): void {
466
+ state.used_evidence_ids.push(id);
467
+ state.used_evidence_digests.push(evidenceDigest);
468
+ }
469
+
470
+ function expectedContext(state: DataRecord): Readonly<DataRecord> {
471
+ return snapshot({
472
+ instance_id: state.instance_id,
473
+ tenant_id: state.tenant_id,
474
+ environment: state.environment,
475
+ audience: state.audience,
476
+ original: clone(state.original),
477
+ original_reconciliation: state.original_reconciliation === null
478
+ ? null : clone(state.original_reconciliation),
479
+ remedy_profile_digest: state.remedy_profile_digest,
480
+ destination_binding_digest: state.destination_binding_digest,
481
+ max_remedy_units: state.max_remedy_units,
482
+ unit: state.unit,
483
+ dispute: state.dispute === null ? null : clone(state.dispute),
484
+ }, 'verifier expectation') as Readonly<DataRecord>;
485
+ }
486
+
487
+ /**
488
+ * In-process atomic CAS store. It is intentionally marked non-durable; callers
489
+ * selecting production mode must provide a durable external CAS store.
490
+ */
491
+ export function createRemedyMemoryStore(): RemedyProgramStore {
492
+ const records = new Map<string, DataRecord>();
493
+ const storageKey = (tenantId: string, instanceId: string) => `${tenantId.length}:${tenantId}${instanceId}`;
494
+ const evidenceIds = new Set<string>();
495
+ const evidenceDigests = new Set<string>();
496
+ const remedyOperations = new Set<string>();
497
+ const remedyActions = new Set<string>();
498
+ const remedyCaids = new Set<string>();
499
+ const claimKey = (tenantId: string, value: string) => `${tenantId.length}:${tenantId}${value}`;
500
+ const attempts = (state: DataRecord): DataRecord[] => [
501
+ ...(state.active_remedy === null ? [] : [state.active_remedy]),
502
+ ...state.remedies,
503
+ ];
504
+ const claimEvidence = (state: DataRecord) => {
505
+ for (const id of state.used_evidence_ids) evidenceIds.add(claimKey(state.tenant_id, id));
506
+ for (const value of state.used_evidence_digests) {
507
+ evidenceDigests.add(claimKey(state.tenant_id, value));
508
+ }
509
+ };
510
+ const claimAttempts = (state: DataRecord) => {
511
+ for (const attempt of attempts(state)) {
512
+ remedyOperations.add(claimKey(state.tenant_id, attempt.remedy_operation_id));
513
+ remedyActions.add(claimKey(state.tenant_id, attempt.remedy_action_digest));
514
+ remedyCaids.add(claimKey(state.tenant_id, attempt.remedy_caid));
515
+ }
516
+ };
517
+ return Object.freeze({
518
+ durable: false,
519
+ async create(state: RemedyProgramState) {
520
+ let candidate: Readonly<DataRecord>;
521
+ try {
522
+ candidate = snapshot(state, 'remedy state') as Readonly<DataRecord>;
523
+ } catch {
524
+ return fail('store_state_invalid');
525
+ }
526
+ if (!validState(candidate)) return fail('store_state_invalid');
527
+ const key = storageKey(candidate.tenant_id, candidate.instance_id);
528
+ if (records.has(key)) return fail('instance_exists');
529
+ if (candidate.used_evidence_ids.some((id: string) => evidenceIds.has(claimKey(candidate.tenant_id, id)))
530
+ || candidate.used_evidence_digests.some((value: string) => (
531
+ evidenceDigests.has(claimKey(candidate.tenant_id, value))
532
+ ))) return fail('evidence_replayed');
533
+ records.set(key, clone(candidate));
534
+ claimEvidence(candidate as DataRecord);
535
+ claimAttempts(candidate as DataRecord);
536
+ return { ok: true, state: clone(candidate) };
537
+ },
538
+ async get({ tenantId, instanceId }: { tenantId: string; instanceId: string }) {
539
+ const state = records.get(storageKey(tenantId, instanceId));
540
+ return state ? { ok: true, state: clone(state) } : fail('instance_not_found');
541
+ },
542
+ async compareAndSwap({ tenantId, instanceId, expectedRevision, state }: DataRecord) {
543
+ const key = storageKey(tenantId, instanceId);
544
+ const current = records.get(key);
545
+ if (!current) return fail('instance_not_found');
546
+ if (current.revision !== expectedRevision) return fail('revision_conflict');
547
+ let candidate: Readonly<DataRecord>;
548
+ try {
549
+ candidate = snapshot(state, 'remedy state') as Readonly<DataRecord>;
550
+ } catch {
551
+ return fail('store_state_invalid');
552
+ }
553
+ if (!validState(candidate, tenantId, instanceId)
554
+ || candidate.revision !== expectedRevision + 1
555
+ || instant(candidate.updated_at) < instant(current.updated_at)) {
556
+ return fail('store_state_invalid');
557
+ }
558
+ const newEvidenceIds = candidate.used_evidence_ids.filter((id: string) => (
559
+ !current.used_evidence_ids.includes(id)
560
+ ));
561
+ const newEvidenceDigests = candidate.used_evidence_digests.filter((value: string) => (
562
+ !current.used_evidence_digests.includes(value)
563
+ ));
564
+ if (newEvidenceIds.some((id: string) => evidenceIds.has(claimKey(tenantId, id)))
565
+ || newEvidenceDigests.some((value: string) => evidenceDigests.has(claimKey(tenantId, value)))) {
566
+ return fail('evidence_replayed');
567
+ }
568
+ const currentOperations = new Set(attempts(current).map((attempt) => attempt.remedy_operation_id));
569
+ const newAttempts = attempts(candidate as DataRecord).filter((attempt) => (
570
+ !currentOperations.has(attempt.remedy_operation_id)
571
+ ));
572
+ if (newAttempts.some((attempt) => (
573
+ remedyOperations.has(claimKey(tenantId, attempt.remedy_operation_id))
574
+ || remedyActions.has(claimKey(tenantId, attempt.remedy_action_digest))
575
+ || remedyCaids.has(claimKey(tenantId, attempt.remedy_caid))
576
+ ))) return fail('remedy_operation_replayed');
577
+ records.set(key, clone(candidate));
578
+ claimEvidence(candidate as DataRecord);
579
+ claimAttempts(candidate as DataRecord);
580
+ return { ok: true, state: clone(candidate) };
581
+ },
582
+ } as RemedyProgramStore);
583
+ }
584
+
585
+ function verifierInput(fields: DataRecord): Readonly<DataRecord> {
586
+ return snapshot(fields, 'verifier input') as Readonly<DataRecord>;
587
+ }
588
+
589
+ function validClock(value: unknown): value is number {
590
+ return Number.isSafeInteger(value) && Number(value) >= 0 && Number(value) <= 8_640_000_000_000_000;
591
+ }
592
+
593
+ /** Build a remedy kernel with all trust callbacks and store methods pinned. */
594
+ export function createRemedyProgramKernel(options: RemedyProgramKernelConfig) {
595
+ if (!isDataRecord(options) || Object.keys(options).some((key) => !CONFIG_KEYS.has(key))) {
596
+ throw new TypeError('remedy program configuration invalid');
597
+ }
598
+ const sourceStore = options.store;
599
+ if (!sourceStore || typeof sourceStore.create !== 'function'
600
+ || typeof sourceStore.get !== 'function'
601
+ || typeof sourceStore.compareAndSwap !== 'function') {
602
+ throw new TypeError('remedy program CAS store required');
603
+ }
604
+ for (const key of [
605
+ 'verifyOriginalEffect', 'verifyRevocation', 'verifyDispute',
606
+ 'verifyRemedyAuthorization', 'verifyRemedyOutcome', 'verifyOriginalReconciliation',
607
+ ] as const) {
608
+ if (typeof options[key] !== 'function') throw new TypeError(`${key} callback required`);
609
+ }
610
+ if (options.verifyResolution !== undefined && typeof options.verifyResolution !== 'function') {
611
+ throw new TypeError('verifyResolution callback invalid');
612
+ }
613
+ if (options.now !== undefined && typeof options.now !== 'function') {
614
+ throw new TypeError('remedy program clock invalid');
615
+ }
616
+ if (options.production === true && options.allowEphemeralState === true) {
617
+ throw new TypeError('production remedy program cannot allow ephemeral state');
618
+ }
619
+ if ((options.production === true || options.allowEphemeralState === false)
620
+ && sourceStore.durable !== true) {
621
+ throw new TypeError('durable remedy program CAS store required');
622
+ }
623
+ const maxDisputeAgeMs = options.maxDisputeAgeMs ?? DEFAULT_MAX_DISPUTE_AGE_MS;
624
+ if (!Number.isSafeInteger(maxDisputeAgeMs) || maxDisputeAgeMs < 1) {
625
+ throw new TypeError('maxDisputeAgeMs invalid');
626
+ }
627
+
628
+ const store = Object.freeze({
629
+ durable: sourceStore.durable === true,
630
+ create: sourceStore.create.bind(sourceStore),
631
+ get: sourceStore.get.bind(sourceStore),
632
+ compareAndSwap: sourceStore.compareAndSwap.bind(sourceStore),
633
+ });
634
+ const verifyOriginalEffect = options.verifyOriginalEffect;
635
+ const verifyRevocation = options.verifyRevocation;
636
+ const verifyDispute = options.verifyDispute;
637
+ const verifyRemedyAuthorization = options.verifyRemedyAuthorization;
638
+ const verifyRemedyOutcome = options.verifyRemedyOutcome;
639
+ const verifyOriginalReconciliation = options.verifyOriginalReconciliation;
640
+ const verifyResolution = options.verifyResolution;
641
+ const now = options.now ?? Date.now;
642
+
643
+ const clock = (): number => {
644
+ let value: unknown;
645
+ try {
646
+ value = now();
647
+ } catch {
648
+ return NaN;
649
+ }
650
+ return validClock(value) ? Number(value) : NaN;
651
+ };
652
+
653
+ const checkedState = (value: unknown, tenantId: string, instanceId: string): RemedyProgramResult => {
654
+ let state: Readonly<DataRecord>;
655
+ try {
656
+ state = snapshot(value, 'stored remedy state') as Readonly<DataRecord>;
657
+ } catch {
658
+ return fail('store_state_invalid');
659
+ }
660
+ return validState(state, tenantId, instanceId)
661
+ ? pass({ ok: true, state }) as RemedyProgramResult
662
+ : fail('store_state_invalid');
663
+ };
664
+
665
+ const normalizeStoreResult = (
666
+ value: unknown,
667
+ tenantId: string,
668
+ instanceId: string,
669
+ proposedState?: DataRecord,
670
+ ): RemedyProgramResult => {
671
+ if (!isDataRecord(value)) return fail('store_response_invalid');
672
+ if (value.ok === true) return checkedState(value.state ?? proposedState, tenantId, instanceId);
673
+ if (value.applied === true && proposedState) return checkedState(value.state ?? proposedState, tenantId, instanceId);
674
+ const reason = value.reason;
675
+ if ([
676
+ 'instance_not_found', 'instance_exists', 'revision_conflict', 'clock_regression',
677
+ 'evidence_replayed', 'remedy_operation_replayed',
678
+ ].includes(reason)) {
679
+ return fail(reason);
680
+ }
681
+ if (value.ok === false || value.applied === false) return fail('store_operation_failed');
682
+ return fail('store_response_invalid');
683
+ };
684
+
685
+ const load = async (
686
+ tenantId: unknown,
687
+ instanceId: unknown,
688
+ creationProbe = false,
689
+ ): Promise<RemedyProgramResult> => {
690
+ if (!validContext(tenantId) || !validId(instanceId)) return fail('remedy_lookup_invalid');
691
+ try {
692
+ const result = normalizeStoreResult(
693
+ await store.get({ tenantId, instanceId }), tenantId, instanceId,
694
+ );
695
+ return result.reason === 'instance_not_found' && !creationProbe
696
+ ? fail('remedy_case_not_found') : result;
697
+ } catch {
698
+ return fail('store_unavailable');
699
+ }
700
+ };
701
+
702
+ const commit = async (state: DataRecord, next: DataRecord): Promise<RemedyProgramResult> => {
703
+ try {
704
+ const result = normalizeStoreResult(await store.compareAndSwap({
705
+ tenantId: state.tenant_id,
706
+ instanceId: state.instance_id,
707
+ expectedRevision: state.revision,
708
+ state: next as RemedyProgramState,
709
+ }), state.tenant_id, state.instance_id, next);
710
+ return result.reason === 'revision_conflict' ? fail('state_transition_conflict') : result;
711
+ } catch {
712
+ return fail('store_unavailable');
713
+ }
714
+ };
715
+
716
+ const operationInput = (input: unknown, keys: Set<string>, reason: string): Readonly<DataRecord> | Failure => {
717
+ let value: Readonly<DataRecord>;
718
+ try {
719
+ value = snapshot(input, reason) as Readonly<DataRecord>;
720
+ } catch {
721
+ return fail(reason);
722
+ }
723
+ return exactKeys(value, keys) ? value : fail(reason);
724
+ };
725
+
726
+ const transitionTime = (state: DataRecord): number | Failure => {
727
+ const current = clock();
728
+ if (!Number.isFinite(current)) return fail('clock_invalid');
729
+ if (current < instant(state.updated_at)) return fail('clock_regression');
730
+ return current;
731
+ };
732
+
733
+ const touch = (state: DataRecord, at: number): DataRecord => {
734
+ const next = clone(state);
735
+ next.revision += 1;
736
+ next.updated_at = new Date(at).toISOString();
737
+ return next;
738
+ };
739
+
740
+ const invoke = async (
741
+ callback: (input: Readonly<DataRecord>) => unknown | Promise<unknown>,
742
+ input: DataRecord,
743
+ ): Promise<Readonly<DataRecord> | null> => {
744
+ try {
745
+ const result = await callback(verifierInput(input));
746
+ return snapshot(result, 'verifier projection') as Readonly<DataRecord>;
747
+ } catch {
748
+ return null;
749
+ }
750
+ };
751
+
752
+ async function create(input: unknown): Promise<RemedyProgramResult> {
753
+ const parsed = operationInput(input, CREATE_KEYS, 'create_input_invalid');
754
+ if (isFailure(parsed)) return parsed;
755
+ const value = parsed as Readonly<DataRecord>;
756
+ if (!validId(value.instanceId) || !validContext(value.tenantId)
757
+ || !validContext(value.environment) || !validContext(value.audience)
758
+ || !validOriginal(value.original)
759
+ || typeof value.remedyProfileDigest !== 'string' || !DIGEST.test(value.remedyProfileDigest)
760
+ || typeof value.destinationBindingDigest !== 'string' || !DIGEST.test(value.destinationBindingDigest)
761
+ || !Number.isSafeInteger(value.maxRemedyUnits) || value.maxRemedyUnits < 1
762
+ || !validContext(value.unit) || !isDataRecord(value.evidence)) {
763
+ return fail('create_input_invalid');
764
+ }
765
+ const requestDigest = digest(value);
766
+ const existing = await load(value.tenantId, value.instanceId, true);
767
+ if (existing.ok) {
768
+ return existing.state!.create_request_digest === requestDigest
769
+ ? pass({ ok: true, idempotent: true, state: existing.state }) as RemedyProgramResult
770
+ : fail('instance_exists');
771
+ }
772
+ if (existing.reason !== 'instance_not_found') return existing;
773
+ const current = clock();
774
+ if (!Number.isFinite(current)) return fail('clock_invalid');
775
+ if (instant(value.original.occurred_at) > current) return fail('original_effect_time_invalid');
776
+ const verified = await invoke(verifyOriginalEffect, {
777
+ original: clone(value.original),
778
+ evidence: clone(value.evidence),
779
+ expected: {
780
+ instance_id: value.instanceId,
781
+ tenant_id: value.tenantId,
782
+ environment: value.environment,
783
+ audience: value.audience,
784
+ remedy_profile_digest: value.remedyProfileDigest,
785
+ destination_binding_digest: value.destinationBindingDigest,
786
+ max_remedy_units: value.maxRemedyUnits,
787
+ unit: value.unit,
788
+ },
789
+ });
790
+ if (!verified || verified.ok !== true || !exactKeys(verified, VERIFIED_ORIGINAL_KEYS)) {
791
+ return fail('original_effect_invalid');
792
+ }
793
+ if (!ORIGINAL_KEYS.size || [...ORIGINAL_KEYS].some((key) => !same(verified[key], value.original[key]))
794
+ || verified.evidence_digest !== value.original.terminal_evidence_digest) {
795
+ return fail('original_effect_binding_mismatch');
796
+ }
797
+ const at = new Date(current).toISOString();
798
+ const state: DataRecord = {
799
+ version: REMEDY_PROGRAM_VERSION,
800
+ instance_id: value.instanceId,
801
+ tenant_id: value.tenantId,
802
+ environment: value.environment,
803
+ audience: value.audience,
804
+ status: value.original.outcome === 'executed' ? 'effect_executed' : 'effect_indeterminate',
805
+ revision: 0,
806
+ created_at: at,
807
+ updated_at: at,
808
+ original: withoutOk(verified as DataRecord),
809
+ remedy_profile_digest: value.remedyProfileDigest,
810
+ destination_binding_digest: value.destinationBindingDigest,
811
+ max_remedy_units: value.maxRemedyUnits,
812
+ unit: value.unit,
813
+ remedied_units: 0,
814
+ remaining_units: value.maxRemedyUnits,
815
+ used_evidence_ids: [],
816
+ used_evidence_digests: [value.original.terminal_evidence_digest],
817
+ original_reconciliation: null,
818
+ revocation: null,
819
+ dispute: null,
820
+ active_remedy: null,
821
+ remedies: [],
822
+ resolution: null,
823
+ create_request_digest: requestDigest,
824
+ };
825
+ if (!validState(state, value.tenantId, value.instanceId)) return fail('original_effect_invalid');
826
+ try {
827
+ const result = normalizeStoreResult(
828
+ await store.create(snapshot(state, 'initial remedy state') as RemedyProgramState),
829
+ value.tenantId,
830
+ value.instanceId,
831
+ state,
832
+ );
833
+ if (result.reason !== 'instance_exists') return result;
834
+ const raced = await load(value.tenantId, value.instanceId, true);
835
+ return raced.ok && raced.state!.create_request_digest === requestDigest
836
+ ? pass({ ok: true, idempotent: true, state: raced.state }) as RemedyProgramResult
837
+ : fail('instance_exists');
838
+ } catch {
839
+ return fail('store_unavailable');
840
+ }
841
+ }
842
+
843
+ async function reconcileOriginalEffect(input: unknown): Promise<RemedyProgramResult> {
844
+ const parsed = operationInput(
845
+ input,
846
+ ORIGINAL_RECONCILIATION_INPUT_KEYS,
847
+ 'original_reconciliation_input_invalid',
848
+ );
849
+ if (isFailure(parsed)) return parsed;
850
+ const value = parsed as Readonly<DataRecord>;
851
+ if (!validContext(value.tenantId) || !validId(value.instanceId)
852
+ || !['executed', 'proved_no_effect'].includes(value.outcome)
853
+ || !exactKeys(value.evidence, ORIGINAL_RECONCILIATION_EVIDENCE_KEYS)
854
+ || !validEvidencePair(value.evidence.evidence_id, value.evidence.evidence_digest)
855
+ || !Number.isFinite(instant(value.evidence.observed_at))) {
856
+ return fail('original_reconciliation_input_invalid');
857
+ }
858
+ const loaded = await load(value.tenantId, value.instanceId);
859
+ if (!loaded.ok) return loaded;
860
+ const state = loaded.state as DataRecord;
861
+ const requestDigest = digest(value);
862
+ if (state.original.outcome !== 'indeterminate') return fail('original_effect_already_conclusive');
863
+ if (state.original_reconciliation !== null) {
864
+ return state.original_reconciliation.request_digest === requestDigest
865
+ ? pass({ ok: true, idempotent: true, state }) as RemedyProgramResult
866
+ : fail('original_reconciliation_conflict');
867
+ }
868
+ if (!['effect_indeterminate', 'disputed'].includes(state.status)
869
+ || state.active_remedy !== null || state.remedies.length !== 0) {
870
+ return fail('original_reconciliation_unavailable');
871
+ }
872
+ if (evidenceUsed(state, value.evidence.evidence_id, value.evidence.evidence_digest)) {
873
+ return fail('evidence_replayed');
874
+ }
875
+ const at = transitionTime(state);
876
+ if (typeof at !== 'number') return at;
877
+ const observedAt = instant(value.evidence.observed_at);
878
+ if (observedAt < instant(state.original.occurred_at) || observedAt > at) {
879
+ return fail('original_reconciliation_time_invalid');
880
+ }
881
+ const verified = await invoke(verifyOriginalReconciliation, {
882
+ evidence: clone(value.evidence), outcome: value.outcome,
883
+ expected: expectedContext(state),
884
+ });
885
+ if (!verified || verified.ok !== true
886
+ || !exactKeys(verified, VERIFIED_ORIGINAL_RECONCILIATION_KEYS)) {
887
+ return fail('original_reconciliation_invalid');
888
+ }
889
+ if (verified.evidence_id !== value.evidence.evidence_id
890
+ || verified.evidence_digest !== value.evidence.evidence_digest
891
+ || verified.original_operation_id !== state.original.operation_id
892
+ || verified.original_action_digest !== state.original.action_digest
893
+ || verified.terminal_evidence_digest !== state.original.terminal_evidence_digest
894
+ || verified.outcome !== value.outcome
895
+ || verified.observed_at !== value.evidence.observed_at) {
896
+ return fail('original_reconciliation_binding_mismatch');
897
+ }
898
+ const next = touch(state, at);
899
+ next.original_reconciliation = {
900
+ ...withoutOk(verified as DataRecord), request_digest: requestDigest,
901
+ };
902
+ next.status = value.outcome === 'executed'
903
+ ? (state.dispute === null ? 'effect_executed' : 'disputed')
904
+ : 'original_proved_no_effect';
905
+ consumeEvidence(next, verified.evidence_id, verified.evidence_digest);
906
+ const committed = await commit(state, next);
907
+ return committed.ok ? pass({ ok: true, state: committed.state }) as RemedyProgramResult : committed;
908
+ }
909
+
910
+ async function recordRevocation(input: unknown): Promise<RemedyProgramResult> {
911
+ const parsed = operationInput(input, REVOCATION_INPUT_KEYS, 'revocation_input_invalid');
912
+ if (isFailure(parsed)) return parsed;
913
+ const value = parsed as Readonly<DataRecord>;
914
+ if (!validContext(value.tenantId) || !validId(value.instanceId)
915
+ || !exactKeys(value.evidence, REVOCATION_EVIDENCE_KEYS)
916
+ || !validEvidencePair(value.evidence.id, value.evidence.digest)) {
917
+ return fail('revocation_input_invalid');
918
+ }
919
+ const loaded = await load(value.tenantId, value.instanceId);
920
+ if (!loaded.ok) return loaded;
921
+ const state = loaded.state as DataRecord;
922
+ const requestDigest = digest(value);
923
+ if (state.revocation !== null) {
924
+ return state.revocation.request_digest === requestDigest
925
+ ? pass({ ok: true, idempotent: true, code: 'late_revocation_recorded', state }) as RemedyProgramResult
926
+ : fail('revocation_already_recorded');
927
+ }
928
+ if (evidenceUsed(state, value.evidence.id, value.evidence.digest)) return fail('evidence_replayed');
929
+ const at = transitionTime(state);
930
+ if (typeof at !== 'number') return at;
931
+ const verified = await invoke(verifyRevocation, {
932
+ evidence: clone(value.evidence), expected: expectedContext(state),
933
+ });
934
+ if (!verified || verified.ok !== true || !exactKeys(verified, VERIFIED_REVOCATION_KEYS)) {
935
+ return fail('revocation_invalid');
936
+ }
937
+ if (verified.evidence_id !== value.evidence.id
938
+ || verified.evidence_digest !== value.evidence.digest
939
+ || verified.target_operation_id !== state.original.operation_id
940
+ || verified.action_digest !== state.original.action_digest) {
941
+ return fail('revocation_binding_mismatch');
942
+ }
943
+ const revokedAt = instant(verified.revoked_at);
944
+ if (!validContext(verified.authority_id) || !Number.isFinite(revokedAt)
945
+ || revokedAt < instant(state.original.occurred_at) || revokedAt > at) {
946
+ return fail('revocation_time_invalid');
947
+ }
948
+ const next = touch(state, at);
949
+ next.revocation = {
950
+ ...withoutOk(verified as DataRecord),
951
+ effect: 'future_authority_only',
952
+ request_digest: requestDigest,
953
+ };
954
+ consumeEvidence(next, verified.evidence_id, verified.evidence_digest);
955
+ const committed = await commit(state, next);
956
+ return committed.ok
957
+ ? pass({ ok: true, code: 'late_revocation_recorded', state: committed.state }) as RemedyProgramResult
958
+ : committed;
959
+ }
960
+
961
+ async function openDispute(input: unknown): Promise<RemedyProgramResult> {
962
+ const parsed = operationInput(input, DISPUTE_INPUT_KEYS, 'dispute_input_invalid');
963
+ if (isFailure(parsed)) return parsed;
964
+ const value = parsed as Readonly<DataRecord>;
965
+ if (!validContext(value.tenantId) || !validId(value.instanceId)
966
+ || !exactKeys(value.dispute, DISPUTE_KEYS)
967
+ || !validId(value.dispute.dispute_id)
968
+ || !validEvidencePair(value.dispute.evidence_id, value.dispute.evidence_digest)
969
+ || !validContext(value.dispute.challenger_id)
970
+ || !Number.isSafeInteger(value.dispute.requested_units) || value.dispute.requested_units < 1
971
+ || !Number.isFinite(instant(value.dispute.opened_at))) {
972
+ return fail('dispute_input_invalid');
973
+ }
974
+ const loaded = await load(value.tenantId, value.instanceId);
975
+ if (!loaded.ok) return loaded;
976
+ const state = loaded.state as DataRecord;
977
+ const requestDigest = digest(value);
978
+ if (state.dispute !== null) {
979
+ return state.dispute.request_digest === requestDigest
980
+ ? pass({ ok: true, idempotent: true, state }) as RemedyProgramResult
981
+ : fail('dispute_already_open');
982
+ }
983
+ if (!['effect_executed', 'effect_indeterminate'].includes(state.status)) {
984
+ return fail('remedy_case_unavailable');
985
+ }
986
+ if (evidenceUsed(state, value.dispute.evidence_id, value.dispute.evidence_digest)) {
987
+ return fail('evidence_replayed');
988
+ }
989
+ const at = transitionTime(state);
990
+ if (typeof at !== 'number') return at;
991
+ const openedAt = instant(value.dispute.opened_at);
992
+ if (openedAt < instant(state.original.occurred_at) || openedAt > at || at - openedAt > maxDisputeAgeMs) {
993
+ return fail('dispute_not_fresh');
994
+ }
995
+ if (value.dispute.requested_units > state.remaining_units) return fail('dispute_limit_exceeded');
996
+ const verified = await invoke(verifyDispute, {
997
+ dispute: clone(value.dispute), expected: expectedContext(state),
998
+ });
999
+ if (!verified || verified.ok !== true || !exactKeys(verified, VERIFIED_DISPUTE_KEYS)) {
1000
+ return fail('dispute_invalid');
1001
+ }
1002
+ if ([...DISPUTE_KEYS].some((key) => !same(verified[key], value.dispute[key]))
1003
+ || verified.original_operation_id !== state.original.operation_id
1004
+ || verified.original_action_digest !== state.original.action_digest) {
1005
+ return fail('dispute_binding_mismatch');
1006
+ }
1007
+ const next = touch(state, at);
1008
+ next.status = 'disputed';
1009
+ next.dispute = { ...withoutOk(verified as DataRecord), request_digest: requestDigest };
1010
+ consumeEvidence(next, verified.evidence_id, verified.evidence_digest);
1011
+ const committed = await commit(state, next);
1012
+ return committed.ok ? pass({ ok: true, state: committed.state }) as RemedyProgramResult : committed;
1013
+ }
1014
+
1015
+ const allAttempts = (state: DataRecord): DataRecord[] => [
1016
+ ...(state.active_remedy ? [state.active_remedy] : []), ...state.remedies,
1017
+ ];
1018
+
1019
+ async function authorizeRemedy(input: unknown): Promise<RemedyProgramResult> {
1020
+ const parsed = operationInput(input, AUTHORIZATION_INPUT_KEYS, 'remedy_authorization_input_invalid');
1021
+ if (isFailure(parsed)) return parsed;
1022
+ const value = parsed as Readonly<DataRecord>;
1023
+ if (!validContext(value.tenantId) || !validId(value.instanceId)
1024
+ || !exactKeys(value.authorization, AUTHORIZATION_KEYS)
1025
+ || !validEvidencePair(value.authorization.evidence_id, value.authorization.evidence_digest)
1026
+ || !validId(value.authorization.remedy_operation_id)
1027
+ || typeof value.authorization.remedy_caid !== 'string' || !CAID.test(value.authorization.remedy_caid)
1028
+ || typeof value.authorization.remedy_action_digest !== 'string'
1029
+ || !DIGEST.test(value.authorization.remedy_action_digest)
1030
+ || !Number.isSafeInteger(value.authorization.units) || value.authorization.units < 1
1031
+ || !Number.isFinite(instant(value.authorization.authorized_at))) {
1032
+ return fail('remedy_authorization_input_invalid');
1033
+ }
1034
+ if (!validRemedyOwner(value.authorization)) return fail('remedy_owner_invalid');
1035
+ const loaded = await load(value.tenantId, value.instanceId);
1036
+ if (!loaded.ok) return loaded;
1037
+ const state = loaded.state as DataRecord;
1038
+ const requestDigest = digest(value);
1039
+ if (state.status === 'original_proved_no_effect') return fail('remedy_case_terminal');
1040
+ if (state.original.outcome === 'indeterminate'
1041
+ && state.original_reconciliation?.outcome !== 'executed') {
1042
+ return fail('original_effect_indeterminate');
1043
+ }
1044
+ const priorOperation = allAttempts(state).find((attempt) => (
1045
+ attempt.remedy_operation_id === value.authorization.remedy_operation_id
1046
+ ));
1047
+ if (priorOperation) {
1048
+ return priorOperation.request_digest === requestDigest
1049
+ ? pass({ ok: true, idempotent: true, state }) as RemedyProgramResult
1050
+ : fail('remedy_operation_replayed');
1051
+ }
1052
+ if (state.remaining_units === 0) return fail('remedy_limit_exhausted');
1053
+ if (['remedied', 'resolved_no_remedy'].includes(state.status)) return fail('remedy_case_terminal');
1054
+ if (state.active_remedy !== null) {
1055
+ return state.status === 'remedy_indeterminate'
1056
+ ? fail('remedy_indeterminate') : fail('remedy_already_active');
1057
+ }
1058
+ if (!['disputed', 'partially_remedied'].includes(state.status) || state.dispute === null) {
1059
+ return fail('remedy_case_unavailable');
1060
+ }
1061
+ if (evidenceUsed(state, value.authorization.evidence_id, value.authorization.evidence_digest)) {
1062
+ return fail('evidence_replayed');
1063
+ }
1064
+ const at = transitionTime(state);
1065
+ if (typeof at !== 'number') return at;
1066
+ if (instant(value.authorization.authorized_at) > at
1067
+ || instant(value.authorization.authorized_at) < instant(state.dispute.opened_at)) {
1068
+ return fail('remedy_authorization_time_invalid');
1069
+ }
1070
+ const disputeRemaining = state.dispute.requested_units - state.remedied_units;
1071
+ if (value.authorization.units > state.remaining_units || value.authorization.units > disputeRemaining) {
1072
+ return fail('remedy_limit_exceeded');
1073
+ }
1074
+ if (value.authorization.remedy_operation_id === state.original.operation_id
1075
+ || value.authorization.remedy_action_digest === state.original.action_digest) {
1076
+ return fail('remedy_must_be_compensating');
1077
+ }
1078
+ const verified = await invoke(verifyRemedyAuthorization, {
1079
+ authorization: clone(value.authorization), expected: expectedContext(state),
1080
+ });
1081
+ if (!verified || verified.ok !== true || !exactKeys(verified, VERIFIED_AUTHORIZATION_KEYS)) {
1082
+ return fail('remedy_authorization_invalid');
1083
+ }
1084
+ if (!validRemedyOwner(verified as DataRecord)) {
1085
+ return fail('remedy_owner_invalid');
1086
+ }
1087
+ if ([...AUTHORIZATION_KEYS].some((key) => !same(verified[key], value.authorization[key]))
1088
+ || verified.dispute_id !== state.dispute.dispute_id
1089
+ || verified.original_operation_id !== state.original.operation_id
1090
+ || verified.destination_binding_digest !== state.destination_binding_digest
1091
+ || verified.unit !== state.unit) {
1092
+ return fail('remedy_authorization_binding_mismatch');
1093
+ }
1094
+ const next = touch(state, at);
1095
+ next.status = 'remedy_authorized';
1096
+ next.active_remedy = {
1097
+ ...withoutOk(verified as DataRecord),
1098
+ request_digest: requestDigest,
1099
+ status: 'authorized',
1100
+ claim_token_digest: null,
1101
+ claimed_at: null,
1102
+ claim_request_digest: null,
1103
+ outcome: null,
1104
+ outcome_evidence: null,
1105
+ finalize_request_digest: null,
1106
+ reconciliation: null,
1107
+ reconcile_request_digest: null,
1108
+ };
1109
+ consumeEvidence(next, verified.evidence_id, verified.evidence_digest);
1110
+ const committed = await commit(state, next);
1111
+ return committed.ok ? pass({ ok: true, state: committed.state }) as RemedyProgramResult : committed;
1112
+ }
1113
+
1114
+ async function claimRemedy(input: unknown): Promise<RemedyProgramResult> {
1115
+ const parsed = operationInput(input, CLAIM_KEYS, 'remedy_claim_input_invalid');
1116
+ if (isFailure(parsed)) return parsed;
1117
+ const value = parsed as Readonly<DataRecord>;
1118
+ if (!validContext(value.tenantId) || !validId(value.instanceId)
1119
+ || !validId(value.remedyOperationId)
1120
+ || !validContext(value.claimToken) || Buffer.byteLength(value.claimToken, 'utf8') > 256) {
1121
+ return fail('remedy_claim_input_invalid');
1122
+ }
1123
+ const loaded = await load(value.tenantId, value.instanceId);
1124
+ if (!loaded.ok) return loaded;
1125
+ const state = loaded.state as DataRecord;
1126
+ if (state.status === 'original_proved_no_effect') return fail('remedy_case_terminal');
1127
+ if (state.original.outcome === 'indeterminate'
1128
+ && state.original_reconciliation?.outcome !== 'executed') {
1129
+ return fail('original_effect_indeterminate');
1130
+ }
1131
+ const requestDigest = digest(value);
1132
+ const prior = allAttempts(state).find((attempt) => attempt.remedy_operation_id === value.remedyOperationId);
1133
+ if (prior?.claim_request_digest === requestDigest) {
1134
+ if (state.status === 'remedy_indeterminate' && state.active_remedy === prior) {
1135
+ return fail('remedy_indeterminate');
1136
+ }
1137
+ return pass({ ok: true, idempotent: true, state }) as RemedyProgramResult;
1138
+ }
1139
+ if (state.status === 'remedy_indeterminate') return fail('remedy_indeterminate');
1140
+ if (!state.active_remedy) return prior ? fail('remedy_already_finalized') : fail('remedy_not_authorized');
1141
+ if (state.active_remedy.remedy_operation_id !== value.remedyOperationId) {
1142
+ return fail('remedy_operation_mismatch');
1143
+ }
1144
+ if (state.active_remedy.status === 'claimed') return fail('remedy_claim_owned');
1145
+ if (state.active_remedy.status !== 'authorized' || state.status !== 'remedy_authorized') {
1146
+ return fail('remedy_not_authorized');
1147
+ }
1148
+ const at = transitionTime(state);
1149
+ if (typeof at !== 'number') return at;
1150
+ const next = touch(state, at);
1151
+ next.status = 'remedy_claimed';
1152
+ next.active_remedy.status = 'claimed';
1153
+ next.active_remedy.claim_token_digest = tokenDigest(value.claimToken);
1154
+ next.active_remedy.claimed_at = new Date(at).toISOString();
1155
+ next.active_remedy.claim_request_digest = requestDigest;
1156
+ const committed = await commit(state, next);
1157
+ return committed.ok ? pass({ ok: true, state: committed.state }) as RemedyProgramResult : committed;
1158
+ }
1159
+
1160
+ const outcomeExpected = (state: DataRecord, attempt: DataRecord): DataRecord => ({
1161
+ ...clone(expectedContext(state)),
1162
+ remedy_operation_id: attempt.remedy_operation_id,
1163
+ remedy_caid: attempt.remedy_caid,
1164
+ remedy_action_digest: attempt.remedy_action_digest,
1165
+ destination_binding_digest: attempt.destination_binding_digest,
1166
+ units: attempt.units,
1167
+ unit: attempt.unit,
1168
+ });
1169
+
1170
+ const outcomeBound = (
1171
+ verified: DataRecord,
1172
+ value: DataRecord,
1173
+ attempt: DataRecord,
1174
+ ): boolean => verified.evidence_id === value.evidence.evidence_id
1175
+ && verified.evidence_digest === value.evidence.evidence_digest
1176
+ && verified.remedy_operation_id === attempt.remedy_operation_id
1177
+ && verified.remedy_action_digest === attempt.remedy_action_digest
1178
+ && verified.destination_binding_digest === attempt.destination_binding_digest
1179
+ && verified.units === attempt.units && verified.unit === attempt.unit
1180
+ && verified.outcome === value.outcome
1181
+ && verified.observed_at === value.evidence.observed_at;
1182
+
1183
+ async function finalizeRemedy(input: unknown): Promise<RemedyProgramResult> {
1184
+ const parsed = operationInput(input, FINALIZE_KEYS, 'remedy_outcome_input_invalid');
1185
+ if (isFailure(parsed)) return parsed;
1186
+ const value = parsed as Readonly<DataRecord>;
1187
+ if (!validContext(value.tenantId) || !validId(value.instanceId)
1188
+ || !validId(value.remedyOperationId)
1189
+ || !validContext(value.claimToken) || Buffer.byteLength(value.claimToken, 'utf8') > 256
1190
+ || !['executed', 'proved_no_effect', 'indeterminate'].includes(value.outcome)
1191
+ || !exactKeys(value.evidence, OUTCOME_EVIDENCE_KEYS)
1192
+ || !validEvidencePair(value.evidence.evidence_id, value.evidence.evidence_digest)
1193
+ || !Number.isFinite(instant(value.evidence.observed_at))) {
1194
+ return fail('remedy_outcome_input_invalid');
1195
+ }
1196
+ const loaded = await load(value.tenantId, value.instanceId);
1197
+ if (!loaded.ok) return loaded;
1198
+ const state = loaded.state as DataRecord;
1199
+ const requestDigest = digest(value);
1200
+ const prior = allAttempts(state).find((attempt) => attempt.remedy_operation_id === value.remedyOperationId);
1201
+ if (prior?.finalize_request_digest === requestDigest) {
1202
+ return pass({ ok: true, idempotent: true, state }) as RemedyProgramResult;
1203
+ }
1204
+ if (state.status === 'remedy_indeterminate') return fail('remedy_indeterminate');
1205
+ if (!state.active_remedy) return prior ? fail('remedy_already_finalized') : fail('remedy_not_authorized');
1206
+ const attempt = state.active_remedy;
1207
+ if (attempt.remedy_operation_id !== value.remedyOperationId) return fail('remedy_operation_mismatch');
1208
+ if (attempt.status !== 'claimed' || state.status !== 'remedy_claimed') return fail('remedy_not_claimed');
1209
+ if (attempt.claim_token_digest !== tokenDigest(value.claimToken)) return fail('remedy_claim_owned');
1210
+ if (evidenceUsed(state, value.evidence.evidence_id, value.evidence.evidence_digest)) {
1211
+ return fail('evidence_replayed');
1212
+ }
1213
+ const at = transitionTime(state);
1214
+ if (typeof at !== 'number') return at;
1215
+ if (instant(value.evidence.observed_at) < instant(attempt.authorized_at)
1216
+ || instant(value.evidence.observed_at) > at) return fail('remedy_outcome_time_invalid');
1217
+ const verified = await invoke(verifyRemedyOutcome, {
1218
+ evidence: clone(value.evidence), outcome: value.outcome,
1219
+ expected: outcomeExpected(state, attempt), reconciliation: false,
1220
+ });
1221
+ if (!verified || verified.ok !== true || !exactKeys(verified, VERIFIED_OUTCOME_KEYS)) {
1222
+ return fail('remedy_outcome_invalid');
1223
+ }
1224
+ if (!outcomeBound(verified as DataRecord, value as DataRecord, attempt)) {
1225
+ return fail('remedy_outcome_binding_mismatch');
1226
+ }
1227
+ if (value.outcome === 'executed' && attempt.units > state.remaining_units) {
1228
+ return fail('remedy_limit_exceeded');
1229
+ }
1230
+ const next = touch(state, at);
1231
+ const completed = next.active_remedy;
1232
+ completed.status = value.outcome;
1233
+ completed.outcome = value.outcome;
1234
+ completed.outcome_evidence = withoutOk(verified as DataRecord);
1235
+ completed.finalize_request_digest = requestDigest;
1236
+ consumeEvidence(next, verified.evidence_id, verified.evidence_digest);
1237
+ if (value.outcome === 'indeterminate') {
1238
+ next.status = 'remedy_indeterminate';
1239
+ } else {
1240
+ next.active_remedy = null;
1241
+ next.remedies.push(completed);
1242
+ if (value.outcome === 'executed') {
1243
+ next.remedied_units += completed.units;
1244
+ next.remaining_units -= completed.units;
1245
+ next.status = next.remaining_units === 0 ? 'remedied' : 'partially_remedied';
1246
+ } else {
1247
+ next.status = 'disputed';
1248
+ }
1249
+ }
1250
+ const committed = await commit(state, next);
1251
+ return committed.ok ? pass({ ok: true, state: committed.state }) as RemedyProgramResult : committed;
1252
+ }
1253
+
1254
+ async function reconcileRemedy(input: unknown): Promise<RemedyProgramResult> {
1255
+ const parsed = operationInput(input, RECONCILE_KEYS, 'remedy_reconciliation_input_invalid');
1256
+ if (isFailure(parsed)) return parsed;
1257
+ const value = parsed as Readonly<DataRecord>;
1258
+ if (!validContext(value.tenantId) || !validId(value.instanceId)
1259
+ || !validId(value.remedyOperationId)
1260
+ || !['executed', 'proved_no_effect'].includes(value.outcome)
1261
+ || !exactKeys(value.evidence, OUTCOME_EVIDENCE_KEYS)
1262
+ || !validEvidencePair(value.evidence.evidence_id, value.evidence.evidence_digest)
1263
+ || !Number.isFinite(instant(value.evidence.observed_at))) {
1264
+ return fail('remedy_reconciliation_input_invalid');
1265
+ }
1266
+ const loaded = await load(value.tenantId, value.instanceId);
1267
+ if (!loaded.ok) return loaded;
1268
+ const state = loaded.state as DataRecord;
1269
+ const requestDigest = digest(value);
1270
+ const prior = allAttempts(state).find((attempt) => attempt.remedy_operation_id === value.remedyOperationId);
1271
+ if (prior?.reconcile_request_digest === requestDigest) {
1272
+ return pass({ ok: true, idempotent: true, state }) as RemedyProgramResult;
1273
+ }
1274
+ if (state.status !== 'remedy_indeterminate' || !state.active_remedy
1275
+ || state.active_remedy.status !== 'indeterminate') {
1276
+ return prior ? fail('remedy_reconciliation_conflict') : fail('remedy_not_indeterminate');
1277
+ }
1278
+ const attempt = state.active_remedy;
1279
+ if (attempt.remedy_operation_id !== value.remedyOperationId) return fail('remedy_operation_mismatch');
1280
+ if (evidenceUsed(state, value.evidence.evidence_id, value.evidence.evidence_digest)) {
1281
+ return fail('evidence_replayed');
1282
+ }
1283
+ const at = transitionTime(state);
1284
+ if (typeof at !== 'number') return at;
1285
+ if (instant(value.evidence.observed_at) < instant(attempt.outcome_evidence.observed_at)
1286
+ || instant(value.evidence.observed_at) > at) return fail('remedy_reconciliation_time_invalid');
1287
+ const verified = await invoke(verifyRemedyOutcome, {
1288
+ evidence: clone(value.evidence), outcome: value.outcome,
1289
+ expected: outcomeExpected(state, attempt), reconciliation: true,
1290
+ });
1291
+ if (!verified || verified.ok !== true || !exactKeys(verified, VERIFIED_OUTCOME_KEYS)) {
1292
+ return fail('remedy_reconciliation_invalid');
1293
+ }
1294
+ if (!outcomeBound(verified as DataRecord, value as DataRecord, attempt)) {
1295
+ return fail('remedy_reconciliation_binding_mismatch');
1296
+ }
1297
+ if (value.outcome === 'executed' && attempt.units > state.remaining_units) {
1298
+ return fail('remedy_limit_exceeded');
1299
+ }
1300
+ const next = touch(state, at);
1301
+ const completed = next.active_remedy;
1302
+ completed.status = value.outcome;
1303
+ completed.outcome = value.outcome;
1304
+ completed.reconciliation = withoutOk(verified as DataRecord);
1305
+ completed.reconcile_request_digest = requestDigest;
1306
+ next.active_remedy = null;
1307
+ next.remedies.push(completed);
1308
+ consumeEvidence(next, verified.evidence_id, verified.evidence_digest);
1309
+ if (value.outcome === 'executed') {
1310
+ next.remedied_units += completed.units;
1311
+ next.remaining_units -= completed.units;
1312
+ next.status = next.remaining_units === 0 ? 'remedied' : 'partially_remedied';
1313
+ } else {
1314
+ next.status = 'disputed';
1315
+ }
1316
+ const committed = await commit(state, next);
1317
+ return committed.ok ? pass({ ok: true, state: committed.state }) as RemedyProgramResult : committed;
1318
+ }
1319
+
1320
+ async function resolveDispute(input: unknown): Promise<RemedyProgramResult> {
1321
+ const parsed = operationInput(input, RESOLUTION_INPUT_KEYS, 'resolution_input_invalid');
1322
+ if (isFailure(parsed)) return parsed;
1323
+ const value = parsed as Readonly<DataRecord>;
1324
+ if (!validContext(value.tenantId) || !validId(value.instanceId)
1325
+ || !exactKeys(value.resolution, RESOLUTION_KEYS)
1326
+ || !validEvidencePair(value.resolution.evidence_id, value.resolution.evidence_digest)
1327
+ || value.resolution.outcome !== 'no_remedy'
1328
+ || !Number.isFinite(instant(value.resolution.resolved_at))) {
1329
+ return fail('resolution_input_invalid');
1330
+ }
1331
+ const loaded = await load(value.tenantId, value.instanceId);
1332
+ if (!loaded.ok) return loaded;
1333
+ const state = loaded.state as DataRecord;
1334
+ const requestDigest = digest(value);
1335
+ if (state.resolution !== null) {
1336
+ return state.resolution.request_digest === requestDigest
1337
+ ? pass({ ok: true, idempotent: true, state }) as RemedyProgramResult
1338
+ : fail('remedy_case_terminal');
1339
+ }
1340
+ if (['remedied', 'resolved_no_remedy'].includes(state.status)) return fail('remedy_case_terminal');
1341
+ if (!['disputed', 'partially_remedied'].includes(state.status)
1342
+ || state.dispute === null || state.active_remedy !== null) {
1343
+ return fail('resolution_unavailable');
1344
+ }
1345
+ if (typeof verifyResolution !== 'function') return fail('resolution_verifier_unavailable');
1346
+ if (evidenceUsed(state, value.resolution.evidence_id, value.resolution.evidence_digest)) {
1347
+ return fail('evidence_replayed');
1348
+ }
1349
+ const at = transitionTime(state);
1350
+ if (typeof at !== 'number') return at;
1351
+ if (instant(value.resolution.resolved_at) < instant(state.dispute.opened_at)
1352
+ || instant(value.resolution.resolved_at) > at) return fail('resolution_time_invalid');
1353
+ const verified = await invoke(verifyResolution, {
1354
+ resolution: clone(value.resolution), expected: expectedContext(state),
1355
+ });
1356
+ if (!verified || verified.ok !== true || !exactKeys(verified, VERIFIED_RESOLUTION_KEYS)) {
1357
+ return fail('resolution_invalid');
1358
+ }
1359
+ if ([...RESOLUTION_KEYS].some((key) => !same(verified[key], value.resolution[key]))
1360
+ || verified.dispute_id !== state.dispute.dispute_id) {
1361
+ return fail('resolution_binding_mismatch');
1362
+ }
1363
+ const next = touch(state, at);
1364
+ next.status = 'resolved_no_remedy';
1365
+ next.resolution = { ...withoutOk(verified as DataRecord), request_digest: requestDigest };
1366
+ consumeEvidence(next, verified.evidence_id, verified.evidence_digest);
1367
+ const committed = await commit(state, next);
1368
+ return committed.ok ? pass({ ok: true, state: committed.state }) as RemedyProgramResult : committed;
1369
+ }
1370
+
1371
+ async function status(input: unknown): Promise<RemedyProgramResult> {
1372
+ const parsed = operationInput(input, new Set(['tenantId', 'instanceId']), 'remedy_lookup_invalid');
1373
+ if (isFailure(parsed)) return parsed;
1374
+ const value = parsed as Readonly<DataRecord>;
1375
+ return load(value.tenantId, value.instanceId);
1376
+ }
1377
+
1378
+ return Object.freeze({
1379
+ create, status, recordRevocation, reconcileOriginalEffect, openDispute, authorizeRemedy,
1380
+ claimRemedy, finalizeRemedy, reconcileRemedy, resolveDispute,
1381
+ });
1382
+ }
1383
+
1384
+ export default {
1385
+ REMEDY_PROGRAM_VERSION,
1386
+ createRemedyMemoryStore,
1387
+ createRemedyProgramKernel,
1388
+ };