@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,647 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * EMILIA Gate — break-glass as EVIDENCE, never a bypass (EP-GATE-BREAKGLASS-v1).
4
+ *
5
+ * An emergency override is a FIRST-CLASS SIGNED ARTIFACT, not a config flag.
6
+ * There is no "disable the gate" switch anywhere in this module: to act outside
7
+ * the normal receipt path, operators mint a break-glass authorization — an
8
+ * M-of-N Ed25519 multi-signature over canonical JSON (sorted keys, same idiom
9
+ * as receipts/entitlements/evidence) of ONE shared grant:
10
+ *
11
+ * { grant_id, scope: { action_types[] }, window: { not_before, expires_at },
12
+ * reason, incident_ref, threshold }
13
+ *
14
+ * Every signer signs the SAME payload, so the artifact proves that `threshold`
15
+ * DISTINCT pinned principals authorized exactly this scope, for exactly this
16
+ * window, for exactly this incident. The grant is:
17
+ * - SCOPED — valid only for the listed action_types; anything else refuses;
18
+ * - BOUNDED — valid only inside [not_before, expires_at];
19
+ * - ATTRIBUTED — reason + incident_ref are REQUIRED; an override with no
20
+ * stated cause is refused, not logged-and-allowed;
21
+ * - SINGLE-USE — consumed through the same consumption-store contract as
22
+ * receipts (store.js); consumption is committed BEFORE use, so
23
+ * a crash mid-override burns the grant instead of leaving it
24
+ * replayable (fail direction: unusable, never reusable);
25
+ * - LOGGED — `buildBreakGlassEvidence` shapes a kind:'breakglass' entry
26
+ * for the tamper-evident evidence log.
27
+ *
28
+ * THE MODULE'S EXECUTION CONTRACT: use `runBreakGlass`. It is the one high-level
29
+ * path that enforces pinned-policy verification, permanent fleet-safe
30
+ * consumption, strict evidence acknowledgement, and only then effect invocation.
31
+ * The lower-level verify/consume/evidence helpers are composable primitives; no
32
+ * one of them alone authorizes or executes an override.
33
+ *
34
+ * Verification FAILS CLOSED with machine-readable reasons: threshold unmet,
35
+ * non-distinct signer kids, expired, not-yet-valid, out-of-scope action_type,
36
+ * tampered payload, unknown kid, malformed anything → { valid:false, reason }.
37
+ * A grant carrying ANY signature that does not verify is refused outright —
38
+ * we never "count the good ones" past a bad one.
39
+ *
40
+ * Pure functions: inputs in, verdict out. Time is injected (`now`), never read
41
+ * from the wall clock implicitly, so verification is deterministic.
42
+ */
43
+ import crypto from 'node:crypto';
44
+ import { strictJsonGate } from './strict-json.js';
45
+ import { canonicalize as canonical } from './execution-binding.js';
46
+ import { verifyEvidenceRecord } from './evidence.js';
47
+ import { isSecureConsumptionStore } from './store.js';
48
+
49
+ export const BREAKGLASS_VERSION = 'EP-GATE-BREAKGLASS-v1';
50
+ export const BREAKGLASS_EVIDENCE_KIND = 'breakglass';
51
+
52
+ type Obj = Record<string, any>;
53
+
54
+ /** Relying-party break-glass trust policy: pinned roster + minimum threshold. */
55
+ type BreakGlassPolicy = {
56
+ minimum_threshold: number;
57
+ roster: Array<{ kid: string; principal_id: string; key?: string }>;
58
+ };
59
+
60
+ /** Optional pinned keys when roster entries omit their own `key`. */
61
+ type IssuerKeys = Record<string, string> | Array<{ kid: string; key: string }>;
62
+
63
+ /** Injected clock: fixed ms/ISO value, or a () => ms function (default Date.now). */
64
+ type BreakGlassClock = number | string | (() => number);
65
+
66
+ /** A roster entry normalized to its verified Ed25519 public key + fingerprint. */
67
+ type PinnedRosterEntry = {
68
+ kid: string;
69
+ principal_id: string;
70
+ publicKey: crypto.KeyObject;
71
+ fingerprint: string;
72
+ };
73
+
74
+ function sha256hex(s) {
75
+ return crypto.createHash('sha256').update(s).digest('hex');
76
+ }
77
+
78
+ function toMs(t) {
79
+ if (t == null) return null;
80
+ const ms = typeof t === 'number' ? t : Date.parse(t);
81
+ return Number.isFinite(ms) ? ms : null;
82
+ }
83
+
84
+ /** Every refusal shape is identical and machine-readable. Fail closed. */
85
+ function refuse(reason, extra = {}) {
86
+ return { valid: false, reason, ...extra };
87
+ }
88
+
89
+ function isNonEmptyString(s) {
90
+ return typeof s === 'string' && s.length > 0;
91
+ }
92
+
93
+ function isActionTypeList(a) {
94
+ return Array.isArray(a) && a.length > 0 && a.every(isNonEmptyString);
95
+ }
96
+
97
+ /**
98
+ * Mint a break-glass authorization: every signer signs the canonical JSON of
99
+ * the SAME grant payload. Throws on invalid fields — a malformed grant must
100
+ * never be issued, only refused. Signer kids must already be distinct at mint
101
+ * time: one key can never pre-fill two threshold slots. Relying-party principal
102
+ * uniqueness is enforced at verification through the pinned policy roster.
103
+ *
104
+ * grant_id is CONTENT-DERIVED (sha-256 of the canonical grant fields), so the
105
+ * id is deterministic and re-minting the identical grant yields the identical
106
+ * single-use consumption key — the same emergency authorization cannot be
107
+ * "refreshed" into extra uses by minting it twice.
108
+ *
109
+ * @param {Array<{ privateKey: crypto.KeyObject, kid: string }>} signers
110
+ * @param {object} fields { scope: { action_types: string[] }, window: { not_before, expires_at }, reason, incident_ref, threshold }
111
+ * @returns {{ '@version': string, payload: object, signatures: Array<{ kid: string, algorithm: 'Ed25519', value: string }> }}
112
+ */
113
+ export function mintBreakGlassAuthorization(signers, {
114
+ scope, window: win, reason, incident_ref, threshold,
115
+ }: {
116
+ scope?: { action_types: string[] };
117
+ window?: { not_before: number | string; expires_at: number | string };
118
+ reason?: string;
119
+ incident_ref?: string;
120
+ threshold?: number;
121
+ } = {}) {
122
+ if (!Array.isArray(signers) || signers.length === 0) {
123
+ throw new Error('breakglass: signers must be a non-empty array of { privateKey, kid }');
124
+ }
125
+ for (const s of signers) {
126
+ if (!s || !s.privateKey || !isNonEmptyString(s.kid)) {
127
+ throw new Error('breakglass: each signer needs a privateKey and a kid');
128
+ }
129
+ }
130
+ const kids = signers.map((s) => s.kid);
131
+ if (new Set(kids).size !== kids.length) {
132
+ throw new Error('breakglass: signer kids must be distinct — one principal cannot fill two threshold slots');
133
+ }
134
+ let signerFingerprints;
135
+ try {
136
+ signerFingerprints = signers.map((s) => {
137
+ // @types/node's createPublicKey overloads omit KeyObject input even
138
+ // though Node derives a public key from a private KeyObject at runtime.
139
+ const publicKey = crypto.createPublicKey(/** @type {any} */ (s.privateKey));
140
+ if (publicKey.asymmetricKeyType !== 'ed25519') throw new Error('not Ed25519');
141
+ const spki = publicKey.export({ type: 'spki', format: 'der' });
142
+ return sha256hex(spki);
143
+ });
144
+ } catch {
145
+ throw new Error('breakglass: every signer privateKey must be an Ed25519 key');
146
+ }
147
+ if (new Set(signerFingerprints).size !== signerFingerprints.length) {
148
+ throw new Error('breakglass: signer SPKI keys must be distinct — one key cannot fill two threshold slots');
149
+ }
150
+ if (!scope || !isActionTypeList(scope.action_types)) {
151
+ throw new Error('breakglass: scope.action_types must be a non-empty array of action-type strings');
152
+ }
153
+ if (typeof threshold !== 'number' || !Number.isInteger(threshold) || threshold < 2) {
154
+ throw new Error('breakglass: threshold must be an integer >= 2');
155
+ }
156
+ if (threshold > signers.length) {
157
+ throw new Error(`breakglass: threshold ${threshold} exceeds signer count ${signers.length} — the grant could never verify`);
158
+ }
159
+ const nbf = toMs(win?.not_before);
160
+ const exp = toMs(win?.expires_at);
161
+ if (nbf == null || exp == null) {
162
+ throw new Error('breakglass: window.not_before and window.expires_at are required (ISO or ms)');
163
+ }
164
+ if (exp <= nbf) throw new Error('breakglass: window.expires_at must be after window.not_before');
165
+ // An override with no stated cause must never exist — attribution is the deal.
166
+ if (!isNonEmptyString(reason)) throw new Error('breakglass: reason is required');
167
+ if (!isNonEmptyString(incident_ref)) throw new Error('breakglass: incident_ref is required');
168
+
169
+ const core = {
170
+ scope: { action_types: scope.action_types.slice() },
171
+ // win is guaranteed defined here: nbf/exp are non-null only when
172
+ // win?.not_before / win?.expires_at parsed successfully above.
173
+ window: { not_before: win!.not_before, expires_at: win!.expires_at },
174
+ reason,
175
+ incident_ref,
176
+ threshold,
177
+ };
178
+ const grant_id = `bg_${sha256hex(canonical(core))}`;
179
+ const payload = { grant_id, ...core };
180
+ const msg = Buffer.from(canonical(payload), 'utf8');
181
+ const signatures = signers.map(({ privateKey, kid }) => ({
182
+ kid,
183
+ algorithm: /** @type {'Ed25519'} */ ('Ed25519'),
184
+ value: crypto.sign(null, msg, privateKey).toString('base64url'),
185
+ }));
186
+ return { '@version': BREAKGLASS_VERSION, payload, signatures };
187
+ }
188
+
189
+ /** Resolve a base64url SPKI-DER key for `kid` from a map or an entry list. */
190
+ function issuerKeyFor(issuerKeys, kid) {
191
+ if (!issuerKeys) return null;
192
+ if (Array.isArray(issuerKeys)) {
193
+ const e = issuerKeys.find((x) => x && x.kid === kid && typeof x.key === 'string');
194
+ return e ? e.key : null;
195
+ }
196
+ const k = issuerKeys[kid];
197
+ return typeof k === 'string' ? k : null;
198
+ }
199
+
200
+ function isPlainObject(value) {
201
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
202
+ const prototype = Object.getPrototypeOf(value);
203
+ return prototype === Object.prototype || prototype === null;
204
+ }
205
+
206
+ function pinnedKey(key) {
207
+ if (typeof key !== 'string' || !/^[A-Za-z0-9_-]+$/.test(key)) {
208
+ throw new Error('invalid pinned key encoding');
209
+ }
210
+ const bytes = Buffer.from(key, 'base64url');
211
+ if (bytes.length === 0 || bytes.toString('base64url') !== key) {
212
+ throw new Error('non-canonical pinned key encoding');
213
+ }
214
+ const publicKey = crypto.createPublicKey({ key: bytes, format: 'der', type: 'spki' });
215
+ if (publicKey.asymmetricKeyType !== 'ed25519') throw new Error('pinned key is not Ed25519');
216
+ const canonicalSpki = publicKey.export({ type: 'spki', format: 'der' });
217
+ return {
218
+ publicKey,
219
+ fingerprint: `sha256:${sha256hex(canonicalSpki)}`,
220
+ };
221
+ }
222
+
223
+ /**
224
+ * Normalize the relying-party trust policy; presenter material is never used.
225
+ * @returns {{ok:false, reason:string}|{ok:true, minimum_threshold:number, roster:Map<string,{kid:string, principal_id:string, publicKey:import('crypto').KeyObject, fingerprint:string}>}}
226
+ */
227
+ function normalizePinnedPolicy(policy, issuerKeys): { ok: false; reason: string } | {
228
+ ok: true; minimum_threshold: number; roster: Map<string, PinnedRosterEntry>;
229
+ } {
230
+ if (!isPlainObject(policy)) return { ok: false, reason: 'missing_policy' };
231
+ if (!Number.isSafeInteger(policy.minimum_threshold) || policy.minimum_threshold < 2
232
+ || !Array.isArray(policy.roster)) {
233
+ return { ok: false, reason: 'invalid_policy' };
234
+ }
235
+
236
+ try {
237
+ const roster = new Map();
238
+ for (const entry of policy.roster) {
239
+ if (!isPlainObject(entry) || !isNonEmptyString(entry.kid)
240
+ || !isNonEmptyString(entry.principal_id) || roster.has(entry.kid)) {
241
+ return { ok: false, reason: 'invalid_policy' };
242
+ }
243
+ const key = isNonEmptyString(entry.key) ? entry.key : issuerKeyFor(issuerKeys, entry.kid);
244
+ const keyInfo = pinnedKey(key);
245
+ roster.set(entry.kid, {
246
+ kid: entry.kid,
247
+ principal_id: entry.principal_id,
248
+ ...keyInfo,
249
+ });
250
+ }
251
+ const members = [...roster.values()];
252
+ if (new Set(members.map((entry) => entry.principal_id)).size < policy.minimum_threshold
253
+ || new Set(members.map((entry) => entry.fingerprint)).size < policy.minimum_threshold) {
254
+ return { ok: false, reason: 'invalid_policy' };
255
+ }
256
+ return {
257
+ ok: true,
258
+ minimum_threshold: policy.minimum_threshold,
259
+ roster,
260
+ };
261
+ } catch {
262
+ return { ok: false, reason: 'invalid_policy' };
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Verify a break-glass grant against a relying-party-pinned policy. NEVER throws for a
268
+ * bad artifact — every failure resolves to { valid:false, reason } so the
269
+ * refusal itself is loggable. FAILS CLOSED on every path:
270
+ * no_grant | grant_unparseable | grant_malformed | unsupported_version |
271
+ * unsupported_algorithm | invalid_threshold | invalid_scope |
272
+ * missing_reason | missing_incident_ref | missing_policy | invalid_policy |
273
+ * duplicate_signer | duplicate_signer_principal | duplicate_signer_key |
274
+ * policy_threshold_unmet | threshold_unmet | signer_not_in_roster | bad_signature |
275
+ * invalid_validity_window | not_yet_valid | expired |
276
+ * action_type_required | out_of_scope
277
+ *
278
+ * A grant can never nominate its own threshold, roster, identities, or keys.
279
+ * EVERY listed signature must verify against its pinned roster entry — one
280
+ * tampered or unrostered signature refuses the whole grant. The validity window is checked
281
+ * only AFTER the signatures, so the timestamps themselves are authenticated.
282
+ *
283
+ * @param {object|string|null} grantJson the artifact (object or JSON string)
284
+ * @param {object} [o]
285
+ * @param {{minimum_threshold:number,roster:Array<{kid:string,principal_id:string,key?:string}>}} [o.policy] relying-party policy (a missing policy refuses, never throws)
286
+ * @param {object|Array<{kid:string,key:string}>} [o.issuerKeys] optional pinned keys when roster entries omit key
287
+ * @param {number|string|function} [o.now=Date.now] injected clock (ms, ISO, or () => ms)
288
+ * @param {string} [o.actionType] the action the caller wants to override (a missing actionType refuses, never throws)
289
+ * @returns {{ valid: boolean, reason: string, grant_id?: string, incident_ref?: string, scope?: object, window?: object, threshold?: number, required_threshold?: number, policy_minimum_threshold?: number, signer_kids?: string[], signer_principal_ids?: string[], signer_spki_fingerprints?: string[] }}
290
+ */
291
+ function verifyBreakGlassInternal(grantJson, {
292
+ policy, issuerKeys, now = Date.now, actionType,
293
+ }: {
294
+ policy?: BreakGlassPolicy;
295
+ issuerKeys?: IssuerKeys;
296
+ now?: BreakGlassClock;
297
+ actionType?: string;
298
+ } = {}) {
299
+ if (grantJson == null || grantJson === '') return refuse('no_grant');
300
+
301
+ let doc = grantJson;
302
+ if (typeof doc === 'string') {
303
+ try {
304
+ if (Buffer.byteLength(doc, 'utf8') > 1024 * 1024 || !strictJsonGate(doc).ok) return refuse('grant_unparseable');
305
+ doc = JSON.parse(doc);
306
+ } catch { return refuse('grant_unparseable'); }
307
+ }
308
+ if (!doc || typeof doc !== 'object' || Array.isArray(doc)) return refuse('grant_malformed');
309
+ if (doc['@version'] !== BREAKGLASS_VERSION) return refuse('unsupported_version');
310
+
311
+ const p = doc.payload;
312
+ const sigs = doc.signatures;
313
+ if (!p || typeof p !== 'object' || !Array.isArray(sigs) || sigs.length === 0) {
314
+ return refuse('grant_malformed');
315
+ }
316
+ if (!isNonEmptyString(p.grant_id)) return refuse('grant_malformed');
317
+ if (!Number.isInteger(p.threshold) || p.threshold < 1) return refuse('invalid_threshold');
318
+ if (!p.scope || !isActionTypeList(p.scope.action_types)) return refuse('invalid_scope');
319
+ // Attribution is non-optional: an anonymous or causeless override is refused.
320
+ if (!isNonEmptyString(p.reason)) return refuse('missing_reason');
321
+ if (!isNonEmptyString(p.incident_ref)) return refuse('missing_incident_ref');
322
+
323
+ const pinned = normalizePinnedPolicy(policy, issuerKeys);
324
+ if (!pinned.ok) return refuse(pinned.reason);
325
+
326
+ for (const s of sigs) {
327
+ if (!s || typeof s !== 'object' || !isNonEmptyString(s.kid) || typeof s.value !== 'string') {
328
+ return refuse('grant_malformed');
329
+ }
330
+ if (s.algorithm !== 'Ed25519') return refuse('unsupported_algorithm', { kid: s.kid });
331
+ }
332
+
333
+ // Distinct roster slots, pinned human principals, and canonical SPKI keys are
334
+ // independent predicates. Aliases cannot satisfy separation of duties.
335
+ const kids = sigs.map((s) => s.kid);
336
+ if (new Set(kids).size !== kids.length) {
337
+ const dup = kids.find((k, i) => kids.indexOf(k) !== i);
338
+ return refuse('duplicate_signer', { kid: dup });
339
+ }
340
+ const signerEntries: PinnedRosterEntry[] = [];
341
+ for (const kid of kids) {
342
+ const entry = pinned.roster.get(kid);
343
+ if (!entry) return refuse('signer_not_in_roster', { kid });
344
+ signerEntries.push(entry);
345
+ }
346
+ const principalIds = signerEntries.map((entry) => entry.principal_id);
347
+ if (new Set(principalIds).size !== principalIds.length) {
348
+ const principalId = principalIds.find((id, i) => principalIds.indexOf(id) !== i);
349
+ return refuse('duplicate_signer_principal', { principal_id: principalId });
350
+ }
351
+ const fingerprints = signerEntries.map((entry) => entry.fingerprint);
352
+ if (new Set(fingerprints).size !== fingerprints.length) {
353
+ const fingerprint = fingerprints.find((value, i) => fingerprints.indexOf(value) !== i);
354
+ return refuse('duplicate_signer_key', { spki_fingerprint: fingerprint });
355
+ }
356
+ if (sigs.length < p.threshold) {
357
+ return refuse('threshold_unmet', { threshold: p.threshold, signatures: sigs.length });
358
+ }
359
+ if (sigs.length < pinned.minimum_threshold) {
360
+ return refuse('policy_threshold_unmet', {
361
+ required_threshold: pinned.minimum_threshold,
362
+ presented_threshold: p.threshold,
363
+ signatures: sigs.length,
364
+ });
365
+ }
366
+
367
+ // Every listed signature must verify against its pinned roster key. Never
368
+ // count past one failure.
369
+ let msg;
370
+ try { msg = Buffer.from(canonical(p), 'utf8'); } catch { return refuse('grant_malformed'); }
371
+ for (let i = 0; i < sigs.length; i++) {
372
+ const s = sigs[i];
373
+ let ok;
374
+ try {
375
+ ok = crypto.verify(null, msg, signerEntries[i].publicKey, Buffer.from(s.value, 'base64url'));
376
+ } catch { return refuse('bad_signature', { kid: s.kid }); }
377
+ if (ok !== true) return refuse('bad_signature', { kid: s.kid });
378
+ }
379
+
380
+ // Validity window — checked only AFTER the signatures, so the timestamps
381
+ // themselves are authenticated. Both bounds are required; unparseable fails closed.
382
+ let nowMs;
383
+ try { nowMs = typeof now === 'function' ? toMs(now()) : toMs(now); } catch { nowMs = null; }
384
+ const nbf = toMs(p.window?.not_before);
385
+ const exp = toMs(p.window?.expires_at);
386
+ if (nbf == null || exp == null || nowMs == null) return refuse('invalid_validity_window');
387
+ if (nowMs < nbf) return refuse('not_yet_valid', { not_before: p.window.not_before });
388
+ if (nowMs > exp) return refuse('expired', { expires_at: p.window.expires_at });
389
+
390
+ // Scope: without knowing the action there is nothing to authorize — refuse.
391
+ if (!isNonEmptyString(actionType)) return refuse('action_type_required');
392
+ if (!p.scope.action_types.includes(actionType)) {
393
+ return refuse('out_of_scope', { action_type: actionType, scope: p.scope.action_types.slice() });
394
+ }
395
+
396
+ return {
397
+ valid: true,
398
+ reason: 'breakglass_verified',
399
+ grant_id: p.grant_id,
400
+ incident_ref: p.incident_ref,
401
+ scope: { action_types: p.scope.action_types.slice() },
402
+ window: { not_before: p.window.not_before, expires_at: p.window.expires_at },
403
+ threshold: p.threshold,
404
+ required_threshold: Math.max(p.threshold, pinned.minimum_threshold),
405
+ policy_minimum_threshold: pinned.minimum_threshold,
406
+ signer_kids: kids.slice(),
407
+ signer_principal_ids: principalIds.slice(),
408
+ signer_spki_fingerprints: fingerprints.slice(),
409
+ };
410
+ }
411
+
412
+ export function verifyBreakGlass(grantJson, options = {}) {
413
+ try { return verifyBreakGlassInternal(grantJson, options); } catch { return refuse('grant_malformed'); }
414
+ }
415
+
416
+ /**
417
+ * SINGLE-USE consumption via the consumption-store contract (store.js):
418
+ * `consume(key)` returns true the FIRST time, false on every replay, and marks
419
+ * the key seen BEFORE the caller acts — consumption is committed before use.
420
+ * If the process crashes after consume() and before the override, the grant is
421
+ * burned, not replayable: the fail direction is unusable, never reusable.
422
+ *
423
+ * Accepts the grant document ({ payload: { grant_id } }) or a verified result
424
+ * ({ grant_id }). NEVER throws — a missing store, missing grant_id, or a store
425
+ * error all refuse with a machine-readable reason. This is a low-level primitive;
426
+ * only runBreakGlass also enforces store capabilities, evidence, and execution order.
427
+ *
428
+ * @param {object} grant break-glass grant document or verifyBreakGlass result
429
+ * @param {{ consume(key: string): Promise<boolean> }} store consumption store (store.js contract)
430
+ * @returns {Promise<{ consumed: boolean, reason: string, key?: string }>}
431
+ */
432
+ export async function consumeBreakGlass(grant, store) {
433
+ const grantId = grant?.payload?.grant_id ?? grant?.grant_id;
434
+ if (!isNonEmptyString(grantId)) return { consumed: false, reason: 'missing_grant_id' };
435
+ if (!store || typeof store.consume !== 'function') {
436
+ return { consumed: false, reason: 'no_consumption_store' };
437
+ }
438
+ const key = `breakglass:${grantId}`;
439
+ let first = false;
440
+ try {
441
+ first = (await store.consume(key)) === true;
442
+ } catch {
443
+ // The store could not commit the consumption — fail CLOSED: we cannot
444
+ // prove single-use, so the override must not run.
445
+ return { consumed: false, reason: 'store_error', key };
446
+ }
447
+ if (!first) return { consumed: false, reason: 'already_consumed', key };
448
+ return { consumed: true, reason: 'consumed', key };
449
+ }
450
+
451
+ /**
452
+ * Shape an evidence-log entry (kind 'breakglass') committing to the EXACT
453
+ * grant artifact (grant_hash = sha-256 of canonical grant) and the decision
454
+ * taken under it. Append it via the hash-chained evidence log — with
455
+ * { strict: true } so a sink failure refuses the override.
456
+ *
457
+ * This builder does not execute or authorize anything. runBreakGlass is the
458
+ * enforcing path that requires a strict, independently validated record
459
+ * acknowledgement before invoking an effect. This function never throws, even
460
+ * for a malformed grant or missing decision; an uncanonicalizable artifact has
461
+ * a null grant_hash and cannot pass runBreakGlass verification.
462
+ *
463
+ * @param {object} grant the break-glass grant document as presented (even if invalid)
464
+ * @param {object} decision e.g. { allow, reason, action_type } from verify/consume
465
+ * @param {object} [o]
466
+ * @param {number|string|function} [o.now=Date.now] injected clock for the entry timestamp
467
+ * @returns {object} entry ready for createEvidenceLog().record()
468
+ */
469
+ function buildBreakGlassEvidenceInternal(grant, decision, { now = Date.now } = {}) {
470
+ let nowMs;
471
+ try { nowMs = typeof now === 'function' ? toMs(now()) : toMs(now); } catch { nowMs = null; }
472
+ const p = (grant && typeof grant === 'object' && grant.payload && typeof grant.payload === 'object')
473
+ ? grant.payload : null;
474
+ const sigs = (grant && Array.isArray(grant.signatures)) ? grant.signatures : [];
475
+ const d = (decision && typeof decision === 'object') ? decision : {};
476
+ let grantHash: string | null = null;
477
+ try { grantHash = sha256hex(canonical(grant ?? null)); } catch { /* uncanonicalizable artifacts stay unbound */ }
478
+ const principalIds = Array.isArray(d.signer_principal_ids)
479
+ && d.signer_principal_ids.every(isNonEmptyString) ? d.signer_principal_ids.slice() : null;
480
+ const fingerprints = Array.isArray(d.signer_spki_fingerprints)
481
+ && d.signer_spki_fingerprints.every(isNonEmptyString) ? d.signer_spki_fingerprints.slice() : null;
482
+ return {
483
+ kind: BREAKGLASS_EVIDENCE_KIND,
484
+ '@version': BREAKGLASS_VERSION,
485
+ at: new Date(nowMs ?? 0).toISOString(),
486
+ grant_id: p?.grant_id ?? null,
487
+ incident_ref: p?.incident_ref ?? null,
488
+ grant_reason: p?.reason ?? null,
489
+ scope: p?.scope?.action_types?.slice?.() ?? null,
490
+ threshold: p?.threshold ?? null,
491
+ policy_minimum_threshold: Number.isSafeInteger(d.policy_minimum_threshold)
492
+ ? d.policy_minimum_threshold : null,
493
+ required_threshold: Number.isSafeInteger(d.required_threshold) ? d.required_threshold : null,
494
+ signer_kids: sigs.map((s) => s?.kid ?? null),
495
+ signer_principal_ids: principalIds,
496
+ signer_spki_fingerprints: fingerprints,
497
+ // Commits the log entry to the exact artifact presented — a later dispute
498
+ // can prove which grant (tampered or not) the decision was taken under.
499
+ grant_hash: grantHash,
500
+ decision: {
501
+ allow: d.allow === true, // fail closed: anything else records a refusal
502
+ reason: isNonEmptyString(d.reason) ? d.reason : 'unspecified',
503
+ action_type: d.action_type ?? null,
504
+ },
505
+ };
506
+ }
507
+
508
+ export function buildBreakGlassEvidence(grant, decision, options = {}) {
509
+ try { return buildBreakGlassEvidenceInternal(grant, decision, options); } catch {
510
+ return {
511
+ kind: BREAKGLASS_EVIDENCE_KIND,
512
+ '@version': BREAKGLASS_VERSION,
513
+ at: new Date(0).toISOString(),
514
+ grant_id: null,
515
+ incident_ref: null,
516
+ grant_reason: null,
517
+ scope: null,
518
+ threshold: null,
519
+ policy_minimum_threshold: null,
520
+ required_threshold: null,
521
+ signer_kids: [],
522
+ signer_principal_ids: null,
523
+ signer_spki_fingerprints: null,
524
+ grant_hash: null,
525
+ decision: { allow: false, reason: 'unspecified', action_type: null },
526
+ };
527
+ }
528
+ }
529
+
530
+ /**
531
+ * The sole high-level break-glass execution path. It snapshots the presented
532
+ * artifact, verifies it against relying-party policy, atomically consumes the
533
+ * grant in a capability-marked permanent store, validates a strict evidence
534
+ * acknowledgement, and only then invokes `effect`.
535
+ *
536
+ * @param {object} [args]
537
+ * @param {object|string} [args.grant] the presented break-glass artifact
538
+ * @param {{minimum_threshold:number,roster:Array<{kid:string,principal_id:string,key?:string}>}} [args.policy]
539
+ * @param {object|Array<{kid:string,key:string}>} [args.issuerKeys]
540
+ * @param {string} [args.actionType]
541
+ * @param {{ consume(key: string): Promise<boolean> }} [args.store]
542
+ * @param {{ strict?: boolean, atomicAppend?: boolean, record?: Function }} [args.evidence]
543
+ * @param {number|string|function} [args.now=Date.now]
544
+ * @param {Function} [effect] required at runtime; a missing effect throws
545
+ */
546
+ export async function runBreakGlass({
547
+ grant,
548
+ policy,
549
+ issuerKeys,
550
+ actionType,
551
+ store,
552
+ evidence,
553
+ now = Date.now,
554
+ }: {
555
+ grant?: Obj | string;
556
+ policy?: BreakGlassPolicy;
557
+ issuerKeys?: IssuerKeys;
558
+ actionType?: string;
559
+ store?: { consume(key: string): Promise<boolean> };
560
+ evidence?: { strict?: boolean; atomicAppend?: boolean; record?: (entry: Obj) => any };
561
+ now?: BreakGlassClock;
562
+ } = {}, effect) {
563
+ if (typeof effect !== 'function') throw new Error('runBreakGlass: effect function is required');
564
+
565
+ let snapshot;
566
+ try {
567
+ const parsed = typeof grant === 'string' ? JSON.parse(grant) : grant;
568
+ snapshot = JSON.parse(canonical(parsed));
569
+ } catch {
570
+ const verification = verifyBreakGlass(grant, { policy, issuerKeys, actionType, now });
571
+ return { ok: false, reason: verification.reason, verification, consumption: null, evidence: null };
572
+ }
573
+
574
+ const verification = verifyBreakGlass(snapshot, { policy, issuerKeys, actionType, now });
575
+ if (!verification.valid) {
576
+ return { ok: false, reason: verification.reason, verification, consumption: null, evidence: null };
577
+ }
578
+ if (!isSecureConsumptionStore(store)) {
579
+ return {
580
+ ok: false,
581
+ reason: 'secure_consumption_store_required',
582
+ verification,
583
+ consumption: null,
584
+ evidence: null,
585
+ };
586
+ }
587
+ if (!evidence || evidence.strict !== true || typeof evidence.record !== 'function') {
588
+ return {
589
+ ok: false,
590
+ reason: 'strict_evidence_required',
591
+ verification,
592
+ consumption: null,
593
+ evidence: null,
594
+ };
595
+ }
596
+
597
+ // store is guaranteed defined here: isSecureConsumptionStore(store) returned
598
+ // true above, which only holds for a store with a callable consume().
599
+ const consume = (store as { consume(key: string): Promise<boolean> }).consume.bind(store!);
600
+ const record = evidence.record.bind(evidence);
601
+ const consumption = await consumeBreakGlass(verification, { consume });
602
+ if (!consumption.consumed) {
603
+ return { ok: false, reason: consumption.reason, verification, consumption, evidence: null };
604
+ }
605
+
606
+ const entry = buildBreakGlassEvidence(snapshot, {
607
+ ...verification,
608
+ allow: true,
609
+ action_type: actionType,
610
+ }, { now });
611
+ let evidenceRecord;
612
+ try {
613
+ evidenceRecord = await record(entry);
614
+ if (!verifyEvidenceRecord(evidenceRecord, /** @type {{atomicRequired?: boolean, expectedEntry?: object}} */ ({
615
+ atomicRequired: evidence.atomicAppend === true,
616
+ expectedEntry: entry,
617
+ }))) throw new Error('malformed evidence acknowledgement');
618
+ } catch {
619
+ return {
620
+ ok: false,
621
+ reason: 'evidence_record_failed',
622
+ verification,
623
+ consumption,
624
+ evidence: null,
625
+ };
626
+ }
627
+
628
+ const result = await effect({ verification, consumption, evidence: evidenceRecord });
629
+ return {
630
+ ok: true,
631
+ reason: 'breakglass_executed',
632
+ result,
633
+ verification,
634
+ consumption,
635
+ evidence: evidenceRecord,
636
+ };
637
+ }
638
+
639
+ export default {
640
+ mintBreakGlassAuthorization,
641
+ verifyBreakGlass,
642
+ consumeBreakGlass,
643
+ buildBreakGlassEvidence,
644
+ runBreakGlass,
645
+ BREAKGLASS_VERSION,
646
+ BREAKGLASS_EVIDENCE_KIND,
647
+ };