@emilia-protocol/gate 0.11.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (425) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +310 -0
  3. package/action-control-manifest.js +3 -268
  4. package/action-escrow-custodian.js +3 -394
  5. package/action-escrow-evidence.js +3 -858
  6. package/action-escrow-package.js +3 -1668
  7. package/action-escrow-postgres.js +3 -337
  8. package/action-escrow-state.js +3 -396
  9. package/action-escrow-verifiers.js +3 -331
  10. package/action-escrow.js +3 -2447
  11. package/action-packs.js +3 -140
  12. package/adapters/_kit.js +3 -104
  13. package/adapters/aws.js +3 -108
  14. package/adapters/cloudflare.js +3 -54
  15. package/adapters/gcp.js +3 -70
  16. package/adapters/github.js +3 -109
  17. package/adapters/jira.js +3 -55
  18. package/adapters/k8s.js +3 -70
  19. package/adapters/linear.js +3 -55
  20. package/adapters/salesforce.js +3 -55
  21. package/adapters/stripe.js +3 -81
  22. package/adapters/supabase.js +3 -121
  23. package/adapters/terraform.js +3 -59
  24. package/adapters/vercel.js +3 -84
  25. package/aec-execution.js +3 -312
  26. package/breakglass.js +3 -582
  27. package/capability-receipt.js +4 -0
  28. package/cf1-conformance.js +3 -106
  29. package/challenge-store.js +3 -69
  30. package/control-plane.js +3 -340
  31. package/coverage.js +3 -721
  32. package/demo.mjs +1 -1
  33. package/deployment-attestation.js +3 -247
  34. package/dist/action-control-manifest.d.ts +82 -0
  35. package/dist/action-control-manifest.d.ts.map +1 -0
  36. package/dist/action-control-manifest.js +422 -0
  37. package/dist/action-control-manifest.js.map +1 -0
  38. package/dist/action-escrow-custodian.d.ts +74 -0
  39. package/dist/action-escrow-custodian.d.ts.map +1 -0
  40. package/dist/action-escrow-custodian.js +363 -0
  41. package/dist/action-escrow-custodian.js.map +1 -0
  42. package/dist/action-escrow-evidence.d.ts +130 -0
  43. package/dist/action-escrow-evidence.d.ts.map +1 -0
  44. package/dist/action-escrow-evidence.js +1012 -0
  45. package/dist/action-escrow-evidence.js.map +1 -0
  46. package/dist/action-escrow-package.d.ts +10 -0
  47. package/dist/action-escrow-package.d.ts.map +1 -0
  48. package/dist/action-escrow-package.js +1583 -0
  49. package/dist/action-escrow-package.js.map +1 -0
  50. package/dist/action-escrow-postgres.d.ts +62 -0
  51. package/dist/action-escrow-postgres.d.ts.map +1 -0
  52. package/dist/action-escrow-postgres.js +339 -0
  53. package/dist/action-escrow-postgres.js.map +1 -0
  54. package/dist/action-escrow-state.d.ts +141 -0
  55. package/dist/action-escrow-state.d.ts.map +1 -0
  56. package/dist/action-escrow-state.js +413 -0
  57. package/dist/action-escrow-state.js.map +1 -0
  58. package/dist/action-escrow-verifiers.d.ts +65 -0
  59. package/dist/action-escrow-verifiers.d.ts.map +1 -0
  60. package/dist/action-escrow-verifiers.js +421 -0
  61. package/dist/action-escrow-verifiers.js.map +1 -0
  62. package/dist/action-escrow.d.ts +112 -0
  63. package/dist/action-escrow.d.ts.map +1 -0
  64. package/dist/action-escrow.js +2661 -0
  65. package/dist/action-escrow.js.map +1 -0
  66. package/dist/action-packs.d.ts +54 -0
  67. package/dist/action-packs.d.ts.map +1 -0
  68. package/dist/action-packs.js +143 -0
  69. package/dist/action-packs.js.map +1 -0
  70. package/dist/adapters/_kit.d.ts +50 -0
  71. package/dist/adapters/_kit.d.ts.map +1 -0
  72. package/dist/adapters/_kit.js +98 -0
  73. package/dist/adapters/_kit.js.map +1 -0
  74. package/dist/adapters/aws.d.ts +158 -0
  75. package/dist/adapters/aws.d.ts.map +1 -0
  76. package/dist/adapters/aws.js +136 -0
  77. package/dist/adapters/aws.js.map +1 -0
  78. package/dist/adapters/cloudflare.d.ts +116 -0
  79. package/dist/adapters/cloudflare.d.ts.map +1 -0
  80. package/dist/adapters/cloudflare.js +75 -0
  81. package/dist/adapters/cloudflare.js.map +1 -0
  82. package/dist/adapters/gcp.d.ts +139 -0
  83. package/dist/adapters/gcp.d.ts.map +1 -0
  84. package/dist/adapters/gcp.js +69 -0
  85. package/dist/adapters/gcp.js.map +1 -0
  86. package/dist/adapters/github.d.ts +123 -0
  87. package/dist/adapters/github.d.ts.map +1 -0
  88. package/dist/adapters/github.js +105 -0
  89. package/dist/adapters/github.js.map +1 -0
  90. package/dist/adapters/jira.d.ts +109 -0
  91. package/dist/adapters/jira.d.ts.map +1 -0
  92. package/dist/adapters/jira.js +54 -0
  93. package/dist/adapters/jira.js.map +1 -0
  94. package/dist/adapters/k8s.d.ts +139 -0
  95. package/dist/adapters/k8s.d.ts.map +1 -0
  96. package/dist/adapters/k8s.js +69 -0
  97. package/dist/adapters/k8s.js.map +1 -0
  98. package/dist/adapters/linear.d.ts +109 -0
  99. package/dist/adapters/linear.d.ts.map +1 -0
  100. package/dist/adapters/linear.js +54 -0
  101. package/dist/adapters/linear.js.map +1 -0
  102. package/dist/adapters/salesforce.d.ts +109 -0
  103. package/dist/adapters/salesforce.d.ts.map +1 -0
  104. package/dist/adapters/salesforce.js +54 -0
  105. package/dist/adapters/salesforce.js.map +1 -0
  106. package/dist/adapters/stripe.d.ts +116 -0
  107. package/dist/adapters/stripe.d.ts.map +1 -0
  108. package/dist/adapters/stripe.js +77 -0
  109. package/dist/adapters/stripe.js.map +1 -0
  110. package/dist/adapters/supabase.d.ts +127 -0
  111. package/dist/adapters/supabase.d.ts.map +1 -0
  112. package/dist/adapters/supabase.js +112 -0
  113. package/dist/adapters/supabase.js.map +1 -0
  114. package/dist/adapters/terraform.d.ts +109 -0
  115. package/dist/adapters/terraform.d.ts.map +1 -0
  116. package/dist/adapters/terraform.js +58 -0
  117. package/dist/adapters/terraform.js.map +1 -0
  118. package/dist/adapters/vercel.d.ts +114 -0
  119. package/dist/adapters/vercel.d.ts.map +1 -0
  120. package/dist/adapters/vercel.js +81 -0
  121. package/dist/adapters/vercel.js.map +1 -0
  122. package/dist/aec-execution.d.ts +107 -0
  123. package/dist/aec-execution.d.ts.map +1 -0
  124. package/dist/aec-execution.js +334 -0
  125. package/dist/aec-execution.js.map +1 -0
  126. package/dist/breakglass.d.ts +205 -0
  127. package/dist/breakglass.d.ts.map +1 -0
  128. package/dist/breakglass.js +627 -0
  129. package/dist/breakglass.js.map +1 -0
  130. package/dist/capability-receipt.d.ts +601 -0
  131. package/dist/capability-receipt.d.ts.map +1 -0
  132. package/dist/capability-receipt.js +1177 -0
  133. package/dist/capability-receipt.js.map +1 -0
  134. package/dist/cf1-conformance.d.ts +83 -0
  135. package/dist/cf1-conformance.d.ts.map +1 -0
  136. package/dist/cf1-conformance.js +94 -0
  137. package/dist/cf1-conformance.js.map +1 -0
  138. package/dist/challenge-store.d.ts +20 -0
  139. package/dist/challenge-store.d.ts.map +1 -0
  140. package/dist/challenge-store.js +62 -0
  141. package/dist/challenge-store.js.map +1 -0
  142. package/dist/control-plane.d.ts +136 -0
  143. package/dist/control-plane.d.ts.map +1 -0
  144. package/dist/control-plane.js +349 -0
  145. package/dist/control-plane.js.map +1 -0
  146. package/dist/coverage.d.ts +156 -0
  147. package/dist/coverage.d.ts.map +1 -0
  148. package/dist/coverage.js +768 -0
  149. package/dist/coverage.js.map +1 -0
  150. package/dist/deployment-attestation.d.ts +93 -0
  151. package/dist/deployment-attestation.d.ts.map +1 -0
  152. package/dist/deployment-attestation.js +262 -0
  153. package/dist/deployment-attestation.js.map +1 -0
  154. package/dist/eg1-conformance.d.ts +286 -0
  155. package/dist/eg1-conformance.d.ts.map +1 -0
  156. package/dist/eg1-conformance.js +379 -0
  157. package/dist/eg1-conformance.js.map +1 -0
  158. package/dist/enterprise.d.ts +88 -0
  159. package/dist/enterprise.d.ts.map +1 -0
  160. package/dist/enterprise.js +194 -0
  161. package/dist/enterprise.js.map +1 -0
  162. package/dist/evidence-postgres.d.ts +123 -0
  163. package/dist/evidence-postgres.d.ts.map +1 -0
  164. package/dist/evidence-postgres.js +343 -0
  165. package/dist/evidence-postgres.js.map +1 -0
  166. package/dist/evidence.d.ts +113 -0
  167. package/dist/evidence.d.ts.map +1 -0
  168. package/dist/evidence.js +369 -0
  169. package/dist/evidence.js.map +1 -0
  170. package/dist/execution-binding.d.ts +49 -0
  171. package/dist/execution-binding.d.ts.map +1 -0
  172. package/dist/execution-binding.js +243 -0
  173. package/dist/execution-binding.js.map +1 -0
  174. package/dist/formal-runtime-map.d.ts +42 -0
  175. package/dist/formal-runtime-map.d.ts.map +1 -0
  176. package/dist/formal-runtime-map.js +23 -0
  177. package/dist/formal-runtime-map.js.map +1 -0
  178. package/dist/index.d.ts +1003 -0
  179. package/dist/index.d.ts.map +1 -0
  180. package/dist/index.js +1737 -0
  181. package/dist/index.js.map +1 -0
  182. package/dist/key-registry.d.ts +60 -0
  183. package/dist/key-registry.d.ts.map +1 -0
  184. package/dist/key-registry.js +144 -0
  185. package/dist/key-registry.js.map +1 -0
  186. package/dist/mcp.d.ts +53 -0
  187. package/dist/mcp.d.ts.map +1 -0
  188. package/dist/mcp.js +112 -0
  189. package/dist/mcp.js.map +1 -0
  190. package/dist/metering.d.ts +80 -0
  191. package/dist/metering.d.ts.map +1 -0
  192. package/dist/metering.js +227 -0
  193. package/dist/metering.js.map +1 -0
  194. package/dist/metrics.d.ts +68 -0
  195. package/dist/metrics.d.ts.map +1 -0
  196. package/dist/metrics.js +242 -0
  197. package/dist/metrics.js.map +1 -0
  198. package/dist/network-witness.d.ts +167 -0
  199. package/dist/network-witness.d.ts.map +1 -0
  200. package/dist/network-witness.js +520 -0
  201. package/dist/network-witness.js.map +1 -0
  202. package/dist/proposal-to-effect.d.ts +146 -0
  203. package/dist/proposal-to-effect.d.ts.map +1 -0
  204. package/dist/proposal-to-effect.js +427 -0
  205. package/dist/proposal-to-effect.js.map +1 -0
  206. package/dist/protocol-state.d.ts +141 -0
  207. package/dist/protocol-state.d.ts.map +1 -0
  208. package/dist/protocol-state.js +106 -0
  209. package/dist/protocol-state.js.map +1 -0
  210. package/dist/receipt-program.d.ts +59 -0
  211. package/dist/receipt-program.d.ts.map +1 -0
  212. package/dist/receipt-program.js +1053 -0
  213. package/dist/receipt-program.js.map +1 -0
  214. package/dist/reliance-kernel.d.ts +66 -0
  215. package/dist/reliance-kernel.d.ts.map +1 -0
  216. package/dist/reliance-kernel.js +103 -0
  217. package/dist/reliance-kernel.js.map +1 -0
  218. package/dist/reliance-packet.d.ts +67 -0
  219. package/dist/reliance-packet.d.ts.map +1 -0
  220. package/dist/reliance-packet.js +176 -0
  221. package/dist/reliance-packet.js.map +1 -0
  222. package/dist/remedy-program-postgres.d.ts +37 -0
  223. package/dist/remedy-program-postgres.d.ts.map +1 -0
  224. package/dist/remedy-program-postgres.js +291 -0
  225. package/dist/remedy-program-postgres.js.map +1 -0
  226. package/dist/remedy-program-receipt.d.ts +97 -0
  227. package/dist/remedy-program-receipt.d.ts.map +1 -0
  228. package/dist/remedy-program-receipt.js +817 -0
  229. package/dist/remedy-program-receipt.js.map +1 -0
  230. package/dist/remedy-program.d.ts +76 -0
  231. package/dist/remedy-program.d.ts.map +1 -0
  232. package/dist/remedy-program.js +1346 -0
  233. package/dist/remedy-program.js.map +1 -0
  234. package/dist/reports/art14.d.ts +101 -0
  235. package/dist/reports/art14.d.ts.map +1 -0
  236. package/dist/reports/art14.js +340 -0
  237. package/dist/reports/art14.js.map +1 -0
  238. package/dist/reports/assurance-package.d.ts +197 -0
  239. package/dist/reports/assurance-package.d.ts.map +1 -0
  240. package/dist/reports/assurance-package.js +313 -0
  241. package/dist/reports/assurance-package.js.map +1 -0
  242. package/dist/reports/auditor-workpaper.d.ts +194 -0
  243. package/dist/reports/auditor-workpaper.d.ts.map +1 -0
  244. package/dist/reports/auditor-workpaper.js +545 -0
  245. package/dist/reports/auditor-workpaper.js.map +1 -0
  246. package/dist/reports/external-verification.d.ts +76 -0
  247. package/dist/reports/external-verification.d.ts.map +1 -0
  248. package/dist/reports/external-verification.js +209 -0
  249. package/dist/reports/external-verification.js.map +1 -0
  250. package/dist/reports/reperform.d.ts +122 -0
  251. package/dist/reports/reperform.d.ts.map +1 -0
  252. package/dist/reports/reperform.js +432 -0
  253. package/dist/reports/reperform.js.map +1 -0
  254. package/dist/reports/underwriter.d.ts +127 -0
  255. package/dist/reports/underwriter.d.ts.map +1 -0
  256. package/dist/reports/underwriter.js +348 -0
  257. package/dist/reports/underwriter.js.map +1 -0
  258. package/dist/retention.d.ts +88 -0
  259. package/dist/retention.d.ts.map +1 -0
  260. package/dist/retention.js +90 -0
  261. package/dist/retention.js.map +1 -0
  262. package/dist/roster.d.ts +113 -0
  263. package/dist/roster.d.ts.map +1 -0
  264. package/dist/roster.js +261 -0
  265. package/dist/roster.js.map +1 -0
  266. package/dist/runtime-monitor.d.ts +132 -0
  267. package/dist/runtime-monitor.d.ts.map +1 -0
  268. package/dist/runtime-monitor.js +197 -0
  269. package/dist/runtime-monitor.js.map +1 -0
  270. package/dist/settlement.d.ts +68 -0
  271. package/dist/settlement.d.ts.map +1 -0
  272. package/dist/settlement.js +306 -0
  273. package/dist/settlement.js.map +1 -0
  274. package/dist/siem.d.ts +261 -0
  275. package/dist/siem.d.ts.map +1 -0
  276. package/dist/siem.js +247 -0
  277. package/dist/siem.js.map +1 -0
  278. package/dist/store-postgres.d.ts +107 -0
  279. package/dist/store-postgres.d.ts.map +1 -0
  280. package/dist/store-postgres.js +152 -0
  281. package/dist/store-postgres.js.map +1 -0
  282. package/dist/store.d.ts +95 -0
  283. package/dist/store.d.ts.map +1 -0
  284. package/dist/store.js +202 -0
  285. package/dist/store.js.map +1 -0
  286. package/dist/strict-json.d.ts +14 -0
  287. package/dist/strict-json.d.ts.map +1 -0
  288. package/dist/strict-json.js +132 -0
  289. package/dist/strict-json.js.map +1 -0
  290. package/dist/trust-program-adapters.d.ts +122 -0
  291. package/dist/trust-program-adapters.d.ts.map +1 -0
  292. package/dist/trust-program-adapters.js +802 -0
  293. package/dist/trust-program-adapters.js.map +1 -0
  294. package/dist/trust-program-postgres.d.ts +41 -0
  295. package/dist/trust-program-postgres.d.ts.map +1 -0
  296. package/dist/trust-program-postgres.js +346 -0
  297. package/dist/trust-program-postgres.js.map +1 -0
  298. package/dist/trust-program-revocation.d.ts +206 -0
  299. package/dist/trust-program-revocation.d.ts.map +1 -0
  300. package/dist/trust-program-revocation.js +600 -0
  301. package/dist/trust-program-revocation.js.map +1 -0
  302. package/dist/trust-program.d.ts +197 -0
  303. package/dist/trust-program.d.ts.map +1 -0
  304. package/dist/trust-program.js +1351 -0
  305. package/dist/trust-program.js.map +1 -0
  306. package/dist/witness-postgres.d.ts +43 -0
  307. package/dist/witness-postgres.d.ts.map +1 -0
  308. package/dist/witness-postgres.js +92 -0
  309. package/dist/witness-postgres.js.map +1 -0
  310. package/dist/zk-range-proof.d.ts +78 -0
  311. package/dist/zk-range-proof.d.ts.map +1 -0
  312. package/dist/zk-range-proof.js +228 -0
  313. package/dist/zk-range-proof.js.map +1 -0
  314. package/eg1-conformance.js +3 -404
  315. package/enterprise.js +3 -180
  316. package/ep-assure.mjs +1 -0
  317. package/evidence-postgres.js +3 -356
  318. package/evidence.js +4 -360
  319. package/execution-binding.js +3 -212
  320. package/formal-runtime-map.js +4 -0
  321. package/index.js +4 -1249
  322. package/key-registry.js +3 -134
  323. package/mcp.js +3 -109
  324. package/metering.js +3 -226
  325. package/metrics.js +3 -231
  326. package/network-witness.js +3 -499
  327. package/package.json +35 -4
  328. package/proposal-to-effect.js +4 -0
  329. package/receipt-program.js +4 -0
  330. package/reliance-kernel.js +3 -112
  331. package/reliance-packet.js +3 -175
  332. package/remedy-program-postgres.js +4 -0
  333. package/remedy-program-receipt.js +4 -0
  334. package/remedy-program.js +4 -0
  335. package/reports/art14.js +0 -0
  336. package/reports/assurance-package.js +2 -312
  337. package/reports/auditor-workpaper.js +3 -537
  338. package/reports/external-verification.js +3 -222
  339. package/reports/reperform.js +3 -424
  340. package/reports/underwriter.js +3 -339
  341. package/retention.js +3 -84
  342. package/roster.js +3 -264
  343. package/runtime-monitor.js +4 -0
  344. package/settlement.js +3 -299
  345. package/siem.js +3 -235
  346. package/src/action-control-manifest.ts +424 -0
  347. package/src/action-escrow-custodian.ts +427 -0
  348. package/src/action-escrow-evidence.ts +1178 -0
  349. package/src/action-escrow-package.ts +1821 -0
  350. package/src/action-escrow-postgres.ts +366 -0
  351. package/src/action-escrow-state.ts +498 -0
  352. package/src/action-escrow-verifiers.ts +484 -0
  353. package/src/action-escrow.ts +3039 -0
  354. package/src/action-packs.ts +170 -0
  355. package/src/adapters/_kit.ts +117 -0
  356. package/src/adapters/aws.ts +142 -0
  357. package/src/adapters/cloudflare.ts +78 -0
  358. package/src/adapters/gcp.ts +71 -0
  359. package/src/adapters/github.ts +110 -0
  360. package/src/adapters/jira.ts +56 -0
  361. package/src/adapters/k8s.ts +71 -0
  362. package/src/adapters/linear.ts +56 -0
  363. package/src/adapters/salesforce.ts +56 -0
  364. package/src/adapters/stripe.ts +82 -0
  365. package/src/adapters/supabase.ts +122 -0
  366. package/src/adapters/terraform.ts +60 -0
  367. package/src/adapters/vercel.ts +85 -0
  368. package/src/aec-execution.ts +354 -0
  369. package/src/breakglass.ts +647 -0
  370. package/src/capability-receipt.ts +1258 -0
  371. package/src/cf1-conformance.ts +120 -0
  372. package/src/challenge-store.ts +70 -0
  373. package/src/control-plane.ts +374 -0
  374. package/src/coverage.ts +832 -0
  375. package/src/deployment-attestation.ts +259 -0
  376. package/src/eg1-conformance.ts +471 -0
  377. package/src/enterprise.ts +195 -0
  378. package/src/evidence-postgres.ts +374 -0
  379. package/src/evidence.ts +367 -0
  380. package/src/execution-binding.ts +236 -0
  381. package/src/formal-runtime-map.ts +24 -0
  382. package/src/index.ts +2127 -0
  383. package/src/key-registry.ts +150 -0
  384. package/src/mcp.ts +120 -0
  385. package/src/metering.ts +237 -0
  386. package/src/metrics.ts +232 -0
  387. package/src/network-witness.ts +585 -0
  388. package/src/proposal-to-effect.ts +571 -0
  389. package/src/protocol-state.ts +143 -0
  390. package/src/protocol-state.typecheck.ts +94 -0
  391. package/src/receipt-program.ts +1105 -0
  392. package/src/reliance-kernel.ts +119 -0
  393. package/src/reliance-packet.ts +206 -0
  394. package/src/remedy-program-postgres.ts +367 -0
  395. package/src/remedy-program-receipt.ts +891 -0
  396. package/src/remedy-program.ts +1388 -0
  397. package/src/reports/art14.ts +361 -0
  398. package/src/reports/assurance-package.ts +330 -0
  399. package/src/reports/auditor-workpaper.ts +558 -0
  400. package/src/reports/external-verification.ts +232 -0
  401. package/src/reports/reperform.ts +441 -0
  402. package/src/reports/underwriter.ts +356 -0
  403. package/src/retention.ts +100 -0
  404. package/src/roster.ts +273 -0
  405. package/src/runtime-monitor.ts +217 -0
  406. package/src/settlement.ts +313 -0
  407. package/src/siem.ts +247 -0
  408. package/src/store-postgres.ts +167 -0
  409. package/src/store.ts +214 -0
  410. package/src/strict-json.ts +115 -0
  411. package/src/trust-program-adapters.ts +847 -0
  412. package/src/trust-program-postgres.ts +429 -0
  413. package/src/trust-program-revocation.ts +751 -0
  414. package/src/trust-program.ts +1460 -0
  415. package/src/witness-postgres.ts +106 -0
  416. package/src/zk-range-proof.ts +250 -0
  417. package/store-postgres.js +3 -161
  418. package/store.js +4 -198
  419. package/strict-json.js +3 -102
  420. package/trust-program-adapters.js +4 -0
  421. package/trust-program-postgres.js +4 -0
  422. package/trust-program-revocation.js +4 -0
  423. package/trust-program.js +4 -0
  424. package/witness-postgres.js +3 -96
  425. package/zk-range-proof.js +4 -0
@@ -0,0 +1,891 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * EP-ACTION-REMEDY-RECEIPT-v1
4
+ *
5
+ * A portable, offline-verifiable operator receipt over one exact Remedy
6
+ * Program state snapshot. The receipt preserves the original effect as an
7
+ * immutable fact and describes a later remedy only as a compensating action.
8
+ * It never claims that the original effect was rolled back or erased.
9
+ */
10
+ import crypto from 'node:crypto';
11
+
12
+ import { canonicalize } from '../execution-binding.js';
13
+
14
+ export const ACTION_REMEDY_RECEIPT_VERSION = 'EP-ACTION-REMEDY-RECEIPT-v1';
15
+ export const REMEDY_PROGRAM_RECEIPT_VERSION = ACTION_REMEDY_RECEIPT_VERSION;
16
+ export const ACTION_REMEDY_RECEIPT_DOMAIN = `${ACTION_REMEDY_RECEIPT_VERSION}\0`;
17
+
18
+ const REMEDY_PROGRAM_VERSION = 'EP-GATE-REMEDY-PROGRAM-PROFILE-v1';
19
+ const DIGEST = /^sha256:[0-9a-f]{64}$/;
20
+ const CAID = /^caid:1:[a-z][a-z0-9.-]*\.[1-9][0-9]*:jcs-sha256:[A-Za-z0-9_-]{43}$/;
21
+ const ID = /^[A-Za-z0-9][A-Za-z0-9._:/#@+-]{0,255}$/;
22
+ const BASE64URL = /^[A-Za-z0-9_-]+$/;
23
+ const FORBIDDEN_FIELD_NAMES = new Set(['__proto__', 'prototype', 'constructor']);
24
+ const MAX_CONTEXT_BYTES = 512;
25
+ const MAX_REMEDIES = 1024;
26
+
27
+ const TOP_KEYS = new Set(['version', 'issuer', 'payload', 'content_digest', 'signature']);
28
+ const ISSUER_KEYS = new Set(['issuer', 'tenant', 'environment', 'audience', 'key_id']);
29
+ const PAYLOAD_KEYS = new Set([
30
+ 'case', 'original_effect', 'original_reconciliation', 'remedy', 'semantics',
31
+ ]);
32
+ const CASE_KEYS = new Set([
33
+ 'instance_id', 'revision', 'status', 'state_snapshot_digest', 'updated_at',
34
+ ]);
35
+ const ORIGINAL_KEYS = new Set([
36
+ 'caid', 'action_digest', 'operation_id', 'consequence_mode',
37
+ 'consequence_digest', 'terminal_evidence_digest', 'outcome', 'occurred_at',
38
+ 'evidence_digest',
39
+ ]);
40
+ const REMEDY_KEYS = new Set([
41
+ 'operation_id', 'caid', 'action_digest', 'destination_binding_digest',
42
+ 'units', 'unit', 'owner_mode', 'owner_digest', 'status', 'outcome',
43
+ ]);
44
+ const SEMANTICS_KEYS = new Set(['original_effect', 'remedy_effect', 'rollback']);
45
+ const SIGNATURE_KEYS = new Set(['algorithm', 'value']);
46
+ const EXPECTED_KEYS = new Set([
47
+ 'original_operation_id', 'original_action_digest',
48
+ 'original_terminal_evidence_digest', 'case_instance_id', 'case_revision',
49
+ 'case_status', 'remedy_operation_id', 'remedy_action_digest', 'remedy_caid',
50
+ 'destination_binding_digest', 'units', 'unit', 'owner_mode', 'owner_digest',
51
+ ]);
52
+ const STATE_KEYS = new Set([
53
+ 'version', 'instance_id', 'tenant_id', 'environment', 'audience', 'status',
54
+ 'revision', 'created_at', 'updated_at', 'original', 'remedy_profile_digest',
55
+ 'destination_binding_digest', 'max_remedy_units', 'unit', 'remedied_units',
56
+ 'remaining_units', 'used_evidence_ids', 'used_evidence_digests',
57
+ 'original_reconciliation', 'revocation', 'dispute', 'active_remedy',
58
+ 'remedies', 'resolution',
59
+ 'create_request_digest',
60
+ ]);
61
+ const ATTEMPT_KEYS = new Set([
62
+ 'evidence_id', 'evidence_digest', 'dispute_id', 'original_operation_id',
63
+ 'remedy_operation_id', 'remedy_caid', 'remedy_action_digest',
64
+ 'consequence_mode', 'capability_template_digest', 'escrow_profile_digest',
65
+ 'destination_binding_digest', 'units', 'unit', 'authorized_at',
66
+ 'request_digest', 'status', 'claim_token_digest', 'claimed_at',
67
+ 'claim_request_digest', 'outcome', 'outcome_evidence',
68
+ 'finalize_request_digest', 'reconciliation', 'reconcile_request_digest',
69
+ ]);
70
+ const OUTCOME_EVIDENCE_KEYS = new Set([
71
+ 'evidence_id', 'evidence_digest', 'remedy_operation_id',
72
+ 'remedy_action_digest', 'destination_binding_digest', 'units', 'unit',
73
+ 'outcome', 'observed_at',
74
+ ]);
75
+ const DISPUTE_KEYS = new Set([
76
+ 'dispute_id', 'evidence_id', 'evidence_digest', 'challenger_id',
77
+ 'requested_units', 'opened_at', 'original_operation_id',
78
+ 'original_action_digest', 'request_digest',
79
+ ]);
80
+ const REVOCATION_KEYS = new Set([
81
+ 'evidence_id', 'evidence_digest', 'target_operation_id', 'action_digest',
82
+ 'authority_id', 'revoked_at', 'effect', 'request_digest',
83
+ ]);
84
+ const RESOLUTION_KEYS = new Set([
85
+ 'evidence_id', 'evidence_digest', 'outcome', 'resolved_at', 'dispute_id',
86
+ 'request_digest',
87
+ ]);
88
+
89
+ const CASE_STATUSES = new Set([
90
+ 'effect_executed', 'effect_indeterminate', 'disputed', 'remedy_authorized',
91
+ 'remedy_claimed', 'remedy_indeterminate', 'partially_remedied', 'remedied',
92
+ 'resolved_no_remedy',
93
+ 'original_proved_no_effect',
94
+ ]);
95
+ const REMEDY_STATUSES = new Set([
96
+ 'authorized', 'claimed', 'indeterminate', 'executed', 'proved_no_effect',
97
+ ]);
98
+ const REMEDY_OUTCOMES = new Set(['executed', 'proved_no_effect', 'indeterminate']);
99
+
100
+ type DataRecord = Record<string, any>;
101
+
102
+ export interface RemedyReceiptExpectedBindings extends Record<string, unknown> {
103
+ original_operation_id: string;
104
+ original_action_digest: string;
105
+ original_terminal_evidence_digest: string;
106
+ case_instance_id: string;
107
+ case_revision: number;
108
+ case_status: string;
109
+ remedy_operation_id: string;
110
+ remedy_action_digest: string;
111
+ remedy_caid: string;
112
+ destination_binding_digest: string;
113
+ units: number;
114
+ unit: string;
115
+ owner_mode: string;
116
+ owner_digest: string;
117
+ }
118
+
119
+ function isRecord(value: unknown): value is DataRecord {
120
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
121
+ const prototype = Object.getPrototypeOf(value);
122
+ return prototype === Object.prototype || prototype === null;
123
+ }
124
+
125
+ function isDataRecord(value: unknown): value is DataRecord {
126
+ return isRecord(value) && Reflect.ownKeys(value).every((key) => {
127
+ if (typeof key !== 'string') return false;
128
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
129
+ return descriptor?.enumerable === true && Object.hasOwn(descriptor, 'value');
130
+ });
131
+ }
132
+
133
+ function exactKeys(value: unknown, keys: Set<string>): value is DataRecord {
134
+ return isDataRecord(value)
135
+ && Reflect.ownKeys(value).length === keys.size
136
+ && Object.keys(value).every((key) => keys.has(key));
137
+ }
138
+
139
+ function assertNoPrototypeNamedFields(value: unknown): void {
140
+ if (value === null || typeof value !== 'object') return;
141
+ const stack: object[] = [value as object];
142
+ const seen = new WeakSet<object>();
143
+ while (stack.length > 0) {
144
+ const current = stack.pop()!;
145
+ if (seen.has(current)) continue;
146
+ seen.add(current);
147
+ for (const key of Reflect.ownKeys(current)) {
148
+ if (typeof key === 'string' && FORBIDDEN_FIELD_NAMES.has(key)) {
149
+ throw new TypeError(`prototype-named field ${key} is forbidden`);
150
+ }
151
+ const descriptor = Object.getOwnPropertyDescriptor(current, key);
152
+ if (!descriptor || !Object.hasOwn(descriptor, 'value')) continue;
153
+ const child = descriptor.value;
154
+ if (typeof child === 'number' && Object.is(child, -0)) {
155
+ throw new TypeError('negative zero is not canonical JSON');
156
+ }
157
+ if (child !== null && typeof child === 'object') stack.push(child);
158
+ }
159
+ }
160
+ }
161
+
162
+ function canonicalCopy<T>(value: T, label: string): T {
163
+ try {
164
+ assertNoPrototypeNamedFields(value);
165
+ return JSON.parse(canonicalize(value)) as T;
166
+ } catch (error) {
167
+ if (error instanceof TypeError && /prototype-named field/.test(error.message)) throw error;
168
+ throw new TypeError(`${label} must be bounded canonical JSON`);
169
+ }
170
+ }
171
+
172
+ function deepFreeze<T>(value: T): Readonly<T> {
173
+ if (value === null || typeof value !== 'object') return value;
174
+ const stack: object[] = [value as object];
175
+ const seen = new WeakSet<object>();
176
+ while (stack.length > 0) {
177
+ const current = stack.pop()!;
178
+ if (seen.has(current)) continue;
179
+ seen.add(current);
180
+ for (const child of Object.values(current)) {
181
+ if (child !== null && typeof child === 'object') stack.push(child);
182
+ }
183
+ Object.freeze(current);
184
+ }
185
+ return value;
186
+ }
187
+
188
+ function validId(value: unknown): value is string {
189
+ return typeof value === 'string' && ID.test(value)
190
+ && !FORBIDDEN_FIELD_NAMES.has(value);
191
+ }
192
+
193
+ function validContext(value: unknown): value is string {
194
+ return typeof value === 'string' && value.length > 0
195
+ && Buffer.byteLength(value, 'utf8') <= MAX_CONTEXT_BYTES
196
+ && !/[\u0000-\u001f\u007f]/.test(value)
197
+ && !FORBIDDEN_FIELD_NAMES.has(value);
198
+ }
199
+
200
+ function strictInstant(value: unknown): number {
201
+ if (typeof value !== 'string'
202
+ || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)) return NaN;
203
+ const parsed = Date.parse(value);
204
+ return Number.isFinite(parsed) && new Date(parsed).toISOString() === value ? parsed : NaN;
205
+ }
206
+
207
+ function canonicalDigest(value: unknown): string {
208
+ return `sha256:${crypto.createHash('sha256').update(canonicalize(value)).digest('hex')}`;
209
+ }
210
+
211
+ function strictBase64url(value: unknown, length?: number): Buffer | null {
212
+ if (typeof value !== 'string' || !BASE64URL.test(value) || value.length % 4 === 1) return null;
213
+ const bytes = Buffer.from(value, 'base64url');
214
+ if (bytes.toString('base64url') !== value
215
+ || (length !== undefined && bytes.length !== length)) return null;
216
+ return bytes;
217
+ }
218
+
219
+ function publicKey(value: unknown): crypto.KeyObject | null {
220
+ try {
221
+ if (value instanceof crypto.KeyObject) {
222
+ return value.type === 'public' && value.asymmetricKeyType === 'ed25519' ? value : null;
223
+ }
224
+ const bytes = strictBase64url(value);
225
+ if (!bytes) return null;
226
+ const key = crypto.createPublicKey({ key: bytes, type: 'spki', format: 'der' });
227
+ return key.asymmetricKeyType === 'ed25519' ? key : null;
228
+ } catch {
229
+ return null;
230
+ }
231
+ }
232
+
233
+ function privateKey(value: unknown): crypto.KeyObject {
234
+ try {
235
+ const key = value instanceof crypto.KeyObject ? value : crypto.createPrivateKey(value as any);
236
+ if (key.type !== 'private' || key.asymmetricKeyType !== 'ed25519') throw new Error('wrong key');
237
+ return key;
238
+ } catch {
239
+ throw new TypeError('remedy receipt private key must be Ed25519');
240
+ }
241
+ }
242
+
243
+ function publicKeyB64u(value: crypto.KeyObject): string {
244
+ return crypto.createPublicKey(value as any)
245
+ .export({ type: 'spki', format: 'der' }).toString('base64url');
246
+ }
247
+
248
+ function canonicalSignature(value: unknown): { bytes: Buffer; encoded: string } {
249
+ const bytes = Buffer.isBuffer(value)
250
+ ? Buffer.from(value)
251
+ : value instanceof Uint8Array ? Buffer.from(value)
252
+ : strictBase64url(value, 64);
253
+ if (!bytes || bytes.length !== 64) {
254
+ throw new TypeError('remedy receipt signer returned a malformed Ed25519 signature');
255
+ }
256
+ const encoded = bytes.toString('base64url');
257
+ if (typeof value === 'string' && value !== encoded) {
258
+ throw new TypeError('remedy receipt signer returned a noncanonical signature');
259
+ }
260
+ return { bytes, encoded };
261
+ }
262
+
263
+ function validOriginal(value: unknown): value is DataRecord {
264
+ return exactKeys(value, ORIGINAL_KEYS)
265
+ && typeof value.caid === 'string' && CAID.test(value.caid)
266
+ && typeof value.action_digest === 'string' && DIGEST.test(value.action_digest)
267
+ && validId(value.operation_id)
268
+ && validContext(value.consequence_mode)
269
+ && typeof value.consequence_digest === 'string' && DIGEST.test(value.consequence_digest)
270
+ && typeof value.terminal_evidence_digest === 'string'
271
+ && DIGEST.test(value.terminal_evidence_digest)
272
+ && ['executed', 'indeterminate'].includes(value.outcome)
273
+ && Number.isFinite(strictInstant(value.occurred_at))
274
+ && typeof value.evidence_digest === 'string' && DIGEST.test(value.evidence_digest);
275
+ }
276
+
277
+ function validOutcomeEvidence(value: unknown): value is DataRecord {
278
+ return exactKeys(value, OUTCOME_EVIDENCE_KEYS)
279
+ && validId(value.evidence_id)
280
+ && typeof value.evidence_digest === 'string' && DIGEST.test(value.evidence_digest)
281
+ && validId(value.remedy_operation_id)
282
+ && typeof value.remedy_action_digest === 'string' && DIGEST.test(value.remedy_action_digest)
283
+ && typeof value.destination_binding_digest === 'string'
284
+ && DIGEST.test(value.destination_binding_digest)
285
+ && Number.isSafeInteger(value.units) && value.units > 0
286
+ && validContext(value.unit)
287
+ && REMEDY_OUTCOMES.has(value.outcome)
288
+ && Number.isFinite(strictInstant(value.observed_at));
289
+ }
290
+
291
+ function ownerDigest(value: DataRecord): string | null {
292
+ if (value.consequence_mode === 'receipt-program'
293
+ && typeof value.capability_template_digest === 'string'
294
+ && DIGEST.test(value.capability_template_digest)
295
+ && value.escrow_profile_digest === null) return value.capability_template_digest;
296
+ if (value.consequence_mode === 'action-escrow'
297
+ && value.capability_template_digest === null
298
+ && typeof value.escrow_profile_digest === 'string'
299
+ && DIGEST.test(value.escrow_profile_digest)) return value.escrow_profile_digest;
300
+ return null;
301
+ }
302
+
303
+ function validNullableDigest(value: unknown): boolean {
304
+ return value === null || (typeof value === 'string' && DIGEST.test(value));
305
+ }
306
+
307
+ function validAttempt(value: unknown): value is DataRecord {
308
+ return exactKeys(value, ATTEMPT_KEYS)
309
+ && validId(value.evidence_id)
310
+ && typeof value.evidence_digest === 'string' && DIGEST.test(value.evidence_digest)
311
+ && validId(value.dispute_id) && validId(value.original_operation_id)
312
+ && validId(value.remedy_operation_id)
313
+ && typeof value.remedy_caid === 'string' && CAID.test(value.remedy_caid)
314
+ && typeof value.remedy_action_digest === 'string' && DIGEST.test(value.remedy_action_digest)
315
+ && ownerDigest(value) !== null
316
+ && typeof value.destination_binding_digest === 'string'
317
+ && DIGEST.test(value.destination_binding_digest)
318
+ && Number.isSafeInteger(value.units) && value.units > 0
319
+ && validContext(value.unit)
320
+ && Number.isFinite(strictInstant(value.authorized_at))
321
+ && typeof value.request_digest === 'string' && DIGEST.test(value.request_digest)
322
+ && REMEDY_STATUSES.has(value.status)
323
+ && validNullableDigest(value.claim_token_digest)
324
+ && (value.claimed_at === null || Number.isFinite(strictInstant(value.claimed_at)))
325
+ && validNullableDigest(value.claim_request_digest)
326
+ && (value.outcome === null || REMEDY_OUTCOMES.has(value.outcome))
327
+ && (value.outcome_evidence === null || validOutcomeEvidence(value.outcome_evidence))
328
+ && validNullableDigest(value.finalize_request_digest)
329
+ && (value.reconciliation === null || validOutcomeEvidence(value.reconciliation))
330
+ && validNullableDigest(value.reconcile_request_digest);
331
+ }
332
+
333
+ function validDispute(value: unknown): boolean {
334
+ return exactKeys(value, DISPUTE_KEYS)
335
+ && validId(value.dispute_id) && validId(value.evidence_id)
336
+ && typeof value.evidence_digest === 'string' && DIGEST.test(value.evidence_digest)
337
+ && validContext(value.challenger_id)
338
+ && Number.isSafeInteger(value.requested_units) && value.requested_units > 0
339
+ && Number.isFinite(strictInstant(value.opened_at))
340
+ && validId(value.original_operation_id)
341
+ && typeof value.original_action_digest === 'string' && DIGEST.test(value.original_action_digest)
342
+ && typeof value.request_digest === 'string' && DIGEST.test(value.request_digest);
343
+ }
344
+
345
+ function validRevocation(value: unknown): boolean {
346
+ return exactKeys(value, REVOCATION_KEYS)
347
+ && validId(value.evidence_id)
348
+ && typeof value.evidence_digest === 'string' && DIGEST.test(value.evidence_digest)
349
+ && validId(value.target_operation_id)
350
+ && typeof value.action_digest === 'string' && DIGEST.test(value.action_digest)
351
+ && validContext(value.authority_id)
352
+ && Number.isFinite(strictInstant(value.revoked_at))
353
+ && value.effect === 'future_authority_only'
354
+ && typeof value.request_digest === 'string' && DIGEST.test(value.request_digest);
355
+ }
356
+
357
+ function validResolution(value: unknown): boolean {
358
+ return exactKeys(value, RESOLUTION_KEYS)
359
+ && validId(value.evidence_id)
360
+ && typeof value.evidence_digest === 'string' && DIGEST.test(value.evidence_digest)
361
+ && value.outcome === 'no_remedy'
362
+ && Number.isFinite(strictInstant(value.resolved_at))
363
+ && validId(value.dispute_id)
364
+ && typeof value.request_digest === 'string' && DIGEST.test(value.request_digest);
365
+ }
366
+
367
+ function validOriginalReconciliation(value: unknown): boolean {
368
+ return exactKeys(value, new Set([
369
+ 'evidence_id', 'evidence_digest', 'original_operation_id',
370
+ 'original_action_digest', 'terminal_evidence_digest', 'outcome',
371
+ 'observed_at', 'request_digest',
372
+ ]))
373
+ && validId(value.evidence_id)
374
+ && typeof value.evidence_digest === 'string' && DIGEST.test(value.evidence_digest)
375
+ && validId(value.original_operation_id)
376
+ && typeof value.original_action_digest === 'string' && DIGEST.test(value.original_action_digest)
377
+ && typeof value.terminal_evidence_digest === 'string' && DIGEST.test(value.terminal_evidence_digest)
378
+ && ['executed', 'proved_no_effect'].includes(value.outcome)
379
+ && Number.isFinite(strictInstant(value.observed_at))
380
+ && typeof value.request_digest === 'string' && DIGEST.test(value.request_digest);
381
+ }
382
+
383
+ function validState(state: unknown): state is DataRecord {
384
+ if (!exactKeys(state, STATE_KEYS)
385
+ || state.version !== REMEDY_PROGRAM_VERSION
386
+ || !validId(state.instance_id)
387
+ || !validContext(state.tenant_id)
388
+ || !validContext(state.environment)
389
+ || !validContext(state.audience)
390
+ || !CASE_STATUSES.has(state.status)
391
+ || !Number.isSafeInteger(state.revision) || state.revision < 0
392
+ || !Number.isFinite(strictInstant(state.created_at))
393
+ || !Number.isFinite(strictInstant(state.updated_at))
394
+ || strictInstant(state.updated_at) < strictInstant(state.created_at)
395
+ || !validOriginal(state.original)
396
+ || typeof state.remedy_profile_digest !== 'string' || !DIGEST.test(state.remedy_profile_digest)
397
+ || typeof state.destination_binding_digest !== 'string'
398
+ || !DIGEST.test(state.destination_binding_digest)
399
+ || !Number.isSafeInteger(state.max_remedy_units) || state.max_remedy_units < 1
400
+ || !validContext(state.unit)
401
+ || !Number.isSafeInteger(state.remedied_units) || state.remedied_units < 0
402
+ || !Number.isSafeInteger(state.remaining_units) || state.remaining_units < 0
403
+ || state.remedied_units + state.remaining_units !== state.max_remedy_units
404
+ || !Array.isArray(state.used_evidence_ids)
405
+ || !state.used_evidence_ids.every(validId)
406
+ || new Set(state.used_evidence_ids).size !== state.used_evidence_ids.length
407
+ || !Array.isArray(state.used_evidence_digests)
408
+ || !state.used_evidence_digests.every((entry: unknown) => (
409
+ typeof entry === 'string' && DIGEST.test(entry)
410
+ ))
411
+ || new Set(state.used_evidence_digests).size !== state.used_evidence_digests.length
412
+ || !Array.isArray(state.remedies) || state.remedies.length > MAX_REMEDIES
413
+ || !state.remedies.every(validAttempt)
414
+ || (state.active_remedy !== null && !validAttempt(state.active_remedy))
415
+ || (state.dispute !== null && !validDispute(state.dispute))
416
+ || (state.revocation !== null && !validRevocation(state.revocation))
417
+ || (state.original_reconciliation !== null
418
+ && !validOriginalReconciliation(state.original_reconciliation))
419
+ || (state.resolution !== null && !validResolution(state.resolution))
420
+ || typeof state.create_request_digest !== 'string'
421
+ || !DIGEST.test(state.create_request_digest)) return false;
422
+
423
+ if (state.original_reconciliation !== null
424
+ && (state.original.outcome !== 'indeterminate'
425
+ || state.original_reconciliation.original_operation_id !== state.original.operation_id
426
+ || state.original_reconciliation.original_action_digest !== state.original.action_digest
427
+ || state.original_reconciliation.terminal_evidence_digest !== state.original.terminal_evidence_digest
428
+ || strictInstant(state.original_reconciliation.observed_at) < strictInstant(state.original.occurred_at))) {
429
+ return false;
430
+ }
431
+
432
+ const attempts = [
433
+ ...(state.active_remedy === null ? [] : [state.active_remedy]),
434
+ ...state.remedies,
435
+ ];
436
+ return attempts.every((attempt: DataRecord) => (
437
+ attempt.original_operation_id === state.original.operation_id
438
+ && attempt.destination_binding_digest === state.destination_binding_digest
439
+ && attempt.unit === state.unit
440
+ ));
441
+ }
442
+
443
+ function selectAttempt(state: DataRecord, remedyOperationId: string): DataRecord {
444
+ const attempts = [
445
+ ...(state.active_remedy === null ? [] : [state.active_remedy]),
446
+ ...state.remedies,
447
+ ].filter((attempt: DataRecord) => attempt.remedy_operation_id === remedyOperationId);
448
+ if (attempts.length === 0) throw new TypeError('remedy operation is absent from the state snapshot');
449
+ if (attempts.length !== 1) throw new TypeError('remedy operation is not unique in the state snapshot');
450
+ const attempt = attempts[0];
451
+ if (attempt.remedy_operation_id === state.original.operation_id
452
+ || attempt.remedy_action_digest === state.original.action_digest) {
453
+ throw new TypeError('a remedy receipt must describe a compensating action, never rollback');
454
+ }
455
+ return attempt;
456
+ }
457
+
458
+ function expectedBindings(state: DataRecord, attempt: DataRecord): RemedyReceiptExpectedBindings {
459
+ return {
460
+ original_operation_id: state.original.operation_id,
461
+ original_action_digest: state.original.action_digest,
462
+ original_terminal_evidence_digest: state.original.terminal_evidence_digest,
463
+ case_instance_id: state.instance_id,
464
+ case_revision: state.revision,
465
+ case_status: state.status,
466
+ remedy_operation_id: attempt.remedy_operation_id,
467
+ remedy_action_digest: attempt.remedy_action_digest,
468
+ remedy_caid: attempt.remedy_caid,
469
+ destination_binding_digest: attempt.destination_binding_digest,
470
+ units: attempt.units,
471
+ unit: attempt.unit,
472
+ owner_mode: attempt.consequence_mode,
473
+ owner_digest: ownerDigest(attempt)!,
474
+ };
475
+ }
476
+
477
+ function snapshotAndAttempt(
478
+ value: unknown,
479
+ remedyOperationId: unknown,
480
+ ): { state: DataRecord; attempt: DataRecord } {
481
+ const state = canonicalCopy(value, 'remedy state snapshot') as unknown;
482
+ if (!validState(state)) throw new TypeError('remedy state snapshot is invalid');
483
+ if (!validId(remedyOperationId)) throw new TypeError('remedyOperationId is invalid');
484
+ return { state, attempt: selectAttempt(state, remedyOperationId) };
485
+ }
486
+
487
+ /** Derive every relying-party binding that must be independently expected. */
488
+ export function expectedRemedyProgramReceiptBindings(
489
+ state: unknown,
490
+ remedyOperationId: string,
491
+ ): Readonly<RemedyReceiptExpectedBindings> {
492
+ const selected = snapshotAndAttempt(state, remedyOperationId);
493
+ return deepFreeze(expectedBindings(selected.state, selected.attempt));
494
+ }
495
+
496
+ function payloadFor(state: DataRecord, attempt: DataRecord): DataRecord {
497
+ return {
498
+ case: {
499
+ instance_id: state.instance_id,
500
+ revision: state.revision,
501
+ status: state.status,
502
+ state_snapshot_digest: canonicalDigest(state),
503
+ updated_at: state.updated_at,
504
+ },
505
+ original_effect: canonicalCopy(state.original, 'original effect'),
506
+ original_reconciliation: state.original_reconciliation === null
507
+ ? null : canonicalCopy(state.original_reconciliation, 'original reconciliation'),
508
+ remedy: {
509
+ operation_id: attempt.remedy_operation_id,
510
+ caid: attempt.remedy_caid,
511
+ action_digest: attempt.remedy_action_digest,
512
+ destination_binding_digest: attempt.destination_binding_digest,
513
+ units: attempt.units,
514
+ unit: attempt.unit,
515
+ owner_mode: attempt.consequence_mode,
516
+ owner_digest: ownerDigest(attempt),
517
+ status: attempt.status,
518
+ outcome: attempt.outcome,
519
+ },
520
+ semantics: {
521
+ original_effect: 'immutable_fact',
522
+ remedy_effect: 'compensating_action',
523
+ rollback: false,
524
+ },
525
+ };
526
+ }
527
+
528
+ function contentBody(receipt: DataRecord): DataRecord {
529
+ return {
530
+ version: receipt.version,
531
+ issuer: receipt.issuer,
532
+ payload: receipt.payload,
533
+ };
534
+ }
535
+
536
+ function signingBody(receipt: DataRecord): DataRecord {
537
+ return {
538
+ ...contentBody(receipt),
539
+ content_digest: receipt.content_digest,
540
+ };
541
+ }
542
+
543
+ /** Return the exact domain-separated canonical bytes signed by Ed25519. */
544
+ export function remedyProgramReceiptSigningBytes(receipt: unknown): Buffer {
545
+ if (!isDataRecord(receipt)) throw new TypeError('remedy receipt signing body is invalid');
546
+ return Buffer.from(
547
+ ACTION_REMEDY_RECEIPT_DOMAIN + canonicalize(signingBody(receipt)),
548
+ 'utf8',
549
+ );
550
+ }
551
+
552
+ function validIssuer(value: unknown): value is DataRecord {
553
+ return exactKeys(value, ISSUER_KEYS) && Object.values(value).every(validContext);
554
+ }
555
+
556
+ function validExpected(value: unknown): value is RemedyReceiptExpectedBindings {
557
+ return exactKeys(value, EXPECTED_KEYS)
558
+ && validId(value.original_operation_id)
559
+ && typeof value.original_action_digest === 'string' && DIGEST.test(value.original_action_digest)
560
+ && typeof value.original_terminal_evidence_digest === 'string'
561
+ && DIGEST.test(value.original_terminal_evidence_digest)
562
+ && validId(value.case_instance_id)
563
+ && Number.isSafeInteger(value.case_revision) && value.case_revision >= 0
564
+ && CASE_STATUSES.has(value.case_status)
565
+ && validId(value.remedy_operation_id)
566
+ && typeof value.remedy_action_digest === 'string' && DIGEST.test(value.remedy_action_digest)
567
+ && typeof value.remedy_caid === 'string' && CAID.test(value.remedy_caid)
568
+ && typeof value.destination_binding_digest === 'string'
569
+ && DIGEST.test(value.destination_binding_digest)
570
+ && Number.isSafeInteger(value.units) && value.units > 0
571
+ && validContext(value.unit)
572
+ && ['receipt-program', 'action-escrow'].includes(value.owner_mode)
573
+ && typeof value.owner_digest === 'string' && DIGEST.test(value.owner_digest);
574
+ }
575
+
576
+ function validPayload(value: unknown): value is DataRecord {
577
+ return exactKeys(value, PAYLOAD_KEYS)
578
+ && exactKeys(value.case, CASE_KEYS)
579
+ && validId(value.case.instance_id)
580
+ && Number.isSafeInteger(value.case.revision) && value.case.revision >= 0
581
+ && CASE_STATUSES.has(value.case.status)
582
+ && typeof value.case.state_snapshot_digest === 'string'
583
+ && DIGEST.test(value.case.state_snapshot_digest)
584
+ && Number.isFinite(strictInstant(value.case.updated_at))
585
+ && validOriginal(value.original_effect)
586
+ && (value.original_reconciliation === null
587
+ || validOriginalReconciliation(value.original_reconciliation))
588
+ && exactKeys(value.remedy, REMEDY_KEYS)
589
+ && validId(value.remedy.operation_id)
590
+ && typeof value.remedy.caid === 'string' && CAID.test(value.remedy.caid)
591
+ && typeof value.remedy.action_digest === 'string' && DIGEST.test(value.remedy.action_digest)
592
+ && typeof value.remedy.destination_binding_digest === 'string'
593
+ && DIGEST.test(value.remedy.destination_binding_digest)
594
+ && Number.isSafeInteger(value.remedy.units) && value.remedy.units > 0
595
+ && validContext(value.remedy.unit)
596
+ && ['receipt-program', 'action-escrow'].includes(value.remedy.owner_mode)
597
+ && typeof value.remedy.owner_digest === 'string' && DIGEST.test(value.remedy.owner_digest)
598
+ && REMEDY_STATUSES.has(value.remedy.status)
599
+ && (value.remedy.outcome === null || REMEDY_OUTCOMES.has(value.remedy.outcome))
600
+ && exactKeys(value.semantics, SEMANTICS_KEYS)
601
+ && value.semantics.original_effect === 'immutable_fact'
602
+ && value.semantics.remedy_effect === 'compensating_action'
603
+ && value.semantics.rollback === false
604
+ && value.remedy.operation_id !== value.original_effect.operation_id
605
+ && value.remedy.action_digest !== value.original_effect.action_digest;
606
+ }
607
+
608
+ function validReceipt(value: unknown): value is DataRecord {
609
+ return exactKeys(value, TOP_KEYS)
610
+ && value.version === ACTION_REMEDY_RECEIPT_VERSION
611
+ && validIssuer(value.issuer)
612
+ && validPayload(value.payload)
613
+ && typeof value.content_digest === 'string' && DIGEST.test(value.content_digest)
614
+ && exactKeys(value.signature, SIGNATURE_KEYS)
615
+ && value.signature.algorithm === 'Ed25519'
616
+ && strictBase64url(value.signature.value, 64) !== null;
617
+ }
618
+
619
+ function sameCanonical(left: unknown, right: unknown): boolean {
620
+ try {
621
+ return canonicalize(left) === canonicalize(right);
622
+ } catch {
623
+ return false;
624
+ }
625
+ }
626
+
627
+ function receiptExpected(receipt: DataRecord): RemedyReceiptExpectedBindings {
628
+ return {
629
+ original_operation_id: receipt.payload.original_effect.operation_id,
630
+ original_action_digest: receipt.payload.original_effect.action_digest,
631
+ original_terminal_evidence_digest: receipt.payload.original_effect.terminal_evidence_digest,
632
+ case_instance_id: receipt.payload.case.instance_id,
633
+ case_revision: receipt.payload.case.revision,
634
+ case_status: receipt.payload.case.status,
635
+ remedy_operation_id: receipt.payload.remedy.operation_id,
636
+ remedy_action_digest: receipt.payload.remedy.action_digest,
637
+ remedy_caid: receipt.payload.remedy.caid,
638
+ destination_binding_digest: receipt.payload.remedy.destination_binding_digest,
639
+ units: receipt.payload.remedy.units,
640
+ unit: receipt.payload.remedy.unit,
641
+ owner_mode: receipt.payload.remedy.owner_mode,
642
+ owner_digest: receipt.payload.remedy.owner_digest,
643
+ };
644
+ }
645
+
646
+ function configureSigner(options: DataRecord, context: DataRecord) {
647
+ const hasPrivateKey = options.privateKey !== undefined;
648
+ const hasSigner = options.signer !== undefined;
649
+ if (hasPrivateKey === hasSigner) throw new TypeError('configure exactly one remedy receipt signer');
650
+ const allowEphemeralState = options.allowEphemeralState === true;
651
+
652
+ if (hasPrivateKey) {
653
+ if (!allowEphemeralState) {
654
+ throw new TypeError('production remedy receipt issuance requires an external KMS/HSM signer');
655
+ }
656
+ const key = privateKey(options.privateKey);
657
+ return Object.freeze({
658
+ keyId: context.key_id,
659
+ publicKey: publicKeyB64u(key),
660
+ sign: async (bytes: Buffer) => crypto.sign(null, bytes, key),
661
+ });
662
+ }
663
+
664
+ const signer = options.signer;
665
+ if (!isDataRecord(signer)) throw new TypeError('remedy receipt signer must be a data object');
666
+ if (!validContext(signer.keyId)
667
+ || typeof signer.publicKey !== 'string'
668
+ || typeof signer.sign !== 'function') {
669
+ throw new TypeError('remedy receipt signer requires keyId, publicKey, and sign(bytes)');
670
+ }
671
+ if (!allowEphemeralState && !['kms', 'hsm'].includes(signer.custody)) {
672
+ throw new TypeError('production remedy receipt signer custody must be kms or hsm');
673
+ }
674
+ if (!publicKey(signer.publicKey)) {
675
+ throw new TypeError('remedy receipt signer public key must be Ed25519 SPKI base64url');
676
+ }
677
+ return Object.freeze({
678
+ keyId: signer.keyId,
679
+ publicKey: signer.publicKey,
680
+ sign: signer.sign,
681
+ });
682
+ }
683
+
684
+ /**
685
+ * Issue one receipt. Local private keys require an explicit ephemeral/test
686
+ * opt-in; production issuance requires an external signer declaring KMS/HSM
687
+ * custody. Every returned signature is verified against the configured public
688
+ * key before the receipt leaves this function.
689
+ */
690
+ export async function issueRemedyProgramReceipt(
691
+ input: {
692
+ state?: unknown;
693
+ remedyOperationId?: string;
694
+ } = {},
695
+ options: {
696
+ context?: unknown;
697
+ privateKey?: unknown;
698
+ signer?: unknown;
699
+ allowEphemeralState?: boolean;
700
+ } = {},
701
+ ) {
702
+ if (!exactKeys(input, new Set(['state', 'remedyOperationId']))) {
703
+ throw new TypeError('remedy receipt input must contain exactly state and remedyOperationId');
704
+ }
705
+ const selected = snapshotAndAttempt(input.state, input.remedyOperationId);
706
+ const context = canonicalCopy(options.context, 'remedy receipt context') as unknown;
707
+ if (!validIssuer(context)) {
708
+ throw new TypeError('remedy receipt context must contain exact pinned issuer fields');
709
+ }
710
+ if (context.tenant !== selected.state.tenant_id
711
+ || context.environment !== selected.state.environment
712
+ || context.audience !== selected.state.audience) {
713
+ throw new TypeError('remedy receipt context does not match state tenant/environment/audience');
714
+ }
715
+ const signer = configureSigner(options as DataRecord, context);
716
+ if (signer.keyId !== context.key_id) {
717
+ throw new TypeError('remedy receipt context key_id does not match signer keyId');
718
+ }
719
+
720
+ const unsigned = {
721
+ version: ACTION_REMEDY_RECEIPT_VERSION,
722
+ issuer: context,
723
+ payload: payloadFor(selected.state, selected.attempt),
724
+ };
725
+ const signedBody = {
726
+ ...unsigned,
727
+ content_digest: canonicalDigest(unsigned),
728
+ };
729
+ const signingBytes = remedyProgramReceiptSigningBytes(signedBody);
730
+ const signature = canonicalSignature(await signer.sign(Buffer.from(signingBytes)));
731
+ const verificationKey = publicKey(signer.publicKey)!;
732
+ if (!crypto.verify(null, signingBytes, verificationKey, signature.bytes)) {
733
+ throw new TypeError('remedy receipt signer self-verification failed');
734
+ }
735
+ const receipt = canonicalCopy({
736
+ ...signedBody,
737
+ signature: { algorithm: 'Ed25519', value: signature.encoded },
738
+ }, 'remedy receipt');
739
+ const expected = expectedBindings(selected.state, selected.attempt);
740
+ const selfCheck = verifyRemedyProgramReceipt(receipt, {
741
+ trustedKeys: { [context.key_id]: signer.publicKey },
742
+ expectedIssuer: context,
743
+ state: selected.state,
744
+ expected,
745
+ });
746
+ if (!selfCheck.valid) {
747
+ throw new TypeError(`remedy receipt self-verification failed: ${selfCheck.reason}`);
748
+ }
749
+ return deepFreeze(receipt);
750
+ }
751
+
752
+ export const signRemedyProgramReceipt = issueRemedyProgramReceipt;
753
+ export const createRemedyProgramReceipt = issueRemedyProgramReceipt;
754
+
755
+ function refusal(reason: string, checks: Record<string, boolean>) {
756
+ return Object.freeze({
757
+ valid: false,
758
+ reason,
759
+ checks: Object.freeze({ ...checks }),
760
+ content_digest: null,
761
+ payload: null,
762
+ });
763
+ }
764
+
765
+ /**
766
+ * Verify a receipt without network access. Trust keys, all issuer fields, the
767
+ * exact current state snapshot, and every material original/remedy binding are
768
+ * relying-party inputs; none are accepted from the receipt itself.
769
+ */
770
+ export function verifyRemedyProgramReceipt(receipt: unknown, {
771
+ trustedKeys,
772
+ expectedIssuer,
773
+ state,
774
+ expected,
775
+ }: {
776
+ trustedKeys?: unknown;
777
+ expectedIssuer?: unknown;
778
+ state?: unknown;
779
+ expected?: unknown;
780
+ } = {}) {
781
+ const checks = {
782
+ structure: false,
783
+ payload: false,
784
+ content_digest: false,
785
+ issuer_pin: false,
786
+ key: false,
787
+ signature: false,
788
+ state_snapshot: false,
789
+ expected_bindings: false,
790
+ };
791
+ try {
792
+ const snapshot = canonicalCopy(receipt, 'remedy receipt') as unknown;
793
+ if (!exactKeys(snapshot, TOP_KEYS)
794
+ || snapshot.version !== ACTION_REMEDY_RECEIPT_VERSION
795
+ || !validIssuer(snapshot.issuer)
796
+ || !exactKeys(snapshot.signature, SIGNATURE_KEYS)) {
797
+ return refusal('receipt_structure_invalid', checks);
798
+ }
799
+ checks.structure = true;
800
+ if (!validPayload(snapshot.payload)) {
801
+ return refusal('receipt_structure_invalid', checks);
802
+ }
803
+ checks.payload = true;
804
+ if (typeof snapshot.content_digest !== 'string'
805
+ || !DIGEST.test(snapshot.content_digest)
806
+ || canonicalDigest(contentBody(snapshot)) !== snapshot.content_digest) {
807
+ return refusal('receipt_content_digest_mismatch', checks);
808
+ }
809
+ checks.content_digest = true;
810
+
811
+ let issuerPin: unknown;
812
+ try {
813
+ issuerPin = canonicalCopy(expectedIssuer, 'expected remedy receipt issuer');
814
+ } catch {
815
+ return refusal('receipt_expected_issuer_mismatch', checks);
816
+ }
817
+ checks.issuer_pin = validIssuer(issuerPin) && sameCanonical(snapshot.issuer, issuerPin);
818
+ if (!checks.issuer_pin) return refusal('receipt_expected_issuer_mismatch', checks);
819
+
820
+ const trusted = isDataRecord(trustedKeys)
821
+ && Object.hasOwn(trustedKeys, snapshot.issuer.key_id)
822
+ ? trustedKeys[snapshot.issuer.key_id] : null;
823
+ const key = publicKey(trusted);
824
+ checks.key = key !== null;
825
+ if (!checks.key) return refusal('receipt_key_untrusted', checks);
826
+
827
+ const signatureBytes = snapshot.signature.algorithm === 'Ed25519'
828
+ ? strictBase64url(snapshot.signature.value, 64) : null;
829
+ checks.signature = signatureBytes !== null
830
+ && crypto.verify(null, remedyProgramReceiptSigningBytes(snapshot), key!, signatureBytes);
831
+ if (!checks.signature) return refusal('receipt_signature_invalid', checks);
832
+
833
+ let stateCopy: unknown;
834
+ try {
835
+ stateCopy = canonicalCopy(state, 'expected remedy state snapshot');
836
+ } catch {
837
+ return refusal('receipt_state_snapshot_mismatch', checks);
838
+ }
839
+ checks.state_snapshot = validState(stateCopy)
840
+ && canonicalDigest(stateCopy) === snapshot.payload.case.state_snapshot_digest
841
+ && stateCopy.instance_id === snapshot.payload.case.instance_id
842
+ && stateCopy.revision === snapshot.payload.case.revision
843
+ && stateCopy.status === snapshot.payload.case.status
844
+ && stateCopy.updated_at === snapshot.payload.case.updated_at;
845
+ if (!checks.state_snapshot) return refusal('receipt_state_snapshot_mismatch', checks);
846
+
847
+ let expectedCopy: unknown;
848
+ try {
849
+ expectedCopy = canonicalCopy(expected, 'expected remedy bindings');
850
+ } catch {
851
+ return refusal('receipt_expected_binding_mismatch', checks);
852
+ }
853
+ let derivedAttempt: DataRecord;
854
+ try {
855
+ if (!validExpected(expectedCopy)) {
856
+ return refusal('receipt_expected_binding_mismatch', checks);
857
+ }
858
+ derivedAttempt = selectAttempt(stateCopy as DataRecord, expectedCopy.remedy_operation_id);
859
+ } catch {
860
+ return refusal('receipt_expected_binding_mismatch', checks);
861
+ }
862
+ checks.expected_bindings = sameCanonical(receiptExpected(snapshot), expectedCopy)
863
+ && sameCanonical(expectedBindings(stateCopy as DataRecord, derivedAttempt), expectedCopy)
864
+ && sameCanonical(payloadFor(stateCopy as DataRecord, derivedAttempt), snapshot.payload);
865
+ if (!checks.expected_bindings) {
866
+ return refusal('receipt_expected_binding_mismatch', checks);
867
+ }
868
+
869
+ return deepFreeze({
870
+ valid: true,
871
+ reason: 'verified',
872
+ checks,
873
+ content_digest: snapshot.content_digest,
874
+ payload: canonicalCopy(snapshot.payload, 'verified remedy receipt payload'),
875
+ });
876
+ } catch {
877
+ return refusal('receipt_structure_invalid', checks);
878
+ }
879
+ }
880
+
881
+ export default {
882
+ ACTION_REMEDY_RECEIPT_VERSION,
883
+ REMEDY_PROGRAM_RECEIPT_VERSION,
884
+ ACTION_REMEDY_RECEIPT_DOMAIN,
885
+ expectedRemedyProgramReceiptBindings,
886
+ remedyProgramReceiptSigningBytes,
887
+ issueRemedyProgramReceipt,
888
+ signRemedyProgramReceipt,
889
+ createRemedyProgramReceipt,
890
+ verifyRemedyProgramReceipt,
891
+ };