@emilia-protocol/gate 0.11.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (425) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +310 -0
  3. package/action-control-manifest.js +3 -268
  4. package/action-escrow-custodian.js +3 -394
  5. package/action-escrow-evidence.js +3 -858
  6. package/action-escrow-package.js +3 -1668
  7. package/action-escrow-postgres.js +3 -337
  8. package/action-escrow-state.js +3 -396
  9. package/action-escrow-verifiers.js +3 -331
  10. package/action-escrow.js +3 -2447
  11. package/action-packs.js +3 -140
  12. package/adapters/_kit.js +3 -104
  13. package/adapters/aws.js +3 -108
  14. package/adapters/cloudflare.js +3 -54
  15. package/adapters/gcp.js +3 -70
  16. package/adapters/github.js +3 -109
  17. package/adapters/jira.js +3 -55
  18. package/adapters/k8s.js +3 -70
  19. package/adapters/linear.js +3 -55
  20. package/adapters/salesforce.js +3 -55
  21. package/adapters/stripe.js +3 -81
  22. package/adapters/supabase.js +3 -121
  23. package/adapters/terraform.js +3 -59
  24. package/adapters/vercel.js +3 -84
  25. package/aec-execution.js +3 -312
  26. package/breakglass.js +3 -582
  27. package/capability-receipt.js +4 -0
  28. package/cf1-conformance.js +3 -106
  29. package/challenge-store.js +3 -69
  30. package/control-plane.js +3 -340
  31. package/coverage.js +3 -721
  32. package/demo.mjs +1 -1
  33. package/deployment-attestation.js +3 -247
  34. package/dist/action-control-manifest.d.ts +82 -0
  35. package/dist/action-control-manifest.d.ts.map +1 -0
  36. package/dist/action-control-manifest.js +422 -0
  37. package/dist/action-control-manifest.js.map +1 -0
  38. package/dist/action-escrow-custodian.d.ts +74 -0
  39. package/dist/action-escrow-custodian.d.ts.map +1 -0
  40. package/dist/action-escrow-custodian.js +363 -0
  41. package/dist/action-escrow-custodian.js.map +1 -0
  42. package/dist/action-escrow-evidence.d.ts +130 -0
  43. package/dist/action-escrow-evidence.d.ts.map +1 -0
  44. package/dist/action-escrow-evidence.js +1012 -0
  45. package/dist/action-escrow-evidence.js.map +1 -0
  46. package/dist/action-escrow-package.d.ts +10 -0
  47. package/dist/action-escrow-package.d.ts.map +1 -0
  48. package/dist/action-escrow-package.js +1583 -0
  49. package/dist/action-escrow-package.js.map +1 -0
  50. package/dist/action-escrow-postgres.d.ts +62 -0
  51. package/dist/action-escrow-postgres.d.ts.map +1 -0
  52. package/dist/action-escrow-postgres.js +339 -0
  53. package/dist/action-escrow-postgres.js.map +1 -0
  54. package/dist/action-escrow-state.d.ts +141 -0
  55. package/dist/action-escrow-state.d.ts.map +1 -0
  56. package/dist/action-escrow-state.js +413 -0
  57. package/dist/action-escrow-state.js.map +1 -0
  58. package/dist/action-escrow-verifiers.d.ts +65 -0
  59. package/dist/action-escrow-verifiers.d.ts.map +1 -0
  60. package/dist/action-escrow-verifiers.js +421 -0
  61. package/dist/action-escrow-verifiers.js.map +1 -0
  62. package/dist/action-escrow.d.ts +112 -0
  63. package/dist/action-escrow.d.ts.map +1 -0
  64. package/dist/action-escrow.js +2661 -0
  65. package/dist/action-escrow.js.map +1 -0
  66. package/dist/action-packs.d.ts +54 -0
  67. package/dist/action-packs.d.ts.map +1 -0
  68. package/dist/action-packs.js +143 -0
  69. package/dist/action-packs.js.map +1 -0
  70. package/dist/adapters/_kit.d.ts +50 -0
  71. package/dist/adapters/_kit.d.ts.map +1 -0
  72. package/dist/adapters/_kit.js +98 -0
  73. package/dist/adapters/_kit.js.map +1 -0
  74. package/dist/adapters/aws.d.ts +158 -0
  75. package/dist/adapters/aws.d.ts.map +1 -0
  76. package/dist/adapters/aws.js +136 -0
  77. package/dist/adapters/aws.js.map +1 -0
  78. package/dist/adapters/cloudflare.d.ts +116 -0
  79. package/dist/adapters/cloudflare.d.ts.map +1 -0
  80. package/dist/adapters/cloudflare.js +75 -0
  81. package/dist/adapters/cloudflare.js.map +1 -0
  82. package/dist/adapters/gcp.d.ts +139 -0
  83. package/dist/adapters/gcp.d.ts.map +1 -0
  84. package/dist/adapters/gcp.js +69 -0
  85. package/dist/adapters/gcp.js.map +1 -0
  86. package/dist/adapters/github.d.ts +123 -0
  87. package/dist/adapters/github.d.ts.map +1 -0
  88. package/dist/adapters/github.js +105 -0
  89. package/dist/adapters/github.js.map +1 -0
  90. package/dist/adapters/jira.d.ts +109 -0
  91. package/dist/adapters/jira.d.ts.map +1 -0
  92. package/dist/adapters/jira.js +54 -0
  93. package/dist/adapters/jira.js.map +1 -0
  94. package/dist/adapters/k8s.d.ts +139 -0
  95. package/dist/adapters/k8s.d.ts.map +1 -0
  96. package/dist/adapters/k8s.js +69 -0
  97. package/dist/adapters/k8s.js.map +1 -0
  98. package/dist/adapters/linear.d.ts +109 -0
  99. package/dist/adapters/linear.d.ts.map +1 -0
  100. package/dist/adapters/linear.js +54 -0
  101. package/dist/adapters/linear.js.map +1 -0
  102. package/dist/adapters/salesforce.d.ts +109 -0
  103. package/dist/adapters/salesforce.d.ts.map +1 -0
  104. package/dist/adapters/salesforce.js +54 -0
  105. package/dist/adapters/salesforce.js.map +1 -0
  106. package/dist/adapters/stripe.d.ts +116 -0
  107. package/dist/adapters/stripe.d.ts.map +1 -0
  108. package/dist/adapters/stripe.js +77 -0
  109. package/dist/adapters/stripe.js.map +1 -0
  110. package/dist/adapters/supabase.d.ts +127 -0
  111. package/dist/adapters/supabase.d.ts.map +1 -0
  112. package/dist/adapters/supabase.js +112 -0
  113. package/dist/adapters/supabase.js.map +1 -0
  114. package/dist/adapters/terraform.d.ts +109 -0
  115. package/dist/adapters/terraform.d.ts.map +1 -0
  116. package/dist/adapters/terraform.js +58 -0
  117. package/dist/adapters/terraform.js.map +1 -0
  118. package/dist/adapters/vercel.d.ts +114 -0
  119. package/dist/adapters/vercel.d.ts.map +1 -0
  120. package/dist/adapters/vercel.js +81 -0
  121. package/dist/adapters/vercel.js.map +1 -0
  122. package/dist/aec-execution.d.ts +107 -0
  123. package/dist/aec-execution.d.ts.map +1 -0
  124. package/dist/aec-execution.js +334 -0
  125. package/dist/aec-execution.js.map +1 -0
  126. package/dist/breakglass.d.ts +205 -0
  127. package/dist/breakglass.d.ts.map +1 -0
  128. package/dist/breakglass.js +627 -0
  129. package/dist/breakglass.js.map +1 -0
  130. package/dist/capability-receipt.d.ts +601 -0
  131. package/dist/capability-receipt.d.ts.map +1 -0
  132. package/dist/capability-receipt.js +1177 -0
  133. package/dist/capability-receipt.js.map +1 -0
  134. package/dist/cf1-conformance.d.ts +83 -0
  135. package/dist/cf1-conformance.d.ts.map +1 -0
  136. package/dist/cf1-conformance.js +94 -0
  137. package/dist/cf1-conformance.js.map +1 -0
  138. package/dist/challenge-store.d.ts +20 -0
  139. package/dist/challenge-store.d.ts.map +1 -0
  140. package/dist/challenge-store.js +62 -0
  141. package/dist/challenge-store.js.map +1 -0
  142. package/dist/control-plane.d.ts +136 -0
  143. package/dist/control-plane.d.ts.map +1 -0
  144. package/dist/control-plane.js +349 -0
  145. package/dist/control-plane.js.map +1 -0
  146. package/dist/coverage.d.ts +156 -0
  147. package/dist/coverage.d.ts.map +1 -0
  148. package/dist/coverage.js +768 -0
  149. package/dist/coverage.js.map +1 -0
  150. package/dist/deployment-attestation.d.ts +93 -0
  151. package/dist/deployment-attestation.d.ts.map +1 -0
  152. package/dist/deployment-attestation.js +262 -0
  153. package/dist/deployment-attestation.js.map +1 -0
  154. package/dist/eg1-conformance.d.ts +286 -0
  155. package/dist/eg1-conformance.d.ts.map +1 -0
  156. package/dist/eg1-conformance.js +379 -0
  157. package/dist/eg1-conformance.js.map +1 -0
  158. package/dist/enterprise.d.ts +88 -0
  159. package/dist/enterprise.d.ts.map +1 -0
  160. package/dist/enterprise.js +194 -0
  161. package/dist/enterprise.js.map +1 -0
  162. package/dist/evidence-postgres.d.ts +123 -0
  163. package/dist/evidence-postgres.d.ts.map +1 -0
  164. package/dist/evidence-postgres.js +343 -0
  165. package/dist/evidence-postgres.js.map +1 -0
  166. package/dist/evidence.d.ts +113 -0
  167. package/dist/evidence.d.ts.map +1 -0
  168. package/dist/evidence.js +369 -0
  169. package/dist/evidence.js.map +1 -0
  170. package/dist/execution-binding.d.ts +49 -0
  171. package/dist/execution-binding.d.ts.map +1 -0
  172. package/dist/execution-binding.js +243 -0
  173. package/dist/execution-binding.js.map +1 -0
  174. package/dist/formal-runtime-map.d.ts +42 -0
  175. package/dist/formal-runtime-map.d.ts.map +1 -0
  176. package/dist/formal-runtime-map.js +23 -0
  177. package/dist/formal-runtime-map.js.map +1 -0
  178. package/dist/index.d.ts +1003 -0
  179. package/dist/index.d.ts.map +1 -0
  180. package/dist/index.js +1737 -0
  181. package/dist/index.js.map +1 -0
  182. package/dist/key-registry.d.ts +60 -0
  183. package/dist/key-registry.d.ts.map +1 -0
  184. package/dist/key-registry.js +144 -0
  185. package/dist/key-registry.js.map +1 -0
  186. package/dist/mcp.d.ts +53 -0
  187. package/dist/mcp.d.ts.map +1 -0
  188. package/dist/mcp.js +112 -0
  189. package/dist/mcp.js.map +1 -0
  190. package/dist/metering.d.ts +80 -0
  191. package/dist/metering.d.ts.map +1 -0
  192. package/dist/metering.js +227 -0
  193. package/dist/metering.js.map +1 -0
  194. package/dist/metrics.d.ts +68 -0
  195. package/dist/metrics.d.ts.map +1 -0
  196. package/dist/metrics.js +242 -0
  197. package/dist/metrics.js.map +1 -0
  198. package/dist/network-witness.d.ts +167 -0
  199. package/dist/network-witness.d.ts.map +1 -0
  200. package/dist/network-witness.js +520 -0
  201. package/dist/network-witness.js.map +1 -0
  202. package/dist/proposal-to-effect.d.ts +146 -0
  203. package/dist/proposal-to-effect.d.ts.map +1 -0
  204. package/dist/proposal-to-effect.js +427 -0
  205. package/dist/proposal-to-effect.js.map +1 -0
  206. package/dist/protocol-state.d.ts +141 -0
  207. package/dist/protocol-state.d.ts.map +1 -0
  208. package/dist/protocol-state.js +106 -0
  209. package/dist/protocol-state.js.map +1 -0
  210. package/dist/receipt-program.d.ts +59 -0
  211. package/dist/receipt-program.d.ts.map +1 -0
  212. package/dist/receipt-program.js +1053 -0
  213. package/dist/receipt-program.js.map +1 -0
  214. package/dist/reliance-kernel.d.ts +66 -0
  215. package/dist/reliance-kernel.d.ts.map +1 -0
  216. package/dist/reliance-kernel.js +103 -0
  217. package/dist/reliance-kernel.js.map +1 -0
  218. package/dist/reliance-packet.d.ts +67 -0
  219. package/dist/reliance-packet.d.ts.map +1 -0
  220. package/dist/reliance-packet.js +176 -0
  221. package/dist/reliance-packet.js.map +1 -0
  222. package/dist/remedy-program-postgres.d.ts +37 -0
  223. package/dist/remedy-program-postgres.d.ts.map +1 -0
  224. package/dist/remedy-program-postgres.js +291 -0
  225. package/dist/remedy-program-postgres.js.map +1 -0
  226. package/dist/remedy-program-receipt.d.ts +97 -0
  227. package/dist/remedy-program-receipt.d.ts.map +1 -0
  228. package/dist/remedy-program-receipt.js +817 -0
  229. package/dist/remedy-program-receipt.js.map +1 -0
  230. package/dist/remedy-program.d.ts +76 -0
  231. package/dist/remedy-program.d.ts.map +1 -0
  232. package/dist/remedy-program.js +1346 -0
  233. package/dist/remedy-program.js.map +1 -0
  234. package/dist/reports/art14.d.ts +101 -0
  235. package/dist/reports/art14.d.ts.map +1 -0
  236. package/dist/reports/art14.js +340 -0
  237. package/dist/reports/art14.js.map +1 -0
  238. package/dist/reports/assurance-package.d.ts +197 -0
  239. package/dist/reports/assurance-package.d.ts.map +1 -0
  240. package/dist/reports/assurance-package.js +313 -0
  241. package/dist/reports/assurance-package.js.map +1 -0
  242. package/dist/reports/auditor-workpaper.d.ts +194 -0
  243. package/dist/reports/auditor-workpaper.d.ts.map +1 -0
  244. package/dist/reports/auditor-workpaper.js +545 -0
  245. package/dist/reports/auditor-workpaper.js.map +1 -0
  246. package/dist/reports/external-verification.d.ts +76 -0
  247. package/dist/reports/external-verification.d.ts.map +1 -0
  248. package/dist/reports/external-verification.js +209 -0
  249. package/dist/reports/external-verification.js.map +1 -0
  250. package/dist/reports/reperform.d.ts +122 -0
  251. package/dist/reports/reperform.d.ts.map +1 -0
  252. package/dist/reports/reperform.js +432 -0
  253. package/dist/reports/reperform.js.map +1 -0
  254. package/dist/reports/underwriter.d.ts +127 -0
  255. package/dist/reports/underwriter.d.ts.map +1 -0
  256. package/dist/reports/underwriter.js +348 -0
  257. package/dist/reports/underwriter.js.map +1 -0
  258. package/dist/retention.d.ts +88 -0
  259. package/dist/retention.d.ts.map +1 -0
  260. package/dist/retention.js +90 -0
  261. package/dist/retention.js.map +1 -0
  262. package/dist/roster.d.ts +113 -0
  263. package/dist/roster.d.ts.map +1 -0
  264. package/dist/roster.js +261 -0
  265. package/dist/roster.js.map +1 -0
  266. package/dist/runtime-monitor.d.ts +132 -0
  267. package/dist/runtime-monitor.d.ts.map +1 -0
  268. package/dist/runtime-monitor.js +197 -0
  269. package/dist/runtime-monitor.js.map +1 -0
  270. package/dist/settlement.d.ts +68 -0
  271. package/dist/settlement.d.ts.map +1 -0
  272. package/dist/settlement.js +306 -0
  273. package/dist/settlement.js.map +1 -0
  274. package/dist/siem.d.ts +261 -0
  275. package/dist/siem.d.ts.map +1 -0
  276. package/dist/siem.js +247 -0
  277. package/dist/siem.js.map +1 -0
  278. package/dist/store-postgres.d.ts +107 -0
  279. package/dist/store-postgres.d.ts.map +1 -0
  280. package/dist/store-postgres.js +152 -0
  281. package/dist/store-postgres.js.map +1 -0
  282. package/dist/store.d.ts +95 -0
  283. package/dist/store.d.ts.map +1 -0
  284. package/dist/store.js +202 -0
  285. package/dist/store.js.map +1 -0
  286. package/dist/strict-json.d.ts +14 -0
  287. package/dist/strict-json.d.ts.map +1 -0
  288. package/dist/strict-json.js +132 -0
  289. package/dist/strict-json.js.map +1 -0
  290. package/dist/trust-program-adapters.d.ts +122 -0
  291. package/dist/trust-program-adapters.d.ts.map +1 -0
  292. package/dist/trust-program-adapters.js +802 -0
  293. package/dist/trust-program-adapters.js.map +1 -0
  294. package/dist/trust-program-postgres.d.ts +41 -0
  295. package/dist/trust-program-postgres.d.ts.map +1 -0
  296. package/dist/trust-program-postgres.js +346 -0
  297. package/dist/trust-program-postgres.js.map +1 -0
  298. package/dist/trust-program-revocation.d.ts +206 -0
  299. package/dist/trust-program-revocation.d.ts.map +1 -0
  300. package/dist/trust-program-revocation.js +600 -0
  301. package/dist/trust-program-revocation.js.map +1 -0
  302. package/dist/trust-program.d.ts +197 -0
  303. package/dist/trust-program.d.ts.map +1 -0
  304. package/dist/trust-program.js +1351 -0
  305. package/dist/trust-program.js.map +1 -0
  306. package/dist/witness-postgres.d.ts +43 -0
  307. package/dist/witness-postgres.d.ts.map +1 -0
  308. package/dist/witness-postgres.js +92 -0
  309. package/dist/witness-postgres.js.map +1 -0
  310. package/dist/zk-range-proof.d.ts +78 -0
  311. package/dist/zk-range-proof.d.ts.map +1 -0
  312. package/dist/zk-range-proof.js +228 -0
  313. package/dist/zk-range-proof.js.map +1 -0
  314. package/eg1-conformance.js +3 -404
  315. package/enterprise.js +3 -180
  316. package/ep-assure.mjs +1 -0
  317. package/evidence-postgres.js +3 -356
  318. package/evidence.js +4 -360
  319. package/execution-binding.js +3 -212
  320. package/formal-runtime-map.js +4 -0
  321. package/index.js +4 -1249
  322. package/key-registry.js +3 -134
  323. package/mcp.js +3 -109
  324. package/metering.js +3 -226
  325. package/metrics.js +3 -231
  326. package/network-witness.js +3 -499
  327. package/package.json +35 -4
  328. package/proposal-to-effect.js +4 -0
  329. package/receipt-program.js +4 -0
  330. package/reliance-kernel.js +3 -112
  331. package/reliance-packet.js +3 -175
  332. package/remedy-program-postgres.js +4 -0
  333. package/remedy-program-receipt.js +4 -0
  334. package/remedy-program.js +4 -0
  335. package/reports/art14.js +0 -0
  336. package/reports/assurance-package.js +2 -312
  337. package/reports/auditor-workpaper.js +3 -537
  338. package/reports/external-verification.js +3 -222
  339. package/reports/reperform.js +3 -424
  340. package/reports/underwriter.js +3 -339
  341. package/retention.js +3 -84
  342. package/roster.js +3 -264
  343. package/runtime-monitor.js +4 -0
  344. package/settlement.js +3 -299
  345. package/siem.js +3 -235
  346. package/src/action-control-manifest.ts +424 -0
  347. package/src/action-escrow-custodian.ts +427 -0
  348. package/src/action-escrow-evidence.ts +1178 -0
  349. package/src/action-escrow-package.ts +1821 -0
  350. package/src/action-escrow-postgres.ts +366 -0
  351. package/src/action-escrow-state.ts +498 -0
  352. package/src/action-escrow-verifiers.ts +484 -0
  353. package/src/action-escrow.ts +3039 -0
  354. package/src/action-packs.ts +170 -0
  355. package/src/adapters/_kit.ts +117 -0
  356. package/src/adapters/aws.ts +142 -0
  357. package/src/adapters/cloudflare.ts +78 -0
  358. package/src/adapters/gcp.ts +71 -0
  359. package/src/adapters/github.ts +110 -0
  360. package/src/adapters/jira.ts +56 -0
  361. package/src/adapters/k8s.ts +71 -0
  362. package/src/adapters/linear.ts +56 -0
  363. package/src/adapters/salesforce.ts +56 -0
  364. package/src/adapters/stripe.ts +82 -0
  365. package/src/adapters/supabase.ts +122 -0
  366. package/src/adapters/terraform.ts +60 -0
  367. package/src/adapters/vercel.ts +85 -0
  368. package/src/aec-execution.ts +354 -0
  369. package/src/breakglass.ts +647 -0
  370. package/src/capability-receipt.ts +1258 -0
  371. package/src/cf1-conformance.ts +120 -0
  372. package/src/challenge-store.ts +70 -0
  373. package/src/control-plane.ts +374 -0
  374. package/src/coverage.ts +832 -0
  375. package/src/deployment-attestation.ts +259 -0
  376. package/src/eg1-conformance.ts +471 -0
  377. package/src/enterprise.ts +195 -0
  378. package/src/evidence-postgres.ts +374 -0
  379. package/src/evidence.ts +367 -0
  380. package/src/execution-binding.ts +236 -0
  381. package/src/formal-runtime-map.ts +24 -0
  382. package/src/index.ts +2127 -0
  383. package/src/key-registry.ts +150 -0
  384. package/src/mcp.ts +120 -0
  385. package/src/metering.ts +237 -0
  386. package/src/metrics.ts +232 -0
  387. package/src/network-witness.ts +585 -0
  388. package/src/proposal-to-effect.ts +571 -0
  389. package/src/protocol-state.ts +143 -0
  390. package/src/protocol-state.typecheck.ts +94 -0
  391. package/src/receipt-program.ts +1105 -0
  392. package/src/reliance-kernel.ts +119 -0
  393. package/src/reliance-packet.ts +206 -0
  394. package/src/remedy-program-postgres.ts +367 -0
  395. package/src/remedy-program-receipt.ts +891 -0
  396. package/src/remedy-program.ts +1388 -0
  397. package/src/reports/art14.ts +361 -0
  398. package/src/reports/assurance-package.ts +330 -0
  399. package/src/reports/auditor-workpaper.ts +558 -0
  400. package/src/reports/external-verification.ts +232 -0
  401. package/src/reports/reperform.ts +441 -0
  402. package/src/reports/underwriter.ts +356 -0
  403. package/src/retention.ts +100 -0
  404. package/src/roster.ts +273 -0
  405. package/src/runtime-monitor.ts +217 -0
  406. package/src/settlement.ts +313 -0
  407. package/src/siem.ts +247 -0
  408. package/src/store-postgres.ts +167 -0
  409. package/src/store.ts +214 -0
  410. package/src/strict-json.ts +115 -0
  411. package/src/trust-program-adapters.ts +847 -0
  412. package/src/trust-program-postgres.ts +429 -0
  413. package/src/trust-program-revocation.ts +751 -0
  414. package/src/trust-program.ts +1460 -0
  415. package/src/witness-postgres.ts +106 -0
  416. package/src/zk-range-proof.ts +250 -0
  417. package/store-postgres.js +3 -161
  418. package/store.js +4 -198
  419. package/strict-json.js +3 -102
  420. package/trust-program-adapters.js +4 -0
  421. package/trust-program-postgres.js +4 -0
  422. package/trust-program-revocation.js +4 -0
  423. package/trust-program.js +4 -0
  424. package/witness-postgres.js +3 -96
  425. package/zk-range-proof.js +4 -0
@@ -0,0 +1,471 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * EG-1 Conformance — the binary artifact that turns "we adopted EMILIA Gate"
4
+ * from a claim into a test result.
5
+ *
6
+ * The question EG-1 answers: *does your integration actually ENFORCE the gate,
7
+ * or are you only claiming it?* An integration earns EG-1 only if it
8
+ * demonstrably:
9
+ * 1. refuses a high-risk action with NO receipt (428);
10
+ * 2. refuses a software-tier receipt on a Class-A action;
11
+ * 3. refuses when the observed execution drifts from the authorized fields;
12
+ * 4. RUNS the action for a valid Class-A/quorum receipt;
13
+ * 5. refuses a replay of the same receipt;
14
+ * 6. refuses a tampered receipt;
15
+ * 7. emits an execution proof bound to the authorization decision;
16
+ * 8. produces a reliance packet whose verdict is "rely".
17
+ *
18
+ * This module is pure (no dependency on index.js, so no import cycle): it owns a
19
+ * throwaway issuer keypair, mints the scenario receipts, and drives any
20
+ * "subject" through the eight checks. A subject is an async `invoke` function
21
+ * representing one attempt at the guarded dangerous action:
22
+ *
23
+ * invoke({ receipt, observedAction }) -> {
24
+ * allowed: boolean, status: number, reason: string,
25
+ * // present only on an allowed run:
26
+ * decisionHash?: string, execution?: { authorizes_decision }, packet?: { verdict }
27
+ * }
28
+ *
29
+ * For integrations built on @emilia-protocol/gate, `makeGateInvoke(gate, ...)`
30
+ * produces a conformant `invoke` from `gate.run()`. Custom integrations (an HTTP
31
+ * service, a different language) implement `invoke` themselves and configure
32
+ * their gate to trust `harness.publicKey` for the run.
33
+ */
34
+ import crypto from 'node:crypto';
35
+
36
+ export const EG1_VERSION = 'EG-1';
37
+
38
+ // Same sorted-key canonical JSON the receipt signature is computed over.
39
+ const canon = (v) => (v == null ? JSON.stringify(v)
40
+ : Array.isArray(v) ? `[${v.map(canon).join(',')}]`
41
+ : typeof v === 'object' ? `{${Object.keys(v).sort().map((k) => JSON.stringify(k) + ':' + canon(v[k])).join(',')}}`
42
+ : JSON.stringify(v));
43
+ const sha256Hex = (v) => crypto.createHash('sha256').update(v, 'utf8').digest('hex');
44
+ const sha256Bytes = (v) => crypto.createHash('sha256').update(v).digest();
45
+
46
+ const RP_ID = 'emiliaprotocol.ai';
47
+ const RP_ORIGIN = `https://www.${RP_ID}`;
48
+
49
+ /**
50
+ * Mint a GENUINE WebAuthn ECDSA-P256 device signoff over an authorization
51
+ * context — the same structure @emilia-protocol/verify verifyWebAuthnSignoff
52
+ * checks. This is what earns a receipt its class_a tier: a real per-signer
53
+ * assertion, not a self-asserted `outcome` string. Used to build the Class-A and
54
+ * quorum evidence the EG-1 harness embeds so the Gate can CRYPTOGRAPHICALLY
55
+ * credit the tier.
56
+ * @param {{ actionHash?: string, approver?: string, issuedAtMs?: number, nonce?: string, prevContextHash?: string }} [opts]
57
+ */
58
+ export function mintDeviceSignoff({
59
+ actionHash,
60
+ approver,
61
+ issuedAtMs = Date.now(),
62
+ nonce,
63
+ prevContextHash = undefined,
64
+ }: {
65
+ actionHash?: string;
66
+ approver?: string;
67
+ issuedAtMs?: number;
68
+ nonce?: string;
69
+ prevContextHash?: string;
70
+ } = {}) {
71
+ const signer = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' });
72
+ const context = {
73
+ ep_version: '1.0', context_type: 'ep.signoff.v1',
74
+ action_hash: actionHash,
75
+ policy: 'policy_eg1',
76
+ nonce: nonce || ('sig_' + crypto.randomBytes(16).toString('hex')),
77
+ approver,
78
+ initiator: 'ent_agent_eg1',
79
+ issued_at: new Date(issuedAtMs).toISOString(),
80
+ expires_at: new Date(issuedAtMs + 5 * 60_000).toISOString(),
81
+ ...(prevContextHash !== undefined ? { prev_context_hash: prevContextHash } : {}),
82
+ };
83
+ const challenge = crypto.createHash('sha256').update(canon(context), 'utf8').digest().toString('base64url');
84
+ const clientData = Buffer.from(JSON.stringify({ type: 'webauthn.get', challenge, origin: `https://www.${RP_ID}` }), 'utf8');
85
+ const authData = Buffer.concat([
86
+ crypto.createHash('sha256').update(RP_ID, 'utf8').digest(),
87
+ Buffer.from([0x05]), // UP | UV
88
+ Buffer.from([0, 0, 0, 1]),
89
+ ]);
90
+ const signed = Buffer.concat([authData, crypto.createHash('sha256').update(clientData).digest()]);
91
+ const signature = crypto.sign('sha256', signed, signer.privateKey).toString('base64url');
92
+ return {
93
+ signoff: {
94
+ '@type': 'ep.signoff',
95
+ context,
96
+ webauthn: {
97
+ authenticator_data: authData.toString('base64url'),
98
+ client_data_json: clientData.toString('base64url'),
99
+ signature,
100
+ },
101
+ approver_public_key: signer.publicKey.export({ type: 'spki', format: 'der' }).toString('base64url'),
102
+ },
103
+ approver_public_key: signer.publicKey.export({ type: 'spki', format: 'der' }).toString('base64url'),
104
+ context,
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Mint a GENUINE EP-QUORUM-v1 evidence document: N distinct humans, each on a
110
+ * distinct device key, each with a real WebAuthn assertion bound to the SAME
111
+ * action_hash, within a window. verifyQuorum returns valid for it. This is what
112
+ * earns a receipt its `quorum` tier — never a bare {signers,threshold} block.
113
+ * @param {{ actionHash?: string, threshold?: number, approvers?: Array<{ role: string, approver: string }>, issuedAtMs?: number }} [opts]
114
+ */
115
+ export function mintQuorumEvidence({
116
+ actionHash,
117
+ threshold = 2,
118
+ approvers,
119
+ issuedAtMs = Date.now(),
120
+ }: {
121
+ actionHash?: string;
122
+ threshold?: number;
123
+ approvers?: Array<{ role: string; approver: string }>;
124
+ issuedAtMs?: number;
125
+ } = {}) {
126
+ const people = approvers || Array.from({ length: threshold }, (_, i) => ({ role: `approver_${i + 1}`, approver: `ep:approver:eg1_${i + 1}` }));
127
+ const members = people.map((p, i) => {
128
+ const s = mintDeviceSignoff({ actionHash, approver: p.approver, issuedAtMs: issuedAtMs + i * 1000 });
129
+ return { role: p.role, approver_public_key: s.approver_public_key, signoff: { '@type': s.signoff['@type'], context: s.signoff.context, webauthn: s.signoff.webauthn } };
130
+ });
131
+ return {
132
+ '@type': 'ep.quorum',
133
+ action_hash: actionHash,
134
+ policy: {
135
+ mode: 'threshold',
136
+ required: threshold,
137
+ approvers: people,
138
+ distinct_humans: true,
139
+ window_sec: 900,
140
+ },
141
+ members,
142
+ };
143
+ }
144
+
145
+ // The default high-risk action EG-1 exercises: a Class-A money movement, which
146
+ // the default gate manifest guards (selector { protocol:'mcp', tool:'release_payment' }).
147
+ export const EG1_DEFAULT_SELECTOR = Object.freeze({ protocol: 'mcp', tool: 'release_payment' });
148
+ export const EG1_DEFAULT_ACTION = Object.freeze({
149
+ action_type: 'payment.release',
150
+ amount_usd: 40000,
151
+ currency: 'USD',
152
+ payment_instruction_id: 'pi_eg1_40000',
153
+ beneficiary_account_hash: 'sha256:eg1-beneficiary',
154
+ });
155
+
156
+ export const EG1_CHECKS = Object.freeze([
157
+ { id: 'missing_receipt_refused', title: 'missing receipt → 428' },
158
+ { id: 'software_on_classA_refused', title: 'software receipt on Class-A action → refused' },
159
+ { id: 'execution_drift_refused', title: 'observed execution drift → refused' },
160
+ { id: 'valid_classA_runs', title: 'valid Class-A/quorum receipt → runs' },
161
+ { id: 'replay_refused', title: 'same receipt replay → refused' },
162
+ { id: 'tampered_refused', title: 'tampered receipt → refused' },
163
+ { id: 'execution_proof_binds', title: 'execution proof binds to authorization decision' },
164
+ { id: 'reliance_packet_rely', title: 'reliance packet returns verdict "rely"' },
165
+ ]);
166
+
167
+ /**
168
+ * Create an EG-1 harness: a throwaway issuer key + a receipt minter for the
169
+ * scenarios. Configure the subject's gate to trust `publicKey` for the run.
170
+ */
171
+ export function createEg1Harness({
172
+ now = Date.now,
173
+ action = EG1_DEFAULT_ACTION,
174
+ idPrefix = 'eg1',
175
+ }: { now?: () => number; action?: Record<string, unknown>; idPrefix?: string } = {}) {
176
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
177
+ const pub = publicKey.export({ type: 'spki', format: 'der' }).toString('base64url');
178
+ const approverA = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' });
179
+ const approverA2 = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' });
180
+ const approverB = crypto.generateKeyPairSync('ed25519');
181
+ const approverKeys = {
182
+ 'ep:key:eg1:class-a': {
183
+ approver_id: 'ep:approver:eg1:cfo',
184
+ public_key: approverA.publicKey.export({ type: 'spki', format: 'der' }).toString('base64url'),
185
+ key_class: 'A',
186
+ },
187
+ 'ep:key:eg1:class-a-2': {
188
+ approver_id: 'ep:approver:eg1:security-officer',
189
+ public_key: approverA2.publicKey.export({ type: 'spki', format: 'der' }).toString('base64url'),
190
+ key_class: 'A',
191
+ },
192
+ 'ep:key:eg1:controller': {
193
+ approver_id: 'ep:approver:eg1:controller',
194
+ public_key: approverB.publicKey.export({ type: 'spki', format: 'der' }).toString('base64url'),
195
+ key_class: 'B',
196
+ },
197
+ };
198
+ const quorumPolicy = {
199
+ mode: 'threshold', required: 2, distinct_humans: true, window_sec: 900,
200
+ approvers: [
201
+ { role: 'cfo', approver: approverKeys['ep:key:eg1:class-a'].approver_id },
202
+ { role: 'security_officer', approver: approverKeys['ep:key:eg1:class-a-2'].approver_id },
203
+ ],
204
+ };
205
+ let counter = 0;
206
+ const nowMs = () => (typeof now === 'function' ? now() : now);
207
+
208
+ // The action_hash the self-contained human device assertions are bound to.
209
+ // Derived from the action so the signoff/quorum evidence is about THIS action.
210
+ const actionHash = crypto.createHash('sha256').update(canon(action), 'utf8').digest('hex');
211
+
212
+ function assuranceContext(payload) {
213
+ return {
214
+ '@version': 'EP-ASSURANCE-CONTEXT-v1',
215
+ receipt_id: payload.receipt_id,
216
+ claim_hash: `sha256:${sha256Hex(canon(payload.claim))}`,
217
+ };
218
+ }
219
+
220
+ function classASignoff(digest, {
221
+ keyId = 'ep:key:eg1:class-a',
222
+ keyPair = approverA,
223
+ approver = 'ep:approver:eg1:cfo',
224
+ } = {}) {
225
+ const challenge = Buffer.from(digest).toString('base64url');
226
+ const clientDataJSON = Buffer.from(JSON.stringify({ type: 'webauthn.get', challenge, origin: RP_ORIGIN }), 'utf8');
227
+ const rpIdHash = crypto.createHash('sha256').update(RP_ID).digest();
228
+ const authData = Buffer.concat([rpIdHash, Buffer.from([0x05]), Buffer.from([0, 0, 0, 0])]); // UP + UV
229
+ const signedData = Buffer.concat([authData, sha256Bytes(clientDataJSON)]);
230
+ return {
231
+ approver,
232
+ approver_key_id: keyId,
233
+ key_class: 'A',
234
+ webauthn: {
235
+ authenticator_data: authData.toString('base64url'),
236
+ client_data_json: clientDataJSON.toString('base64url'),
237
+ signature: crypto.sign('sha256', signedData, keyPair.privateKey).toString('base64url'),
238
+ },
239
+ };
240
+ }
241
+
242
+ function softwareSignoff(digest) {
243
+ return {
244
+ approver: 'ep:approver:eg1:controller',
245
+ approver_key_id: 'ep:key:eg1:controller',
246
+ key_class: 'B',
247
+ signature: crypto.sign(null, digest, approverB.privateKey).toString('base64url'),
248
+ };
249
+ }
250
+
251
+ function assuranceProof(payload, quorum) {
252
+ const context = assuranceContext(payload);
253
+ const contextHash = `sha256:${sha256Hex(canon(context))}`;
254
+ const digest = Buffer.from(contextHash.replace(/^sha256:/, ''), 'hex');
255
+ const threshold = Number(quorum?.threshold ?? quorum?.m ?? 1);
256
+ const signoffs = [classASignoff(digest)];
257
+ if (threshold >= 2) {
258
+ signoffs.push(classASignoff(digest, {
259
+ keyId: 'ep:key:eg1:class-a-2',
260
+ keyPair: approverA2,
261
+ approver: 'ep:approver:eg1:security-officer',
262
+ }));
263
+ }
264
+ return {
265
+ '@version': 'EP-ASSURANCE-PROOF-v1',
266
+ context_hash: contextHash,
267
+ threshold: threshold >= 2 ? threshold : 1,
268
+ signoffs,
269
+ };
270
+ }
271
+
272
+ /**
273
+ * Mint a scenario receipt.
274
+ * @param {object} o
275
+ * @param {'allow'|'allow_with_signoff'} [o.outcome] 'allow'=software; 'allow_with_signoff'
276
+ * embeds a REAL WebAuthn device signoff so the receipt cryptographically earns class_a.
277
+ * @param {object|boolean} [o.quorum] request quorum-tier evidence. Truthy -> a REAL
278
+ * EP-QUORUM-v1 doc (distinct humans + distinct keys + per-signer assertions). If an
279
+ * object with `threshold`/`signers`, its size sets the quorum size.
280
+ * @param {boolean} [o.fakeQuorum] embed an UNVERIFIABLE self-asserted quorum block
281
+ * ({signers,threshold}) with no per-signer signatures — used to prove the Gate
282
+ * REFUSES it (must NOT be credited quorum). For adversarial tests only.
283
+ * @param {object} [o.tamper] fields assigned to the claim AFTER signing (breaks the signature)
284
+ * @param {object} [o.extra] extra fields merged into the claim before signing
285
+ */
286
+ function mint({
287
+ outcome = 'allow_with_signoff',
288
+ quorum = null,
289
+ fakeQuorum = false,
290
+ tamper = null,
291
+ extra = {},
292
+ }: {
293
+ outcome?: 'allow' | 'allow_with_signoff';
294
+ quorum?: { threshold?: number; signers?: any[] } | null;
295
+ fakeQuorum?: boolean;
296
+ tamper?: Record<string, any> | null;
297
+ extra?: Record<string, any>;
298
+ } = {}) {
299
+ const approver = outcome === 'allow'
300
+ ? 'ep:approver:eg1:controller'
301
+ : 'ep:approver:eg1:cfo';
302
+ const claim = { ...action, outcome, approver, ...extra };
303
+ const payload: {
304
+ receipt_id: string;
305
+ subject: string;
306
+ issuer: string;
307
+ created_at: string;
308
+ claim: Record<string, any>;
309
+ quorum?: any;
310
+ assurance_proof?: any;
311
+ signoff?: any;
312
+ approver_public_key?: string;
313
+ } = {
314
+ receipt_id: `${idPrefix}_${++counter}`,
315
+ subject: 'agent:eg1-conformance',
316
+ issuer: 'ep:org:eg1',
317
+ created_at: new Date(nowMs()).toISOString(),
318
+ claim,
319
+ };
320
+ if (fakeQuorum) {
321
+ // Self-asserted ONLY — NO members / NO signatures / NO pinned proof. The
322
+ // Gate must refuse to credit this as quorum (assurance_too_low). For
323
+ // adversarial tests that prove payload claims are never trusted.
324
+ payload.quorum = { signers: ['ep:a', 'ep:b'], threshold: 2 };
325
+ } else if (outcome === 'allow_with_signoff' || quorum) {
326
+ // Genuine, per-signer-verifiable evidence. The PRIMARY proof is the pinned
327
+ // EP-ASSURANCE-PROOF-v1 (verified against the harness's pinned approverKeys),
328
+ // which is what the EG-1 gate/custody path checks. We ALSO embed self-contained
329
+ // evidence (EP-QUORUM-v1 / WebAuthn device signoff) so a relying party that
330
+ // does NOT pin keys can still cryptographically credit the tier (DoD audit fix).
331
+ payload.assurance_proof = assuranceProof(payload, quorum);
332
+ if (quorum) {
333
+ const threshold = Number.isInteger(quorum.threshold) ? quorum.threshold
334
+ : (Array.isArray(quorum.signers) ? quorum.signers.length : 2);
335
+ payload.quorum = mintQuorumEvidence({
336
+ actionHash,
337
+ threshold,
338
+ approvers: quorumPolicy.approvers.slice(0, threshold),
339
+ issuedAtMs: nowMs(),
340
+ });
341
+ } else {
342
+ const s = mintDeviceSignoff({ actionHash, approver, issuedAtMs: nowMs() });
343
+ payload.signoff = s.signoff;
344
+ payload.approver_public_key = s.approver_public_key;
345
+ }
346
+ }
347
+ const value = crypto.sign(null, Buffer.from(canon(payload), 'utf8'), privateKey).toString('base64url');
348
+ const receipt = { '@version': 'EP-RECEIPT-v1', payload, signature: { algorithm: 'Ed25519', value } };
349
+ if (tamper) Object.assign(receipt.payload.claim, tamper); // tamper AFTER signing -> signature no longer binds
350
+ return receipt;
351
+ }
352
+
353
+ return { publicKey: pub, approverKeys, quorumPolicy, mint, action, actionHash, now: nowMs, rpId: RP_ID, allowedOrigins: [RP_ORIGIN] };
354
+ }
355
+
356
+ /**
357
+ * Adapt an @emilia-protocol/gate instance into an EG-1 `invoke`. The gate must
358
+ * have been built trusting the harness public key. Uses gate.run() so the
359
+ * execution proof + reliance packet are produced on the allowed path.
360
+ */
361
+ export function makeGateInvoke(gate, { selector = EG1_DEFAULT_SELECTOR, action = EG1_DEFAULT_ACTION } = {}) {
362
+ if (!gate || typeof gate.run !== 'function') {
363
+ throw new Error('makeGateInvoke requires an EMILIA Gate instance (with .run)');
364
+ }
365
+ return async ({ receipt, observedAction }) => {
366
+ const out = await gate.run(
367
+ { selector, receipt, observedAction: observedAction ?? action },
368
+ async () => ({ eg1: 'side-effect-ran' }),
369
+ );
370
+ if (!out.ok) {
371
+ return { allowed: false, status: out.status, reason: out.authorization?.reason ?? 'refused' };
372
+ }
373
+ return {
374
+ allowed: true,
375
+ status: 200,
376
+ reason: out.authorization?.reason ?? 'allow',
377
+ decisionHash: out.authorization?.evidence?.hash ?? null,
378
+ execution: out.execution ?? null,
379
+ packet: out.packet ?? null,
380
+ };
381
+ };
382
+ }
383
+
384
+ const pick = (r) => ({ allowed: !!r.allowed, status: r.status ?? null, reason: r.reason ?? null });
385
+
386
+ /**
387
+ * Drive a subject through the eight EG-1 checks and return a JSON report.
388
+ * @param {object} [o]
389
+ * @param {(scenario:object)=>Promise<object>} [o.invoke] the integration under test
390
+ * @param {object} [o.harness] from createEg1Harness()
391
+ * @param {object} [o.action] the high-risk action (defaults to the harness action)
392
+ */
393
+ export async function runEg1({
394
+ invoke,
395
+ harness,
396
+ action,
397
+ }: {
398
+ invoke?: (scenario: { receipt: any; observedAction: any }) => Promise<{
399
+ allowed: boolean;
400
+ status?: number | null;
401
+ reason?: string | null;
402
+ decisionHash?: string | null;
403
+ execution?: { authorizes_decision?: string | null } | null;
404
+ packet?: { verdict?: string | null } | null;
405
+ }>;
406
+ harness?: ReturnType<typeof createEg1Harness>;
407
+ action?: Record<string, any>;
408
+ } = {}) {
409
+ if (typeof invoke !== 'function') throw new Error('runEg1 requires an invoke(scenario) function');
410
+ if (!harness || typeof harness.mint !== 'function') throw new Error('runEg1 requires a harness from createEg1Harness()');
411
+ const act = action || harness.action || EG1_DEFAULT_ACTION;
412
+ const observed = act;
413
+ const drift = { ...act, amount_usd: Number(act.amount_usd ?? 0) + 1 };
414
+
415
+ const results = {};
416
+ const set = (id, pass, observed_) => { results[id] = { pass: !!pass, observed: observed_ }; };
417
+
418
+ // 1. missing receipt → 428
419
+ let r = await invoke({ receipt: null, observedAction: observed });
420
+ set('missing_receipt_refused', !r.allowed && r.status === 428, pick(r));
421
+
422
+ // 2. software receipt on a Class-A action → refused
423
+ r = await invoke({ receipt: harness.mint({ outcome: 'allow' }), observedAction: observed });
424
+ set('software_on_classA_refused', !r.allowed && /assurance/i.test(r.reason || ''), pick(r));
425
+
426
+ // 3. observed execution drift → refused
427
+ r = await invoke({ receipt: harness.mint({ outcome: 'allow_with_signoff' }), observedAction: drift });
428
+ set('execution_drift_refused', !r.allowed && /binding/i.test(r.reason || ''), pick(r));
429
+
430
+ // 4. valid Class-A receipt → runs (capture for 5/7/8)
431
+ const valid = harness.mint({ outcome: 'allow_with_signoff' });
432
+ r = await invoke({ receipt: valid, observedAction: observed });
433
+ const validAllowed = r.allowed === true;
434
+ set('valid_classA_runs', validAllowed, pick(r));
435
+
436
+ // 7. execution proof binds to the authorization decision
437
+ const boundOk = validAllowed
438
+ && !!r.execution && !!r.execution.authorizes_decision
439
+ && !!r.decisionHash && r.execution.authorizes_decision === r.decisionHash;
440
+ set('execution_proof_binds', boundOk, {
441
+ authorizes_decision: r.execution?.authorizes_decision ?? null,
442
+ decision_hash: r.decisionHash ?? null,
443
+ });
444
+
445
+ // 8. reliance packet verdict "rely"
446
+ set('reliance_packet_rely', validAllowed && String(r.packet?.verdict || '').toLowerCase() === 'rely', {
447
+ verdict: r.packet?.verdict ?? null,
448
+ });
449
+
450
+ // 5. same receipt replay → refused
451
+ r = await invoke({ receipt: valid, observedAction: observed });
452
+ set('replay_refused', !r.allowed && /replay/i.test(r.reason || ''), pick(r));
453
+
454
+ // 6. tampered receipt → refused
455
+ r = await invoke({ receipt: harness.mint({ outcome: 'allow_with_signoff', tamper: { amount_usd: 9_999_999 } }), observedAction: observed });
456
+ set('tampered_refused', !r.allowed && r.status === 428, pick(r));
457
+
458
+ const checks = EG1_CHECKS.map((c) => ({ id: c.id, title: c.title, ...results[c.id] }));
459
+ const passedCount = checks.filter((c) => c.pass).length;
460
+ const passed = passedCount === checks.length;
461
+ return {
462
+ standard: EG1_VERSION,
463
+ passed,
464
+ badge: passed ? 'EG-1 Enforced' : 'EG-1 not earned',
465
+ summary: { passed: passedCount, total: checks.length },
466
+ checks,
467
+ generated_at: new Date(harness.now ? harness.now() : Date.now()).toISOString(),
468
+ };
469
+ }
470
+
471
+ export default { EG1_VERSION, EG1_CHECKS, EG1_DEFAULT_ACTION, EG1_DEFAULT_SELECTOR, createEg1Harness, makeGateInvoke, runEg1, mintDeviceSignoff, mintQuorumEvidence };
@@ -0,0 +1,195 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * EMILIA Gate — enterprise entitlement layer (EP-GATE-ENTITLEMENT-v1).
4
+ *
5
+ * The license key IS an EP-style artifact: a signed entitlement — Ed25519 over
6
+ * canonical JSON (sorted keys, same idiom as receipts/evidence) of
7
+ * { org, tier, features[], limits, not_before, expires_at, kid }. Verifiers pin
8
+ * issuer keys by kid; nothing in the artifact is trusted until the signature
9
+ * verifies against a pinned key.
10
+ *
11
+ * OPEN-CORE SEMANTICS — two different fail directions, by design:
12
+ * - The CORE gate is never bricked. No entitlement, an expired one, a
13
+ * tampered one, an unknown kid — all resolve to { valid:false,
14
+ * tier:'community' } with a machine-readable reason. Community tier always
15
+ * works; a licensing failure can never block the firewall itself.
16
+ * - Enterprise FEATURES fail closed. `requireFeature` returns true ONLY for
17
+ * a cryptographically valid, in-window entitlement that explicitly lists
18
+ * the feature. Everything else — including community fallback — is false.
19
+ *
20
+ * Pure functions: inputs in, verdict out. Time is injected (`now`), never read
21
+ * from the wall clock implicitly, so verification is deterministic.
22
+ */
23
+ import crypto from 'node:crypto';
24
+ import { strictJsonGate } from './strict-json.js';
25
+
26
+ export const ENTITLEMENT_VERSION = 'EP-GATE-ENTITLEMENT-v1';
27
+ export const ENTITLEMENT_TIERS = ['community', 'team', 'business', 'enterprise', 'regulated'];
28
+
29
+ /** The tier every failure path resolves to — the gate keeps working on it. */
30
+ const COMMUNITY = 'community';
31
+
32
+ /** Canonical JSON (recursive sorted keys) — matches @emilia-protocol/verify. */
33
+ function canonical(v) {
34
+ if (v === null || v === undefined) return JSON.stringify(v);
35
+ if (Array.isArray(v)) return `[${v.map(canonical).join(',')}]`;
36
+ if (typeof v === 'object') {
37
+ return `{${Object.keys(v).sort().map((k) => JSON.stringify(k) + ':' + canonical(v[k])).join(',')}}`;
38
+ }
39
+ return JSON.stringify(v);
40
+ }
41
+
42
+ function toMs(t) {
43
+ if (t == null) return null;
44
+ const ms = typeof t === 'number' ? t : Date.parse(t);
45
+ return Number.isFinite(ms) ? ms : null;
46
+ }
47
+
48
+ /** Community fallback — every refusal shape is identical and machine-readable. */
49
+ function community(reason, extra = {}) {
50
+ return { valid: false, tier: COMMUNITY, features: [], limits: null, reason, ...extra };
51
+ }
52
+
53
+ /**
54
+ * Mint a signed entitlement (test/ops helper — the licensing service, not the
55
+ * verifier, holds the private key). Throws on invalid fields: a malformed
56
+ * license must never be issued, only refused.
57
+ * @param {crypto.KeyObject} privateKey Ed25519 private key
58
+ * @param {object} fields { org, tier, features?, limits?, not_before, expires_at, kid }
59
+ * @returns {{ '@version': string, payload: object, signature: { algorithm: 'Ed25519', value: string } }}
60
+ */
61
+ export function mintEntitlement(privateKey, {
62
+ org, tier, features = [], limits = {}, not_before, expires_at, kid,
63
+ }: {
64
+ org: string;
65
+ tier: string;
66
+ features?: string[];
67
+ limits?: Record<string, any>;
68
+ not_before: string | number;
69
+ expires_at: string | number;
70
+ kid: string;
71
+ }) {
72
+ if (!org || typeof org !== 'string') throw new Error('entitlement: org is required');
73
+ if (!ENTITLEMENT_TIERS.includes(tier)) {
74
+ throw new Error(`entitlement: unknown tier "${tier}" (expected one of ${ENTITLEMENT_TIERS.join('|')})`);
75
+ }
76
+ if (!Array.isArray(features) || features.some((f) => typeof f !== 'string')) {
77
+ throw new Error('entitlement: features must be an array of strings');
78
+ }
79
+ if (!limits || typeof limits !== 'object' || Array.isArray(limits)) {
80
+ throw new Error('entitlement: limits must be an object');
81
+ }
82
+ if (toMs(not_before) == null) throw new Error('entitlement: not_before is required (ISO or ms)');
83
+ if (toMs(expires_at) == null) throw new Error('entitlement: expires_at is required (ISO or ms)');
84
+ if (!kid || typeof kid !== 'string') throw new Error('entitlement: kid is required');
85
+ // Snapshot features/limits into the signed payload: embedding the caller's live
86
+ // array/object would let a licensing service mutate them after minting and
87
+ // diverge the entitlement from its signature.
88
+ const payload = { org, tier, features: structuredClone(features), limits: structuredClone(limits), not_before, expires_at, kid };
89
+ const value = crypto.sign(null, Buffer.from(canonical(payload), 'utf8'), privateKey).toString('base64url');
90
+ return { '@version': ENTITLEMENT_VERSION, payload, signature: { algorithm: 'Ed25519', value } };
91
+ }
92
+
93
+ /** Resolve a base64url SPKI-DER key for `kid` from a map or an entry list. */
94
+ function issuerKeyFor(issuerKeys, kid) {
95
+ if (!issuerKeys) return null;
96
+ if (Array.isArray(issuerKeys)) {
97
+ const e = issuerKeys.find((x) => x && x.kid === kid && typeof x.key === 'string');
98
+ return e ? e.key : null;
99
+ }
100
+ const k = issuerKeys[kid];
101
+ return typeof k === 'string' ? k : null;
102
+ }
103
+
104
+ /**
105
+ * Verify an entitlement. NEVER throws for a bad artifact — every failure
106
+ * resolves to the community tier with a machine-readable reason, so a licensing
107
+ * problem degrades gracefully instead of bricking the gate. Enterprise features
108
+ * remain gated by `requireFeature`, which fails closed on any non-valid result.
109
+ *
110
+ * @param {object|string|null} entitlementJson the artifact (object or JSON string); absence -> community
111
+ * @param {object} [o]
112
+ * @param {object|Array<{kid:string,key:string}>} [o.issuerKeys] pinned kid -> base64url SPKI-DER public key
113
+ * @param {number|string|function} [o.now=Date.now] injected clock (ms, ISO, or () => ms)
114
+ * @returns {{ valid: boolean, tier: string, features: string[], limits: object|null, reason: string, org?: string, kid?: string, not_before?: any, expires_at?: any }}
115
+ */
116
+ export function verifyEntitlement(entitlementJson, {
117
+ issuerKeys,
118
+ now = Date.now,
119
+ }: {
120
+ issuerKeys?: Record<string, string> | Array<{ kid: string; key: string }>;
121
+ now?: number | string | (() => number);
122
+ } = {}) {
123
+ // Absence is NOT an error: the open-core floor. Community keeps working.
124
+ if (entitlementJson == null || entitlementJson === '') return community('no_entitlement');
125
+
126
+ let doc = entitlementJson;
127
+ if (typeof doc === 'string') {
128
+ try {
129
+ if (Buffer.byteLength(doc, 'utf8') > 1024 * 1024 || !strictJsonGate(doc).ok) return community('entitlement_unparseable');
130
+ doc = JSON.parse(doc);
131
+ } catch { return community('entitlement_unparseable'); }
132
+ }
133
+ if (!doc || typeof doc !== 'object' || Array.isArray(doc)) return community('entitlement_malformed');
134
+ if (doc['@version'] !== ENTITLEMENT_VERSION) return community('unsupported_version');
135
+
136
+ const p = doc.payload;
137
+ const sig = doc.signature;
138
+ if (!p || typeof p !== 'object' || !sig || typeof sig !== 'object') return community('entitlement_malformed');
139
+ if (sig.algorithm !== 'Ed25519' || typeof sig.value !== 'string') return community('unsupported_algorithm');
140
+ if (!ENTITLEMENT_TIERS.includes(p.tier)) return community('unknown_tier');
141
+ if (!Array.isArray(p.features) || p.features.some((f) => typeof f !== 'string')) return community('entitlement_malformed');
142
+
143
+ // Issuer pinning: the kid must resolve to a PINNED key. An entitlement can
144
+ // never nominate its own key — unknown kid (or no pins at all) fails closed.
145
+ const keyB64 = issuerKeyFor(issuerKeys, p.kid);
146
+ if (!keyB64) return community('unknown_kid', { kid: p.kid ?? null });
147
+
148
+ let ok = false;
149
+ try {
150
+ const pub = crypto.createPublicKey({ key: Buffer.from(keyB64, 'base64url'), format: 'der', type: 'spki' });
151
+ ok = crypto.verify(null, Buffer.from(canonical(p), 'utf8'), pub, Buffer.from(sig.value, 'base64url'));
152
+ } catch { ok = false; }
153
+ // One reason covers both tampering and a wrong key: the signature does not
154
+ // verify against the pinned key for this kid.
155
+ if (!ok) return community('bad_signature', { kid: p.kid });
156
+
157
+ // Validity window — checked only AFTER the signature, so the timestamps
158
+ // themselves are authenticated. Both bounds are required; an unparseable
159
+ // window fails closed.
160
+ const nowMs = typeof now === 'function' ? now() : toMs(now);
161
+ const nbf = toMs(p.not_before);
162
+ const exp = toMs(p.expires_at);
163
+ if (nbf == null || exp == null || nowMs == null) return community('invalid_validity_window', { kid: p.kid });
164
+ if (nowMs < nbf) return community('not_yet_valid', { kid: p.kid, not_before: p.not_before });
165
+ if (nowMs > exp) return community('expired', { kid: p.kid, expires_at: p.expires_at });
166
+
167
+ return {
168
+ valid: true,
169
+ tier: p.tier,
170
+ features: p.features.slice(),
171
+ limits: (p.limits && typeof p.limits === 'object') ? { ...p.limits } : {},
172
+ reason: 'entitlement_verified',
173
+ org: p.org,
174
+ kid: p.kid,
175
+ not_before: p.not_before,
176
+ expires_at: p.expires_at,
177
+ };
178
+ }
179
+
180
+ /**
181
+ * Is `feature` licensed? FAIL CLOSED: true only for a valid entitlement that
182
+ * explicitly lists the feature. Community fallback, invalid/expired/tampered
183
+ * artifacts, and unlisted features are all false — no enterprise code path
184
+ * runs without a live license naming it.
185
+ * @param {object} verified the result of verifyEntitlement
186
+ * @param {string} feature
187
+ * @returns {boolean}
188
+ */
189
+ export function requireFeature(verified, feature) {
190
+ if (!verified || verified.valid !== true) return false;
191
+ if (typeof feature !== 'string' || feature.length === 0) return false;
192
+ return Array.isArray(verified.features) && verified.features.includes(feature);
193
+ }
194
+
195
+ export default { mintEntitlement, verifyEntitlement, requireFeature, ENTITLEMENT_VERSION, ENTITLEMENT_TIERS };