@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
package/src/index.ts ADDED
@@ -0,0 +1,2122 @@
1
+ /**
2
+ * @emilia-protocol/gate — EMILIA Gate: the Consequence Firewall.
3
+ * @license Apache-2.0
4
+ *
5
+ * Deny-by-default enforcement for consequential machine actions. A guarded
6
+ * action runs ONLY if it arrives with a receipt that is:
7
+ * 1. valid — Ed25519 over canonical JSON, signed by a pinned issuer;
8
+ * 2. in-scope — bound to the exact action the manifest guards;
9
+ * 3. sufficiently — meets the action's required assurance tier, and the
10
+ * assured credited tier is CRYPTOGRAPHICALLY VERIFIED, not read
11
+ * from self-asserted payload fields: class_a requires a
12
+ * valid WebAuthn device signoff, quorum requires a valid
13
+ * EP-QUORUM-v1 (distinct humans + distinct keys +
14
+ * threshold + per-signer assertions);
15
+ * 4. fresh — within max age; and
16
+ * 5. unused — not a replay (one-time consumption).
17
+ * Otherwise it is refused with a machine-readable Receipt-Required challenge
18
+ * (HTTP 428). Every decision is appended to a tamper-evident evidence log.
19
+ *
20
+ * It is NOT authentication ("who are you") and NOT permissions ("are you
21
+ * allowed here"). It is a policy-enforcement point that requires portable proof
22
+ * a named human authorized THIS exact action before the world is mutated.
23
+ *
24
+ * Composes @emilia-protocol/require-receipt (manifest + verify + challenge) and
25
+ * adds the three things a firewall needs over a bare verifier: assurance-tier
26
+ * enforcement, replay defense, and the evidence log. Fails closed.
27
+ */
28
+ import crypto from 'node:crypto';
29
+ import {
30
+ verifyEmiliaReceipt,
31
+ receiptChallenge,
32
+ receiptRequiredHeader,
33
+ validateActionRiskManifest,
34
+ findActionRequirement,
35
+ evaluateReceiptAssurance,
36
+ validatePinnedQuorumPolicy,
37
+ receiptAssuranceTier as receiptAssuranceTierFromProof,
38
+ parseReceiptCarrier,
39
+ RECEIPT_REQUIRED_STATUS,
40
+ RECEIPT_REQUIRED_HEADER,
41
+ } from '@emilia-protocol/require-receipt';
42
+ import { verifyWebAuthnSignoff, verifyQuorum } from '@emilia-protocol/verify';
43
+ import { MemoryConsumptionStore, isSecureConsumptionStore } from './store.js';
44
+ import {
45
+ canonicalEvidenceJson,
46
+ createAtomicEvidenceLog,
47
+ createEvidenceLog,
48
+ createMemoryAtomicEvidenceBackend,
49
+ } from './evidence.js';
50
+ import { DEFAULT_GATE_MANIFEST, HIGH_RISK_ACTION_PACKS, createDefaultActionRiskManifest } from './action-packs.js';
51
+ import { hashCanonical, verifyExecutionBinding } from './execution-binding.js';
52
+ import { buildReliancePacket, ADMISSIBILITY_VERDICTS } from './reliance-packet.js';
53
+ import { createEg1Harness, makeGateInvoke, runEg1, EG1_DEFAULT_SELECTOR, mintDeviceSignoff, mintQuorumEvidence } from './eg1-conformance.js';
54
+ import { CF1_VERSION, CF1_CHECKS, runCf1 } from './cf1-conformance.js';
55
+ import { createKeyRegistry, asKeyRegistry } from './key-registry.js';
56
+ import { classifyRetention, buildRetentionExport } from './retention.js';
57
+ import {
58
+ ACTION_CONTROL_MANIFEST_VERSION,
59
+ createDefaultActionControlManifest,
60
+ findActionControl,
61
+ resolveActionControl,
62
+ validateActionControlManifest,
63
+ } from './action-control-manifest.js';
64
+ import {
65
+ createRuntimeMonitor,
66
+ RUNTIME_MONITOR_VERSION,
67
+ RUNTIME_MONITOR_MODES,
68
+ RUNTIME_INVARIANTS,
69
+ } from './runtime-monitor.js';
70
+ import {
71
+ FORMAL_RUNTIME_BRIDGE_VERSION,
72
+ FORMAL_RUNTIME_SPEC,
73
+ FORMAL_RUNTIME_CONFIG,
74
+ FORMAL_RUNTIME_INVARIANT_MAP,
75
+ } from './formal-runtime-map.js';
76
+ import {
77
+ CAPABILITY_RECEIPT_VERSION,
78
+ CAPABILITY_STATE_VERSION,
79
+ CAPABILITY_SHARE_VERSION,
80
+ CAPABILITY_SCOPE_PROFILE,
81
+ CAPABILITY_CAID_SCOPE_PROFILE,
82
+ CAPABILITY_STATE_DDL,
83
+ CAPABILITY_SQL,
84
+ capabilityBaseReceiptDigest,
85
+ capabilityActionDigest,
86
+ verifyCapabilityScope,
87
+ mintCapabilityReceipt,
88
+ verifyCapabilityReceipt,
89
+ splitCapabilitySecret,
90
+ reconstructCapabilitySecret,
91
+ createMemoryCapabilityStore,
92
+ createPostgresCapabilityStore,
93
+ executeWithCapability,
94
+ executeWithThreshold,
95
+ reconcileCapabilityOperation,
96
+ delegateCapabilityReceipt,
97
+ } from './capability-receipt.js';
98
+ import {
99
+ ZK_RANGE_RECEIPT_VERSION,
100
+ ZK_RANGE_SCHEME,
101
+ ZK_RANGE_BACKEND_PACKAGE,
102
+ deriveZkRangeBases,
103
+ loadBulletproofBackend,
104
+ mintZkRangeReceipt,
105
+ verifyZkRangeReceipt,
106
+ } from './zk-range-proof.js';
107
+ import {
108
+ mintBreakGlassAuthorization,
109
+ verifyBreakGlass,
110
+ consumeBreakGlass,
111
+ buildBreakGlassEvidence,
112
+ runBreakGlass,
113
+ BREAKGLASS_VERSION,
114
+ BREAKGLASS_EVIDENCE_KIND,
115
+ } from './breakglass.js';
116
+
117
+ type Obj = Record<string, any>;
118
+
119
+ /** Shared shape for the `opts`/selector-bag argument accepted by route(), guard(),
120
+ * requestGateInput(), run(), and runCapability(). Each field may be a literal value
121
+ * or a `(req) => value` resolver function; see requestGateInput for resolution. */
122
+ interface GateCallOpts {
123
+ selector?: any;
124
+ action?: any;
125
+ receipt?: any;
126
+ observedAction?: any;
127
+ admissibilityProfile?: any;
128
+ reliancePacket?: any;
129
+ admissibility?: any;
130
+ capability?: any;
131
+ recordExecution?: boolean;
132
+ }
133
+
134
+ /** The real (loosely-typed) shape of run()/runCapability()'s settled result, used
135
+ * at the point of use in route()/guard() — run()'s inferred return type is a large
136
+ * union across its own branches and runCapability()'s, so plain truthiness checks
137
+ * on it (`out.refusal || out.authorization`) can't be statically discriminated. */
138
+ type GateRunResult = {
139
+ ok: boolean;
140
+ status?: any;
141
+ body?: any;
142
+ authorization?: any;
143
+ refusal?: any;
144
+ execution?: any;
145
+ packet?: any;
146
+ result?: any;
147
+ capability?: any;
148
+ };
149
+
150
+ export {
151
+ MemoryConsumptionStore,
152
+ canonicalEvidenceJson,
153
+ createEvidenceLog,
154
+ createAtomicEvidenceLog,
155
+ createMemoryAtomicEvidenceBackend,
156
+ };
157
+ export {
158
+ createDurableConsumptionStore,
159
+ createMemoryBackend,
160
+ isSecureConsumptionStore,
161
+ DURABLE_CONSUMPTION_VERSION,
162
+ } from './store.js';
163
+ export { createDurableChallengeStore, challengeStorageKey, challengeBodyDigest, DURABLE_CHALLENGE_STORE_VERSION } from './challenge-store.js';
164
+ export { createKeyRegistry, asKeyRegistry } from './key-registry.js';
165
+ export { classifyRetention, buildRetentionExport, RETENTION_EXPORT_VERSION } from './retention.js';
166
+ export { DEFAULT_GATE_MANIFEST, HIGH_RISK_ACTION_PACKS, createDefaultActionRiskManifest };
167
+ export {
168
+ ACTION_CONTROL_MANIFEST_VERSION,
169
+ ACTION_CONTROL_SCHEMA_URL,
170
+ ACTION_CONTROL_CONFORMANCE_LEVEL,
171
+ ACTION_CONTROL_DEFAULTS,
172
+ ACTION_CONTROL_EVIDENCE_PROFILES,
173
+ ACTION_CONTROL_CONFORMANCE_CHECKS,
174
+ toActionControl,
175
+ createDefaultActionControlManifest,
176
+ findActionControl,
177
+ resolveActionControl,
178
+ validateActionControlManifest,
179
+ } from './action-control-manifest.js';
180
+ export { EXECUTION_BINDING_VERSION, canonicalize, hashCanonical, materialFieldsFor, verifyExecutionBinding } from './execution-binding.js';
181
+ export { RELIANCE_PACKET_VERSION, ADMISSIBILITY_VERDICTS, buildReliancePacket } from './reliance-packet.js';
182
+ export {
183
+ EXTERNAL_VERIFICATION_STATEMENT_VERSION,
184
+ EXTERNAL_VERIFICATION_DOMAIN,
185
+ externalVerificationDigest,
186
+ signExternalVerificationStatement,
187
+ verifyExternalVerificationStatement,
188
+ } from './reports/external-verification.js';
189
+ export {
190
+ EG1_VERSION, EG1_CHECKS, EG1_DEFAULT_ACTION, EG1_DEFAULT_SELECTOR,
191
+ createEg1Harness, makeGateInvoke, runEg1, mintDeviceSignoff, mintQuorumEvidence,
192
+ } from './eg1-conformance.js';
193
+ export { CF1_VERSION, CF1_CHECKS, runCf1 } from './cf1-conformance.js';
194
+ export {
195
+ mintBreakGlassAuthorization,
196
+ verifyBreakGlass,
197
+ consumeBreakGlass,
198
+ buildBreakGlassEvidence,
199
+ runBreakGlass,
200
+ BREAKGLASS_VERSION,
201
+ BREAKGLASS_EVIDENCE_KIND,
202
+ };
203
+ export { createRuntimeMonitor, RUNTIME_MONITOR_VERSION, RUNTIME_MONITOR_MODES, RUNTIME_INVARIANTS } from './runtime-monitor.js';
204
+ export {
205
+ FORMAL_RUNTIME_BRIDGE_VERSION,
206
+ FORMAL_RUNTIME_SPEC,
207
+ FORMAL_RUNTIME_CONFIG,
208
+ FORMAL_RUNTIME_INVARIANT_MAP,
209
+ } from './formal-runtime-map.js';
210
+ export {
211
+ CAPABILITY_RECEIPT_VERSION,
212
+ CAPABILITY_STATE_VERSION,
213
+ CAPABILITY_SHARE_VERSION,
214
+ CAPABILITY_SCOPE_PROFILE,
215
+ CAPABILITY_CAID_SCOPE_PROFILE,
216
+ CAPABILITY_STATE_DDL,
217
+ CAPABILITY_SQL,
218
+ capabilityBaseReceiptDigest,
219
+ capabilityActionDigest,
220
+ verifyCapabilityScope,
221
+ mintCapabilityReceipt,
222
+ verifyCapabilityReceipt,
223
+ splitCapabilitySecret,
224
+ reconstructCapabilitySecret,
225
+ createMemoryCapabilityStore,
226
+ createPostgresCapabilityStore,
227
+ executeWithCapability,
228
+ executeWithThreshold,
229
+ reconcileCapabilityOperation,
230
+ delegateCapabilityReceipt,
231
+ } from './capability-receipt.js';
232
+ export {
233
+ ZK_RANGE_RECEIPT_VERSION,
234
+ ZK_RANGE_SCHEME,
235
+ ZK_RANGE_BACKEND_PACKAGE,
236
+ deriveZkRangeBases,
237
+ loadBulletproofBackend,
238
+ mintZkRangeReceipt,
239
+ verifyZkRangeReceipt,
240
+ } from './zk-range-proof.js';
241
+ export {
242
+ RECEIPT_PROGRAM_VERSION,
243
+ RECEIPT_PROGRAM_CERTIFICATE_VERSION,
244
+ RECEIPT_PROGRAM_SIGNATURE_ALGORITHM,
245
+ createReceiptProgramKernel,
246
+ verifyReceiptProgramCertificate,
247
+ } from './receipt-program.js';
248
+ export {
249
+ TRUST_PROGRAM_VERSION,
250
+ TRUST_STAGE_RECEIPT_VERSION,
251
+ validateTrustProgram,
252
+ trustProgramDigest,
253
+ verifyTrustStageReceipt,
254
+ createMemoryTrustProgramStore,
255
+ createTrustProgramKernel,
256
+ } from './trust-program.js';
257
+ export {
258
+ TRUST_PROGRAM_REVOCATION_TARGET_VERSION,
259
+ deriveTrustProgramRevocationTargetObject,
260
+ deriveTrustProgramRevocationTarget,
261
+ verifyTrustProgramRevocation,
262
+ applyTrustProgramRevocation,
263
+ } from './trust-program-revocation.js';
264
+ export {
265
+ REMEDY_PROGRAM_VERSION,
266
+ createRemedyMemoryStore,
267
+ createRemedyProgramKernel,
268
+ } from './remedy-program.js';
269
+ export {
270
+ ACTION_REMEDY_RECEIPT_VERSION,
271
+ REMEDY_PROGRAM_RECEIPT_VERSION,
272
+ ACTION_REMEDY_RECEIPT_DOMAIN,
273
+ expectedRemedyProgramReceiptBindings,
274
+ remedyProgramReceiptSigningBytes,
275
+ issueRemedyProgramReceipt,
276
+ signRemedyProgramReceipt,
277
+ createRemedyProgramReceipt,
278
+ verifyRemedyProgramReceipt,
279
+ } from './remedy-program-receipt.js';
280
+ export {
281
+ REMEDY_PROGRAM_PG_STORE_VERSION,
282
+ REMEDY_PROGRAM_MAX_STATE_BYTES,
283
+ REMEDY_PROGRAM_MAX_FORWARD_SKEW_MINUTES,
284
+ REMEDY_PROGRAM_POSTGRES_SQL,
285
+ createRemedyProgramPostgresStore,
286
+ } from './remedy-program-postgres.js';
287
+ export const ASSURANCE_TIERS = ['software', 'class_a', 'quorum'];
288
+ const TIER_RANK = { software: 0, class_a: 1, quorum: 2 };
289
+ const CAPABILITY_FAILURE_STATUS = 409;
290
+
291
+ function gateOutcomeError(cause: any, { outcome, reason, authorization, execution, result = null }: any): any {
292
+ const error: any = new Error(cause?.message ?? `EMILIA Gate terminal outcome: ${reason}`, { cause });
293
+ error.code = 'EMILIA_GATE_TERMINAL_OUTCOME';
294
+ let authorizationEvidence = null;
295
+ let executionEvidence = null;
296
+ let resultSnapshot = null;
297
+ try { authorizationEvidence = structuredClone(authorization?.evidence ?? null); } catch { /* unavailable */ }
298
+ try { executionEvidence = structuredClone(execution ?? null); } catch { /* unavailable */ }
299
+ try { resultSnapshot = structuredClone(result); } catch { /* unavailable */ }
300
+ Object.defineProperty(error, 'emiliaGateOutcome', {
301
+ value: Object.freeze({
302
+ outcome,
303
+ reason,
304
+ authorizationEvidence,
305
+ execution: executionEvidence,
306
+ result: resultSnapshot,
307
+ }),
308
+ enumerable: false,
309
+ configurable: false,
310
+ writable: false,
311
+ });
312
+ return error;
313
+ }
314
+
315
+ function safeCanonicalHash(value) {
316
+ try { return hashCanonical(value); } catch { return null; }
317
+ }
318
+
319
+ /**
320
+ * Capability spending is intentionally bound to the executor's observed
321
+ * monetary action, not to a presenter-selected amount. The default action
322
+ * packs use `amount_usd`; integrations that use the generic `amount` field
323
+ * are supported as well. A missing or mismatched amount/currency fails closed
324
+ * before the capability store is touched.
325
+ */
326
+ function verifyCapabilityActionBinding({ capability, observedAction }: { capability?: any; observedAction?: any } = {}) {
327
+ if (capability === null || capability === undefined) return { ok: true, required: false };
328
+ const action = capability?.action;
329
+ if (!action || typeof action !== 'object' || Array.isArray(action)
330
+ || !Number.isSafeInteger(action.amount) || action.amount <= 0
331
+ || typeof action.currency !== 'string' || action.currency.length === 0) {
332
+ return { ok: false, reason: 'capability_action_invalid', required: true };
333
+ }
334
+ const observedAmount = Number.isSafeInteger(observedAction?.amount)
335
+ ? observedAction.amount
336
+ : observedAction?.amount_usd;
337
+ const observedCurrency = observedAction?.currency;
338
+ if (observedAmount !== action.amount || observedCurrency !== action.currency) {
339
+ return {
340
+ ok: false,
341
+ reason: 'capability_action_binding_failed',
342
+ required: true,
343
+ capability_amount: action.amount,
344
+ capability_currency: action.currency,
345
+ observed_amount: observedAmount ?? null,
346
+ observed_currency: observedCurrency ?? null,
347
+ };
348
+ }
349
+ return {
350
+ ok: true,
351
+ required: true,
352
+ amount: action.amount,
353
+ currency: action.currency,
354
+ };
355
+ }
356
+
357
+ function capabilitySummary(capability, operationId = null) {
358
+ return {
359
+ capability_id: capability?.capabilityReceipt?.capability?.id ?? null,
360
+ operation_id: operationId ?? capability?.operationId ?? null,
361
+ amount: capability?.action?.amount ?? null,
362
+ currency: capability?.action?.currency ?? null,
363
+ };
364
+ }
365
+
366
+ /**
367
+ * Re-check the material execution fields at the proof-recording boundary.
368
+ * Authorization-time binding proves what was observed before the effect; this
369
+ * second check proves the execution record carries the same observation. A
370
+ * reliance packet must not be able to reuse an earlier green binding after the
371
+ * executor reports different system-of-record values.
372
+ */
373
+ function bindExecutionProof({ authorization, observedAction, binding }) {
374
+ if (!binding?.required) return binding;
375
+ const requirement = authorization?.requirement || {
376
+ execution_binding: { required_fields: binding.required_fields || [] },
377
+ };
378
+ const replay = verifyExecutionBinding({
379
+ requirement,
380
+ receipt: { payload: { claim: observedAction || {} } },
381
+ observedAction,
382
+ });
383
+ const matchesAuthorization = replay.ok
384
+ && typeof binding.observed_hash === 'string'
385
+ && replay.observed_hash === binding.observed_hash;
386
+ return {
387
+ ...binding,
388
+ execution_observed_hash: replay.observed_hash,
389
+ execution_mismatched_fields: replay.mismatched_fields || [],
390
+ execution_missing_observed_fields: replay.missing_observed_fields || [],
391
+ execution_binding_match: matchesAuthorization,
392
+ ok: binding.ok === true && matchesAuthorization,
393
+ };
394
+ }
395
+
396
+ /**
397
+ * Structurally compare a PRE-COMPUTED admissibility block with a profile hash.
398
+ * This helper does NOT authenticate the block or establish evaluator provenance.
399
+ * An execution gate must first verify a signature over the packet or recompute
400
+ * the verdict from trusted evidence. createGate enforces that boundary through
401
+ * its verifyAdmissibilityPacket callback whenever a profile is pinned.
402
+ *
403
+ * @param {{id?:string, profile_hash:string}} pinned the profile the relying party requires
404
+ * @param {object|null} presented a reliance packet, or its `.admissibility` block,
405
+ * as produced by buildReliancePacket / the relying party's evaluator
406
+ * @returns {{ok:boolean, reason:string|null, pinned_hash:string|null, presented_hash:string|null, verdict:string|null}}
407
+ * ok:true ONLY when the presented profile_hash equals the pinned hash AND the
408
+ * verdict is exactly 'admissible'. Every other case fails closed with a distinct reason.
409
+ */
410
+ export function verifyAdmissibilityAgainstPinnedProfile(pinned, presented) {
411
+ const pinnedHash = pinned && typeof pinned.profile_hash === 'string' ? pinned.profile_hash : null;
412
+ // A pin with no hash is a misconfiguration: refuse, do not silently pass.
413
+ if (!pinnedHash) {
414
+ return { ok: false, reason: 'pinned_profile_missing_hash', pinned_hash: null, presented_hash: null, verdict: null };
415
+ }
416
+ // Accept either a full reliance packet (has .admissibility) or the block itself.
417
+ const adm = presented && typeof presented === 'object'
418
+ ? (presented.admissibility !== undefined ? presented.admissibility : presented)
419
+ : null;
420
+ if (!adm || typeof adm !== 'object') {
421
+ return { ok: false, reason: 'admissibility_profile_pinned_but_absent', pinned_hash: pinnedHash, presented_hash: null, verdict: null };
422
+ }
423
+ const presentedHash = typeof adm.profile_hash === 'string' ? adm.profile_hash : null;
424
+ const verdict = typeof adm.verdict === 'string' ? adm.verdict : null;
425
+ // Constant-work equality is unnecessary (hashes are public), but the mismatch
426
+ // MUST be a distinct, named refusal: a presented verdict for a DIFFERENT bar is
427
+ // not evidence about the pinned bar.
428
+ if (presentedHash === null || presentedHash !== pinnedHash) {
429
+ return { ok: false, reason: 'profile_hash_mismatch', pinned_hash: pinnedHash, presented_hash: presentedHash, verdict };
430
+ }
431
+ // Verdict must be recognized AND exactly 'admissible'. Any other closed-set
432
+ // member (missing_evidence/stale/conflicted/unverifiable), an unrecognized
433
+ // string, or a missing verdict fails closed and names the verdict it saw.
434
+ if (verdict === null || !ADMISSIBILITY_VERDICTS.includes(verdict)) {
435
+ return { ok: false, reason: 'admissibility_verdict_unrecognized', pinned_hash: pinnedHash, presented_hash: presentedHash, verdict };
436
+ }
437
+ if (verdict !== 'admissible') {
438
+ return { ok: false, reason: `admissibility_not_admissible:${verdict}`, pinned_hash: pinnedHash, presented_hash: presentedHash, verdict };
439
+ }
440
+ return { ok: true, reason: null, pinned_hash: pinnedHash, presented_hash: presentedHash, verdict };
441
+ }
442
+
443
+ /**
444
+ * The assurance tier a receipt has CRYPTOGRAPHICALLY EARNED.
445
+ *
446
+ * SECURITY: the credited tier is NEVER inferred from self-asserted payload
447
+ * fields. A bare `quorum:{signers,threshold}` block or an `outcome:
448
+ * 'allow_with_signoff'` string with no verifiable signature earns only
449
+ * `software` — it will be refused `assurance_too_low` by any guard that needs
450
+ * more. Fail-closed by construction.
451
+ *
452
+ * Two independent cryptographic proof shapes are accepted; a receipt earns the
453
+ * HIGHEST tier any of them proves:
454
+ *
455
+ * (a) Pinned assurance proof (`payload.assurance_proof`, EP-ASSURANCE-PROOF-v1):
456
+ * per-signer signatures verified against PINNED approver keys (opts.approverKeys)
457
+ * or a caller-supplied verifier (opts.verifyAssurance). This is the primary,
458
+ * strongest model — the verifier never trusts a key that travels inside the
459
+ * receipt. Delegated to require-receipt's receiptAssuranceTierFromProof.
460
+ *
461
+ * (b) Embedded evidence (DoD audit fix): a full EP-QUORUM-v1
462
+ * document (payload.quorum) whose per-signer WebAuthn assertions verify via
463
+ * verifyQuorum (distinct humans + distinct keys + threshold + action-binding
464
+ * + window) earns `quorum`; a WebAuthn device signoff (payload.signoff =
465
+ * {context, webauthn}) that verifies against the approver's own key via
466
+ * verifyWebAuthnSignoff earns `class_a`. Quorum additionally requires an
467
+ * out-of-band organizational policy and identity-bound approver directory.
468
+ *
469
+ * TRUST-LAUNDERING GUARD: an approver key carried INSIDE the receipt proves
470
+ * only that whoever minted the receipt also holds that key — it is NOT proof
471
+ * the relying party trusts that human. Crediting an elevated tier off such a
472
+ * key would collapse VERIFIED into ACCEPTED (any party can mint a fresh
473
+ * keypair, self-sign a signoff, and embed both). So path (b) elevates the
474
+ * tier ONLY when either: (i) the caller explicitly opts in with
475
+ * `allowEmbeddedApproverKeys:true` for a single Class-A integrity demo
476
+ * (DEFAULT OFF); or (ii) the embedded approver key that would earn the
477
+ * credit is present in the relying party's PINNED approver key set
478
+ * (opts.approverKeys). With no pin and no opt-in, path (b) may still VERIFY
479
+ * the signoff/quorum, but it does NOT elevate above `software`. Fail-closed.
480
+ *
481
+ * @param {object} doc the EP-RECEIPT-v1 document
482
+ * @param {object} [opts]
483
+ * @param {object} [opts.approverKeys] pinned approver keys for path (a) and the
484
+ * path-(b) fallback: a receipt-embedded approver key elevates the tier only if
485
+ * it is one of these pinned keys (unless allowEmbeddedApproverKeys is set)
486
+ * @param {boolean} [opts.allowEmbeddedApproverKeys=false] explicit opt-in where
487
+ * one unpinned embedded key may earn Class-A integrity. It never earns quorum.
488
+ * @param {object} [opts.quorumPolicy] relying-party-pinned organizational rule
489
+ * @param {function|null} [opts.verifyAssurance] custom assurance verifier for path (a)
490
+ * @param {string|null} [opts.rpId] bind embedded device assertions to this WebAuthn RP id (path b)
491
+ * @param {string[]} [opts.allowedOrigins] exact WebAuthn origins accepted for embedded-evidence verification (path b)
492
+ * @param {object} [opts.quorum_policy] legacy snake_case alias for opts.quorumPolicy
493
+ * @param {boolean} [opts.detail] return a {tier, quorum, signoff} object instead of the string
494
+ * @returns {'software'|'class_a'|'quorum'|object} the highest tier proven
495
+ */
496
+ interface AssuranceQuorumDetail {
497
+ valid: boolean;
498
+ checks: Obj;
499
+ policy_pinned: boolean;
500
+ embedded_keys_trusted: boolean;
501
+ approvers: string[];
502
+ roles: { subject: string | null; role: string | null }[];
503
+ refusal: string | null;
504
+ }
505
+ interface AssuranceSignoffDetail {
506
+ valid: boolean;
507
+ checks: Obj;
508
+ embedded_key_trusted: boolean;
509
+ approver: string | null;
510
+ }
511
+ interface AssuranceTierDetail {
512
+ tier: string;
513
+ quorum: AssuranceQuorumDetail | null;
514
+ signoff: AssuranceSignoffDetail | null;
515
+ }
516
+ interface ReceiptAssuranceOpts {
517
+ approverKeys?: Obj;
518
+ allowEmbeddedApproverKeys?: boolean;
519
+ quorumPolicy?: Obj | null;
520
+ quorum_policy?: Obj | null;
521
+ verifyAssurance?: ((...args: any[]) => any) | null;
522
+ rpId?: string | null;
523
+ allowedOrigins?: readonly string[];
524
+ detail?: boolean;
525
+ }
526
+ export function receiptAssuranceTier(doc: any, opts: ReceiptAssuranceOpts = {}) {
527
+ const detail: AssuranceTierDetail = { tier: 'software', quorum: null, signoff: null };
528
+
529
+ // --- Path (a): pinned assurance proof / caller-supplied verifier. ---
530
+ // Never inferred from receipt fields without a pinned key or explicit verifier.
531
+ let proofTier = 'software';
532
+ try {
533
+ proofTier = receiptAssuranceTierFromProof(doc, opts) || 'software';
534
+ } catch { proofTier = 'software'; }
535
+ if ((TIER_RANK[proofTier] ?? 0) > (TIER_RANK[detail.tier] ?? 0)) detail.tier = proofTier;
536
+
537
+ // --- Path (b): self-contained embedded per-signer evidence (DoD audit fix). ---
538
+ const p = doc?.payload || {};
539
+ const verifyOpts = {
540
+ ...(opts.rpId ? { rpId: opts.rpId } : {}),
541
+ ...(Array.isArray(opts.allowedOrigins) ? { allowedOrigins: opts.allowedOrigins } : {}),
542
+ };
543
+ // The relying party's PINNED approver public keys (base64url SPKI-DER strings).
544
+ // An embedded approver key elevates the tier only if it is in this set, unless
545
+ // the caller explicitly opts into the self-contained mode.
546
+ const allowEmbedded = opts.allowEmbeddedApproverKeys === true;
547
+ const keyIsTrusted = (k, approver) => allowEmbedded
548
+ || Boolean(findPinnedApproverKey(opts.approverKeys, k, approver));
549
+
550
+ // quorum: a real, self-contained EP-QUORUM-v1 evidence document. Accept it
551
+ // under payload.quorum or payload.claim.quorum. It only counts if it is a full
552
+ // quorum document (policy + members with WebAuthn signoffs) AND verifyQuorum
553
+ // returns valid. A bare {signers,threshold} block has no members to verify and
554
+ // therefore CANNOT be credited quorum. The cryptographic verification runs
555
+ // regardless (so `detail.quorum` reports validity), but the tier elevates only
556
+ // when every member's embedded approver key is pinned (or the caller opted in).
557
+ const q = p.quorum || p.claim?.quorum;
558
+ if (detail.tier !== 'quorum' && isQuorumEvidence(q)) {
559
+ const policy = validatePinnedQuorumPolicy((opts.quorumPolicy || opts.quorum_policy) as Obj);
560
+ const trustedMembers = Array.isArray(q.members) ? q.members.map((member) => {
561
+ const entry = findPinnedApproverKey(
562
+ opts.approverKeys,
563
+ member?.approver_public_key,
564
+ member?.signoff?.context?.approver,
565
+ );
566
+ return entry ? { ...member, approver_public_key: entry.public_key } : null;
567
+ }) : [];
568
+ const membersTrusted = trustedMembers.length > 0 && trustedMembers.every(Boolean);
569
+ const qr = policy.ok && membersTrusted
570
+ ? verifyQuorum({ ...q, policy: policy.policy, members: trustedMembers }, verifyOpts)
571
+ : { valid: false, checks: {} };
572
+ detail.quorum = {
573
+ valid: qr.valid,
574
+ checks: qr.checks,
575
+ policy_pinned: policy.ok,
576
+ embedded_keys_trusted: membersTrusted,
577
+ approvers: qr.valid
578
+ ? trustedMembers.map((member) => member?.signoff?.context?.approver).filter(nonEmptyString)
579
+ : [],
580
+ roles: qr.valid
581
+ ? trustedMembers.map((member) => ({
582
+ subject: member?.signoff?.context?.approver ?? null,
583
+ role: member?.role ?? null,
584
+ }))
585
+ : [],
586
+ refusal: !policy.ok ? policy.reason : (!membersTrusted ? 'quorum_member_key_unpinned' : null),
587
+ };
588
+ if (qr.valid && policy.ok && membersTrusted) detail.tier = 'quorum';
589
+ }
590
+
591
+ // class_a: a verifiable WebAuthn device signoff. The signoff evidence is
592
+ // {context, webauthn}; the approver key travels with it (signoff.approver_public_key)
593
+ // or alongside it (payload.approver_public_key). That key elevates the tier only
594
+ // when it is pinned by the relying party (or the caller opted into embedded keys).
595
+ if ((TIER_RANK[detail.tier] ?? 0) < TIER_RANK.class_a) {
596
+ const so = p.signoff || p.claim?.signoff;
597
+ if (isSignoffEvidence(so)) {
598
+ const keyId = p.approver_key_id || p.claim?.approver_key_id;
599
+ const pinnedById = typeof keyId === 'string' && opts.approverKeys
600
+ && typeof opts.approverKeys === 'object' && !Array.isArray(opts.approverKeys)
601
+ ? opts.approverKeys[keyId]
602
+ : null;
603
+ const key = pinnedById?.public_key
604
+ || so.approver_public_key || p.approver_public_key || p.claim?.approver_public_key;
605
+ if (key) {
606
+ const sr = verifyWebAuthnSignoff(so, key, verifyOpts);
607
+ const sourceHash = p.claim?.source_receipt_action_hash;
608
+ // Acquisition terminal receipts carry a second, explicit signoff
609
+ // binding. Enforce it whenever present without retroactively changing
610
+ // the contract for ordinary receipts whose signoff directly supports
611
+ // the receipt itself.
612
+ const transitiveBinding = sourceHash === undefined
613
+ || (typeof sourceHash === 'string'
614
+ && so?.context?.action_hash === sourceHash
615
+ && p.claim?.approver === so?.context?.approver);
616
+ const trusted = (pinnedById?.key_class === 'A'
617
+ && pinnedById?.approver_id === so?.context?.approver)
618
+ || keyIsTrusted(key, so?.context?.approver);
619
+ detail.signoff = {
620
+ valid: sr.valid && transitiveBinding,
621
+ checks: sr.checks,
622
+ embedded_key_trusted: trusted,
623
+ approver: so?.context?.approver ?? null,
624
+ };
625
+ if (sr.valid && transitiveBinding && trusted
626
+ && (TIER_RANK[detail.tier] ?? 0) < TIER_RANK.class_a) detail.tier = 'class_a';
627
+ }
628
+ }
629
+ }
630
+
631
+ return opts.detail ? detail : detail.tier;
632
+ }
633
+
634
+ /**
635
+ * The set of PINNED approver public keys (base64url SPKI-DER strings) a relying
636
+ * party trusts, from the same `approverKeys` map path (a) uses. Accepts either a
637
+ * map of { keyId: { public_key } } (the EP-ASSURANCE-PROOF-v1 shape) or a plain
638
+ * array/set of key strings. Used to decide whether a receipt-embedded approver
639
+ * key may elevate the path-(b) tier. Never throws.
640
+ */
641
+ function spkiFingerprint(value) {
642
+ try {
643
+ const key = crypto.createPublicKey({ key: Buffer.from(value, 'base64url'), format: 'der', type: 'spki' });
644
+ return crypto.createHash('sha256').update(key.export({ type: 'spki', format: 'der' })).digest('hex');
645
+ } catch { return null; }
646
+ }
647
+
648
+ function findPinnedApproverKey(approverKeys: Obj | undefined | null, presentedKey: any, approver: any) {
649
+ if (!approverKeys || typeof approverKeys !== 'object' || Array.isArray(approverKeys)) return null;
650
+ const presentedFingerprint = spkiFingerprint(presentedKey);
651
+ if (!presentedFingerprint || typeof approver !== 'string' || !approver) return null;
652
+ const matches = Object.values(approverKeys).filter((entry) => entry && typeof entry === 'object'
653
+ && entry.approver_id === approver
654
+ && spkiFingerprint(entry.public_key) === presentedFingerprint);
655
+ return matches.length === 1 ? matches[0] : null;
656
+ }
657
+
658
+ /** A quorum evidence doc must carry members with per-signer signoffs to be verifiable. */
659
+ function isQuorumEvidence(q) {
660
+ return !!q && typeof q === 'object' && q.policy && Array.isArray(q.members) && q.members.length > 0
661
+ && typeof q.action_hash === 'string' && q.action_hash.length > 0;
662
+ }
663
+ /** A device signoff must carry the WebAuthn assertion material to be verifiable. */
664
+ function isSignoffEvidence(s) {
665
+ return !!s && typeof s === 'object' && s.context && s.webauthn;
666
+ }
667
+
668
+ function nonEmptyString(value) {
669
+ return typeof value === 'string' && value.length > 0 ? value : null;
670
+ }
671
+
672
+ function businessAuthorizationSource(requirement) {
673
+ if (!requirement || typeof requirement !== 'object') return null;
674
+ const nested = requirement.business_authorization
675
+ || requirement.businessAuthorization
676
+ || requirement.authorization_requirement
677
+ || requirement.authorizationRequirement
678
+ || requirement.authorization_policy
679
+ || requirement.authorizationPolicy
680
+ || requirement.business_policy
681
+ || requirement.control?.business_authorization
682
+ || requirement.control?.authorization_requirement
683
+ || requirement.control?.authorization_policy;
684
+ if (nested && typeof nested === 'object' && !Array.isArray(nested)) return nested;
685
+ const hasDirect = [
686
+ 'policy', 'business_policy', 'policy_id', 'policy_hash', 'tenant_id', 'allowed_approvers',
687
+ 'allowed_approver_subjects', 'allowed_approver_roles',
688
+ ].some((field) => Object.prototype.hasOwnProperty.call(requirement, field));
689
+ return hasDirect ? requirement : null;
690
+ }
691
+
692
+ /**
693
+ * Normalize the relying party's business-authorization pin for one action.
694
+ *
695
+ * Canonical manifest shape:
696
+ * business_authorization: {
697
+ * policy: { id, hash }, tenant_id,
698
+ * allowed_approvers: [{ subject, role }]
699
+ * }
700
+ *
701
+ * Flat policy_id/policy_hash and approver aliases are accepted so an existing
702
+ * manifest can add the control without changing its surrounding schema. Once
703
+ * any part is configured, every part is required; a partial pin is invalid.
704
+ */
705
+ export function businessAuthorizationRequirement(requirement) {
706
+ const source = businessAuthorizationSource(requirement);
707
+ if (!source) {
708
+ return {
709
+ configured: false, ok: true, reason: null,
710
+ policy_id: null, policy_hash: null, tenant_id: null, allowed_approvers: [],
711
+ };
712
+ }
713
+ const policy = source.policy && typeof source.policy === 'object' && !Array.isArray(source.policy)
714
+ ? source.policy
715
+ : (source.business_policy && typeof source.business_policy === 'object' && !Array.isArray(source.business_policy)
716
+ ? source.business_policy : {});
717
+ const root = requirement && typeof requirement === 'object' ? requirement : {};
718
+ const policyId = nonEmptyString(source.policy_id ?? source.id ?? policy.policy_id ?? policy.id ?? root.policy_id);
719
+ const policyHash = nonEmptyString(source.policy_hash ?? source.hash ?? policy.policy_hash ?? policy.hash ?? root.policy_hash);
720
+ const tenantId = nonEmptyString(
721
+ source.tenant_id ?? source.tenant ?? source.organization_id
722
+ ?? root.tenant_id ?? root.tenant ?? root.organization_id,
723
+ );
724
+ const rawApprovers = source.allowed_approvers ?? source.approvers ?? source.approver_roster
725
+ ?? root.allowed_approvers ?? root.approvers ?? root.approver_roster;
726
+ const subjects = source.allowed_approver_subjects ?? source.approver_subjects
727
+ ?? root.allowed_approver_subjects ?? root.approver_subjects;
728
+ const roles = source.allowed_approver_roles ?? source.approver_roles
729
+ ?? root.allowed_approver_roles ?? root.approver_roles;
730
+ let allowed: { subject: string | null; role: string | null }[] = [];
731
+ if (Array.isArray(rawApprovers)) {
732
+ allowed = rawApprovers.map((entry) => {
733
+ if (typeof entry === 'string') return { subject: entry, role: null };
734
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return { subject: null, role: null };
735
+ return {
736
+ subject: nonEmptyString(entry.subject ?? entry.approver ?? entry.principal_id),
737
+ role: nonEmptyString(entry.role ?? entry.approver_role),
738
+ };
739
+ });
740
+ } else if (Array.isArray(subjects)) {
741
+ const validRoles = Array.isArray(roles) ? roles.filter((role) => nonEmptyString(role)) : [];
742
+ allowed = subjects.flatMap((subject) => {
743
+ if (!validRoles.length) return [{ subject: nonEmptyString(subject), role: null }];
744
+ return validRoles.map((role) => ({ subject: nonEmptyString(subject), role }));
745
+ });
746
+ }
747
+ const malformed = !policyId || !policyHash || !tenantId || allowed.length === 0
748
+ || allowed.some((entry) => !entry.subject || !entry.role);
749
+ const duplicate = new Set(allowed.map((entry) => `${entry.subject}\u0000${entry.role}`)).size !== allowed.length;
750
+ return {
751
+ configured: true,
752
+ ok: !malformed && !duplicate,
753
+ reason: malformed ? 'business_authorization_incomplete'
754
+ : (duplicate ? 'business_authorization_duplicate_approver' : null),
755
+ policy_id: policyId,
756
+ policy_hash: policyHash,
757
+ tenant_id: tenantId,
758
+ allowed_approvers: allowed,
759
+ };
760
+ }
761
+
762
+ function signedString(candidates) {
763
+ const present = candidates.filter((value) => value !== undefined && value !== null);
764
+ if (!present.length) return { ok: true, value: null };
765
+ if (present.some((value) => !nonEmptyString(value))) return { ok: false, value: null };
766
+ const distinct = [...new Set(present)];
767
+ return distinct.length === 1 ? { ok: true, value: distinct[0] } : { ok: false, value: null };
768
+ }
769
+
770
+ function receiptRoleAssertions(receipt, tierResult) {
771
+ const claim = receipt?.payload?.claim || {};
772
+ const assertions: { subject: string; role: string }[] = [];
773
+ const arrays = [claim.approvers, claim.approver_authorizations, claim.approver_roles];
774
+ for (const entries of arrays) {
775
+ if (!Array.isArray(entries)) continue;
776
+ for (const entry of entries) {
777
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
778
+ const subject = nonEmptyString(entry.subject ?? entry.approver ?? entry.principal_id);
779
+ const role = nonEmptyString(entry.role ?? entry.approver_role);
780
+ if (subject && role) assertions.push({ subject, role });
781
+ }
782
+ }
783
+ const singleSubject = nonEmptyString(claim.approver ?? claim.approver_subject);
784
+ const singleRole = nonEmptyString(claim.approver_role ?? claim.role);
785
+ if (singleSubject && singleRole) assertions.push({ subject: singleSubject, role: singleRole });
786
+
787
+ if (tierResult?.quorum?.valid === true) {
788
+ for (const member of (receipt?.payload?.quorum?.members || [])) {
789
+ const subject = nonEmptyString(member?.signoff?.context?.approver);
790
+ const role = nonEmptyString(member?.role);
791
+ if (subject && role) assertions.push({ subject, role });
792
+ }
793
+ }
794
+ return assertions;
795
+ }
796
+
797
+ function verifiedApproverSubjects(assurance, tierResult) {
798
+ const subjects = new Set(Array.isArray(assurance?.approvers) ? assurance.approvers.filter(nonEmptyString) : []);
799
+ if (tierResult?.signoff?.valid === true && nonEmptyString(tierResult.signoff.approver)) {
800
+ subjects.add(tierResult.signoff.approver);
801
+ }
802
+ if (tierResult?.quorum?.valid === true) {
803
+ for (const subject of (tierResult.quorum.approvers || [])) {
804
+ if (nonEmptyString(subject)) subjects.add(subject);
805
+ }
806
+ }
807
+ return [...subjects].sort();
808
+ }
809
+
810
+ /**
811
+ * Verify signed business-policy and tenant fields plus the cryptographically
812
+ * credited human approvers against one action's relying-party pins.
813
+ * @returns {{required:boolean, ok:boolean, reason:string|null, expected:object, evaluated:{policy_id:string|null, policy_hash:string|null, tenant_id:string|null, approvers:{subject:string, roles:string[]}[]}}}
814
+ */
815
+ export function verifyBusinessAuthorization({ requirement, receipt, assurance, tierResult }: {
816
+ requirement?: any; receipt?: any; assurance?: any; tierResult?: any;
817
+ } = {}) {
818
+ const expected = businessAuthorizationRequirement(requirement);
819
+ const claim = receipt?.payload?.claim || {};
820
+ const payload = receipt?.payload || {};
821
+ const policyId = signedString([claim.policy_id, payload.policy_id]);
822
+ const policyHash = signedString([claim.policy_hash, payload.policy_hash]);
823
+ const tenantId = signedString([
824
+ claim.tenant_id, claim.organization_id, payload.tenant_id, payload.organization_id,
825
+ ]);
826
+ const subjects = verifiedApproverSubjects(assurance, tierResult);
827
+ const roleAssertions = receiptRoleAssertions(receipt, tierResult);
828
+ const evaluatedApprovers = subjects.map((subject) => {
829
+ const asserted = [...new Set(roleAssertions.filter((entry) => entry.subject === subject).map((entry) => entry.role))];
830
+ return { subject, roles: asserted };
831
+ });
832
+ const evaluated = {
833
+ policy_id: policyId.value,
834
+ policy_hash: policyHash.value,
835
+ tenant_id: tenantId.value,
836
+ approvers: evaluatedApprovers,
837
+ };
838
+ const base = { required: expected.configured, ok: true, reason: null, expected, evaluated };
839
+ if (!expected.configured) return base;
840
+ if (!expected.ok) return { ...base, ok: false, reason: expected.reason };
841
+ if (!policyId.ok) return { ...base, ok: false, reason: 'business_policy_id_ambiguous' };
842
+ if (!policyHash.ok) return { ...base, ok: false, reason: 'business_policy_hash_ambiguous' };
843
+ if (!tenantId.ok) return { ...base, ok: false, reason: 'business_tenant_ambiguous' };
844
+ if (policyId.value !== expected.policy_id) return { ...base, ok: false, reason: 'business_policy_id_mismatch' };
845
+ if (policyHash.value !== expected.policy_hash) return { ...base, ok: false, reason: 'business_policy_hash_mismatch' };
846
+ if (tenantId.value !== expected.tenant_id) return { ...base, ok: false, reason: 'business_tenant_mismatch' };
847
+ if (!subjects.length) return { ...base, ok: false, reason: 'business_approver_required' };
848
+
849
+ for (const approver of evaluatedApprovers) {
850
+ const allowedForSubject = expected.allowed_approvers.filter((entry) => entry.subject === approver.subject);
851
+ if (!allowedForSubject.length) return { ...base, ok: false, reason: 'business_approver_not_allowed' };
852
+ // The early `if (!expected.ok) return ...` above guarantees every allowed_approvers
853
+ // entry has a non-null role (a null role would have made businessAuthorizationRequirement
854
+ // mark the manifest malformed and expected.ok false).
855
+ const allowedRoles = new Set(allowedForSubject.map((entry) => entry.role as string));
856
+ if (approver.roles.length && !approver.roles.some((role) => allowedRoles.has(role))) {
857
+ return { ...base, ok: false, reason: 'business_approver_role_not_allowed' };
858
+ }
859
+ // The manifest is the authoritative subject-to-role assignment. When the
860
+ // receipt does not repeat a role, record the pinned role rather than trusting
861
+ // an unsigned/free-text role label from elsewhere.
862
+ if (!approver.roles.length) approver.roles = [...allowedRoles].sort();
863
+ }
864
+ return base;
865
+ }
866
+
867
+ /**
868
+ * Create a gate.
869
+ * @param {object} opts
870
+ * @param {object} [opts.manifest] EP-ACTION-RISK-MANIFEST-v0.1 (which actions are guarded, their tier)
871
+ * @param {string[]} [opts.trustedKeys] base64url SPKI-DER issuer keys you trust
872
+ * @param {number} [opts.maxAgeSec=900] reject receipts older than this
873
+ * @param {object} [opts.store] durable, ownership-fenced, permanent consumption store
874
+ * @param {object} [opts.capabilityStore] Marvel capability budget store. A
875
+ * capability run reserves here before the effect and commits after it.
876
+ * @param {string[]} [opts.capabilityTrustedIssuerKeys] pinned capability
877
+ * envelope issuer keys. Required when capabilityStore is configured.
878
+ * @param {function|null} [opts.capabilityCaidResolver] relying-party-pinned resolver
879
+ * for `urn:emilia:scope:caid-set-v1`. Missing resolver fails CAID scope closed.
880
+ * @param {boolean} [opts.allowEphemeralStore=false] explicit test/demo opt-in for in-memory state
881
+ * @param {object} [opts.log] evidence log (default in-memory, hash-chained)
882
+ * @param {boolean} [opts.allowInlineKey=false] accept the receipt's own key (integrity, NOT trust)
883
+ * @param {object} [opts.keyRegistry] a key registry (createKeyRegistry) for rotation + revocation;
884
+ * if given it supersedes trustedKeys — a receipt is verified only against keys valid (and not
885
+ * revoked) at its issuance time.
886
+ * @param {object} [opts.approverKeys] PINNED approver keys ({ keyId: { public_key, key_class } }).
887
+ * Used both for the pinned assurance-proof path and to authorize receipt-embedded
888
+ * approver keys under the self-contained embedded-evidence path.
889
+ * @param {boolean} [opts.allowEmbeddedApproverKeys=false] allow one embedded key
890
+ * to earn Class-A integrity in demos. Embedded keys never establish quorum.
891
+ * @param {object} [opts.quorumPolicy] global relying-party-pinned quorum rule
892
+ * @param {object} [opts.quorumPolicies] action_type -> pinned quorum rule
893
+ * @param {string|null} [opts.rpId] WebAuthn relying-party identifier. Required for
894
+ * built-in Class-A or quorum assurance verification.
895
+ * @param {string[]} [opts.allowedOrigins] exact WebAuthn origins accepted by the
896
+ * relying party. Required for built-in Class-A or quorum verification.
897
+ * @param {function|null} [opts.verifyAdmissibilityPacket] trusted relying-party hook.
898
+ * Required whenever an admissibility profile is pinned. It must authenticate
899
+ * the presented packet or recompute the verdict and return the trusted block.
900
+ * @param {boolean} [opts.strictEvidence=true] make the evidence log strict (fail on record errors)
901
+ * @param {() => number} [opts.now] clock source (defaults to Date.now)
902
+ * @param {object|null} [opts.approver_keys] legacy snake_case alias for opts.approverKeys
903
+ * @param {function|null} [opts.verifyAssurance] caller-supplied assurance verifier (assurance-proof path a)
904
+ * @param {object} [opts.requiredAdmissibilityProfile] gate-level pinned admissibility profile {id, profile_hash}
905
+ * @param {object} [opts.runtimeMonitor] runtime invariant monitor (defaults to createRuntimeMonitor)
906
+ */
907
+ interface CreateGateOptions {
908
+ manifest?: Obj | null;
909
+ trustedKeys?: readonly string[];
910
+ maxAgeSec?: number;
911
+ store?: any;
912
+ log?: any;
913
+ capabilityStore?: any;
914
+ capabilityTrustedIssuerKeys?: string[];
915
+ capabilityCaidResolver?: ((...args: any[]) => any) | null;
916
+ allowInlineKey?: boolean;
917
+ allowEphemeralStore?: boolean;
918
+ strictEvidence?: boolean;
919
+ now?: number | (() => number);
920
+ keyRegistry?: any;
921
+ approverKeys?: Obj;
922
+ approver_keys?: Obj | null;
923
+ verifyAssurance?: ((...args: any[]) => any) | null;
924
+ rpId?: string | null;
925
+ allowedOrigins?: readonly string[];
926
+ quorumPolicy?: Obj | null;
927
+ quorumPolicies?: Obj;
928
+ requiredAdmissibilityProfile?: Obj | null;
929
+ verifyAdmissibilityPacket?: ((...args: any[]) => any) | null;
930
+ allowEmbeddedApproverKeys?: boolean;
931
+ runtimeMonitor?: ReturnType<typeof createRuntimeMonitor> | null;
932
+ }
933
+ export function createGate({ manifest = null, trustedKeys = [], maxAgeSec = 900, store, log, capabilityStore = null, capabilityTrustedIssuerKeys = [], capabilityCaidResolver = null, allowInlineKey = false, allowEphemeralStore = false, strictEvidence = true, now = Date.now, keyRegistry = null, approverKeys = {}, approver_keys = null, verifyAssurance = null, rpId = null, allowedOrigins = [], quorumPolicy = null, quorumPolicies = {}, requiredAdmissibilityProfile = null, verifyAdmissibilityPacket = null, allowEmbeddedApproverKeys = false, runtimeMonitor = createRuntimeMonitor({ now }) }: CreateGateOptions = {}) {
934
+ // Production key custody: a registry (rotation + revocation) supersedes a flat
935
+ // trustedKeys list. A flat list is coerced to an always-valid registry, so
936
+ // existing callers are unchanged.
937
+ const registry = keyRegistry ? asKeyRegistry(keyRegistry) : (trustedKeys.length ? asKeyRegistry(trustedKeys) : null);
938
+ const usesActionControlManifest = manifest?.['@version'] === ACTION_CONTROL_MANIFEST_VERSION;
939
+ if (manifest) {
940
+ const m = usesActionControlManifest
941
+ ? validateActionControlManifest(manifest)
942
+ : validateActionRiskManifest(manifest);
943
+ if (!m.ok) throw new Error(`EMILIA Gate: invalid ${usesActionControlManifest ? 'action-control' : 'action-risk'} manifest: ${m.errors.join('; ')}`);
944
+ for (const [index, actionRequirement] of (manifest.actions || []).entries()) {
945
+ if (actionRequirement?.receipt_required !== true) continue;
946
+ const business = businessAuthorizationRequirement(actionRequirement);
947
+ if (business.configured && !business.ok) {
948
+ throw new Error(`EMILIA Gate: invalid action-risk manifest: actions[${index}].business_authorization ${business.reason}`);
949
+ }
950
+ }
951
+ }
952
+ if (allowInlineKey) {
953
+ // eslint-disable-next-line no-console
954
+ console.warn('EMILIA Gate: allowInlineKey=true accepts a receipt\'s OWN key. This proves INTEGRITY (the receipt was not tampered with) but NOT issuer TRUST (anyone can mint a receipt with their own key). Use for demos only; pin trustedKeys in production.');
955
+ }
956
+ if (capabilityStore && (typeof capabilityStore.registerCapability !== 'function'
957
+ || typeof capabilityStore.reserveSpend !== 'function'
958
+ || typeof capabilityStore.commitSpend !== 'function')) {
959
+ throw new Error('EMILIA Gate capabilityStore must implement registerCapability(), reserveSpend(), and commitSpend()');
960
+ }
961
+ if (capabilityStore && (!Array.isArray(capabilityTrustedIssuerKeys)
962
+ || capabilityTrustedIssuerKeys.length === 0
963
+ || capabilityTrustedIssuerKeys.some((key) => typeof key !== 'string' || key.length === 0))) {
964
+ throw new Error('EMILIA Gate capabilityTrustedIssuerKeys must explicitly pin at least one capability issuer');
965
+ }
966
+ const effectiveCapabilityTrustedIssuerKeys = [...capabilityTrustedIssuerKeys];
967
+ // Replay defense is only sound if the store is shared, ownership-fenced, and
968
+ // permanent. This is a security property in every environment; NODE_ENV must
969
+ // never silently decide whether a receipt can be replayed.
970
+ let consumption = store;
971
+ if (!consumption) {
972
+ if (!allowEphemeralStore) throw new Error(
973
+ 'EMILIA Gate requires a durable, ownership-fenced, permanent consumption store. '
974
+ + 'Pass allowEphemeralStore:true only for an explicit test/demo gate.',
975
+ );
976
+ consumption = new MemoryConsumptionStore();
977
+ }
978
+ for (const method of ['consume', 'reserve', 'commit']) {
979
+ if (typeof consumption?.[method] !== 'function') {
980
+ throw new Error(`EMILIA Gate consumption store must implement ${method}()`);
981
+ }
982
+ }
983
+ if (!allowEphemeralStore && !isSecureConsumptionStore(consumption)) {
984
+ throw new Error(
985
+ 'EMILIA Gate requires a durable, ownership-fenced, permanent consumption store. '
986
+ + 'Pass allowEphemeralStore:true only for an explicit test/demo gate.',
987
+ );
988
+ }
989
+ const evidence = log || createEvidenceLog({ strict: strictEvidence });
990
+
991
+ function resolveRequirement(selector: Obj): Obj | null {
992
+ if (!manifest) return null;
993
+ const controlResolution = usesActionControlManifest
994
+ ? resolveActionControl(manifest, selector)
995
+ : null;
996
+ const raw = usesActionControlManifest
997
+ ? (controlResolution?.status === 'one'
998
+ ? controlResolution.action
999
+ : (controlResolution?.status === 'ambiguous'
1000
+ ? {
1001
+ action_type: selector.action_type || selector.action || 'ambiguous.action',
1002
+ receipt_required: true,
1003
+ assurance_class: 'quorum',
1004
+ __resolution_ambiguous: true,
1005
+ ambiguous_action_ids: controlResolution.action_ids,
1006
+ }
1007
+ : null))
1008
+ : findActionRequirement(manifest, selector);
1009
+ if (!raw || !usesActionControlManifest) return raw;
1010
+ // Action Control v0.2 keeps enforcement details under `control`; the Gate's
1011
+ // existing execution/business checks consume the normalized flat view.
1012
+ // The original object remains available under `control` for evidence.
1013
+ return {
1014
+ ...raw,
1015
+ execution_binding: raw.control?.execution_binding || raw.execution_binding,
1016
+ authorization: raw.control?.authorization || null,
1017
+ caid_selector: raw.control?.execution_binding?.caid_selector
1018
+ || raw.control?.caid_selector
1019
+ || raw.execution_binding?.caid_selector
1020
+ || null,
1021
+ };
1022
+ }
1023
+
1024
+ function acquisitionChallengeOptions(requirement: Obj | null, selector: Obj, requiredTier: string, observed: Obj | null = null) {
1025
+ const observedHash = observed ? safeCanonicalHash(observed) : null;
1026
+ const acquisition = requirement?.authorization || requirement?.control?.authorization || undefined;
1027
+ return {
1028
+ status: RECEIPT_REQUIRED_STATUS,
1029
+ assuranceClass: requiredTier,
1030
+ maxAgeSec,
1031
+ manifest: selector.manifestUrl
1032
+ || (usesActionControlManifest ? manifest?.service?.manifest_url : null),
1033
+ // An acquisition endpoint whose POST contract requires an exact action
1034
+ // hash is a dead end unless this executor actually observed and hashed
1035
+ // the system-of-record action. Do not advertise a machine-completable
1036
+ // flow from a type-only challenge.
1037
+ authorization: !usesActionControlManifest || observedHash ? acquisition : undefined,
1038
+ requiredFields: requirement?.execution_binding?.required_fields
1039
+ || requirement?.control?.execution_binding?.required_fields
1040
+ || undefined,
1041
+ caidSelector: requirement?.caid_selector || requirement?.control?.caid_selector || undefined,
1042
+ actionHash: observedHash ? `sha256:${observedHash}` : undefined,
1043
+ };
1044
+ }
1045
+
1046
+ async function check({ selector = {}, receipt = null, observedAction = null, consumptionMode = 'consume', admissibilityProfile = null, reliancePacket: presentedPacket = null, admissibility = null, capability = null }: {
1047
+ selector?: any; receipt?: any; observedAction?: any; consumptionMode?: string;
1048
+ admissibilityProfile?: any; reliancePacket?: any; admissibility?: any; capability?: any;
1049
+ } = {}) {
1050
+ const requirement = /** @type {any} */ (resolveRequirement(selector));
1051
+ const action = requirement?.action_type || selector.action_type || selector.action || null;
1052
+ const guarded = Boolean(requirement && requirement.receipt_required !== false);
1053
+ const runtimeCycleId = runtimeMonitor?.beginCheck({
1054
+ action,
1055
+ receipt_id: receipt?.payload?.receipt_id ?? null,
1056
+ });
1057
+ // Assurance tier the action requires (cryptographically checked below). For a
1058
+ // manifest-guarded action the tier MUST be declared explicitly: never fall
1059
+ // back to the weakest 'software' tier because assurance_class was omitted —
1060
+ // that would let a guarded, possibly critical, action accept a bare
1061
+ // machine-signed receipt (a fail-open). A guarded requirement with no tier
1062
+ // is a misconfiguration and fails closed just below. Only selector-only
1063
+ // checks (no manifest requirement) use the documented 'software' default.
1064
+ const declaredRequiredTier = requirement
1065
+ ? requirement.assurance_class
1066
+ : (selector.assurance_class || 'software');
1067
+ const requiredTier = runtimeMonitor?.minimumAssuranceTier(declaredRequiredTier) ?? declaredRequiredTier;
1068
+ const pinnedQuorumPolicy = requirement?.quorum_policy
1069
+ || (quorumPolicies && typeof quorumPolicies === 'object' ? quorumPolicies[action] : null)
1070
+ || quorumPolicy;
1071
+ const observed = observedAction || selector.observedAction || selector.actionDetails || null;
1072
+ const businessExpected = businessAuthorizationRequirement(requirement);
1073
+ let businessEvaluation: ReturnType<typeof verifyBusinessAuthorization> = {
1074
+ required: businessExpected.configured,
1075
+ ok: !businessExpected.configured,
1076
+ reason: null,
1077
+ expected: businessExpected,
1078
+ evaluated: { policy_id: null, policy_hash: null, tenant_id: null, approvers: [] },
1079
+ };
1080
+
1081
+ async function decide(allow, status, reason, extra = {}) {
1082
+ let runtimeExtra = {};
1083
+ // Cast to the real transition()/fail() runtime shape: the auto-inferred union
1084
+ // widens `ok` to `boolean` on both branches, which defeats the `!runtimeDecision.ok`
1085
+ // narrowing below (TS can't discriminate on a non-literal, same-typed property).
1086
+ const runtimeDecision = runtimeMonitor?.recordDecision(runtimeCycleId, {
1087
+ allow,
1088
+ status,
1089
+ reason,
1090
+ guarded,
1091
+ receipt_id: receipt?.payload?.receipt_id ?? null,
1092
+ }) as { ok: true } | { ok: false; reason: string; event: any } | undefined;
1093
+ if (runtimeDecision && !runtimeDecision.ok) {
1094
+ allow = false;
1095
+ status = RECEIPT_REQUIRED_STATUS;
1096
+ reason = runtimeDecision.reason;
1097
+ runtimeExtra = { runtime_monitor: runtimeDecision.event };
1098
+ }
1099
+ const entry = {
1100
+ kind: 'decision',
1101
+ at: new Date(typeof now === 'function' ? now() : now).toISOString(),
1102
+ action,
1103
+ allow,
1104
+ status,
1105
+ reason,
1106
+ selector: { ...selector },
1107
+ required_tier: requiredTier,
1108
+ receipt_id: receipt?.payload?.receipt_id ?? null,
1109
+ subject: receipt?.payload?.subject ?? null,
1110
+ observed_action_hash: observed ? safeCanonicalHash(observed) : null,
1111
+ business_authorization: businessEvaluation,
1112
+ evaluated_policy_id: businessEvaluation.evaluated.policy_id,
1113
+ evaluated_policy_hash: businessEvaluation.evaluated.policy_hash,
1114
+ evaluated_tenant_id: businessEvaluation.evaluated.tenant_id,
1115
+ evaluated_approvers: businessEvaluation.evaluated.approvers,
1116
+ ...extra,
1117
+ ...runtimeExtra,
1118
+ };
1119
+ let record;
1120
+ try {
1121
+ record = await evidence.record(entry);
1122
+ } catch (e) {
1123
+ // The decision could not be durably recorded. Fail CLOSED: never
1124
+ // authorize an action we cannot account for. Downgrade any allow to a
1125
+ // refusal and best-effort note the downgrade (non-fatal if that fails too).
1126
+ allow = false;
1127
+ status = RECEIPT_REQUIRED_STATUS;
1128
+ reason = 'evidence_log_failed';
1129
+ try {
1130
+ record = await evidence.record({ ...entry, allow: false, status, reason, evidence_error: String((e as Error)?.message ?? e) });
1131
+ } catch {
1132
+ record = null;
1133
+ }
1134
+ }
1135
+ const out: {
1136
+ allow: any; status: any; reason: any; action: any; requirement: any; evidence: any;
1137
+ challenge?: ReturnType<typeof receiptChallenge>; header?: string;
1138
+ // Attached below via Object.defineProperty (non-enumerable) when a runtime
1139
+ // cycle is open; declared here so callers that read it stay type-checked.
1140
+ _runtime_cycle_id?: any;
1141
+ } = { allow, status, reason, action, requirement, evidence: record };
1142
+ if (runtimeCycleId) Object.defineProperty(out, '_runtime_cycle_id', {
1143
+ value: runtimeCycleId,
1144
+ enumerable: false,
1145
+ });
1146
+ if (!allow) {
1147
+ const challengeOptions = acquisitionChallengeOptions(requirement, selector, requiredTier, observed);
1148
+ out.challenge = receiptChallenge(action, reason, challengeOptions);
1149
+ out.header = receiptRequiredHeader({ action, ...challengeOptions });
1150
+ }
1151
+ return out;
1152
+ }
1153
+
1154
+ const runtimeSafeMode = runtimeMonitor?.getMode() !== RUNTIME_MONITOR_MODES.NORMAL;
1155
+ const runtimePreflight = runtimeMonitor?.preflight({ hasReceipt: Boolean(receipt) });
1156
+ if (runtimePreflight && !runtimePreflight.ok) {
1157
+ return decide(false, RECEIPT_REQUIRED_STATUS, runtimePreflight.reason, {
1158
+ runtime_mode: runtimePreflight.mode,
1159
+ });
1160
+ }
1161
+ if (requirement?.__resolution_ambiguous === true) {
1162
+ return decide(false, RECEIPT_REQUIRED_STATUS, 'manifest_selector_ambiguous', {
1163
+ ambiguous_action_ids: requirement.ambiguous_action_ids,
1164
+ });
1165
+ }
1166
+ // Manifest present and this selector is not guarded (or explicitly not required): pass through.
1167
+ // A runtime safe mode disables pass-through so every action must present
1168
+ // cryptographic signoff before the monitor permits execution again.
1169
+ if (manifest && (!requirement || requirement.receipt_required === false) && !runtimeSafeMode) {
1170
+ return decide(true, 200, 'not_guarded');
1171
+ }
1172
+ // Guarded, but no receipt was presented.
1173
+ if (!receipt) {
1174
+ return decide(false, RECEIPT_REQUIRED_STATUS, 'receipt_required');
1175
+ }
1176
+ // A capability issuance receipt is not a bearer bypass around the budget
1177
+ // path. The marker is inside the signed claim; stripping it breaks the
1178
+ // receipt signature. Such a receipt is accepted only while the capability
1179
+ // executor supplies the signed envelope and durable budget context.
1180
+ if (receipt?.payload?.claim?.capability_only === true && !capability) {
1181
+ return decide(false, RECEIPT_REQUIRED_STATUS, 'capability_required');
1182
+ }
1183
+ // A manifest-guarded action that declares no assurance_class is a
1184
+ // misconfiguration. Fail CLOSED rather than defaulting to the weakest tier
1185
+ // (which would accept a bare machine-signed receipt for a guarded action).
1186
+ // validateActionRiskManifest also rejects such a manifest at author time;
1187
+ // this is defense in depth for a manifest loaded without re-validation.
1188
+ if (requirement && requirement.receipt_required !== false && !requiredTier) {
1189
+ return decide(false, RECEIPT_REQUIRED_STATUS, 'manifest_missing_assurance_class');
1190
+ }
1191
+ // Signature / freshness / action-binding / outcome. Production key custody:
1192
+ // resolve the issuer keys valid (and not revoked) at THIS receipt's issuance
1193
+ // time. A revoked or out-of-window key is excluded, so its signature does not
1194
+ // verify and the action is refused (fail closed).
1195
+ const effectiveKeys = registry
1196
+ ? registry.keysValidAt(receipt?.payload?.created_at)
1197
+ : trustedKeys;
1198
+ const observedActionHash = usesActionControlManifest && observed
1199
+ ? safeCanonicalHash(observed)
1200
+ : null;
1201
+ if (usesActionControlManifest && observed && !observedActionHash) {
1202
+ return decide(false, RECEIPT_REQUIRED_STATUS, 'observed_action_invalid');
1203
+ }
1204
+ const v = verifyEmiliaReceipt(receipt, {
1205
+ trustedKeys: effectiveKeys,
1206
+ allowInlineKey,
1207
+ action,
1208
+ maxAgeSec,
1209
+ now,
1210
+ ...(usesActionControlManifest && observed ? {
1211
+ actionHash: `sha256:${observedActionHash}`,
1212
+ requiredFields: requirement?.execution_binding?.required_fields,
1213
+ caidSelector: requirement?.caid_selector || undefined,
1214
+ } : {}),
1215
+ });
1216
+ if (!v.ok) {
1217
+ return decide(false, RECEIPT_REQUIRED_STATUS, `receipt_rejected:${v.reason}`, { rejected: v });
1218
+ }
1219
+ // Assurance tier. CRYPTOGRAPHICALLY VERIFIED — never inferred from
1220
+ // self-asserted payload fields. The credited tier is the HIGHER of two
1221
+ // independent proof paths:
1222
+ // (a) pinned assurance proof (payload.assurance_proof verified against
1223
+ // pinned approverKeys) or a caller-supplied verifyAssurance hook;
1224
+ // (b) self-contained embedded per-signer evidence (EP-QUORUM-v1 /
1225
+ // WebAuthn device signoff) re-verified via verifyQuorum /
1226
+ // verifyWebAuthnSignoff (DoD audit fix).
1227
+ // A receipt that only CLAIMS a higher tier earns 'software' and is refused.
1228
+ const needRank = TIER_RANK[requiredTier];
1229
+ if (needRank === undefined) {
1230
+ return decide(false, RECEIPT_REQUIRED_STATUS, 'unknown_required_tier', { have_tier: 'software', need_tier: requiredTier, assurance_tier_source: 'cryptographic_verification' });
1231
+ }
1232
+ if (needRank >= TIER_RANK.class_a && typeof verifyAssurance !== 'function'
1233
+ && (typeof rpId !== 'string' || !rpId
1234
+ || !Array.isArray(allowedOrigins) || allowedOrigins.length === 0
1235
+ || allowedOrigins.some((origin) => typeof origin !== 'string' || !origin))) {
1236
+ return decide(false, RECEIPT_REQUIRED_STATUS, 'assurance_context_unpinned', {
1237
+ have_tier: 'software', need_tier: requiredTier,
1238
+ assurance_tier_source: 'cryptographic_verification',
1239
+ });
1240
+ }
1241
+ if (requiredTier === 'quorum' && typeof verifyAssurance !== 'function') {
1242
+ const policy = validatePinnedQuorumPolicy(pinnedQuorumPolicy);
1243
+ if (!policy.ok) {
1244
+ return decide(false, RECEIPT_REQUIRED_STATUS, policy.reason, {
1245
+ have_tier: 'software', need_tier: requiredTier,
1246
+ assurance_tier_source: 'cryptographic_verification',
1247
+ });
1248
+ }
1249
+ }
1250
+ const assurance = evaluateReceiptAssurance(receipt, requiredTier, {
1251
+ approverKeys: approver_keys || approverKeys,
1252
+ verifyAssurance,
1253
+ rpId,
1254
+ allowedOrigins,
1255
+ quorumPolicy: pinnedQuorumPolicy,
1256
+ });
1257
+ // detail:true guarantees receiptAssuranceTier returns the AssuranceTierDetail
1258
+ // object branch, not the plain tier string.
1259
+ const tierResult = receiptAssuranceTier(receipt, {
1260
+ rpId, allowedOrigins, detail: true, approverKeys: approver_keys || approverKeys,
1261
+ verifyAssurance, quorumPolicy: pinnedQuorumPolicy,
1262
+ // Trust-laundering guard: a receipt-embedded approver key does NOT elevate
1263
+ // the tier unless it is in the pinned approverKeys set, or the operator
1264
+ // explicitly opted into the self-contained embedded-evidence mode. DEFAULT OFF.
1265
+ allowEmbeddedApproverKeys,
1266
+ }) as AssuranceTierDetail;
1267
+ // Take the strongest tier either path proves.
1268
+ const have = (TIER_RANK[assurance.have] ?? 0) >= (TIER_RANK[tierResult.tier] ?? 0)
1269
+ ? assurance.have : tierResult.tier;
1270
+ if ((TIER_RANK[have] ?? 0) < needRank) {
1271
+ // The credited tier (from either proof path) is below what the action
1272
+ // requires. The canonical machine-readable reason is 'assurance_too_low';
1273
+ // main's proof-path detail (e.g. 'assurance_proof_required') is surfaced
1274
+ // separately so callers keep the diagnostic without changing the contract.
1275
+ return decide(false, RECEIPT_REQUIRED_STATUS, 'assurance_too_low', {
1276
+ have_tier: have, need_tier: requiredTier,
1277
+ assurance_tier_source: 'cryptographic_verification',
1278
+ assurance_detail: assurance.reason || null,
1279
+ tier_evidence: { quorum: tierResult.quorum, signoff: tierResult.signoff },
1280
+ });
1281
+ }
1282
+ // Business authorization is a distinct trust root from receipt issuer
1283
+ // integrity and assurance tier. The signed claim must name the exact policy
1284
+ // id+hash and tenant this action requirement pins, and the humans who
1285
+ // cryptographically earned the tier must belong to its subject/role roster.
1286
+ // This runs BEFORE execution binding, admissibility, and receipt reservation.
1287
+ businessEvaluation = verifyBusinessAuthorization({
1288
+ requirement,
1289
+ receipt,
1290
+ assurance,
1291
+ tierResult,
1292
+ });
1293
+ if (!businessEvaluation.ok) {
1294
+ return decide(false, RECEIPT_REQUIRED_STATUS, businessEvaluation.reason, {
1295
+ have_tier: have,
1296
+ assurance_tier_source: 'cryptographic_verification',
1297
+ });
1298
+ }
1299
+ // The high-risk action packs define material fields that must be observed
1300
+ // by the executor from the system of record. A signed, harmless-looking
1301
+ // claim cannot authorize a different real mutation.
1302
+ const executionBinding = verifyExecutionBinding({ requirement, receipt, observedAction: observed });
1303
+ if (!executionBinding.ok) {
1304
+ return decide(false, RECEIPT_REQUIRED_STATUS, 'execution_binding_failed', { execution_binding: executionBinding, have_tier: have, assurance_tier_source: 'cryptographic_verification' });
1305
+ }
1306
+ // OPT-IN admissibility pinning. When the caller pins a required admissibility
1307
+ // profile {id, profile_hash} (gate-level requiredAdmissibilityProfile, a
1308
+ // per-call admissibilityProfile, or selector.admissibilityProfile), the gate
1309
+ // REFUSES unless a presented reliance packet's admissibility block was computed
1310
+ // against the SAME pinned profile_hash AND carries an 'admissible' verdict. The
1311
+ // gate does NOT re-evaluate raw evidence and does NOT define the bar — the
1312
+ // relying party's own evaluator produced the verdict OFFLINE against its pinned
1313
+ // profile. Checked BEFORE consumption so a mismatch never burns the receipt.
1314
+ // When no profile is pinned, this whole block is inert — behavior is
1315
+ // byte-for-byte unchanged from the pre-admissibility gate.
1316
+ const pinnedProfile = admissibilityProfile || selector.admissibilityProfile || requiredAdmissibilityProfile;
1317
+ let trustedAdmissibility = null;
1318
+ if (pinnedProfile) {
1319
+ const presentedAdmissibility = admissibility ?? presentedPacket ?? selector.reliancePacket ?? selector.admissibility ?? null;
1320
+ if (typeof verifyAdmissibilityPacket !== 'function') {
1321
+ return decide(false, RECEIPT_REQUIRED_STATUS, 'admissibility_verifier_required', {
1322
+ pinned_profile: { id: pinnedProfile.id ?? null, profile_hash: pinnedProfile.profile_hash ?? null },
1323
+ have_tier: have,
1324
+ assurance_tier_source: 'cryptographic_verification',
1325
+ });
1326
+ }
1327
+ try {
1328
+ trustedAdmissibility = await verifyAdmissibilityPacket({
1329
+ pinned_profile: structuredClone(pinnedProfile),
1330
+ presented: structuredClone(presentedAdmissibility),
1331
+ receipt: structuredClone(receipt),
1332
+ selector: structuredClone(selector),
1333
+ observed_action: observed === null ? null : structuredClone(observed),
1334
+ });
1335
+ } catch {
1336
+ return decide(false, RECEIPT_REQUIRED_STATUS, 'admissibility_verification_failed', {
1337
+ pinned_profile: { id: pinnedProfile.id ?? null, profile_hash: pinnedProfile.profile_hash ?? null },
1338
+ have_tier: have,
1339
+ assurance_tier_source: 'cryptographic_verification',
1340
+ });
1341
+ }
1342
+ const adm = verifyAdmissibilityAgainstPinnedProfile(pinnedProfile, trustedAdmissibility);
1343
+ if (!adm.ok) {
1344
+ return decide(false, RECEIPT_REQUIRED_STATUS, adm.reason, {
1345
+ admissibility_check: adm,
1346
+ pinned_profile: { id: pinnedProfile.id ?? null, profile_hash: pinnedProfile.profile_hash ?? null },
1347
+ have_tier: have,
1348
+ assurance_tier_source: 'cryptographic_verification',
1349
+ });
1350
+ }
1351
+ }
1352
+ const capabilityBinding = verifyCapabilityActionBinding({ capability, observedAction: observed });
1353
+ if (!capabilityBinding.ok) {
1354
+ return decide(false, RECEIPT_REQUIRED_STATUS, capabilityBinding.reason, {
1355
+ capability: {
1356
+ ...capabilitySummary(capability, capability?.operationId),
1357
+ ...capabilityBinding,
1358
+ },
1359
+ have_tier: have,
1360
+ assurance_tier_source: 'cryptographic_verification',
1361
+ });
1362
+ }
1363
+ // One-time consumption (replay defense). Require a stable, issuer-generated
1364
+ // receipt_id — never fall back to a content hash, whose canonicalization can
1365
+ // differ across language implementations and silently break replay detection
1366
+ // when services of different languages share a store.
1367
+ const receiptId = receipt?.payload?.receipt_id;
1368
+ if (!receiptId) {
1369
+ return decide(false, RECEIPT_REQUIRED_STATUS, 'receipt_rejected:missing_receipt_id');
1370
+ }
1371
+ let fresh;
1372
+ if (consumptionMode === 'reserve') {
1373
+ if (typeof consumption.reserve !== 'function') {
1374
+ return decide(false, RECEIPT_REQUIRED_STATUS, 'consumption_store_lacks_reserve', { consumption_key: receiptId });
1375
+ }
1376
+ fresh = await consumption.reserve(receiptId);
1377
+ } else if (consumptionMode === 'none') {
1378
+ fresh = true;
1379
+ } else {
1380
+ fresh = await consumption.consume(receiptId);
1381
+ }
1382
+ if (!fresh) {
1383
+ return decide(false, RECEIPT_REQUIRED_STATUS, 'replay_refused', { consumption_key: receiptId });
1384
+ }
1385
+ const allowExtra: Obj = {
1386
+ signer: v.signer,
1387
+ outcome: v.outcome,
1388
+ have_tier: have,
1389
+ assurance_tier_source: 'cryptographic_verification',
1390
+ execution_binding: executionBinding,
1391
+ consumption_mode: consumptionMode,
1392
+ ...(capability ? { capability: { ...capabilitySummary(capability, capability?.operationId), ...capabilityBinding } } : {}),
1393
+ };
1394
+ // Carry the admissibility block (from the presented packet) onto the decision
1395
+ // so a reliance packet built from this decision embeds the verdict the relying
1396
+ // party's evaluator computed. Only when something was actually presented.
1397
+ const presentedAdmForAllow = pinnedProfile
1398
+ ? trustedAdmissibility
1399
+ : (admissibility ?? presentedPacket ?? selector.reliancePacket ?? selector.admissibility ?? null);
1400
+ if (presentedAdmForAllow) {
1401
+ const admBlock = presentedAdmForAllow.admissibility !== undefined ? presentedAdmForAllow.admissibility : presentedAdmForAllow;
1402
+ if (admBlock) allowExtra.admissibility = admBlock;
1403
+ }
1404
+ return decide(true, 200, 'allow', allowExtra);
1405
+ }
1406
+
1407
+ async function requestGateInput(req, opts: GateCallOpts = {}) {
1408
+ let selector = typeof opts.selector === 'function'
1409
+ ? await opts.selector(req)
1410
+ : { ...(opts.selector || {}) };
1411
+ if (!selector || typeof selector !== 'object' || Array.isArray(selector)) selector = {};
1412
+ if (opts.action && !selector.action_type) {
1413
+ selector.action_type = typeof opts.action === 'function' ? await opts.action(req) : opts.action;
1414
+ }
1415
+ let receipt = typeof opts.receipt === 'function' ? await opts.receipt(req) : (opts.receipt ?? null);
1416
+ if (!receipt) {
1417
+ const hdr = req.headers?.['x-emilia-receipt'];
1418
+ if (hdr) receipt = parseReceiptCarrier(hdr);
1419
+ if (!receipt && req.body?.emilia_receipt) receipt = req.body.emilia_receipt;
1420
+ }
1421
+ const observedAction = typeof opts.observedAction === 'function'
1422
+ ? await opts.observedAction(req)
1423
+ : (opts.observedAction || req.emiliaObservedAction || null);
1424
+ const admissibilityProfile = typeof opts.admissibilityProfile === 'function'
1425
+ ? await opts.admissibilityProfile(req)
1426
+ : (opts.admissibilityProfile ?? null);
1427
+ const presentedPacket = typeof opts.reliancePacket === 'function'
1428
+ ? await opts.reliancePacket(req)
1429
+ : (opts.reliancePacket ?? opts.admissibility ?? null);
1430
+ const capability = typeof opts.capability === 'function'
1431
+ ? await opts.capability(req)
1432
+ : (opts.capability ?? req.emiliaCapability ?? req.body?.emilia_capability ?? null);
1433
+ return { selector, receipt, observedAction, admissibilityProfile, reliancePacket: presentedPacket, capability };
1434
+ }
1435
+
1436
+ function sendRefusal(res, authorization) {
1437
+ if (typeof res?.setHeader === 'function' && authorization.header) {
1438
+ res.setHeader(RECEIPT_REQUIRED_HEADER, authorization.header);
1439
+ }
1440
+ if (typeof res?.setHeader === 'function') {
1441
+ res.setHeader('content-type', 'application/problem+json');
1442
+ res.setHeader('cache-control', 'no-store');
1443
+ }
1444
+ if (typeof res?.status === 'function' && typeof res?.json === 'function') {
1445
+ return res.status(authorization.status).json(authorization.challenge);
1446
+ }
1447
+ if (res) res.statusCode = authorization.status;
1448
+ if (typeof res?.end === 'function') {
1449
+ return res.end(JSON.stringify(authorization.challenge));
1450
+ }
1451
+ return authorization;
1452
+ }
1453
+
1454
+ /**
1455
+ * Express/Connect route wrapper. The route handler itself is the effect
1456
+ * callback owned by gate.run(), so authorization, receipt reservation,
1457
+ * execution, consumption, and evidence form one lifecycle.
1458
+ */
1459
+ function route(handler, opts: GateCallOpts = {}) {
1460
+ if (typeof handler !== 'function') throw new Error('EMILIA Gate route(): handler is required');
1461
+ return async function emiliaGateRoute(req, res) {
1462
+ const input = await requestGateInput(req, opts);
1463
+ const out: GateRunResult = await run(input, async (authorization) => {
1464
+ req.emiliaGate = authorization;
1465
+ return handler(req, res, authorization);
1466
+ });
1467
+ if (!out.ok) return sendRefusal(res, out.refusal || out.authorization);
1468
+ req.emiliaGate = out.authorization;
1469
+ req.emiliaGateExecution = out.execution;
1470
+ req.emiliaReliancePacket = out.packet;
1471
+ return out.result;
1472
+ };
1473
+ }
1474
+
1475
+ /**
1476
+ * @deprecated Middleware cannot prove that code after next() actually ran.
1477
+ * It therefore fails closed without parsing or consuming a presented receipt.
1478
+ * Use gate.route(handler, opts), gate.guard(), or gate.run().
1479
+ */
1480
+ function middleware(opts: GateCallOpts = {}) {
1481
+ return async function emiliaGateDeprecatedMiddleware(req, res) {
1482
+ const selector = typeof opts.selector === 'function'
1483
+ ? await opts.selector(req)
1484
+ : { ...(opts.selector || {}) };
1485
+ const action = (selector && (selector.action_type || selector.action))
1486
+ || (typeof opts.action === 'function' ? await opts.action(req) : opts.action)
1487
+ || null;
1488
+ const reason = 'unsafe_middleware_deprecated';
1489
+ const challenge = receiptChallenge(action, reason, {
1490
+ status: RECEIPT_REQUIRED_STATUS,
1491
+ assuranceClass: selector?.assurance_class || null,
1492
+ maxAgeSec,
1493
+ manifest: selector?.manifestUrl,
1494
+ });
1495
+ const authorization = {
1496
+ allow: false,
1497
+ status: RECEIPT_REQUIRED_STATUS,
1498
+ reason,
1499
+ action,
1500
+ challenge,
1501
+ header: receiptRequiredHeader({ action, assuranceClass: selector?.assurance_class || null, maxAgeSec }),
1502
+ };
1503
+ return sendRefusal(res, authorization);
1504
+ };
1505
+ }
1506
+
1507
+ /**
1508
+ * Emit a post-execution receipt bound to a prior authorization decision — the
1509
+ * "execution emits proof" half of the loop (maps to the EP Commit seal). It
1510
+ * commits to the exact authorization decision (`authorizes_decision` = that
1511
+ * decision's evidence hash), so authorization and execution are one chain.
1512
+ */
1513
+ async function recordExecution({ authorization, outcome = 'executed', detail, observedAction = null, executionBinding = null }: {
1514
+ authorization?: any; outcome?: string; detail?: any; observedAction?: any; executionBinding?: any;
1515
+ } = {}) {
1516
+ const auth = authorization?.evidence || authorization || {};
1517
+ const authorizationBinding = authorization?.evidence?.execution_binding
1518
+ || authorization?.execution_binding
1519
+ || executionBinding
1520
+ || null;
1521
+ const boundExecution = bindExecutionProof({
1522
+ authorization,
1523
+ observedAction,
1524
+ binding: authorizationBinding,
1525
+ });
1526
+ return evidence.record({
1527
+ kind: 'execution',
1528
+ at: new Date(typeof now === 'function' ? now() : now).toISOString(),
1529
+ authorizes_decision: auth.hash ?? null,
1530
+ action: authorization?.action ?? auth.action ?? null,
1531
+ receipt_id: auth.receipt_id ?? null,
1532
+ outcome, // 'executed' | 'failed'
1533
+ observed_action_hash: observedAction ? safeCanonicalHash(observedAction) : null,
1534
+ execution_binding: boundExecution,
1535
+ ...(detail !== undefined ? { detail } : {}),
1536
+ });
1537
+ }
1538
+
1539
+ async function recordCapabilityEvent({ authorization, capability, outcome, reason = null }: {
1540
+ authorization?: any; capability?: any; outcome?: string; reason?: any;
1541
+ } = {}) {
1542
+ return evidence.record({
1543
+ kind: 'capability',
1544
+ at: new Date(typeof now === 'function' ? now() : now).toISOString(),
1545
+ authorizes_decision: authorization?.evidence?.hash ?? null,
1546
+ action: authorization?.action ?? null,
1547
+ ...capabilitySummary(capability, capability?.operationId),
1548
+ outcome,
1549
+ ...(reason ? { reason } : {}),
1550
+ });
1551
+ }
1552
+
1553
+ function capabilityRefusal({ authorization = null, capability = null, reason, status = CAPABILITY_FAILURE_STATUS, event = null }: {
1554
+ authorization?: any; capability?: any; reason?: any; status?: number; event?: any;
1555
+ } = {}): {
1556
+ ok: false; status: any; body: any; authorization: any; refusal: any; capability: any; evidence: any;
1557
+ result?: undefined; execution?: undefined; packet?: undefined;
1558
+ } {
1559
+ const body = {
1560
+ rejected: {
1561
+ type: 'capability',
1562
+ reason,
1563
+ ...capabilitySummary(capability, capability?.operationId),
1564
+ },
1565
+ };
1566
+ return {
1567
+ ok: false,
1568
+ status,
1569
+ body,
1570
+ authorization,
1571
+ refusal: {
1572
+ allow: false,
1573
+ status,
1574
+ reason,
1575
+ action: authorization?.action ?? null,
1576
+ challenge: body,
1577
+ header: null,
1578
+ },
1579
+ capability: { reason, ...capabilitySummary(capability, capability?.operationId) },
1580
+ evidence: event,
1581
+ };
1582
+ }
1583
+
1584
+ /**
1585
+ * Capability-backed execution is the real Marvel guard path. The ordinary
1586
+ * EP receipt is verified first without consuming it; the capability store
1587
+ * then reserves the exact observed monetary amount before the effect. A
1588
+ * successful effect commits the reservation, while an exception commits it
1589
+ * as indeterminate so the budget can never silently reopen.
1590
+ */
1591
+ async function runCapability({ selector = {}, receipt = null, observedAction = null, admissibilityProfile = null, reliancePacket: presentedPacket = null, admissibility = null, capability = null }: {
1592
+ selector?: any; receipt?: any; observedAction?: any; admissibilityProfile?: any;
1593
+ reliancePacket?: any; admissibility?: any; capability?: any;
1594
+ } = {}, fn, opts: GateCallOpts = {}): Promise<
1595
+ | ReturnType<typeof capabilityRefusal>
1596
+ | { ok: true; result: any; authorization: any; execution: any; packet: any; capability: any; status?: undefined; body?: undefined; refusal?: undefined; evidence?: undefined }
1597
+ | { ok: false; status: any; body: any; authorization: any; refusal: any; capability: any; result?: undefined; execution?: undefined; packet?: undefined; evidence?: undefined }
1598
+ > {
1599
+ if (!capabilityStore) return capabilityRefusal({ capability, reason: 'capability_store_required', status: 500 });
1600
+ if (!capability || typeof capability !== 'object' || Array.isArray(capability)
1601
+ || !capability.capabilityReceipt || !capability.action) {
1602
+ return capabilityRefusal({ capability, reason: 'capability_request_invalid' });
1603
+ }
1604
+ if (typeof capability.operationId !== 'string' || capability.operationId.length === 0
1605
+ || Buffer.byteLength(capability.operationId, 'utf8') > 128) {
1606
+ return capabilityRefusal({ capability, reason: 'capability_operation_id_required' });
1607
+ }
1608
+ const operationId = capability.operationId;
1609
+ const context = { ...capability, operationId };
1610
+ const baseReceipt = capability.capabilityReceipt.receipt;
1611
+ if (receipt && safeCanonicalHash(receipt) !== safeCanonicalHash(baseReceipt)) {
1612
+ return capabilityRefusal({ capability: context, reason: 'capability_receipt_mismatch' });
1613
+ }
1614
+
1615
+ let authorization: any = null;
1616
+ let effectError: unknown = null;
1617
+ let effectStarted = false;
1618
+ const capabilityGate = {
1619
+ check: (input = {}) => check({
1620
+ ...input,
1621
+ capability: {
1622
+ capabilityReceipt: context.capabilityReceipt,
1623
+ action: context.action,
1624
+ operationId,
1625
+ },
1626
+ }),
1627
+ };
1628
+ const executeAction = async (_action, executionContext) => {
1629
+ authorization = executionContext.authorization;
1630
+ const runtimeCycleId = authorization?._runtime_cycle_id;
1631
+ const runtimeStart = runtimeMonitor?.beginExecution(runtimeCycleId, authorization) as
1632
+ { ok: true } | { ok: false; reason: string; event: any } | undefined;
1633
+ if (runtimeStart && !runtimeStart.ok) {
1634
+ const error = new Error(`EMILIA Gate runtime monitor refused capability execution (${runtimeStart.reason})`) as Error & { code?: string };
1635
+ error.code = 'EMILIA_RUNTIME_MONITOR_REFUSED';
1636
+ throw error;
1637
+ }
1638
+ effectStarted = true;
1639
+ try {
1640
+ const value = await fn(authorization, {
1641
+ operationId,
1642
+ providerIdempotencyKey: operationId,
1643
+ actionDigest: executionContext.action_digest,
1644
+ observedAction: executionContext.observed_action,
1645
+ });
1646
+ runtimeMonitor?.effectReturned(runtimeCycleId);
1647
+ return value;
1648
+ } catch (error) {
1649
+ effectError = error;
1650
+ runtimeMonitor?.effectFailed(runtimeCycleId);
1651
+ throw error;
1652
+ }
1653
+ };
1654
+ const executorInput = {
1655
+ capabilityReceipt: context.capabilityReceipt,
1656
+ action: context.action,
1657
+ store: capabilityStore,
1658
+ gate: capabilityGate,
1659
+ selector,
1660
+ observedAction,
1661
+ trustedIssuerKeys: effectiveCapabilityTrustedIssuerKeys,
1662
+ resolveCaid: capabilityCaidResolver,
1663
+ operationId,
1664
+ now,
1665
+ executeAction,
1666
+ };
1667
+ const capabilityResult = Array.isArray(context.shares)
1668
+ ? await executeWithThreshold(/** @type {any} */ ({ ...executorInput, shares: context.shares }))
1669
+ : await executeWithCapability(/** @type {any} */ ({ ...executorInput, secret: context.secret }));
1670
+
1671
+ authorization = authorization || capabilityResult.authorization || null;
1672
+ const runtimeCycleId = authorization?._runtime_cycle_id;
1673
+ if (capabilityResult.ok) {
1674
+ runtimeMonitor?.consumptionCommitted(runtimeCycleId);
1675
+ if (opts.recordExecution === false) {
1676
+ runtimeMonitor?.executionSkipped(runtimeCycleId);
1677
+ return { ok: true, result: capabilityResult.result, authorization, execution: null, packet: null, capability: capabilityResult };
1678
+ }
1679
+ let execution: any = null;
1680
+ try {
1681
+ execution = await recordExecution({
1682
+ authorization,
1683
+ outcome: 'executed',
1684
+ observedAction,
1685
+ detail: { capability: { ...capabilitySummary(context, operationId), outcome: 'executed' } },
1686
+ });
1687
+ runtimeMonitor?.executionRecorded(runtimeCycleId);
1688
+ const packet = await reliancePacket({ authorization, execution });
1689
+ return { ok: true, result: capabilityResult.result, authorization, execution, packet, capability: capabilityResult };
1690
+ } catch (error) {
1691
+ throw gateOutcomeError(error, {
1692
+ outcome: 'executed',
1693
+ reason: execution ? 'reliance_packet_unavailable' : 'execution_evidence_unavailable',
1694
+ authorization,
1695
+ execution,
1696
+ result: capabilityResult.result,
1697
+ });
1698
+ }
1699
+ }
1700
+
1701
+ if (effectStarted && (capabilityResult.reason === 'effect_indeterminate'
1702
+ || capabilityResult.reason === 'capability_commit_indeterminate')) {
1703
+ if (capabilityResult.reason === 'effect_indeterminate') runtimeMonitor?.consumptionCommitted(runtimeCycleId);
1704
+ let execution: any = null;
1705
+ if (opts.recordExecution !== false) {
1706
+ try {
1707
+ execution = await recordExecution({
1708
+ authorization,
1709
+ outcome: 'indeterminate',
1710
+ observedAction,
1711
+ detail: {
1712
+ code: 'effect_attempted_outcome_unknown',
1713
+ capability: { ...capabilitySummary(context, operationId), outcome: 'indeterminate' },
1714
+ },
1715
+ });
1716
+ } catch (error) {
1717
+ throw gateOutcomeError(effectError ?? error, {
1718
+ outcome: 'indeterminate',
1719
+ reason: 'execution_evidence_unavailable',
1720
+ authorization,
1721
+ execution: null,
1722
+ });
1723
+ }
1724
+ }
1725
+ if (capabilityResult.reason === 'effect_indeterminate' && opts.recordExecution !== false) {
1726
+ runtimeMonitor?.executionRecorded(runtimeCycleId);
1727
+ }
1728
+ if (effectError) {
1729
+ throw gateOutcomeError(effectError, {
1730
+ outcome: 'indeterminate',
1731
+ reason: capabilityResult.reason,
1732
+ authorization,
1733
+ execution,
1734
+ });
1735
+ }
1736
+ return capabilityRefusal({ authorization, capability: context, reason: capabilityResult.reason, event: execution });
1737
+ }
1738
+
1739
+ if (authorization?.allow === true) {
1740
+ const event = await recordCapabilityEvent({
1741
+ authorization,
1742
+ capability: context,
1743
+ outcome: 'refused',
1744
+ reason: capabilityResult.reason,
1745
+ });
1746
+ runtimeMonitor?.capabilityRefused(runtimeCycleId);
1747
+ return capabilityRefusal({ authorization, capability: context, reason: capabilityResult.reason, event });
1748
+ }
1749
+ if (authorization) {
1750
+ return {
1751
+ ok: false,
1752
+ status: authorization.status,
1753
+ body: authorization.challenge,
1754
+ authorization,
1755
+ refusal: authorization,
1756
+ capability: { reason: capabilityResult.reason, ...capabilitySummary(context, operationId) },
1757
+ };
1758
+ }
1759
+ return capabilityRefusal({ authorization, capability: context, reason: capabilityResult.reason, status: RECEIPT_REQUIRED_STATUS });
1760
+ }
1761
+
1762
+ /**
1763
+ * Recommended end-to-end path. Reserves the receipt, runs the side effect,
1764
+ * commits one-time consumption after the effect attempt, and records execution.
1765
+ * Once the executor is invoked, an exception is an INDETERMINATE outcome: the
1766
+ * external effect may have happened before its response was lost. The receipt
1767
+ * is therefore committed (or left reserved if the store is unavailable),
1768
+ * never released automatically. Callers that need retries must make the
1769
+ * downstream effect idempotent under the receipt id and reconcile its result.
1770
+ */
1771
+ async function run({ selector = {}, receipt = null, observedAction = null, admissibilityProfile = null, reliancePacket: presentedPacket = null, admissibility = null, capability = null }: {
1772
+ selector?: any; receipt?: any; observedAction?: any; admissibilityProfile?: any;
1773
+ reliancePacket?: any; admissibility?: any; capability?: any;
1774
+ } = {}, fn, opts: GateCallOpts = {}): Promise<
1775
+ | Awaited<ReturnType<typeof runCapability>>
1776
+ | { ok: false; status: any; body: any; authorization: any; result?: undefined; execution?: undefined; packet?: undefined }
1777
+ | { ok: true; result: any; authorization: any; execution: any; packet: any; status?: undefined; body?: undefined }
1778
+ > {
1779
+ if (typeof fn !== 'function') throw new Error('EMILIA Gate run(): fn is required');
1780
+ if (capability) {
1781
+ return runCapability({ selector, receipt, observedAction, admissibilityProfile, reliancePacket: presentedPacket, admissibility, capability }, fn, opts);
1782
+ }
1783
+ const authorization = await check({ selector, receipt, observedAction, consumptionMode: 'reserve', admissibilityProfile, reliancePacket: presentedPacket, admissibility });
1784
+ if (!authorization.allow) {
1785
+ return { ok: false, status: authorization.status, body: authorization.challenge, authorization };
1786
+ }
1787
+ const receiptId = authorization.evidence?.receipt_id;
1788
+ const runtimeCycleId = authorization._runtime_cycle_id;
1789
+ if (runtimeMonitor && runtimeCycleId) {
1790
+ const runtimeStart = runtimeMonitor.beginExecution(runtimeCycleId, authorization) as
1791
+ { ok: true } | { ok: false; reason: string; event: any };
1792
+ if (!runtimeStart.ok) {
1793
+ const error = new Error(`EMILIA Gate runtime monitor refused execution (${runtimeStart.reason})`) as Error & { code?: string };
1794
+ error.code = 'EMILIA_RUNTIME_MONITOR_REFUSED';
1795
+ throw error;
1796
+ }
1797
+ }
1798
+ let phase = 'reserved';
1799
+ let consumptionCommitted = false;
1800
+ let result: any = null;
1801
+ let execution: any = null;
1802
+ try {
1803
+ phase = 'effect_attempted';
1804
+ result = await fn(authorization);
1805
+ phase = 'effect_returned';
1806
+ runtimeMonitor?.effectReturned(runtimeCycleId);
1807
+ if (typeof consumption.commit === 'function') await consumption.commit(receiptId);
1808
+ consumptionCommitted = true;
1809
+ phase = 'consumed';
1810
+ runtimeMonitor?.consumptionCommitted(runtimeCycleId);
1811
+ if (opts.recordExecution === false) {
1812
+ runtimeMonitor?.executionSkipped(runtimeCycleId);
1813
+ return { ok: true, result, authorization, execution: null, packet: null };
1814
+ }
1815
+ phase = 'recording_execution';
1816
+ execution = await recordExecution({ authorization, outcome: 'executed', observedAction });
1817
+ runtimeMonitor?.executionRecorded(runtimeCycleId);
1818
+ phase = 'execution_recorded';
1819
+ const packet = await reliancePacket({ authorization, execution });
1820
+ return { ok: true, result, authorization, execution, packet };
1821
+ } catch (e) {
1822
+ if (consumptionCommitted
1823
+ && ['consumed', 'recording_execution', 'execution_recorded'].includes(phase)) {
1824
+ throw gateOutcomeError(e, {
1825
+ outcome: 'executed',
1826
+ reason: execution ? 'reliance_packet_unavailable' : 'execution_evidence_unavailable',
1827
+ authorization,
1828
+ execution,
1829
+ result,
1830
+ });
1831
+ }
1832
+ if (runtimeMonitor && runtimeCycleId && (phase === 'effect_attempted' || phase === 'effect_returned')) {
1833
+ runtimeMonitor.effectFailed(runtimeCycleId);
1834
+ }
1835
+ // An exception after invoking fn() cannot establish that no external
1836
+ // effect occurred. Burn the approval if possible; if storage is down, the
1837
+ // ownership-fenced reservation remains and still blocks replay.
1838
+ let consumptionError: unknown = null;
1839
+ if (!consumptionCommitted && phase !== 'reserved' && typeof consumption.commit === 'function') {
1840
+ try {
1841
+ await consumption.commit(receiptId);
1842
+ consumptionCommitted = true;
1843
+ phase = 'consumed';
1844
+ runtimeMonitor?.consumptionCommitted(runtimeCycleId);
1845
+ } catch (commitError) {
1846
+ consumptionError = commitError;
1847
+ }
1848
+ }
1849
+ let indeterminateExecution: any = null;
1850
+ if (opts.recordExecution !== false && phase !== 'recording_execution') {
1851
+ try {
1852
+ indeterminateExecution = await recordExecution({
1853
+ authorization,
1854
+ outcome: 'indeterminate',
1855
+ // Exception text frequently contains provider payloads, record IDs,
1856
+ // or secrets. The caller still receives the original exception;
1857
+ // the portable evidence record carries only the closed outcome.
1858
+ detail: { code: 'effect_attempted_outcome_unknown' },
1859
+ observedAction,
1860
+ });
1861
+ const runtimeState = runtimeMonitor?.getState(runtimeCycleId);
1862
+ if (runtimeState && runtimeState.complete !== true) {
1863
+ runtimeMonitor?.executionRecorded(runtimeCycleId);
1864
+ }
1865
+ } catch (recordError) {
1866
+ if (!consumptionError) consumptionError = recordError;
1867
+ }
1868
+ }
1869
+ if (consumptionError && e && typeof e === 'object') {
1870
+ (e as Obj).consumption_error = String((consumptionError as Error)?.message ?? consumptionError);
1871
+ }
1872
+ throw gateOutcomeError(e, {
1873
+ outcome: 'indeterminate',
1874
+ reason: 'effect_attempted_outcome_unknown',
1875
+ authorization,
1876
+ execution: indeterminateExecution,
1877
+ });
1878
+ }
1879
+ }
1880
+
1881
+ /**
1882
+ * Wrap any function so it runs only behind a passing gate check, and (unless
1883
+ * disabled) emits an execution receipt after it runs — the full firewall loop:
1884
+ * request -> check -> execute -> execution receipt. Framework-agnostic.
1885
+ */
1886
+ function guard(fn, opts: GateCallOpts = {}) {
1887
+ return async function guarded(...args) {
1888
+ // Guard providers may load the selector, receipt, or system-of-record
1889
+ // action asynchronously. Resolve each one before check() so a Promise
1890
+ // object can never be mistaken for a selector/receipt and accidentally
1891
+ // bypass the manifest's guarded action requirement.
1892
+ const selector = typeof opts.selector === 'function'
1893
+ ? await opts.selector(...args)
1894
+ : (opts.selector || {});
1895
+ const receipt = typeof opts.receipt === 'function'
1896
+ ? await opts.receipt(...args)
1897
+ : (opts.receipt ?? null);
1898
+ const observedAction = typeof opts.observedAction === 'function'
1899
+ ? await opts.observedAction(...args)
1900
+ : (opts.observedAction || selector.observedAction || null);
1901
+ const admissibilityProfile = typeof opts.admissibilityProfile === 'function'
1902
+ ? await opts.admissibilityProfile(...args)
1903
+ : (opts.admissibilityProfile ?? null);
1904
+ const presentedPacket = typeof opts.reliancePacket === 'function'
1905
+ ? await opts.reliancePacket(...args)
1906
+ : (opts.reliancePacket ?? opts.admissibility ?? null);
1907
+ const capability = typeof opts.capability === 'function'
1908
+ ? await opts.capability(...args)
1909
+ : (opts.capability ?? null);
1910
+ const out: GateRunResult = await run({ selector, receipt, observedAction, admissibilityProfile, reliancePacket: presentedPacket, capability }, () => fn(...args), { recordExecution: opts.recordExecution });
1911
+ if (!out.ok) {
1912
+ const refusal = out.refusal || out.authorization;
1913
+ const e = new Error(`EMILIA Gate refused (${refusal.reason})`) as Error & { code?: string; gate?: any };
1914
+ e.code = 'EMILIA_RECEIPT_REQUIRED';
1915
+ e.gate = refusal;
1916
+ throw e;
1917
+ }
1918
+ return out.result;
1919
+ };
1920
+ }
1921
+
1922
+ async function reliancePacket({ authorization, execution = null, binding = null, admissibility = null }: {
1923
+ authorization?: any; execution?: any; binding?: any; admissibility?: any;
1924
+ } = {}) {
1925
+ // The admissibility block rides on the authorization decision's evidence when
1926
+ // a reliance packet was presented at check() time; an explicit `admissibility`
1927
+ // arg overrides it. buildReliancePacket fails closed on a non-'admissible'
1928
+ // block, so a do_not_rely verdict can never be laundered into rely here.
1929
+ const adm = admissibility
1930
+ ?? authorization?.evidence?.admissibility
1931
+ ?? authorization?.admissibility
1932
+ ?? null;
1933
+ return buildReliancePacket({
1934
+ decision: authorization,
1935
+ execution,
1936
+ evidence,
1937
+ manifest,
1938
+ binding: binding || execution?.execution_binding || null,
1939
+ admissibility: adm,
1940
+ });
1941
+ }
1942
+
1943
+ /** Retention classification over this gate's evidence log (hot/cold/expired/legal-hold). */
1944
+ function retention(opts = {}) {
1945
+ return classifyRetention(evidence.all(), opts);
1946
+ }
1947
+ /** The auditor/SIEM export manifest for this gate's evidence log. */
1948
+ function retentionExport(opts = {}) {
1949
+ return buildRetentionExport(evidence.all(), opts);
1950
+ }
1951
+
1952
+ return {
1953
+ check, run, recordExecution, route, wrapRoute: route, middleware, guard, reliancePacket, evidence,
1954
+ store: consumption, capabilityStore, keyRegistry: registry, retention, retentionExport,
1955
+ };
1956
+ }
1957
+
1958
+ export function createTrustedActionFirewall(opts: CreateGateOptions = {}) {
1959
+ const { manifest = createDefaultActionRiskManifest(), ...rest } = opts;
1960
+ return createGate({ ...rest, manifest });
1961
+ }
1962
+
1963
+ /**
1964
+ * EG-1 conformance for an existing gate. The gate MUST have been built trusting
1965
+ * `harness.publicKey` (otherwise every valid receipt is rejected and the gate
1966
+ * cannot earn EG-1). Returns the EG-1 JSON report.
1967
+ * @param {object} [o]
1968
+ * @param {object} [o.gate] an EMILIA Gate (createGate/createTrustedActionFirewall)
1969
+ * @param {object} [o.harness] the harness whose key the gate trusts (createEg1Harness)
1970
+ * @param {object} [o.action] the high-risk action to exercise
1971
+ * @param {object} [o.selector] the manifest selector for that action
1972
+ */
1973
+ export async function gateConformance({ gate, harness, action, selector = EG1_DEFAULT_SELECTOR }: {
1974
+ gate?: any; harness?: any; action?: any; selector?: any;
1975
+ } = {}) {
1976
+ if (!gate || typeof gate.run !== 'function') {
1977
+ throw new Error('gateConformance requires a gate built trusting harness.publicKey');
1978
+ }
1979
+ if (!harness) throw new Error('gateConformance requires the harness whose key the gate trusts');
1980
+ const act = action || harness.action;
1981
+ const invoke = makeGateInvoke(gate, { selector, action: act });
1982
+ return runEg1({ invoke, harness, action: act });
1983
+ }
1984
+
1985
+ /**
1986
+ * Self-certify the reference gate: build a default Consequence Firewall that
1987
+ * trusts a fresh EG-1 harness key, then run all eight checks. This is the
1988
+ * canonical "EMILIA Gate earns EG-1" proof — runnable as a CLI (`eg1.mjs`),
1989
+ * shown on /gate, and the template an adopter copies for their integration.
1990
+ * @param {{now?: any}} [o]
1991
+ */
1992
+ export async function gateConformanceSelfTest({ now }: { now?: any } = {}) {
1993
+ const harness = createEg1Harness({ now });
1994
+ const gate = createTrustedActionFirewall({
1995
+ trustedKeys: [harness.publicKey], approverKeys: harness.approverKeys,
1996
+ rpId: harness.rpId, allowedOrigins: harness.allowedOrigins,
1997
+ now, allowEphemeralStore: true,
1998
+ });
1999
+ return gateConformance({ gate, harness });
2000
+ }
2001
+
2002
+ /**
2003
+ * CF-1 (Consequence Firewall) conformance for an existing gate. Runs the eight
2004
+ * EG-1 runtime checks plus the three CF-1 category checks: the action is
2005
+ * declared consequential by the manifest, a gate pinned to the WRONG issuer key
2006
+ * refuses a valid receipt, and the allowed run emits offline-verifiable reliance
2007
+ * evidence. The `gate` MUST trust `harness.publicKey`; `wrongGate` MUST trust a
2008
+ * DIFFERENT key (otherwise wrong_authority_refused cannot be demonstrated).
2009
+ * @param {object} [o]
2010
+ * @param {object} [o.gate] an EMILIA Gate trusting harness.publicKey
2011
+ * @param {object} [o.wrongGate] a sibling gate trusting a different (wrong) key
2012
+ * @param {object} [o.harness] from createEg1Harness()
2013
+ * @param {object} [o.manifest] the action-risk manifest (to resolve the requirement)
2014
+ * @param {object} [o.selector] the manifest selector for the action
2015
+ * @param {object} [o.action] the high-risk action to exercise
2016
+ */
2017
+ export async function cf1Conformance({ gate, wrongGate, harness, manifest = null, selector = EG1_DEFAULT_SELECTOR, action }: {
2018
+ gate?: any; wrongGate?: any; harness?: any; manifest?: any; selector?: any; action?: any;
2019
+ } = {}) {
2020
+ if (!gate || typeof gate.run !== 'function') throw new Error('cf1Conformance requires a gate built trusting harness.publicKey');
2021
+ if (!harness) throw new Error('cf1Conformance requires the harness whose key the gate trusts');
2022
+ const act = action || harness.action;
2023
+ const invoke = makeGateInvoke(gate, { selector, action: act });
2024
+ const wrongInvoke = (wrongGate && typeof wrongGate.run === 'function')
2025
+ ? makeGateInvoke(wrongGate, { selector, action: act }) : undefined;
2026
+ const requirement = manifest ? findActionRequirement(manifest, selector) : null;
2027
+ return runCf1({ invoke, wrongInvoke, harness, action: act, requirement });
2028
+ }
2029
+
2030
+ /**
2031
+ * Self-certify the reference gate against CF-1: a default Trusted Action
2032
+ * Firewall trusting a fresh harness key, a sibling firewall trusting a DIFFERENT
2033
+ * key (for wrong_authority_refused), and the default action-risk manifest (for
2034
+ * consequential_action_declared). The canonical "reference gate earns CF-1"
2035
+ * proof — runnable as a CLI (`cf1.mjs`).
2036
+ * @param {{now?: any}} [o]
2037
+ */
2038
+ export async function cf1ConformanceSelfTest({ now }: { now?: any } = {}) {
2039
+ const harness = createEg1Harness({ now });
2040
+ const manifest = createDefaultActionRiskManifest();
2041
+ const gate = createTrustedActionFirewall({
2042
+ trustedKeys: [harness.publicKey], approverKeys: harness.approverKeys,
2043
+ rpId: harness.rpId, allowedOrigins: harness.allowedOrigins,
2044
+ now, allowEphemeralStore: true,
2045
+ });
2046
+ const wrongHarness = createEg1Harness({ now });
2047
+ const wrongGate = createTrustedActionFirewall({
2048
+ trustedKeys: [wrongHarness.publicKey], approverKeys: wrongHarness.approverKeys,
2049
+ rpId: wrongHarness.rpId, allowedOrigins: wrongHarness.allowedOrigins,
2050
+ now, allowEphemeralStore: true,
2051
+ });
2052
+ return cf1Conformance({ gate, wrongGate, harness, manifest, selector: EG1_DEFAULT_SELECTOR, action: harness.action });
2053
+ }
2054
+
2055
+ export default {
2056
+ createGate,
2057
+ createTrustedActionFirewall,
2058
+ runBreakGlass,
2059
+ receiptAssuranceTier,
2060
+ businessAuthorizationRequirement,
2061
+ verifyBusinessAuthorization,
2062
+ verifyAdmissibilityAgainstPinnedProfile,
2063
+ ADMISSIBILITY_VERDICTS,
2064
+ MemoryConsumptionStore,
2065
+ createEvidenceLog,
2066
+ createAtomicEvidenceLog,
2067
+ createMemoryAtomicEvidenceBackend,
2068
+ ASSURANCE_TIERS,
2069
+ DEFAULT_GATE_MANIFEST,
2070
+ HIGH_RISK_ACTION_PACKS,
2071
+ gateConformance,
2072
+ gateConformanceSelfTest,
2073
+ cf1Conformance,
2074
+ cf1ConformanceSelfTest,
2075
+ CF1_VERSION,
2076
+ CF1_CHECKS,
2077
+ runCf1,
2078
+ createEg1Harness,
2079
+ runEg1,
2080
+ createKeyRegistry,
2081
+ asKeyRegistry,
2082
+ classifyRetention,
2083
+ buildRetentionExport,
2084
+ createDefaultActionControlManifest,
2085
+ findActionControl,
2086
+ resolveActionControl,
2087
+ validateActionControlManifest,
2088
+ createRuntimeMonitor,
2089
+ RUNTIME_MONITOR_VERSION,
2090
+ RUNTIME_MONITOR_MODES,
2091
+ RUNTIME_INVARIANTS,
2092
+ FORMAL_RUNTIME_BRIDGE_VERSION,
2093
+ FORMAL_RUNTIME_SPEC,
2094
+ FORMAL_RUNTIME_CONFIG,
2095
+ FORMAL_RUNTIME_INVARIANT_MAP,
2096
+ CAPABILITY_RECEIPT_VERSION,
2097
+ CAPABILITY_STATE_VERSION,
2098
+ CAPABILITY_SHARE_VERSION,
2099
+ CAPABILITY_SCOPE_PROFILE,
2100
+ CAPABILITY_CAID_SCOPE_PROFILE,
2101
+ CAPABILITY_STATE_DDL,
2102
+ CAPABILITY_SQL,
2103
+ capabilityBaseReceiptDigest,
2104
+ capabilityActionDigest,
2105
+ verifyCapabilityScope,
2106
+ mintCapabilityReceipt,
2107
+ verifyCapabilityReceipt,
2108
+ splitCapabilitySecret,
2109
+ reconstructCapabilitySecret,
2110
+ createMemoryCapabilityStore,
2111
+ createPostgresCapabilityStore,
2112
+ executeWithCapability,
2113
+ executeWithThreshold,
2114
+ reconcileCapabilityOperation,
2115
+ ZK_RANGE_RECEIPT_VERSION,
2116
+ ZK_RANGE_SCHEME,
2117
+ ZK_RANGE_BACKEND_PACKAGE,
2118
+ deriveZkRangeBases,
2119
+ loadBulletproofBackend,
2120
+ mintZkRangeReceipt,
2121
+ verifyZkRangeReceipt,
2122
+ };