@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,119 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * EP-RELIANCE-KERNEL-v1 — runtime enforcement wrapper.
4
+ *
5
+ * The pure verdict lives in @emilia-protocol/verify/reliance (evaluateReliance).
6
+ * This is the deny-by-default RUNTIME point a relying party puts in front of a
7
+ * consequential action: it evaluates the evidence packet against the relying
8
+ * party's pinned EP-RELIANCE-PROFILE-v1, appends the decision to a tamper-evident
9
+ * evidence log, and — on anything other than `rely` — returns a machine-readable
10
+ * refusal (HTTP 428, the same Receipt-Required status the Gate uses) naming the
11
+ * closed verdict and what evidence was required.
12
+ *
13
+ * ALLOW iff verdict === 'rely'. Every other closed verdict, a thrown verifier,
14
+ * or a strict evidence-log failure denies. The kernel never re-derives a verdict
15
+ * of its own — it enforces the one the pure offline verifier computed.
16
+ */
17
+ import { createEvidenceLog } from './evidence.js';
18
+ import {
19
+ evaluateReliance,
20
+ RELIANCE_PROFILE_VERSION,
21
+ RELIANCE_VERDICTS,
22
+ } from '@emilia-protocol/verify/reliance';
23
+
24
+ type Obj = Record<string, any>;
25
+
26
+ const RECEIPT_REQUIRED_STATUS = 428;
27
+
28
+ export { RELIANCE_VERDICTS, RELIANCE_PROFILE_VERSION };
29
+
30
+ /** Build the 428 refusal for a non-`rely` verdict. */
31
+ function relianceChallenge(verdict, reasons, profile) {
32
+ return {
33
+ status: RECEIPT_REQUIRED_STATUS,
34
+ error: 'do_not_rely',
35
+ verdict,
36
+ reasons: Array.isArray(reasons) ? reasons : [],
37
+ required_assurance: profile?.required_assurance ?? null,
38
+ required_authority: profile?.required_authority === true,
39
+ required_evidence: Array.isArray(profile?.required_evidence) ? profile.required_evidence : [],
40
+ header: { name: 'Reliance-Refused', value: verdict },
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Create a reliance kernel bound to one relying-party profile.
46
+ *
47
+ * @param {object} [cfg]
48
+ * @param {object} [cfg.profile] the pinned EP-RELIANCE-PROFILE-v1
49
+ * @param {object} [cfg.log] an evidence log (createEvidenceLog); one is created if absent
50
+ * @param {boolean} [cfg.strictEvidence=true] fail closed if the evidence log sink fails
51
+ * @returns {{ check: Function, evidence: object }}
52
+ */
53
+ export function createRelianceKernel({
54
+ profile,
55
+ log,
56
+ strictEvidence = true,
57
+ }: { profile?: Obj; log?: Obj; strictEvidence?: boolean } = {}) {
58
+ const evidence = log || createEvidenceLog({ strict: strictEvidence });
59
+
60
+ /**
61
+ * Evaluate + enforce one evidence packet.
62
+ * @param {object} input the evaluateReliance input MINUS relying_party_profile (bound here)
63
+ * @param {object} [opts] verifier options { approverKeys, logPublicKey, rpId, revokerKeys }
64
+ * @returns {Promise<{ allow:boolean, status:number, verdict:string, reasons:string[], checks:object, challenge:(object|null), decision:object }>}
65
+ */
66
+ async function check(input: Obj = {}, opts: Obj = {}) {
67
+ let result;
68
+ try {
69
+ result = evaluateReliance({ ...input, relying_party_profile: profile }, opts);
70
+ } catch (err) {
71
+ // A thrown verifier is not a maybe — it is a refusal.
72
+ result = { verdict: 'do_not_rely_unsigned', rely: false, reasons: [`verifier_error:${(err as { message?: string })?.message || 'threw'}`], checks: {} };
73
+ }
74
+
75
+ const allow = result.verdict === 'rely';
76
+ const actionHash = input?.action?.action_hash ?? input?.receipt?.action_hash ?? null;
77
+
78
+ // Deny-by-default: record the decision to the tamper-evident log first. In
79
+ // strict mode a log-sink failure THROWS, which we convert to a refusal — an
80
+ // action whose decision cannot be durably recorded must not proceed.
81
+ let decision;
82
+ try {
83
+ decision = await evidence.record({
84
+ type: 'reliance.decision',
85
+ verdict: result.verdict,
86
+ allow,
87
+ action_hash: actionHash,
88
+ reasons: result.reasons,
89
+ checks: result.checks,
90
+ profile: result.profile ?? null,
91
+ });
92
+ } catch (err) {
93
+ return {
94
+ allow: false,
95
+ status: RECEIPT_REQUIRED_STATUS,
96
+ verdict: 'do_not_rely_unsigned',
97
+ reasons: [`evidence_log_failed:${(err as { message?: string })?.message || 'sink'}`],
98
+ checks: result.checks || {},
99
+ challenge: relianceChallenge('do_not_rely_unsigned', ['evidence_log_failed'], profile),
100
+ decision: null,
101
+ };
102
+ }
103
+
104
+ return {
105
+ allow,
106
+ status: allow ? 200 : RECEIPT_REQUIRED_STATUS,
107
+ verdict: result.verdict,
108
+ reasons: result.reasons,
109
+ checks: result.checks,
110
+ challenge: allow ? null : relianceChallenge(result.verdict, result.reasons, profile),
111
+ decision,
112
+ };
113
+ }
114
+
115
+ return { check, evidence };
116
+ }
117
+
118
+ const relianceKernelApi = { createRelianceKernel };
119
+ export default relianceKernelApi;
@@ -0,0 +1,206 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Auditor / insurer-facing reliance packet for an EMILIA Gate decision.
3
+
4
+ export const RELIANCE_PACKET_VERSION = 'EP-GATE-RELIANCE-PACKET-v1';
5
+
6
+ // The CLOSED admissibility verdict set (mirror of lib/evidence/admissibility.js
7
+ // ADMISSIBILITY_VERDICTS). The gate does NOT import that app module — it stays a
8
+ // dependency-light published package — so the closed set is restated here as a
9
+ // value contract. 'admissible' is the ONLY verdict that reads as success; every
10
+ // other member, and anything not in this set, fails closed. Precedence lives in
11
+ // the evaluator; the gate never re-derives a verdict, it only checks the one the
12
+ // relying party's offline evaluator already computed.
13
+ export const ADMISSIBILITY_VERDICTS = Object.freeze([
14
+ 'admissible', 'missing_evidence', 'stale', 'conflicted', 'unverifiable',
15
+ ]);
16
+
17
+ type Obj = Record<string, any>;
18
+
19
+ /**
20
+ * Normalize an admissibility block onto the packet. FAIL CLOSED: a missing block,
21
+ * a missing/unrecognized verdict, or a non-'admissible' verdict all produce
22
+ * admissible:false. The block is only carried when the caller supplies one; when
23
+ * absent, `admissibility` is null and no admissibility gating applies (backwards
24
+ * compatible with pre-profile packets).
25
+ *
26
+ * @param {object|null} adm the relying-party evaluator's result, carrying
27
+ * { admissibility_profile:{id,version}, profile_hash, verdict, replay_digest, challenge_id?, challenge_digest? }
28
+ */
29
+ function normalizeAdmissibility(adm: Obj | null) {
30
+ if (!adm || typeof adm !== 'object') return null;
31
+ const verdict = typeof adm.verdict === 'string' ? adm.verdict : null;
32
+ const recognized = verdict !== null && ADMISSIBILITY_VERDICTS.includes(verdict);
33
+ // Only a recognized 'admissible' verdict reads as success. Missing, malformed,
34
+ // or any other closed-set member (missing_evidence/stale/conflicted/unverifiable)
35
+ // is non-admissible.
36
+ const admissible = recognized && verdict === 'admissible';
37
+ const profile = adm.admissibility_profile && typeof adm.admissibility_profile === 'object'
38
+ ? { id: adm.admissibility_profile.id ?? null, version: adm.admissibility_profile.version ?? null }
39
+ : null;
40
+ return {
41
+ admissibility_profile: profile,
42
+ profile_hash: typeof adm.profile_hash === 'string' ? adm.profile_hash : null,
43
+ verdict: verdict, // preserved verbatim (including null / unrecognized) for the auditor
44
+ verdict_recognized: recognized,
45
+ admissible,
46
+ replay_digest: typeof adm.replay_digest === 'string' ? adm.replay_digest : null,
47
+ // The evidence-challenge loop (lib/negotiate/evidence-challenge.js) keys each
48
+ // round by challenge_id; challenge_digest is carried through when the caller
49
+ // hashes the challenge. Either identifies which challenge round this verdict
50
+ // answers. Both null-safe.
51
+ challenge_id: adm.challenge_id ?? null,
52
+ challenge_digest: typeof adm.challenge_digest === 'string' ? adm.challenge_digest : null,
53
+ };
54
+ }
55
+
56
+ async function evidenceStatus(evidence) {
57
+ if (!evidence) {
58
+ return { ok: false, length: null, head: null, reason: 'evidence_verification_unavailable' };
59
+ }
60
+ try {
61
+ const status = typeof evidence.verify === 'function'
62
+ ? await evidence.verify()
63
+ : await evidence;
64
+ if (!status || typeof status !== 'object' || Array.isArray(status)) {
65
+ return { ok: false, length: null, head: null, reason: 'evidence_verification_malformed' };
66
+ }
67
+ if (status.ok !== true) {
68
+ return {
69
+ ok: false,
70
+ length: Number.isSafeInteger(status.length) ? status.length : null,
71
+ head: typeof status.head === 'string' ? status.head : null,
72
+ reason: status.reason || 'evidence_verification_rejected',
73
+ };
74
+ }
75
+ return {
76
+ ...status,
77
+ ok: true,
78
+ length: Number.isSafeInteger(status.length) ? status.length : null,
79
+ head: typeof status.head === 'string' ? status.head : null,
80
+ };
81
+ } catch {
82
+ return { ok: false, length: null, head: null, reason: 'evidence_verification_failed' };
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Generic check-result builder. `detail` is an opaque payload — sometimes a
88
+ * plain string reason, sometimes a structured object — carried through
89
+ * verbatim for the auditor. Its real type varies per call site; annotate it
90
+ * as `*` rather than over-constraining it to the `null` the default value
91
+ * would otherwise narrow it to.
92
+ * @param {string} id
93
+ * @param {boolean|null} ok
94
+ * @param {*} [detail]
95
+ */
96
+ function check(id, ok, detail: string | Obj | null = null) {
97
+ return { id, ok, ...(detail ? { detail } : {}) };
98
+ }
99
+
100
+ /**
101
+ * @param {{ decision?: any, execution?: any, evidence?: any, manifest?: any, binding?: any, admissibility?: any, verifier?: string }} [o]
102
+ */
103
+ export async function buildReliancePacket({
104
+ decision,
105
+ execution = null,
106
+ evidence = null,
107
+ manifest = null,
108
+ binding = null,
109
+ admissibility = null,
110
+ verifier = '@emilia-protocol/gate',
111
+ }: {
112
+ decision?: Obj | null;
113
+ execution?: Obj | null;
114
+ evidence?: Obj | null;
115
+ manifest?: Obj | null;
116
+ binding?: Obj | null;
117
+ admissibility?: Obj | null;
118
+ verifier?: string;
119
+ } = {}) {
120
+ const evidenceCheck = await evidenceStatus(evidence);
121
+ const decisionHash = decision?.evidence?.hash || decision?.hash || null;
122
+ const executionBound = Boolean(
123
+ execution
124
+ && execution.kind === 'execution'
125
+ && decisionHash
126
+ && execution.authorizes_decision === decisionHash,
127
+ );
128
+ // Prefer the binding carried by the execution proof. Authorization-time
129
+ // binding alone is not enough: an executor could otherwise attest a
130
+ // different mutation and still receive a rely verdict.
131
+ const bindingCheck = execution?.execution_binding
132
+ || binding
133
+ || decision?.evidence?.execution_binding
134
+ || decision?.execution_binding
135
+ || null;
136
+ const allowed = decision?.allow === true;
137
+ const evidenceOk = evidenceCheck.ok === true;
138
+ const bindingOk = bindingCheck ? bindingCheck.ok === true : true;
139
+
140
+ // Admissibility block (relying-party evaluator output, computed OFFLINE against
141
+ // its PINNED profile — never re-derived here). When present it is an additional
142
+ // gate on 'rely': a non-'admissible' verdict (or a missing/unrecognized one)
143
+ // fails closed. When absent, admissibility does not affect the verdict.
144
+ const adm = normalizeAdmissibility(admissibility);
145
+ const admissibilityOk = adm === null ? true : adm.admissible === true;
146
+
147
+ const verdict = allowed && executionBound && evidenceOk && bindingOk && admissibilityOk ? 'rely' : 'do_not_rely';
148
+
149
+ return {
150
+ '@version': RELIANCE_PACKET_VERSION,
151
+ product: 'EMILIA Gate',
152
+ verifier,
153
+ verdict,
154
+ summary: {
155
+ action: decision?.action || null,
156
+ receipt_id: decision?.evidence?.receipt_id || decision?.receipt_id || null,
157
+ subject: decision?.evidence?.subject || null,
158
+ policy_id: decision?.evidence?.evaluated_policy_id || null,
159
+ policy_hash: decision?.evidence?.evaluated_policy_hash || null,
160
+ tenant_id: decision?.evidence?.evaluated_tenant_id || null,
161
+ approvers: decision?.evidence?.evaluated_approvers || [],
162
+ required_tier: decision?.evidence?.required_tier || decision?.required_tier || null,
163
+ observed_tier: decision?.evidence?.have_tier || decision?.have_tier || null,
164
+ decision_hash: decisionHash,
165
+ execution_hash: execution?.hash || null,
166
+ evidence_head: evidenceCheck.head || null,
167
+ // Surface the admissibility verdict + pinned profile in the summary so an
168
+ // auditor sees WHICH bar was cleared without digging into checks. Null when
169
+ // no admissibility block was supplied.
170
+ admissibility_verdict: adm ? adm.verdict : null,
171
+ admissibility_profile: adm ? adm.admissibility_profile : null,
172
+ admissibility_profile_hash: adm ? adm.profile_hash : null,
173
+ },
174
+ // Full admissibility block (or null). The auditor / gate can re-check
175
+ // {profile_hash, verdict, replay_digest} against the profile the relying party
176
+ // pinned. Carried verbatim, fail-closed by construction (see normalizeAdmissibility).
177
+ admissibility: adm,
178
+ checks: [
179
+ check('receipt_present_and_valid', allowed && !String(decision?.reason || '').startsWith('receipt_rejected'), decision?.reason || null),
180
+ check('assurance_sufficient', allowed || decision?.reason !== 'assurance_too_low', decision?.reason === 'assurance_too_low' ? 'receipt tier below action requirement' : null),
181
+ check('receipt_one_time_consumed', allowed || decision?.reason === 'replay_refused' ? decision?.reason !== 'replay_refused' : null),
182
+ check('execution_fields_bound', bindingCheck ? bindingCheck.ok === true : null, bindingCheck ? { missing_observed_fields: bindingCheck.missing_observed_fields || [], mismatched_fields: bindingCheck.mismatched_fields || [] } : 'no material execution-field binding required by this action'),
183
+ check('execution_attests_decision', executionBound, execution ? null : 'no execution record supplied'),
184
+ check('evidence_log_intact', evidenceCheck.ok === true, evidenceCheck.reason || null),
185
+ // Admissibility verdict against the relying party's PINNED profile. null when
186
+ // no profile/verdict was supplied (this reliance did not gate on admissibility);
187
+ // true only for a recognized 'admissible' verdict; false otherwise (fail closed).
188
+ check(
189
+ 'admissibility_verdict_admissible',
190
+ adm === null ? null : adm.admissible === true,
191
+ adm === null
192
+ ? 'no admissibility profile / verdict supplied for this reliance'
193
+ : { verdict: adm.verdict, verdict_recognized: adm.verdict_recognized, profile: adm.admissibility_profile, profile_hash: adm.profile_hash, replay_digest: adm.replay_digest },
194
+ ),
195
+ ],
196
+ manifest_version: manifest?.['@version'] || null,
197
+ limitations: [
198
+ 'The packet proves the gate verified a receipt and enforced its configured policy; it does not prove the human made a wise decision.',
199
+ 'Identity, authority enrollment, and key custody remain external trust roots that must be operated correctly.',
200
+ 'For execution-field binding, observedAction must come from the system of record, not from attacker-controlled request input.',
201
+ 'An admissible verdict means the evidence bundle cleared the bar THIS relying party pinned; it does not establish the action is correct, safe, or currently valid beyond the freshness bounds evaluated. The verdict is computed OFFLINE by the relying party against its own pinned profile — EMILIA neither hosts the authoritative registry nor adjudicates.',
202
+ ],
203
+ };
204
+ }
205
+
206
+ export default { RELIANCE_PACKET_VERSION, ADMISSIBILITY_VERDICTS, buildReliancePacket };
@@ -0,0 +1,367 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Durable, tenant-aware PostgreSQL CAS store for the Remedy Program kernel.
4
+ *
5
+ * Tables are never addressed directly. Each operation calls one service-role
6
+ * RPC on one pinned pg client inside an explicit transaction. Logical refusals
7
+ * have stable reasons; malformed rows, connection failures, and uncertain
8
+ * commit outcomes throw so callers cannot mistake ambiguity for success.
9
+ */
10
+ import { createHash } from 'node:crypto';
11
+ import { canonicalize } from '../execution-binding.js';
12
+ import type {
13
+ RemedyProgramResult,
14
+ RemedyProgramState,
15
+ RemedyProgramStore,
16
+ } from './remedy-program.js';
17
+
18
+ export const REMEDY_PROGRAM_PG_STORE_VERSION = 'EP-GATE-REMEDY-PROGRAM-PG-STORE-v1';
19
+ export const REMEDY_PROGRAM_MAX_STATE_BYTES = 4 * 1024 * 1024;
20
+ export const REMEDY_PROGRAM_MAX_FORWARD_SKEW_MINUTES = 5;
21
+
22
+ const INSTANCE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
23
+ const DIGEST = /^sha256:[0-9a-f]{64}$/;
24
+ const STRICT_INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/;
25
+ const ENVELOPE_KEYS = new Set([
26
+ 'ok', 'reason', 'tenant_id', 'instance_id', 'revision',
27
+ 'state_json', 'state_digest', 'recorded_at',
28
+ ]);
29
+
30
+ type RecordLike = Record<string, any>;
31
+ type QueryResult = { rowCount: number | null; rows?: any[] };
32
+ type PgClient = {
33
+ query: (text: string, params?: any[]) => Promise<QueryResult>;
34
+ release: () => void;
35
+ };
36
+ type PgPool = { connect: () => Promise<PgClient> };
37
+
38
+ export const REMEDY_PROGRAM_POSTGRES_SQL = Object.freeze({
39
+ create: `SELECT ok, reason, tenant_id, instance_id, revision, state_json, state_digest, recorded_at
40
+ FROM remedy_program_private.remedy_program_create(
41
+ $1::text, $2::text, $3::text, $4::text, $5::text
42
+ )`,
43
+ get: `SELECT ok, reason, tenant_id, instance_id, revision, state_json, state_digest, recorded_at
44
+ FROM remedy_program_private.remedy_program_get($1::text, $2::text)`,
45
+ compareAndSwap: `SELECT ok, reason, tenant_id, instance_id, revision, state_json, state_digest, recorded_at
46
+ FROM remedy_program_private.remedy_program_compare_and_swap(
47
+ $1::text, $2::text, $3::bigint, $4::bigint, $5::text, $6::text, $7::text
48
+ )`,
49
+ });
50
+
51
+ function isRecord(value: unknown): value is RecordLike {
52
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
53
+ const prototype = Object.getPrototypeOf(value);
54
+ return prototype === Object.prototype || prototype === null;
55
+ }
56
+
57
+ function exactKeys(value: RecordLike, expected: ReadonlySet<string>) {
58
+ const keys = Reflect.ownKeys(value);
59
+ return keys.length === expected.size
60
+ && keys.every((key) => typeof key === 'string' && expected.has(key));
61
+ }
62
+
63
+ function fail(reason: string): RemedyProgramResult {
64
+ return { ok: false, reason };
65
+ }
66
+
67
+ function assertTenantId(value: unknown): asserts value is string {
68
+ if (typeof value !== 'string'
69
+ || Buffer.byteLength(value, 'utf8') < 1
70
+ || Buffer.byteLength(value, 'utf8') > 512
71
+ || /[\u0000-\u001f\u007f]/.test(value)) {
72
+ throw new TypeError('remedy-program tenantId is invalid');
73
+ }
74
+ }
75
+
76
+ function assertInstanceId(value: unknown): asserts value is string {
77
+ if (typeof value !== 'string' || !INSTANCE_ID.test(value)) {
78
+ throw new TypeError('remedy-program instanceId is invalid');
79
+ }
80
+ }
81
+
82
+ function assertRevision(value: unknown, label: string): asserts value is number {
83
+ if (!Number.isSafeInteger(value) || (value as number) < 0) {
84
+ throw new TypeError(`remedy-program ${label} revision is invalid`);
85
+ }
86
+ }
87
+
88
+ function stateDigest(stateJson: string) {
89
+ return `sha256:${createHash('sha256').update(stateJson, 'utf8').digest('hex')}`;
90
+ }
91
+
92
+ function stateEventAt(state: RecordLike) {
93
+ const value = state.updated_at;
94
+ if (typeof value !== 'string'
95
+ || !STRICT_INSTANT.test(value)
96
+ || !Number.isFinite(Date.parse(value))) {
97
+ throw new TypeError('remedy-program state updated_at is invalid');
98
+ }
99
+ return value;
100
+ }
101
+
102
+ function encodeState(
103
+ state: unknown,
104
+ tenantId: string,
105
+ instanceId: string,
106
+ revision: number,
107
+ ) {
108
+ if (!isRecord(state)
109
+ || state.tenant_id !== tenantId
110
+ || state.instance_id !== instanceId
111
+ || state.revision !== revision) {
112
+ throw new TypeError('remedy-program state binding does not match the store operation');
113
+ }
114
+ let stateJson: string;
115
+ try {
116
+ stateJson = canonicalize(state);
117
+ } catch {
118
+ throw new TypeError('remedy-program state must be canonical JSON');
119
+ }
120
+ if (Buffer.byteLength(stateJson, 'utf8') > REMEDY_PROGRAM_MAX_STATE_BYTES) {
121
+ throw new TypeError('remedy-program state exceeds the durable store limit');
122
+ }
123
+ return {
124
+ stateJson,
125
+ stateDigest: stateDigest(stateJson),
126
+ eventAt: stateEventAt(state),
127
+ };
128
+ }
129
+
130
+ function safeRevision(value: unknown) {
131
+ const revision = typeof value === 'string' && /^(?:0|[1-9]\d*)$/.test(value)
132
+ ? Number(value)
133
+ : value;
134
+ if (!Number.isSafeInteger(revision) || (revision as number) < 0) {
135
+ throw new Error('remedy-program Postgres returned an invalid revision');
136
+ }
137
+ return revision as number;
138
+ }
139
+
140
+ function assertRecordedAt(value: unknown) {
141
+ if (typeof value !== 'string'
142
+ || !STRICT_INSTANT.test(value)
143
+ || !Number.isFinite(Date.parse(value))) {
144
+ throw new Error('remedy-program Postgres returned an invalid recorded_at');
145
+ }
146
+ }
147
+
148
+ function decodeState(
149
+ row: RecordLike,
150
+ expectedTenantId: string,
151
+ expectedInstanceId: string,
152
+ expectedRevision?: number,
153
+ ) {
154
+ if (row.tenant_id !== expectedTenantId
155
+ || row.instance_id !== expectedInstanceId
156
+ || typeof row.state_json !== 'string'
157
+ || typeof row.state_digest !== 'string'
158
+ || !DIGEST.test(row.state_digest)
159
+ || Buffer.byteLength(row.state_json, 'utf8') > REMEDY_PROGRAM_MAX_STATE_BYTES
160
+ || row.state_digest !== stateDigest(row.state_json)) {
161
+ throw new Error('remedy-program Postgres returned an invalid state envelope');
162
+ }
163
+ assertRecordedAt(row.recorded_at);
164
+ const revision = safeRevision(row.revision);
165
+ if (expectedRevision !== undefined && revision !== expectedRevision) {
166
+ throw new Error('remedy-program Postgres returned an unexpected revision');
167
+ }
168
+
169
+ let state: unknown;
170
+ try {
171
+ state = JSON.parse(row.state_json);
172
+ if (!isRecord(state)
173
+ || canonicalize(state) !== row.state_json
174
+ || state.tenant_id !== expectedTenantId
175
+ || state.instance_id !== expectedInstanceId
176
+ || state.revision !== revision) {
177
+ throw new Error('state binding mismatch');
178
+ }
179
+ stateEventAt(state);
180
+ } catch {
181
+ throw new Error('remedy-program Postgres returned non-canonical state');
182
+ }
183
+ return state as RemedyProgramState;
184
+ }
185
+
186
+ function definitiveRow(
187
+ result: QueryResult,
188
+ operation: string,
189
+ tenantId: string,
190
+ instanceId: string,
191
+ allowedReasons: ReadonlySet<string>,
192
+ expectedRevision?: number,
193
+ ): RemedyProgramResult {
194
+ if (!result || result.rowCount !== 1 || !Array.isArray(result.rows) || result.rows.length !== 1) {
195
+ throw new Error(`remedy-program ${operation} outcome is ambiguous`);
196
+ }
197
+ const row = result.rows[0];
198
+ if (!isRecord(row) || !exactKeys(row, ENVELOPE_KEYS) || typeof row.ok !== 'boolean') {
199
+ throw new Error(`remedy-program ${operation} outcome is malformed`);
200
+ }
201
+ if (row.tenant_id !== tenantId || row.instance_id !== instanceId) {
202
+ throw new Error(`remedy-program ${operation} Postgres returned a mismatched identity`);
203
+ }
204
+ if (!row.ok) {
205
+ if (typeof row.reason !== 'string'
206
+ || !allowedReasons.has(row.reason)
207
+ || row.revision !== null
208
+ || row.state_json !== null
209
+ || row.state_digest !== null
210
+ || row.recorded_at !== null) {
211
+ throw new Error(`remedy-program ${operation} refusal is unrecognized`);
212
+ }
213
+ return fail(row.reason);
214
+ }
215
+ if (row.reason !== null) {
216
+ throw new Error(`remedy-program ${operation} success carried a refusal reason`);
217
+ }
218
+ return {
219
+ ok: true,
220
+ state: decodeState(row, tenantId, instanceId, expectedRevision),
221
+ };
222
+ }
223
+
224
+ function lookupInput(value: unknown) {
225
+ if (!isRecord(value)
226
+ || Reflect.ownKeys(value).length !== 2
227
+ || !Object.hasOwn(value, 'tenantId')
228
+ || !Object.hasOwn(value, 'instanceId')) {
229
+ throw new TypeError('remedy-program lookup input is invalid');
230
+ }
231
+ assertTenantId(value.tenantId);
232
+ assertInstanceId(value.instanceId);
233
+ return { tenantId: value.tenantId, instanceId: value.instanceId };
234
+ }
235
+
236
+ /** Build the exact durable store consumed by createRemedyProgramKernel(). */
237
+ export function createRemedyProgramPostgresStore(
238
+ { pool }: { pool?: PgPool } = {},
239
+ ): RemedyProgramStore {
240
+ if (!pool || typeof pool.connect !== 'function') {
241
+ throw new TypeError(
242
+ 'createRemedyProgramPostgresStore requires a transaction-capable pg pool',
243
+ );
244
+ }
245
+
246
+ async function transaction<T>(readOnly: boolean, work: (client: PgClient) => Promise<T>) {
247
+ const client = await pool!.connect();
248
+ if (!client || typeof client.query !== 'function' || typeof client.release !== 'function') {
249
+ throw new TypeError('remedy-program pg pool returned an invalid client');
250
+ }
251
+ let began = false;
252
+ try {
253
+ await client.query(readOnly
254
+ ? 'BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY'
255
+ : 'BEGIN ISOLATION LEVEL READ COMMITTED READ WRITE');
256
+ began = true;
257
+ const value = await work(client);
258
+ await client.query('COMMIT');
259
+ began = false;
260
+ return value;
261
+ } catch (error) {
262
+ if (began) {
263
+ try {
264
+ await client.query('ROLLBACK');
265
+ } catch (rollbackError) {
266
+ throw new AggregateError(
267
+ [error, rollbackError],
268
+ 'remedy-program transaction and rollback both failed',
269
+ );
270
+ }
271
+ }
272
+ throw error;
273
+ } finally {
274
+ client.release();
275
+ }
276
+ }
277
+
278
+ return Object.freeze({
279
+ durable: true,
280
+ async create(state: RemedyProgramState) {
281
+ const tenantId = (state as RecordLike)?.tenant_id;
282
+ const instanceId = (state as RecordLike)?.instance_id;
283
+ assertTenantId(tenantId);
284
+ assertInstanceId(instanceId);
285
+ const encoded = encodeState(state, tenantId, instanceId, 0);
286
+ return transaction(false, async (client) => definitiveRow(
287
+ await client.query(REMEDY_PROGRAM_POSTGRES_SQL.create, [
288
+ tenantId,
289
+ instanceId,
290
+ encoded.stateJson,
291
+ encoded.stateDigest,
292
+ encoded.eventAt,
293
+ ]),
294
+ 'create',
295
+ tenantId,
296
+ instanceId,
297
+ new Set([
298
+ 'instance_exists', 'clock_forward_skew',
299
+ 'evidence_replayed', 'remedy_operation_replayed',
300
+ ]),
301
+ 0,
302
+ ));
303
+ },
304
+ async get(input: { tenantId: string; instanceId: string }) {
305
+ const { tenantId, instanceId } = lookupInput(input);
306
+ return transaction(true, async (client) => definitiveRow(
307
+ await client.query(REMEDY_PROGRAM_POSTGRES_SQL.get, [tenantId, instanceId]),
308
+ 'get',
309
+ tenantId,
310
+ instanceId,
311
+ new Set(['instance_not_found']),
312
+ ));
313
+ },
314
+ async compareAndSwap(input: {
315
+ tenantId: string;
316
+ instanceId: string;
317
+ expectedRevision: number;
318
+ state: RemedyProgramState;
319
+ }) {
320
+ if (!isRecord(input)
321
+ || Reflect.ownKeys(input).length !== 4
322
+ || !Object.hasOwn(input, 'tenantId')
323
+ || !Object.hasOwn(input, 'instanceId')
324
+ || !Object.hasOwn(input, 'expectedRevision')
325
+ || !Object.hasOwn(input, 'state')) {
326
+ throw new TypeError('remedy-program compareAndSwap input is invalid');
327
+ }
328
+ const { tenantId, instanceId, expectedRevision, state } = input;
329
+ assertTenantId(tenantId);
330
+ assertInstanceId(instanceId);
331
+ assertRevision(expectedRevision, 'expected');
332
+ const nextRevision = expectedRevision + 1;
333
+ if (!Number.isSafeInteger(nextRevision)) {
334
+ throw new TypeError('remedy-program next revision is invalid');
335
+ }
336
+ const encoded = encodeState(state, tenantId, instanceId, nextRevision);
337
+ return transaction(false, async (client) => definitiveRow(
338
+ await client.query(REMEDY_PROGRAM_POSTGRES_SQL.compareAndSwap, [
339
+ tenantId,
340
+ instanceId,
341
+ expectedRevision,
342
+ nextRevision,
343
+ encoded.stateJson,
344
+ encoded.stateDigest,
345
+ encoded.eventAt,
346
+ ]),
347
+ 'compareAndSwap',
348
+ tenantId,
349
+ instanceId,
350
+ new Set([
351
+ 'instance_not_found', 'revision_conflict',
352
+ 'clock_regression', 'clock_forward_skew',
353
+ 'evidence_replayed', 'remedy_operation_replayed',
354
+ ]),
355
+ nextRevision,
356
+ ));
357
+ },
358
+ });
359
+ }
360
+
361
+ export default {
362
+ REMEDY_PROGRAM_PG_STORE_VERSION,
363
+ REMEDY_PROGRAM_MAX_STATE_BYTES,
364
+ REMEDY_PROGRAM_MAX_FORWARD_SKEW_MINUTES,
365
+ REMEDY_PROGRAM_POSTGRES_SQL,
366
+ createRemedyProgramPostgresStore,
367
+ };