@emilia-protocol/gate 0.10.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 +91 -0
  2. package/README.md +272 -0
  3. package/action-control-manifest.js +3 -268
  4. package/action-escrow-custodian.js +4 -0
  5. package/action-escrow-evidence.js +4 -0
  6. package/action-escrow-package.js +4 -0
  7. package/action-escrow-postgres.js +4 -0
  8. package/action-escrow-state.js +4 -0
  9. package/action-escrow-verifiers.js +4 -0
  10. package/action-escrow.js +4 -0
  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 +47 -4
  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 -85
  414. package/trust-program-adapters.js +4 -0
  415. package/trust-program-postgres.js +4 -0
  416. package/trust-program-revocation.js +4 -0
  417. package/trust-program.js +4 -0
  418. package/witness-postgres.js +3 -96
  419. package/zk-range-proof.js +4 -0
@@ -0,0 +1,232 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * EP-EXTERNAL-VERIFICATION-STATEMENT-v1.
4
+ *
5
+ * A signed statement a NON-EMILIA verifier can issue after it re-performs an
6
+ * evidence log, replays an admissibility profile, or runs a conformance harness.
7
+ * This is the missing adoption rail between "our verifier works" and "an
8
+ * outside party says exactly what they checked."
9
+ *
10
+ * Scope is intentionally narrow:
11
+ * - the statement signs a procedure, inputs, result, and limitations;
12
+ * - it does NOT authorize an action;
13
+ * - it does NOT certify business correctness;
14
+ * - acceptance is by a relying party pinning the external verifier key.
15
+ */
16
+ import crypto from 'node:crypto';
17
+ // In-package canonicalize (byte-identical to lib/canonical-json.js): reports must
18
+ // never import outside the package root or the published tarball cannot resolve it.
19
+ import { canonicalize } from '../execution-binding.js';
20
+
21
+ export const EXTERNAL_VERIFICATION_STATEMENT_VERSION = 'EP-EXTERNAL-VERIFICATION-STATEMENT-v1';
22
+ export const EXTERNAL_VERIFICATION_DOMAIN = 'EP-EXTERNAL-VERIFICATION-STATEMENT-v1\0';
23
+
24
+ const SHA256_RE = /^sha256:[0-9a-f]{64}$/i;
25
+
26
+ function sha256hex(bytes) {
27
+ return crypto.createHash('sha256').update(bytes).digest('hex');
28
+ }
29
+
30
+ function publicKeyToB64u(key) {
31
+ return crypto.createPublicKey(key).export({ type: 'spki', format: 'der' }).toString('base64url');
32
+ }
33
+
34
+ function keyIdFor(publicKeyB64u) {
35
+ return `ep:external-verifier-key:sha256:${sha256hex(Buffer.from(publicKeyB64u, 'base64url')).slice(0, 16)}`;
36
+ }
37
+
38
+ function signingBytes(unsignedStatement) {
39
+ return Buffer.from(EXTERNAL_VERIFICATION_DOMAIN + canonicalize(unsignedStatement), 'utf8');
40
+ }
41
+
42
+ function unsigned(statement) {
43
+ if (!statement || typeof statement !== 'object' || Array.isArray(statement)) {
44
+ throw new Error('statement must be an object');
45
+ }
46
+ const { signature: _signature, ...body } = statement;
47
+ return body;
48
+ }
49
+
50
+ /** Digest of the signed statement body, excluding the signature envelope. */
51
+ export function externalVerificationDigest(statement) {
52
+ return `sha256:${sha256hex(signingBytes(unsigned(statement)))}`;
53
+ }
54
+
55
+ function normalizeChecks(checks) {
56
+ if (!Array.isArray(checks)) return [];
57
+ return checks.map((c) => ({
58
+ id: String(c?.id ?? ''),
59
+ ok: c?.ok === true,
60
+ ...(c?.detail !== undefined ? { detail: c.detail } : {}),
61
+ })).filter((c) => c.id);
62
+ }
63
+
64
+ /**
65
+ * Build and sign an external-verifier statement.
66
+ *
67
+ * @param {object} args
68
+ * @param {object} args.verifier {id, name?, organization?}
69
+ * @param {object} args.subject what was checked, e.g. {kind:'evidence_log', head:'sha256:...'}
70
+ * @param {object} args.procedure {id, version?, tool?, command?}
71
+ * @param {object} args.result {status, checks?, artifact_digest?}
72
+ * @param {object} [args.inputs] stable digests/ids the procedure consumed
73
+ * @param {string[]} [args.limitations] honest non-claims
74
+ * @param {string|number} [args.generated_at] ISO or epoch millis
75
+ * @param {crypto.KeyObject} privateKey Ed25519 private key
76
+ */
77
+ export function signExternalVerificationStatement(args, privateKey) {
78
+ if (!privateKey) throw new Error('privateKey is required');
79
+ const generatedAt = args?.generated_at !== undefined
80
+ ? new Date(args.generated_at).toISOString()
81
+ : new Date().toISOString();
82
+ const publicKey = publicKeyToB64u(privateKey);
83
+ const body = {
84
+ '@version': EXTERNAL_VERIFICATION_STATEMENT_VERSION,
85
+ generated_at: generatedAt,
86
+ verifier: {
87
+ id: args?.verifier?.id ?? keyIdFor(publicKey),
88
+ ...(args?.verifier?.name ? { name: args.verifier.name } : {}),
89
+ ...(args?.verifier?.organization ? { organization: args.verifier.organization } : {}),
90
+ },
91
+ subject: args?.subject ?? {},
92
+ procedure: args?.procedure ?? {},
93
+ inputs: args?.inputs ?? {},
94
+ result: {
95
+ status: String(args?.result?.status ?? 'unknown'),
96
+ checks: normalizeChecks(args?.result?.checks),
97
+ ...(args?.result?.artifact_digest ? { artifact_digest: args.result.artifact_digest } : {}),
98
+ },
99
+ limitations: Array.isArray(args?.limitations) && args.limitations.length
100
+ ? args.limitations.map(String)
101
+ : [
102
+ 'This statement records the external verifier procedure and result; it does not authorize the action.',
103
+ 'It does not certify business correctness, legal compliance, or human wisdom.',
104
+ 'Acceptance depends on the relying party pinning the verifier key out of band.',
105
+ 'The statement carries no expiry and no consumer binding; it is replayable verbatim, and generated_at is asserted by the signer, not verified.',
106
+ ],
107
+ };
108
+
109
+ const digest = externalVerificationDigest(body);
110
+ const sig = crypto.sign(null, signingBytes(body), privateKey).toString('base64url');
111
+ return Object.freeze({
112
+ ...body,
113
+ signature: {
114
+ algorithm: 'Ed25519',
115
+ key_id: keyIdFor(publicKey),
116
+ public_key: publicKey,
117
+ statement_digest: digest,
118
+ signature_b64u: sig,
119
+ },
120
+ });
121
+ }
122
+
123
+ /**
124
+ * Verify a signed external-verifier statement against pinned verifier keys.
125
+ *
126
+ * @param {object} statement
127
+ * @param {{pinnedVerifierKeys?:Array<{verifier_id?:string,key_id?:string,public_key:string}>}} [opts]
128
+ */
129
+ export function verifyExternalVerificationStatement(statement, opts: {
130
+ pinnedVerifierKeys?: Array<{ verifier_id?: string; key_id?: string; public_key: string }>;
131
+ } = {}): {
132
+ verified: boolean;
133
+ accepted: boolean;
134
+ checks: Record<string, boolean>;
135
+ reason?: string;
136
+ statement_digest?: string;
137
+ verifier_id?: string;
138
+ key_id?: string;
139
+ } {
140
+ const fail = (reason, extra = {}) => ({
141
+ verified: false,
142
+ accepted: false,
143
+ checks: {
144
+ version: statement?.['@version'] === EXTERNAL_VERIFICATION_STATEMENT_VERSION,
145
+ signature: false,
146
+ pinned_verifier_key: false,
147
+ statement_digest: false,
148
+ },
149
+ reason,
150
+ ...extra,
151
+ });
152
+
153
+ if (statement?.['@version'] !== EXTERNAL_VERIFICATION_STATEMENT_VERSION) {
154
+ return fail('unsupported_version');
155
+ }
156
+ const sig = statement.signature;
157
+ if (!sig || sig.algorithm !== 'Ed25519' || typeof sig.public_key !== 'string' || typeof sig.signature_b64u !== 'string') {
158
+ return fail('signature_missing_or_malformed');
159
+ }
160
+ if (typeof sig.statement_digest !== 'string' || !SHA256_RE.test(sig.statement_digest)) {
161
+ return fail('statement_digest_missing_or_malformed');
162
+ }
163
+
164
+ let digest;
165
+ try {
166
+ digest = externalVerificationDigest(statement);
167
+ } catch {
168
+ return fail('statement_uncanonicalizable');
169
+ }
170
+ if (digest !== sig.statement_digest) {
171
+ return fail('statement_digest_mismatch', { statement_digest: digest });
172
+ }
173
+
174
+ const pinned = Array.isArray(opts.pinnedVerifierKeys) ? opts.pinnedVerifierKeys : [];
175
+ const verifierId = statement.verifier?.id ?? null;
176
+ // key_id is always DERIVED from the carried public key. The envelope's key_id is
177
+ // outside the signed bytes, so it is attacker-malleable; if present it must match
178
+ // the derived value or the statement is refused.
179
+ const keyId = keyIdFor(sig.public_key);
180
+ if (sig.key_id !== undefined && sig.key_id !== keyId) {
181
+ return fail('key_id_mismatch', { statement_digest: digest });
182
+ }
183
+ // A pin grants an identity, not just a key: every usable pin entry must name the
184
+ // verifier_id it vouches for. A pin that matches the key but omits verifier_id
185
+ // (or names a different one) never binds the statement's claimed identity.
186
+ const keyMatched = pinned.filter((k) => k?.public_key === sig.public_key
187
+ && (k.key_id === undefined || k.key_id === keyId));
188
+ const pin = keyMatched.find((k) => typeof k.verifier_id === 'string' && k.verifier_id === verifierId);
189
+ if (!pin) {
190
+ return {
191
+ verified: false,
192
+ accepted: false,
193
+ checks: { version: true, signature: false, pinned_verifier_key: false, statement_digest: true },
194
+ reason: keyMatched.length ? 'pin_missing_or_mismatched_verifier_id' : 'verifier_key_not_pinned',
195
+ statement_digest: digest,
196
+ };
197
+ }
198
+
199
+ let ok = false;
200
+ try {
201
+ const publicKey = crypto.createPublicKey({ key: Buffer.from(sig.public_key, 'base64url'), type: 'spki', format: 'der' });
202
+ ok = crypto.verify(null, signingBytes(unsigned(statement)), publicKey, Buffer.from(sig.signature_b64u, 'base64url'));
203
+ } catch {
204
+ ok = false;
205
+ }
206
+ if (!ok) {
207
+ return {
208
+ verified: false,
209
+ accepted: false,
210
+ checks: { version: true, signature: false, pinned_verifier_key: true, statement_digest: true },
211
+ reason: 'signature_invalid',
212
+ statement_digest: digest,
213
+ };
214
+ }
215
+
216
+ return {
217
+ verified: true,
218
+ accepted: true,
219
+ checks: { version: true, signature: true, pinned_verifier_key: true, statement_digest: true },
220
+ verifier_id: verifierId,
221
+ key_id: keyId,
222
+ statement_digest: digest,
223
+ };
224
+ }
225
+
226
+ export default {
227
+ EXTERNAL_VERIFICATION_STATEMENT_VERSION,
228
+ EXTERNAL_VERIFICATION_DOMAIN,
229
+ externalVerificationDigest,
230
+ signExternalVerificationStatement,
231
+ verifyExternalVerificationStatement,
232
+ };
@@ -0,0 +1,441 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * EMILIA Gate — independent re-performance verifier (EP-GATE-REPERFORMANCE-v1).
4
+ *
5
+ * The auditor's "trust nothing, recompute everything" tool. Re-performance is
6
+ * the highest form of audit evidence: instead of reading the deployer's report
7
+ * and believing it, the auditor takes the raw evidence entries and REDOES the
8
+ * work — rebuilds the hash chain link by link and checks it with the evidence
9
+ * log's own verifier, cryptographically re-verifies every receipt / signoff /
10
+ * quorum document an entry carries, recomputes the decision counts from
11
+ * scratch, and diffs those recomputed numbers against what a report pack
12
+ * claims. Vendor logs assert; this recomputes.
13
+ *
14
+ * INDEPENDENCE BY CONSTRUCTION: counts are recomputed here from the raw
15
+ * entries — this module deliberately does NOT import metering.js or any report
16
+ * builder, so a bug in a report builder cannot hide from its own cross-check.
17
+ * The only imports are the primitives being re-driven: the evidence log's real
18
+ * verify() and the protocol verifiers (verifyEmiliaReceipt /
19
+ * verifyWebAuthnSignoff / verifyQuorum).
20
+ *
21
+ * FAIL CLOSED everywhere:
22
+ * - one broken chain link fails the chain from that point — the remainder is
23
+ * not vouched for;
24
+ * - one failed re-verification is a NAMED failure ({hash, reason}), never
25
+ * absorbed into a pass count;
26
+ * - an entry that references a receipt but does not carry the verifiable
27
+ * material is counted `not_reverifiable` — never silently passed;
28
+ * - a reported pack of unknown @version is refused, never fuzzily matched.
29
+ *
30
+ * HONESTY BOUNDARY (carried inside the artifact): this SUPPORTS an auditor's
31
+ * re-performance procedure. It does not conclude, opine, or certify — and it
32
+ * cannot establish log COMPLETENESS (a chain rewritten in full, or entries
33
+ * withheld before supply, is invisible from the entries alone; anchor the head
34
+ * hash externally to close that), nor runtime freshness / one-time consumption
35
+ * (properties of the moment of decision, not re-performable after the fact).
36
+ *
37
+ * Deterministic: pure over (entries, issuerKeys); time enters only through the
38
+ * injectable `now` (generated_at). No sampling, no randomness.
39
+ */
40
+ import crypto from 'node:crypto';
41
+ import { createEvidenceLog } from '../evidence.js';
42
+ import { validatePinnedQuorumPolicy, verifyEmiliaReceipt } from '@emilia-protocol/require-receipt';
43
+ import { verifyQuorum, verifyWebAuthnSignoff } from '@emilia-protocol/verify';
44
+
45
+ export const REPERFORMANCE_VERSION = 'EP-GATE-REPERFORMANCE-v1';
46
+
47
+ // Wire-format identifiers of the packs compareToReported() can cross-check.
48
+ // Restated locally — NOT imported from the builders — so this module remains a
49
+ // standalone auditor tool with zero dependency on the code it checks.
50
+ const USAGE_PACK_VERSION = 'EP-GATE-USAGE-v1';
51
+ const UNDERWRITER_PACK_VERSION = 'EP-GATE-UNDERWRITER-ATTESTATION-v1';
52
+
53
+ // A verifiable payload carried by an evidence entry (see collectMaterial below).
54
+ type CarriedMaterial = { type: string; doc: any; key?: any };
55
+
56
+ /** Deterministic key-sorted copy — output is byte-stable regardless of entry order. */
57
+ function sortedCounts(obj) {
58
+ const out = {};
59
+ for (const k of Object.keys(obj).sort()) out[k] = obj[k];
60
+ return out;
61
+ }
62
+
63
+ /** The checks a verifier reported false, as one stable, named string. */
64
+ function failingChecks(checks) {
65
+ const bad = Object.keys(checks || {}).filter((k) => checks[k] === false).sort();
66
+ return bad.length ? bad.join('+') : 'unspecified';
67
+ }
68
+
69
+ /**
70
+ * The verifiable payloads an entry CARRIES (not the verification RESULTS the
71
+ * gate recorded about them — `tier_evidence` / `rejected` are the gate's own
72
+ * conclusions and are deliberately ignored: re-performance trusts nothing the
73
+ * gate concluded, only material it can re-verify itself).
74
+ *
75
+ * Recognized carriers:
76
+ * - entry.receipt full EP-RECEIPT-v1 document
77
+ * - entry.signoff / payload.signoff WebAuthn device-signoff evidence,
78
+ * with the approver key at entry.approver_public_key or
79
+ * receipt.payload.approver_public_key
80
+ * - entry.quorum / payload.quorum full EP-QUORUM-v1 document
81
+ */
82
+ function collectMaterial(e) {
83
+ const out: CarriedMaterial[] = [];
84
+ const receipt = e.receipt;
85
+ const payload = receipt && typeof receipt === 'object' && receipt.payload
86
+ && typeof receipt.payload === 'object' ? receipt.payload : null;
87
+ if (receipt !== undefined && receipt !== null) out.push({ type: 'receipt', doc: receipt });
88
+ const signoff = e.signoff ?? payload?.signoff;
89
+ if (signoff !== undefined && signoff !== null) {
90
+ out.push({
91
+ type: 'signoff',
92
+ doc: signoff,
93
+ key: e.approver_public_key ?? payload?.approver_public_key ?? null,
94
+ });
95
+ }
96
+ const quorum = e.quorum ?? payload?.quorum;
97
+ if (quorum !== undefined && quorum !== null) out.push({ type: 'quorum', doc: quorum });
98
+ return out;
99
+ }
100
+
101
+ /**
102
+ * Re-verify one carried payload. Returns null on pass, a NAMED reason on fail.
103
+ * Freshness is intentionally not re-enforced (maxAgeSec: 0): it was a runtime
104
+ * property of the decision moment and cannot be re-performed at audit time —
105
+ * what IS re-performed is the cryptography (signature, issuer trust, action
106
+ * binding, ceremony checks).
107
+ */
108
+ function spkiFingerprint(value) {
109
+ try {
110
+ const key = crypto.createPublicKey({ key: Buffer.from(value, 'base64url'), format: 'der', type: 'spki' });
111
+ return crypto.createHash('sha256').update(key.export({ type: 'spki', format: 'der' })).digest('hex');
112
+ } catch { return null; }
113
+ }
114
+
115
+ function pinnedApproverEntry(approverKeys: Record<string, any> | null | undefined, approver: any, carriedKey: any = null) {
116
+ if (!approverKeys || typeof approverKeys !== 'object' || Array.isArray(approverKeys)) return null;
117
+ const candidates = Object.values(approverKeys).filter((entry) => entry && typeof entry === 'object'
118
+ && entry.approver_id === approver && typeof entry.public_key === 'string');
119
+ if (carriedKey) {
120
+ const fingerprint = spkiFingerprint(carriedKey);
121
+ const matches = candidates.filter((entry) => fingerprint && spkiFingerprint(entry.public_key) === fingerprint);
122
+ return matches.length === 1 ? matches[0] : null;
123
+ }
124
+ return candidates.length === 1 ? candidates[0] : null;
125
+ }
126
+
127
+ function reverifyMaterial(m, {
128
+ issuerKeys, action, rpId, allowedOrigins, approverKeys, quorumPolicy, quorumPolicies,
129
+ }) {
130
+ if (m.type === 'receipt') {
131
+ const r = verifyEmiliaReceipt(m.doc, {
132
+ trustedKeys: issuerKeys, allowInlineKey: false, action, maxAgeSec: 0,
133
+ });
134
+ return r.ok ? null : `receipt:${r.reason}`;
135
+ }
136
+ if (m.type === 'signoff') {
137
+ if (typeof rpId !== 'string' || !rpId || !Array.isArray(allowedOrigins) || allowedOrigins.length === 0) {
138
+ return 'signoff:relying_party_scope_unpinned';
139
+ }
140
+ const approver = m.doc?.context?.approver;
141
+ const entry = pinnedApproverEntry(approverKeys, approver, m.key);
142
+ if (!entry) return 'signoff:approver_key_unpinned_or_ambiguous';
143
+ const r = verifyWebAuthnSignoff(m.doc, entry.public_key, { rpId, allowedOrigins });
144
+ return r.valid ? null : `signoff:${r.error ? `error:${r.error}` : `checks_failed:${failingChecks(r.checks)}`}`;
145
+ }
146
+ if (m.type === 'quorum') {
147
+ if (typeof rpId !== 'string' || !rpId || !Array.isArray(allowedOrigins) || allowedOrigins.length === 0) {
148
+ return 'quorum:relying_party_scope_unpinned';
149
+ }
150
+ const pinnedPolicy = (quorumPolicies && typeof quorumPolicies === 'object' ? quorumPolicies[action] : null)
151
+ || quorumPolicy;
152
+ const policy = validatePinnedQuorumPolicy(pinnedPolicy);
153
+ if (!policy.ok) return `quorum:${policy.reason}`;
154
+ const members = Array.isArray(m.doc?.members) ? m.doc.members.map((member) => {
155
+ const entry = pinnedApproverEntry(
156
+ approverKeys,
157
+ member?.signoff?.context?.approver,
158
+ member?.approver_public_key,
159
+ );
160
+ return entry ? { ...member, approver_public_key: entry.public_key } : null;
161
+ }) : [];
162
+ if (members.length === 0 || members.some((member) => !member)) return 'quorum:approver_key_unpinned_or_ambiguous';
163
+ const r = verifyQuorum({ ...m.doc, policy: policy.policy, members }, { rpId, allowedOrigins });
164
+ return r.valid ? null : `quorum:checks_failed:${failingChecks(r.checks)}`;
165
+ }
166
+ /* c8 ignore next */
167
+ return `unknown_material:${m.type}`; // unreachable; fail closed regardless
168
+ }
169
+
170
+ /**
171
+ * Re-perform the evidence: rebuild the chain, re-verify carried cryptographic
172
+ * material, recompute the counts. Async because the chain is rebuilt through
173
+ * the evidence log's own (async) record().
174
+ *
175
+ * The entries MUST be the complete log from genesis (`evidence.all()` or a
176
+ * full export). A partial slice fails the chain (fail closed) because its
177
+ * first link cannot chain from 'genesis'.
178
+ *
179
+ * Chain method — drives evidence.js's REAL verify(), twice over:
180
+ * 1. every supplied entry body (its own seq/prev_hash included, its hash
181
+ * stripped) is re-recorded into a fresh createEvidenceLog(), which
182
+ * recomputes the canonical-JSON sha256 for that exact body; a recomputed
183
+ * hash that differs from the SUPPLIED hash is a tampered/forged entry —
184
+ * broken from that point;
185
+ * 2. the rebuilt log's own verify() then walks the whole chain, catching
186
+ * link-level attacks the per-entry recompute cannot (a removed entry, or
187
+ * an entry rewritten WITH a consistently recomputed hash — its successor's
188
+ * prev_hash no longer matches).
189
+ *
190
+ * @param {Array<object>} entries the full evidence log (evidence.all())
191
+ * @param {object} [o]
192
+ * @param {string[]} [o.issuerKeys=[]] pinned base64url SPKI issuer keys, sourced
193
+ * by the AUDITOR out of band — never from the entries themselves
194
+ * @param {number|function} [o.now=Date.now] clock for generated_at (pin for determinism)
195
+ * @param {object} [o.approverKeys={}] auditor-pinned identity-bound approver keys
196
+ * @param {string|null} [o.rpId] bind carried WebAuthn assertions to this relying-party id
197
+ * @param {string[]} [o.allowedOrigins=[]] exact accepted WebAuthn origins
198
+ * @param {object} [o.quorumPolicy] auditor-pinned global organizational quorum rule
199
+ * @param {object} [o.quorumPolicies] action_type -> auditor-pinned quorum rule
200
+ * @returns {Promise<object>} EP-GATE-REPERFORMANCE-v1 document:
201
+ * { chain: {ok, entries, head}, receipts: {reverified, failed, not_reverifiable},
202
+ * counts: {allows, denies, replays_blocked, by_action_type}, ... }
203
+ */
204
+ export async function reperformEvidence(entries: any[] = [], {
205
+ issuerKeys = [],
206
+ approverKeys = {},
207
+ now = Date.now,
208
+ rpId = null,
209
+ allowedOrigins = [],
210
+ quorumPolicy = null,
211
+ quorumPolicies = {},
212
+ }: {
213
+ issuerKeys?: string[];
214
+ approverKeys?: Record<string, any>;
215
+ now?: number | (() => number);
216
+ rpId?: string | null;
217
+ allowedOrigins?: string[];
218
+ quorumPolicy?: any;
219
+ quorumPolicies?: Record<string, any>;
220
+ } = {}) {
221
+ if (!Array.isArray(entries)) {
222
+ throw new Error('reperform: entries must be an array (evidence.all() or a full export)');
223
+ }
224
+ if (!Array.isArray(issuerKeys)) {
225
+ throw new Error('reperform: issuerKeys must be an array of base64url SPKI issuer keys');
226
+ }
227
+
228
+ /* ------------------------------- chain -------------------------------- */
229
+ const rebuilt = createEvidenceLog();
230
+ let broken: { at: number | null; seq: number | null; reason: string | undefined } | null = null;
231
+ for (let i = 0; i < entries.length; i++) {
232
+ const e = entries[i];
233
+ if (!e || typeof e !== 'object' || Array.isArray(e)) { broken = { at: i, seq: null, reason: 'not_an_object' }; break; }
234
+ const seq = Number.isInteger(e.seq) ? e.seq : null;
235
+ if (seq === null) { broken = { at: i, seq: null, reason: 'missing_seq' }; break; }
236
+ if (typeof e.prev_hash !== 'string' || e.prev_hash.length === 0) { broken = { at: i, seq, reason: 'missing_prev_hash' }; break; }
237
+ if (typeof e.hash !== 'string' || e.hash.length === 0) { broken = { at: i, seq, reason: 'missing_hash' }; break; }
238
+ // Re-record the EXACT supplied body (own seq/prev_hash override the fresh
239
+ // log's defaults via the record() spread) so the recomputed hash is over
240
+ // byte-identical canonical material.
241
+ const { hash: suppliedHash, ...body } = e;
242
+ const rec = await rebuilt.record(body);
243
+ if (rec.hash !== suppliedHash) { broken = { at: i, seq, reason: 'hash_mismatch' }; break; }
244
+ }
245
+ // Drive the REAL evidence.js verifier over the rebuilt chain — the link walk
246
+ // (prev_hash continuity from 'genesis') is its judgment, not a reimplementation.
247
+ const rv = rebuilt.verify();
248
+ if (!broken && !rv.ok) broken = { at: rv.at ?? null, seq: rv.at ?? null, reason: rv.reason };
249
+ const chain = {
250
+ ok: !broken,
251
+ entries: entries.length,
252
+ // A broken chain vouches for NO head — even a valid prefix's.
253
+ head: !broken ? (rv.head ?? null) : null,
254
+ ...(broken ? { broken_at: broken.at, broken_seq: broken.seq, reason: broken.reason } : {}),
255
+ };
256
+
257
+ /* ------------------- receipts + counts (from scratch) ------------------ */
258
+ const receipts: {
259
+ reverified: number;
260
+ failed: { hash: string | null; reason: string }[];
261
+ not_reverifiable: number;
262
+ no_receipt_presented: number;
263
+ } = { reverified: 0, failed: [], not_reverifiable: 0, no_receipt_presented: 0 };
264
+ const warnings: { index: number; reason: string }[] = [];
265
+ const byAction = Object.create(null);
266
+ let allows = 0;
267
+ let denies = 0;
268
+ let replaysBlocked = 0;
269
+
270
+ entries.forEach((e, index) => {
271
+ if (!e || typeof e !== 'object' || Array.isArray(e)) { warnings.push({ index, reason: 'not_an_object' }); return; }
272
+ if (typeof e.kind !== 'string' || e.kind.length === 0) { warnings.push({ index, reason: 'missing_kind' }); return; }
273
+
274
+ // Counts: guarded decisions only ('not_guarded' pass-throughs ran without
275
+ // the control; execution records are provenance, not enforcement). Fail
276
+ // closed: only a literal `true` counts as an allow.
277
+ if (e.kind === 'decision' && e.reason !== 'not_guarded') {
278
+ if (e.allow === true) allows += 1; else denies += 1;
279
+ if (e.reason === 'replay_refused') replaysBlocked += 1;
280
+ const action = typeof e.action === 'string' && e.action ? e.action : 'unknown';
281
+ byAction[action] = (byAction[action] || 0) + 1;
282
+ }
283
+
284
+ // Re-verification: any entry that carries material gets it re-verified;
285
+ // ALL carried payloads must verify for the entry to count as reverified —
286
+ // each failure is a named {hash, reason}, never absorbed.
287
+ const hash = typeof e.hash === 'string' && e.hash ? e.hash : null;
288
+ const material = collectMaterial(e);
289
+ if (material.length > 0) {
290
+ let allOk = true;
291
+ for (const m of material) {
292
+ const reason = reverifyMaterial(m, {
293
+ issuerKeys,
294
+ approverKeys,
295
+ action: typeof e.action === 'string' && e.action ? e.action : null,
296
+ rpId,
297
+ allowedOrigins,
298
+ quorumPolicy,
299
+ quorumPolicies,
300
+ });
301
+ if (reason) { allOk = false; receipts.failed.push({ hash, reason }); }
302
+ }
303
+ if (allOk) receipts.reverified += 1;
304
+ return;
305
+ }
306
+ // No material carried. A decision that REFERENCES a receipt (receipt_id
307
+ // recorded) but does not carry it cannot be re-performed — surfaced as
308
+ // not_reverifiable, never silently passed. A decision where no receipt was
309
+ // presented at all (pass-through, receipt_required refusal) has nothing to
310
+ // re-verify and is reported separately for transparency.
311
+ if (e.kind === 'decision') {
312
+ if (e.receipt_id !== null && e.receipt_id !== undefined) receipts.not_reverifiable += 1;
313
+ else receipts.no_receipt_presented += 1;
314
+ }
315
+ });
316
+
317
+ const nowMs = typeof now === 'function' ? now() : now;
318
+
319
+ return {
320
+ '@version': REPERFORMANCE_VERSION,
321
+ product: 'EMILIA Gate',
322
+ generated_at: new Date(nowMs).toISOString(),
323
+ honesty: {
324
+ reperforms:
325
+ 'Independent recomputation from the supplied evidence entries: the hash chain is rebuilt '
326
+ + 'and checked with the evidence log\'s own verifier, carried receipt/signoff/quorum material '
327
+ + 'is cryptographically re-verified against auditor-pinned issuer and identity-bound approver keys, '
328
+ + 'WebAuthn scope, and organizational quorum policy; decision counts '
329
+ + 'are recomputed from scratch without reference to any report builder.',
330
+ does_not_establish: [
331
+ 'Completeness of the log: a chain rewritten in full, or entries withheld before they were supplied, is not detectable from the entries alone. Anchor the head hash externally to close this.',
332
+ 'Runtime freshness or one-time consumption: those were properties of the moment of decision and cannot be re-performed after the fact; cryptographic material is re-verified, runtime state is not.',
333
+ 'Issuer and approver key custody, enrollment, or identity proofing, which remain external trust roots the auditor supplies out of band.',
334
+ 'The business correctness or wisdom of any authorized action.',
335
+ ],
336
+ status: 'Support for an audit re-performance procedure. This document does not conclude, opine, or certify; any conclusion is the auditor\'s.',
337
+ },
338
+ input: {
339
+ entries_supplied: entries.length,
340
+ issuer_keys_pinned: issuerKeys.length,
341
+ approver_keys_pinned: approverKeys && typeof approverKeys === 'object'
342
+ ? Object.keys(approverKeys).length : 0,
343
+ relying_party_scope_pinned: typeof rpId === 'string' && rpId.length > 0
344
+ && Array.isArray(allowedOrigins) && allowedOrigins.length > 0,
345
+ quorum_policies_pinned: (quorumPolicy ? 1 : 0)
346
+ + (quorumPolicies && typeof quorumPolicies === 'object' ? Object.keys(quorumPolicies).length : 0),
347
+ expects: 'the complete evidence log from genesis (evidence.all() or a full export)',
348
+ },
349
+ chain,
350
+ receipts,
351
+ counts: {
352
+ allows,
353
+ denies,
354
+ replays_blocked: replaysBlocked,
355
+ by_action_type: sortedCounts(byAction),
356
+ },
357
+ integrity_warnings: warnings,
358
+ };
359
+ }
360
+
361
+ /** Extract the recomputed counts, fail closed on anything unrecognized. */
362
+ function countsFrom(recomputed) {
363
+ if (!recomputed || typeof recomputed !== 'object') {
364
+ throw new Error('compareToReported: recomputed must be the reperformEvidence result (or its counts)');
365
+ }
366
+ const c = recomputed['@version'] === REPERFORMANCE_VERSION ? recomputed.counts : recomputed;
367
+ if (!c || typeof c !== 'object') {
368
+ throw new Error('compareToReported: recomputed carries no counts');
369
+ }
370
+ for (const k of ['allows', 'denies', 'replays_blocked']) {
371
+ if (!Number.isFinite(c[k]) || c[k] < 0) {
372
+ throw new Error(`compareToReported: recomputed.${k} must be a finite number >= 0 — refusing to compare`);
373
+ }
374
+ }
375
+ const by = c.by_action_type;
376
+ return { ...c, by_action_type: by && typeof by === 'object' && !Array.isArray(by) ? by : {} };
377
+ }
378
+
379
+ /**
380
+ * Diff recomputed counts against a reported pack — the auditor's tie-out.
381
+ *
382
+ * Accepts an EP-GATE-USAGE-v1 pack (meterUsage output or the signed-ready
383
+ * buildUsageStatement body) or an EP-GATE-UNDERWRITER-ATTESTATION-v1 pack.
384
+ * Only the OVERLAPPING NUMERIC fields are compared; an unknown pack @version
385
+ * is refused (fail closed), never fuzzily matched. A reported field that is
386
+ * missing or non-numeric is itself a named drift — a stripped pack can never
387
+ * silently match.
388
+ *
389
+ * The comparison is meaningful only when the recomputation ran over exactly
390
+ * the entries the reported pack was built from (same slice, same window);
391
+ * scoping the slice is the auditor's procedure, not this function's.
392
+ *
393
+ * @param {object} recomputed reperformEvidence() result (or its .counts)
394
+ * @param {object} reportedPack usage or underwriter pack
395
+ * @returns {{match: boolean, pack_version: string, drift: Array<{field, reported, recomputed}>}}
396
+ */
397
+ export function compareToReported(recomputed, reportedPack) {
398
+ const counts = countsFrom(recomputed);
399
+ if (!reportedPack || typeof reportedPack !== 'object') {
400
+ throw new Error('compareToReported: reportedPack must be an object');
401
+ }
402
+ const version = reportedPack['@version'];
403
+ const drift: { field: string; reported: any; recomputed: any }[] = [];
404
+ const check = (field, reported, rec) => {
405
+ if (!Number.isFinite(reported) || reported !== rec) {
406
+ drift.push({ field, reported: reported === undefined ? null : reported, recomputed: rec });
407
+ }
408
+ };
409
+
410
+ if (version === USAGE_PACK_VERSION) {
411
+ check('allows', reportedPack.allows, counts.allows);
412
+ check('denies', reportedPack.denies, counts.denies);
413
+ check('replays_blocked', reportedPack.replays_blocked, counts.replays_blocked);
414
+ check('protected_actions', reportedPack.protected_actions, counts.allows + counts.denies);
415
+ const reportedBy = reportedPack.by_action_type;
416
+ const byOk = reportedBy && typeof reportedBy === 'object' && !Array.isArray(reportedBy);
417
+ const keys = new Set([
418
+ ...Object.keys(counts.by_action_type),
419
+ ...(byOk ? Object.keys(reportedBy) : []),
420
+ ]);
421
+ for (const k of [...keys].sort()) {
422
+ // An absent key in a well-formed histogram means zero occurrences.
423
+ const rep = byOk && reportedBy[k] !== undefined ? reportedBy[k] : 0;
424
+ check(`by_action_type.${k}`, rep, counts.by_action_type[k] ?? 0);
425
+ }
426
+ } else if (version === UNDERWRITER_PACK_VERSION) {
427
+ check('volume.guarded_decisions', reportedPack.volume?.guarded_decisions, counts.allows + counts.denies);
428
+ check('volume.allowed', reportedPack.volume?.allowed, counts.allows);
429
+ check('volume.denied', reportedPack.volume?.denied, counts.denies);
430
+ check('denials.total', reportedPack.denials?.total, counts.denies);
431
+ check('replay.attempts_blocked', reportedPack.replay?.attempts_blocked, counts.replays_blocked);
432
+ } else {
433
+ throw new Error(
434
+ `compareToReported: unknown pack @version ${JSON.stringify(version ?? null)} — refusing to compare (fail closed)`,
435
+ );
436
+ }
437
+
438
+ return { match: drift.length === 0, pack_version: version, drift };
439
+ }
440
+
441
+ export default { REPERFORMANCE_VERSION, reperformEvidence, compareToReported };