@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,1177 @@
1
+ // @ts-nocheck
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * EP Capability Receipt v1.
5
+ *
6
+ * A capability receipt is an issuer-signed envelope around an ordinary EP
7
+ * receipt. The ordinary receipt remains the policy/assurance proof; the
8
+ * capability envelope adds a secret preimage, an immutable budget, an expiry,
9
+ * and (optionally) Shamir shares. Spend state is never trusted from the
10
+ * envelope. Every spend must pass through an atomic capability store.
11
+ *
12
+ * The executor deliberately follows the same indeterminate-outcome rule as
13
+ * Gate: once the external effect is entered, a storage failure cannot reopen
14
+ * the budget. The reservation remains blocked until reconciliation.
15
+ */
16
+ import { createHash, createPublicKey, randomBytes, randomUUID, sign, timingSafeEqual, verify, } from 'node:crypto';
17
+ import { canonicalize } from './execution-binding.js';
18
+ export const CAPABILITY_RECEIPT_VERSION = 'EP-CAPABILITY-RECEIPT-v1';
19
+ export const CAPABILITY_STATE_VERSION = 'EP-CAPABILITY-STATE-v1';
20
+ export const CAPABILITY_SHARE_VERSION = 'EP-CAPABILITY-SHARE-v1';
21
+ export const CAPABILITY_HASH_ALGORITHM = 'sha256';
22
+ export const CAPABILITY_SCOPE_PROFILE = 'urn:emilia:scope:action-digest-set-v1';
23
+ export const CAPABILITY_CAID_SCOPE_PROFILE = 'urn:emilia:scope:caid-set-v1';
24
+ // 2^521 - 1 is a prime and is comfortably larger than a 256-bit secret.
25
+ const FIELD = (2n ** 521n) - 1n;
26
+ const SHARE_BYTES = 66;
27
+ const HASH_BYTES = 32;
28
+ const MAX_CURRENCY_BYTES = 32;
29
+ const MAX_OPERATION_ID_BYTES = 128;
30
+ const MAX_DELEGATES = 64;
31
+ const MAX_SCOPE_ACTIONS = 256;
32
+ const ACTION_DIGEST_RE = /^sha256:[0-9a-f]{64}$/;
33
+ const CAID_RE = /^caid:1:[a-z][a-z0-9.-]*\.[1-9][0-9]*:jcs-sha256:[A-Za-z0-9_-]{43}$/;
34
+ function isRecord(value) {
35
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
36
+ }
37
+ function deepFreeze(value) {
38
+ if (!value || typeof value !== 'object')
39
+ return value;
40
+ const stack = [value];
41
+ const seen = new WeakSet();
42
+ while (stack.length) {
43
+ const current = stack.pop();
44
+ if (!current || typeof current !== 'object' || seen.has(current))
45
+ continue;
46
+ seen.add(current);
47
+ for (const child of Object.values(current))
48
+ stack.push(child);
49
+ Object.freeze(current);
50
+ }
51
+ return value;
52
+ }
53
+ function nowMs(now) {
54
+ const value = typeof now === 'function' ? now() : now;
55
+ if (!Number.isSafeInteger(value) || value < 0)
56
+ throw new TypeError('capability clock must return a non-negative safe integer');
57
+ return value;
58
+ }
59
+ function base64u(bytes) {
60
+ return Buffer.from(bytes).toString('base64url');
61
+ }
62
+ function withoutBase64Padding(value) {
63
+ let end = value.length;
64
+ while (end > 0 && value.charCodeAt(end - 1) === 0x3d)
65
+ end -= 1;
66
+ return value.slice(0, end);
67
+ }
68
+ function decodeBase64u(value, label) {
69
+ if (typeof value !== 'string' || value.length === 0)
70
+ throw new TypeError(`${label} must be base64url`);
71
+ const bytes = Buffer.from(value, 'base64url');
72
+ if (bytes.length === 0 || base64u(bytes) !== withoutBase64Padding(value))
73
+ throw new TypeError(`${label} is not canonical base64url`);
74
+ return bytes;
75
+ }
76
+ function sha256(value) {
77
+ return createHash('sha256').update(value).digest();
78
+ }
79
+ function sha256Hex(value) {
80
+ return sha256(value).toString('hex');
81
+ }
82
+ function digestSecret(secret) {
83
+ const bytes = Buffer.isBuffer(secret) ? Buffer.from(secret) : decodeBase64u(secret, 'secret');
84
+ if (bytes.length !== HASH_BYTES)
85
+ throw new TypeError('capability secret must be exactly 32 bytes');
86
+ return { bytes, hash: `sha256:${sha256Hex(bytes)}` };
87
+ }
88
+ function equalHash(expected, actual) {
89
+ if (typeof expected !== 'string' || typeof actual !== 'string')
90
+ return false;
91
+ if (!/^sha256:[0-9a-f]{64}$/.test(expected) || !/^sha256:[0-9a-f]{64}$/.test(actual))
92
+ return false;
93
+ return timingSafeEqual(Buffer.from(expected.slice(7), 'hex'), Buffer.from(actual.slice(7), 'hex'));
94
+ }
95
+ function keyBytes(value, label) {
96
+ if (value?.type === 'private' || value?.type === 'public')
97
+ return value;
98
+ if (typeof value !== 'string' && !Buffer.isBuffer(value))
99
+ throw new TypeError(`${label} must be a Node KeyObject or encoded key`);
100
+ return value;
101
+ }
102
+ function publicKeyB64u(privateKey) {
103
+ return createPublicKey(keyBytes(privateKey, 'issuerPrivateKey'))
104
+ .export({ type: 'spki', format: 'der' })
105
+ .toString('base64url');
106
+ }
107
+ function validateCurrency(currency) {
108
+ if (typeof currency !== 'string' || currency.length === 0 || Buffer.byteLength(currency, 'utf8') > MAX_CURRENCY_BYTES) {
109
+ throw new TypeError('capability currency must be a short non-empty string');
110
+ }
111
+ return currency;
112
+ }
113
+ function validateAmount(amount, label = 'amount') {
114
+ if (!Number.isSafeInteger(amount) || amount < 0)
115
+ throw new TypeError(`capability ${label} must be a non-negative safe integer`);
116
+ return amount;
117
+ }
118
+ function validateOperationId(operationId) {
119
+ if (typeof operationId !== 'string' || operationId.length === 0 || Buffer.byteLength(operationId, 'utf8') > MAX_OPERATION_ID_BYTES) {
120
+ throw new TypeError('operation_id must be a short non-empty string');
121
+ }
122
+ return operationId;
123
+ }
124
+ /** Digest the exact immutable action snapshot exercised under a capability. */
125
+ export function capabilityActionDigest(action) {
126
+ return `sha256:${sha256Hex(Buffer.from(canonicalize(action), 'utf8'))}`;
127
+ }
128
+ function normalizeCapabilityScope(scope) {
129
+ if (!isRecord(scope) || ![CAPABILITY_SCOPE_PROFILE, CAPABILITY_CAID_SCOPE_PROFILE].includes(scope.profile)) {
130
+ throw new TypeError(`capability scope.profile must be ${CAPABILITY_SCOPE_PROFILE} or ${CAPABILITY_CAID_SCOPE_PROFILE}`);
131
+ }
132
+ const memberField = scope.profile === CAPABILITY_CAID_SCOPE_PROFILE ? 'caids' : 'action_digests';
133
+ const members = scope[memberField];
134
+ const memberPattern = scope.profile === CAPABILITY_CAID_SCOPE_PROFILE ? CAID_RE : ACTION_DIGEST_RE;
135
+ if (!Array.isArray(members)
136
+ || members.length < 1
137
+ || members.length > MAX_SCOPE_ACTIONS
138
+ || members.some((member) => typeof member !== 'string' || !memberPattern.test(member))) {
139
+ throw new TypeError(`capability scope.${memberField} must be a bounded non-empty array of canonical identifiers`);
140
+ }
141
+ if (new Set(members).size !== members.length) {
142
+ throw new TypeError(`capability scope.${memberField} must not contain duplicates`);
143
+ }
144
+ if (typeof scope.operation_id_field !== 'string'
145
+ || !/^[A-Za-z_][A-Za-z0-9_.-]{0,127}$/.test(scope.operation_id_field)) {
146
+ throw new TypeError('capability scope.operation_id_field must name a closed action field');
147
+ }
148
+ return {
149
+ profile: scope.profile,
150
+ [memberField]: [...members].sort(),
151
+ operation_id_field: scope.operation_id_field,
152
+ };
153
+ }
154
+ function valueAtPath(action, path) {
155
+ let value = action;
156
+ for (const segment of path.split('.')) {
157
+ if (!isRecord(value) || !Object.hasOwn(value, segment))
158
+ return undefined;
159
+ value = value[segment];
160
+ }
161
+ return value;
162
+ }
163
+ /**
164
+ * @param {object} capability
165
+ * @param {object} action
166
+ * @param {string} operationId
167
+ * @param {object} [options]
168
+ * @param {Function|null} [options.resolveCaid]
169
+ */
170
+ export function verifyCapabilityScope(capability, action, operationId, { resolveCaid = null } = {}) {
171
+ try {
172
+ const scope = normalizeCapabilityScope(capability?.scope);
173
+ const actionDigest = capabilityActionDigest(action);
174
+ let caid = null;
175
+ if (scope.profile === CAPABILITY_CAID_SCOPE_PROFILE) {
176
+ if (typeof resolveCaid !== 'function') {
177
+ return { ok: false, reason: 'capability_caid_resolver_required', action_digest: actionDigest };
178
+ }
179
+ const resolved = resolveCaid(structuredClone(action));
180
+ caid = typeof resolved === 'string' ? resolved : resolved?.caid;
181
+ if (typeof caid !== 'string' || !CAID_RE.test(caid)) {
182
+ return { ok: false, reason: 'capability_caid_resolution_failed', action_digest: actionDigest };
183
+ }
184
+ if (!scope.caids.includes(caid)) {
185
+ return { ok: false, reason: 'capability_action_out_of_scope', action_digest: actionDigest, caid };
186
+ }
187
+ }
188
+ else if (!scope.action_digests.includes(actionDigest)) {
189
+ return { ok: false, reason: 'capability_action_out_of_scope', action_digest: actionDigest };
190
+ }
191
+ if (valueAtPath(action, scope.operation_id_field) !== operationId) {
192
+ return {
193
+ ok: false,
194
+ reason: 'capability_operation_binding_failed',
195
+ action_digest: actionDigest,
196
+ operation_id_field: scope.operation_id_field,
197
+ };
198
+ }
199
+ return {
200
+ ok: true,
201
+ action_digest: actionDigest,
202
+ ...(caid ? { caid } : {}),
203
+ operation_id_field: scope.operation_id_field,
204
+ };
205
+ }
206
+ catch (error) {
207
+ return { ok: false, reason: 'capability_scope_invalid', detail: error?.message || 'invalid capability scope' };
208
+ }
209
+ }
210
+ function validateCapabilityId(capabilityId) {
211
+ if (typeof capabilityId !== 'string' || capabilityId.length === 0 || Buffer.byteLength(capabilityId, 'utf8') > MAX_OPERATION_ID_BYTES) {
212
+ throw new TypeError('capability id must be a short non-empty string');
213
+ }
214
+ return capabilityId;
215
+ }
216
+ function validateExpiry(expiry) {
217
+ const value = typeof expiry === 'number' ? new Date(expiry).toISOString() : expiry;
218
+ const parsed = Date.parse(value);
219
+ if (typeof value !== 'string' || !Number.isFinite(parsed))
220
+ throw new TypeError('capability expiry must be an ISO-8601 timestamp');
221
+ return new Date(parsed).toISOString();
222
+ }
223
+ function validateThreshold(threshold = { m: 1, n: 1 }) {
224
+ if (!isRecord(threshold)
225
+ || !Number.isSafeInteger(threshold.m) || !Number.isSafeInteger(threshold.n)
226
+ || threshold.m < 1 || threshold.n < threshold.m || threshold.n > 255) {
227
+ throw new TypeError('capability threshold must satisfy 1 <= m <= n <= 255');
228
+ }
229
+ return { m: threshold.m, n: threshold.n };
230
+ }
231
+ function validateBaseReceipt(baseReceipt) {
232
+ if (!isRecord(baseReceipt) || baseReceipt['@version'] !== 'EP-RECEIPT-v1' || !isRecord(baseReceipt.payload)) {
233
+ throw new TypeError('capability base receipt must be an EP-RECEIPT-v1 document');
234
+ }
235
+ if (typeof baseReceipt.payload.receipt_id !== 'string' || baseReceipt.payload.receipt_id.length === 0) {
236
+ throw new TypeError('capability base receipt must carry receipt_id');
237
+ }
238
+ if (!isRecord(baseReceipt.payload.claim) || baseReceipt.payload.claim.capability_only !== true) {
239
+ throw new TypeError('capability base receipt must be signed as capability_only');
240
+ }
241
+ return structuredClone(baseReceipt);
242
+ }
243
+ export function capabilityBaseReceiptDigest(receipt) {
244
+ return `sha256:${sha256Hex(Buffer.from(canonicalize(receipt), 'utf8'))}`;
245
+ }
246
+ function capabilityUnsignedBody(receipt, capability) {
247
+ return {
248
+ '@version': CAPABILITY_RECEIPT_VERSION,
249
+ base_receipt_id: receipt.payload.receipt_id,
250
+ base_receipt_digest: capabilityBaseReceiptDigest(receipt),
251
+ capability,
252
+ };
253
+ }
254
+ function capabilitySignature(capabilityReceipt) {
255
+ const signature = capabilityReceipt?.capability_signature;
256
+ return signature && signature.algorithm === 'Ed25519' && typeof signature.value === 'string' && typeof signature.public_key === 'string'
257
+ ? signature
258
+ : null;
259
+ }
260
+ function capabilityEnvelopeFingerprint(capabilityReceipt) {
261
+ const signature = capabilitySignature(capabilityReceipt);
262
+ if (!signature)
263
+ throw new TypeError('capability signature is required for fingerprinting');
264
+ return `sha256:${sha256Hex(Buffer.from(canonicalize({
265
+ '@version': CAPABILITY_RECEIPT_VERSION,
266
+ base_receipt_id: capabilityReceipt.receipt.payload.receipt_id,
267
+ base_receipt_digest: capabilityBaseReceiptDigest(capabilityReceipt.receipt),
268
+ capability: capabilityReceipt.capability,
269
+ issuer_public_key: signature.public_key,
270
+ }), 'utf8'))}`;
271
+ }
272
+ /**
273
+ * Validate a delegation chain at ingest time.
274
+ *
275
+ * Shape and bounded length are not sufficient: a hand-crafted envelope can
276
+ * carry a cyclic or authority-inflating chain and still be internally
277
+ * consistent. This validator enforces three structural invariants that every
278
+ * chain produced by {@link delegateCapabilityReceipt} satisfies and that no
279
+ * cyclic or forged chain can:
280
+ *
281
+ * 1. Acyclicity. Each delegation is a distinct parent spend, so a
282
+ * delegation_id never recurs; a capability delegates at most once as a
283
+ * parent, so a parent_capability_id never recurs. Either repeat is a cycle
284
+ * in the delegation graph and is rejected. The leaf capability id (when
285
+ * supplied) may never appear as one of its own ancestors' parents.
286
+ * 2. Monotonic authority. No hop may grant more than the hop that delegated
287
+ * to it: amounts are non-increasing from root to leaf. This holds
288
+ * standalone here, independent of the runtime parent reserve guard.
289
+ *
290
+ * Fail-closed: any violation throws and the caller treats the envelope as
291
+ * malformed. Signature and per-entry shape checks are unchanged.
292
+ */
293
+ function assertDelegationChain(chain, capabilityId) {
294
+ if (chain === undefined)
295
+ return [];
296
+ if (!Array.isArray(chain) || chain.length > MAX_DELEGATES)
297
+ throw new TypeError('delegation_chain must be a bounded array');
298
+ const seenDelegationIds = new Set();
299
+ const seenParentIds = new Set();
300
+ let previousAmount = null;
301
+ return chain.map((entry) => {
302
+ if (!isRecord(entry)
303
+ || typeof entry.delegation_id !== 'string'
304
+ || typeof entry.parent_capability_id !== 'string'
305
+ || typeof entry.delegate_id !== 'string'
306
+ || !Number.isSafeInteger(entry.amount)
307
+ || entry.amount < 0
308
+ || typeof entry.currency !== 'string'
309
+ || typeof entry.issued_at !== 'string') {
310
+ throw new TypeError('delegation_chain contains an invalid signed entry');
311
+ }
312
+ if (seenDelegationIds.has(entry.delegation_id))
313
+ throw new TypeError('delegation_chain repeats a delegation_id (cyclic or forged chain)');
314
+ if (seenParentIds.has(entry.parent_capability_id))
315
+ throw new TypeError('delegation_chain repeats a parent_capability_id (cyclic delegation)');
316
+ if (capabilityId !== undefined && entry.parent_capability_id === capabilityId)
317
+ throw new TypeError('delegation_chain references the leaf capability as a parent (broken delegation link)');
318
+ if (previousAmount !== null && entry.amount > previousAmount)
319
+ throw new TypeError('delegation_chain grants increasing authority (non-monotonic amount)');
320
+ seenDelegationIds.add(entry.delegation_id);
321
+ seenParentIds.add(entry.parent_capability_id);
322
+ previousAmount = entry.amount;
323
+ return structuredClone(entry);
324
+ });
325
+ }
326
+ function assertCapabilityShape(capability) {
327
+ if (!isRecord(capability) || capability.version !== CAPABILITY_STATE_VERSION)
328
+ throw new TypeError('invalid capability state version');
329
+ validateCapabilityId(capability.id);
330
+ if (typeof capability.secret_hash !== 'string' || !/^sha256:[0-9a-f]{64}$/.test(capability.secret_hash))
331
+ throw new TypeError('capability secret_hash is invalid');
332
+ if (!isRecord(capability.budget))
333
+ throw new TypeError('capability budget is required');
334
+ validateAmount(capability.budget.amount, 'budget.amount');
335
+ validateCurrency(capability.budget.currency);
336
+ validateExpiry(capability.expiry);
337
+ validateThreshold(capability.threshold);
338
+ normalizeCapabilityScope(capability.scope);
339
+ assertDelegationChain(capability.delegation_chain, capability.id);
340
+ if (capability.consumed !== 0)
341
+ throw new TypeError('capability consumed is issuer-initialized and must be zero');
342
+ return true;
343
+ }
344
+ function verifyTrustedIssuer(publicKey, trustedIssuerKeys, allowUntrustedIssuer) {
345
+ if (!Array.isArray(trustedIssuerKeys) || trustedIssuerKeys.length === 0) {
346
+ return allowUntrustedIssuer === true;
347
+ }
348
+ return trustedIssuerKeys.includes(publicKey);
349
+ }
350
+ /**
351
+ * Mint a signed capability envelope. The issuer must sign the capability
352
+ * metadata; a holder cannot enlarge the budget by editing a bearer object.
353
+ * For m-of-n > 1, the raw secret is not returned; distribute the returned
354
+ * shares instead.
355
+ *
356
+ * @param {object} baseReceipt EP-RECEIPT-v1 document
357
+ * @param {object} [options]
358
+ * @param {KeyMaterial} [options.issuerPrivateKey]
359
+ * @param {CapabilityBudget} [options.budget]
360
+ * @param {string|number} [options.expiry]
361
+ * @param {{m:number,n:number}} [options.threshold]
362
+ * @param {object} [options.scope]
363
+ * @param {any[]} [options.delegationChain]
364
+ * @param {string} [options.capabilityId]
365
+ * @param {Buffer|string} [options.secret]
366
+ */
367
+ export function mintCapabilityReceipt(baseReceipt, { issuerPrivateKey, budget, expiry, threshold = { m: 1, n: 1 }, scope, delegationChain = [], capabilityId = randomUUID(), secret = randomBytes(HASH_BYTES), } = {}) {
368
+ const receipt = validateBaseReceipt(baseReceipt);
369
+ if (!issuerPrivateKey)
370
+ throw new TypeError('mintCapabilityReceipt requires issuerPrivateKey');
371
+ if (!isRecord(budget))
372
+ throw new TypeError('capability budget is required');
373
+ const normalizedThreshold = validateThreshold(threshold);
374
+ const normalizedSecret = digestSecret(secret);
375
+ const publicKey = publicKeyB64u(issuerPrivateKey);
376
+ const capability = {
377
+ version: CAPABILITY_STATE_VERSION,
378
+ id: validateCapabilityId(String(capabilityId)),
379
+ secret_hash: normalizedSecret.hash,
380
+ budget: { amount: validateAmount(budget.amount, 'budget.amount'), currency: validateCurrency(budget.currency) },
381
+ consumed: 0,
382
+ threshold: normalizedThreshold,
383
+ scope: normalizeCapabilityScope(scope),
384
+ delegation_chain: assertDelegationChain(delegationChain),
385
+ expiry: validateExpiry(expiry),
386
+ };
387
+ assertCapabilityShape(capability);
388
+ const value = sign(null, Buffer.from(canonicalize(capabilityUnsignedBody(receipt, capability)), 'utf8'), keyBytes(issuerPrivateKey, 'issuerPrivateKey')).toString('base64url');
389
+ const capabilityReceipt = {
390
+ '@version': CAPABILITY_RECEIPT_VERSION,
391
+ receipt,
392
+ capability,
393
+ capability_signature: { algorithm: 'Ed25519', public_key: publicKey, value },
394
+ };
395
+ const shares = normalizedThreshold.m === 1 && normalizedThreshold.n === 1
396
+ ? null
397
+ : splitCapabilitySecret(normalizedSecret.bytes, normalizedThreshold);
398
+ return Object.freeze({
399
+ capabilityReceipt: Object.freeze(capabilityReceipt),
400
+ secret: shares ? null : Buffer.from(normalizedSecret.bytes),
401
+ shares,
402
+ });
403
+ }
404
+ /**
405
+ * Verify the issuer signature and immutable capability metadata.
406
+ * @param {object} capabilityReceipt
407
+ * @param {object} [options]
408
+ * @param {string[]} [options.trustedIssuerKeys]
409
+ * @param {boolean} [options.allowUntrustedIssuer]
410
+ */
411
+ export function verifyCapabilityReceipt(capabilityReceipt, { trustedIssuerKeys = [], allowUntrustedIssuer = false, } = {}) {
412
+ try {
413
+ if (!isRecord(capabilityReceipt) || capabilityReceipt['@version'] !== CAPABILITY_RECEIPT_VERSION)
414
+ return { ok: false, reason: 'malformed_capability_receipt' };
415
+ const receipt = validateBaseReceipt(capabilityReceipt.receipt);
416
+ assertCapabilityShape(capabilityReceipt.capability);
417
+ const signature = capabilitySignature(capabilityReceipt);
418
+ if (!signature || !verifyTrustedIssuer(signature.public_key, trustedIssuerKeys, allowUntrustedIssuer)) {
419
+ return { ok: false, reason: 'capability_issuer_not_trusted' };
420
+ }
421
+ const ok = verify(null, Buffer.from(canonicalize(capabilityUnsignedBody(receipt, capabilityReceipt.capability)), 'utf8'), createPublicKey({ key: Buffer.from(signature.public_key, 'base64url'), format: 'der', type: 'spki' }), Buffer.from(signature.value, 'base64url'));
422
+ return ok ? { ok: true, receipt, capability: capabilityReceipt.capability, issuer_public_key: signature.public_key } : { ok: false, reason: 'capability_signature_invalid' };
423
+ }
424
+ catch (error) {
425
+ return { ok: false, reason: 'capability_malformed', detail: error?.message || 'invalid capability' };
426
+ }
427
+ }
428
+ function fieldToBytes(value) {
429
+ const bytes = Buffer.alloc(SHARE_BYTES);
430
+ let remaining = BigInt(value);
431
+ for (let i = SHARE_BYTES - 1; i >= 0; i -= 1) {
432
+ bytes[i] = Number(remaining & 0xffn);
433
+ remaining >>= 8n;
434
+ }
435
+ return bytes;
436
+ }
437
+ function bytesToField(bytes) {
438
+ let value = 0n;
439
+ for (const byte of bytes)
440
+ value = (value << 8n) | BigInt(byte);
441
+ return value % FIELD;
442
+ }
443
+ function mod(value) {
444
+ const result = value % FIELD;
445
+ return result < 0n ? result + FIELD : result;
446
+ }
447
+ function modInverse(value) {
448
+ let a = mod(value);
449
+ let b = FIELD;
450
+ let x = 1n;
451
+ let y = 0n;
452
+ while (b !== 0n) {
453
+ const quotient = a / b;
454
+ [a, b] = [b, a - quotient * b];
455
+ [x, y] = [y, x - quotient * y];
456
+ }
457
+ if (a !== 1n)
458
+ throw new Error('capability field inverse does not exist');
459
+ return mod(x);
460
+ }
461
+ function randomField(randomBytesFn) {
462
+ while (true) {
463
+ const bytes = Buffer.from(randomBytesFn(SHARE_BYTES));
464
+ if (bytes.length !== SHARE_BYTES)
465
+ throw new TypeError('randomBytesFn returned the wrong length');
466
+ // 66 bytes carry 528 bits; clear the seven unused high bits so the
467
+ // candidate is sampled directly from the 521-bit field instead of using
468
+ // a biased modulo reduction.
469
+ bytes[0] &= 0x01;
470
+ const value = bytesToField(bytes);
471
+ if (value !== 0n && value < FIELD)
472
+ return value;
473
+ }
474
+ }
475
+ /** Split the 32-byte capability secret using Shamir's polynomial scheme. */
476
+ export function splitCapabilitySecret(secret, threshold, { randomBytesFn = randomBytes } = {}) {
477
+ const normalized = digestSecret(secret);
478
+ const { m, n } = validateThreshold(threshold);
479
+ const coefficients = [bytesToField(normalized.bytes)];
480
+ for (let i = 1; i < m; i += 1)
481
+ coefficients.push(randomField(randomBytesFn));
482
+ const shares = [];
483
+ for (let x = 1; x <= n; x += 1) {
484
+ let y = 0n;
485
+ let power = 1n;
486
+ for (const coefficient of coefficients) {
487
+ y = mod(y + coefficient * power);
488
+ power = mod(power * BigInt(x));
489
+ }
490
+ shares.push(`ep-share-v1.${x}.${base64u(fieldToBytes(y))}`);
491
+ }
492
+ return shares;
493
+ }
494
+ function parseShare(share) {
495
+ if (typeof share !== 'string')
496
+ throw new TypeError('capability share must be a string');
497
+ const parts = share.split('.');
498
+ if (parts.length !== 3 || parts[0] !== 'ep-share-v1')
499
+ throw new TypeError('invalid capability share version');
500
+ const x = Number(parts[1]);
501
+ if (!Number.isSafeInteger(x) || x < 1 || x > 255)
502
+ throw new TypeError('capability share index is invalid');
503
+ const y = decodeBase64u(parts[2], 'capability share');
504
+ if (y.length !== SHARE_BYTES)
505
+ throw new TypeError('capability share scalar has the wrong length');
506
+ return { x, y: bytesToField(y) };
507
+ }
508
+ /** Reconstruct a capability secret from at least m unique shares. */
509
+ export function reconstructCapabilitySecret(shares, threshold) {
510
+ const { m, n } = validateThreshold(threshold);
511
+ if (!Array.isArray(shares) || shares.length < m || shares.length > n)
512
+ throw new TypeError('insufficient capability shares');
513
+ const parsed = shares.map(parseShare);
514
+ if (new Set(parsed.map((share) => share.x)).size !== parsed.length)
515
+ throw new TypeError('duplicate capability share index');
516
+ let secret = 0n;
517
+ for (const current of parsed) {
518
+ let numerator = 1n;
519
+ let denominator = 1n;
520
+ for (const other of parsed) {
521
+ if (current.x === other.x)
522
+ continue;
523
+ numerator = mod(numerator * BigInt(-other.x));
524
+ denominator = mod(denominator * BigInt(current.x - other.x));
525
+ }
526
+ secret = mod(secret + current.y * numerator * modInverse(denominator));
527
+ }
528
+ return fieldToBytes(secret).subarray(SHARE_BYTES - HASH_BYTES);
529
+ }
530
+ function capabilityStateFromEnvelope(capabilityReceipt) {
531
+ const c = capabilityReceipt.capability;
532
+ return {
533
+ capability_id: c.id,
534
+ capability_fingerprint: capabilityEnvelopeFingerprint(capabilityReceipt),
535
+ budget_amount: c.budget.amount,
536
+ currency: c.budget.currency,
537
+ expires_at: Date.parse(c.expiry),
538
+ };
539
+ }
540
+ /**
541
+ * An in-memory atomic reference store. It is intentionally marked non-durable
542
+ * and is suitable only for tests; production callers must use an implementation
543
+ * backed by a transactional database or equivalent linearizable store.
544
+ */
545
+ export function createMemoryCapabilityStore() {
546
+ const states = new Map();
547
+ const operations = new Map();
548
+ return {
549
+ durable: false,
550
+ registerCapability(capabilityReceipt) {
551
+ const verified = verifyCapabilityReceipt(capabilityReceipt, { allowUntrustedIssuer: true });
552
+ if (!verified.ok)
553
+ return false;
554
+ const state = capabilityStateFromEnvelope(capabilityReceipt);
555
+ const existing = states.get(state.capability_id);
556
+ if (existing) {
557
+ return existing.capability_fingerprint === state.capability_fingerprint
558
+ && existing.budget_amount === state.budget_amount
559
+ && existing.currency === state.currency
560
+ && existing.expires_at === state.expires_at;
561
+ }
562
+ states.set(state.capability_id, { ...state, consumed_amount: 0, reserved_amount: 0 });
563
+ return true;
564
+ },
565
+ async reserveSpend({ capabilityId, capabilityFingerprint, operationId, actionDigest, amount, currency, now = Date.now }) {
566
+ validateOperationId(operationId);
567
+ if (typeof actionDigest !== 'string' || !ACTION_DIGEST_RE.test(actionDigest))
568
+ throw new TypeError('action_digest must be SHA-256');
569
+ validateAmount(amount);
570
+ validateCurrency(currency);
571
+ const state = states.get(capabilityId);
572
+ if (!state)
573
+ return { ok: false, reason: 'capability_not_registered' };
574
+ if (state.capability_fingerprint !== capabilityFingerprint)
575
+ return { ok: false, reason: 'capability_envelope_mismatch' };
576
+ const existing = operations.get(operationId);
577
+ if (existing)
578
+ return { ok: false, reason: existing.status === 'reserved' ? 'operation_in_flight' : 'operation_already_committed' };
579
+ const at = nowMs(now);
580
+ if (at >= state.expires_at)
581
+ return { ok: false, reason: 'capability_expired' };
582
+ if (currency !== state.currency)
583
+ return { ok: false, reason: 'currency_mismatch' };
584
+ if (state.consumed_amount + state.reserved_amount + amount > state.budget_amount)
585
+ return { ok: false, reason: 'budget_exceeded' };
586
+ const reservationToken = randomUUID();
587
+ operations.set(operationId, { capability_id: capabilityId, action_digest: actionDigest, amount, currency, status: 'reserved', reservation_token: reservationToken, reserved_at: at });
588
+ state.reserved_amount += amount;
589
+ return { ok: true, operation_id: operationId, reservation_token: reservationToken, remaining: state.budget_amount - state.consumed_amount - state.reserved_amount };
590
+ },
591
+ async commitSpend({ capabilityId, operationId, reservationToken, outcome = 'executed', now = Date.now } = {}) {
592
+ const operation = operations.get(operationId);
593
+ const state = states.get(capabilityId);
594
+ if (!operation || !state || operation.capability_id !== capabilityId)
595
+ return { ok: false, reason: 'capability_operation_not_found' };
596
+ if (operation.status !== 'reserved')
597
+ return { ok: false, reason: 'capability_operation_already_finalized' };
598
+ if (operation.reservation_token !== reservationToken)
599
+ return { ok: false, reason: 'capability_reservation_owner_mismatch' };
600
+ operation.status = 'committed';
601
+ operation.outcome = outcome;
602
+ operation.committed_at = nowMs(now);
603
+ state.reserved_amount -= operation.amount;
604
+ state.consumed_amount += operation.amount;
605
+ return { ok: true, outcome, consumed: state.consumed_amount, remaining: state.budget_amount - state.consumed_amount - state.reserved_amount };
606
+ },
607
+ async reconcileSpend({ capabilityId, operationId, actionDigest, evidenceDigest, outcome = 'executed', now = Date.now } = {}) {
608
+ const operation = operations.get(operationId);
609
+ if (!operation || operation.capability_id !== capabilityId)
610
+ return { ok: false, reason: 'capability_operation_not_found' };
611
+ if (operation.status !== 'committed' || operation.outcome !== 'indeterminate')
612
+ return { ok: false, reason: 'capability_operation_not_indeterminate' };
613
+ if (operation.action_digest !== actionDigest)
614
+ return { ok: false, reason: 'capability_reconciliation_action_mismatch' };
615
+ if (outcome !== 'executed' || typeof evidenceDigest !== 'string' || !ACTION_DIGEST_RE.test(evidenceDigest)) {
616
+ return { ok: false, reason: 'capability_reconciliation_evidence_invalid' };
617
+ }
618
+ if (operation.reconciliation_outcome) {
619
+ return operation.reconciliation_outcome === outcome && operation.reconciliation_evidence_digest === evidenceDigest
620
+ ? { ok: true, idempotent: true, outcome }
621
+ : { ok: false, reason: 'capability_reconciliation_conflict' };
622
+ }
623
+ operation.reconciliation_outcome = outcome;
624
+ operation.reconciliation_evidence_digest = evidenceDigest;
625
+ operation.reconciled_at = nowMs(now);
626
+ return { ok: true, idempotent: false, outcome };
627
+ },
628
+ getState(capabilityId) {
629
+ const state = states.get(capabilityId);
630
+ return state ? Object.freeze({ ...state }) : null;
631
+ },
632
+ getOperation(operationId) {
633
+ const operation = operations.get(operationId);
634
+ return operation ? Object.freeze({ ...operation }) : null;
635
+ },
636
+ };
637
+ }
638
+ export const CAPABILITY_STATE_TABLE = 'ep_capability_state';
639
+ export const CAPABILITY_OPERATION_TABLE = 'ep_capability_operations';
640
+ export const CAPABILITY_STATE_DDL = `CREATE TABLE IF NOT EXISTS ${CAPABILITY_STATE_TABLE} (
641
+ capability_id TEXT PRIMARY KEY,
642
+ capability_fingerprint TEXT NOT NULL CHECK (capability_fingerprint ~ '^sha256:[0-9a-f]{64}$'),
643
+ budget_amount BIGINT NOT NULL CHECK (budget_amount >= 0),
644
+ currency TEXT NOT NULL,
645
+ consumed_amount BIGINT NOT NULL DEFAULT 0 CHECK (consumed_amount >= 0),
646
+ reserved_amount BIGINT NOT NULL DEFAULT 0 CHECK (reserved_amount >= 0),
647
+ expires_at TIMESTAMPTZ NOT NULL,
648
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
649
+ );
650
+ ALTER TABLE ${CAPABILITY_STATE_TABLE} ADD COLUMN IF NOT EXISTS capability_fingerprint TEXT;
651
+ CREATE TABLE IF NOT EXISTS ${CAPABILITY_OPERATION_TABLE} (
652
+ operation_id TEXT PRIMARY KEY,
653
+ capability_id TEXT NOT NULL REFERENCES ${CAPABILITY_STATE_TABLE}(capability_id),
654
+ action_digest TEXT NOT NULL CHECK (action_digest ~ '^sha256:[0-9a-f]{64}$'),
655
+ amount BIGINT NOT NULL CHECK (amount > 0),
656
+ currency TEXT NOT NULL,
657
+ status TEXT NOT NULL CHECK (status IN ('reserved', 'committed')),
658
+ reservation_token TEXT NOT NULL,
659
+ outcome TEXT,
660
+ reconciliation_outcome TEXT CHECK (reconciliation_outcome IN ('executed')),
661
+ reconciliation_evidence_digest TEXT CHECK (reconciliation_evidence_digest ~ '^sha256:[0-9a-f]{64}$'),
662
+ reserved_at TIMESTAMPTZ NOT NULL,
663
+ committed_at TIMESTAMPTZ,
664
+ reconciled_at TIMESTAMPTZ,
665
+ CHECK (
666
+ (reconciliation_outcome IS NULL AND reconciliation_evidence_digest IS NULL AND reconciled_at IS NULL)
667
+ OR
668
+ (reconciliation_outcome IS NOT NULL AND reconciliation_evidence_digest IS NOT NULL AND reconciled_at IS NOT NULL)
669
+ )
670
+ );
671
+ CREATE INDEX IF NOT EXISTS ${CAPABILITY_OPERATION_TABLE}_capability_idx ON ${CAPABILITY_OPERATION_TABLE}(capability_id);`;
672
+ export const CAPABILITY_SQL = Object.freeze({
673
+ register: `INSERT INTO ${CAPABILITY_STATE_TABLE} (capability_id, budget_amount, currency, expires_at, capability_fingerprint) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (capability_id) DO UPDATE SET capability_fingerprint = COALESCE(${CAPABILITY_STATE_TABLE}.capability_fingerprint, EXCLUDED.capability_fingerprint) WHERE ${CAPABILITY_STATE_TABLE}.budget_amount = EXCLUDED.budget_amount AND ${CAPABILITY_STATE_TABLE}.currency = EXCLUDED.currency AND ${CAPABILITY_STATE_TABLE}.expires_at = EXCLUDED.expires_at`,
674
+ readState: `SELECT capability_id, capability_fingerprint, budget_amount, currency, consumed_amount, reserved_amount, expires_at FROM ${CAPABILITY_STATE_TABLE} WHERE capability_id = $1 FOR UPDATE`,
675
+ readOperation: `SELECT operation_id, capability_id, action_digest, amount, currency, status, reservation_token, outcome, reconciliation_outcome, reconciliation_evidence_digest, reconciled_at FROM ${CAPABILITY_OPERATION_TABLE} WHERE operation_id = $1 FOR UPDATE`,
676
+ insertOperation: `INSERT INTO ${CAPABILITY_OPERATION_TABLE} (operation_id, capability_id, action_digest, amount, currency, status, reservation_token, reserved_at) VALUES ($1, $2, $3, $4, $5, 'reserved', $6, $7)`,
677
+ reserveState: `UPDATE ${CAPABILITY_STATE_TABLE} SET reserved_amount = reserved_amount + $2 WHERE capability_id = $1 AND budget_amount - consumed_amount - reserved_amount >= $2`,
678
+ commitOperation: `UPDATE ${CAPABILITY_OPERATION_TABLE} SET status = 'committed', outcome = $3, committed_at = $4 WHERE operation_id = $1 AND capability_id = $2 AND status = 'reserved' AND reservation_token = $5`,
679
+ reconcileOperation: `UPDATE ${CAPABILITY_OPERATION_TABLE} SET reconciliation_outcome = $3, reconciliation_evidence_digest = $4, reconciled_at = $5 WHERE operation_id = $1 AND capability_id = $2 AND status = 'committed' AND outcome = 'indeterminate' AND reconciliation_outcome IS NULL`,
680
+ commitState: `UPDATE ${CAPABILITY_STATE_TABLE} SET reserved_amount = reserved_amount - $2, consumed_amount = consumed_amount + $2 WHERE capability_id = $1 AND reserved_amount >= $2`,
681
+ });
682
+ /**
683
+ * Production adapter. `transaction` MUST run the callback on one database
684
+ * connection with BEGIN/COMMIT/ROLLBACK. The state row is locked before the
685
+ * operation row is inserted, making budget reservation linearizable per
686
+ * capability and refusing all ambiguous database outcomes.
687
+ *
688
+ * @param {object} [options]
689
+ * @param {(callback: (query: Function) => any) => any} [options.transaction]
690
+ */
691
+ export function createPostgresCapabilityStore({ transaction } = {}) {
692
+ if (typeof transaction !== 'function')
693
+ throw new TypeError('createPostgresCapabilityStore requires a transaction(callback) function');
694
+ return {
695
+ durable: true,
696
+ async registerCapability(capabilityReceipt) {
697
+ const verified = verifyCapabilityReceipt(capabilityReceipt, { allowUntrustedIssuer: true });
698
+ if (!verified.ok)
699
+ return false;
700
+ const state = capabilityStateFromEnvelope(capabilityReceipt);
701
+ return transaction(async (query) => {
702
+ await query(CAPABILITY_SQL.register, [state.capability_id, state.budget_amount, capabilityReceipt.capability.budget.currency, new Date(state.expires_at).toISOString(), state.capability_fingerprint]);
703
+ const result = await query(CAPABILITY_SQL.readState, [state.capability_id]);
704
+ const row = result?.rows?.[0];
705
+ return Boolean(row)
706
+ && row.capability_fingerprint === state.capability_fingerprint
707
+ && Number(row.budget_amount) === state.budget_amount
708
+ && row.currency === state.currency
709
+ && Date.parse(row.expires_at) === state.expires_at;
710
+ });
711
+ },
712
+ async reserveSpend({ capabilityId, capabilityFingerprint, operationId, actionDigest, amount, currency, now = Date.now }) {
713
+ validateOperationId(operationId);
714
+ validateAmount(amount);
715
+ validateCurrency(currency);
716
+ if (typeof actionDigest !== 'string' || !ACTION_DIGEST_RE.test(actionDigest))
717
+ throw new TypeError('action_digest must be SHA-256');
718
+ const at = nowMs(now);
719
+ return transaction(async (query) => {
720
+ const stateResult = await query(CAPABILITY_SQL.readState, [capabilityId]);
721
+ const state = stateResult?.rows?.[0];
722
+ if (!state)
723
+ return { ok: false, reason: 'capability_not_registered' };
724
+ if (state.capability_fingerprint !== capabilityFingerprint)
725
+ return { ok: false, reason: 'capability_envelope_mismatch' };
726
+ const operationResult = await query(CAPABILITY_SQL.readOperation, [operationId]);
727
+ if (operationResult?.rows?.[0])
728
+ return { ok: false, reason: operationResult.rows[0].status === 'reserved' ? 'operation_in_flight' : 'operation_already_committed' };
729
+ if (at >= Date.parse(state.expires_at))
730
+ return { ok: false, reason: 'capability_expired' };
731
+ if (currency !== state.currency)
732
+ return { ok: false, reason: 'currency_mismatch' };
733
+ const available = Number(state.budget_amount) - Number(state.consumed_amount) - Number(state.reserved_amount);
734
+ if (!Number.isSafeInteger(available) || available < amount)
735
+ return { ok: false, reason: 'budget_exceeded' };
736
+ const token = randomUUID();
737
+ const reserved = await query(CAPABILITY_SQL.reserveState, [capabilityId, amount]);
738
+ if (reserved?.rowCount !== 1)
739
+ return { ok: false, reason: 'budget_reservation_conflict' };
740
+ await query(CAPABILITY_SQL.insertOperation, [operationId, capabilityId, actionDigest, amount, currency, token, new Date(at).toISOString()]);
741
+ return { ok: true, operation_id: operationId, reservation_token: token, remaining: available - amount };
742
+ });
743
+ },
744
+ async commitSpend({ capabilityId, operationId, reservationToken, outcome = 'executed', now = Date.now } = {}) {
745
+ validateOperationId(operationId);
746
+ if (typeof reservationToken !== 'string' || reservationToken.length < 16)
747
+ return { ok: false, reason: 'capability_reservation_token_invalid' };
748
+ const at = nowMs(now);
749
+ return transaction(async (query) => {
750
+ const operationResult = await query(CAPABILITY_SQL.readOperation, [operationId]);
751
+ const operation = operationResult?.rows?.[0];
752
+ if (!operation || operation.capability_id !== capabilityId)
753
+ return { ok: false, reason: 'capability_operation_not_found' };
754
+ if (operation.status !== 'reserved')
755
+ return { ok: false, reason: 'capability_operation_already_finalized' };
756
+ if (operation.reservation_token !== reservationToken)
757
+ return { ok: false, reason: 'capability_reservation_owner_mismatch' };
758
+ const committed = await query(CAPABILITY_SQL.commitOperation, [operationId, capabilityId, outcome, new Date(at).toISOString(), reservationToken]);
759
+ if (committed?.rowCount !== 1)
760
+ throw new Error('capability operation transition lost ownership; transaction must roll back');
761
+ const updated = await query(CAPABILITY_SQL.commitState, [capabilityId, operation.amount]);
762
+ if (updated?.rowCount !== 1)
763
+ throw new Error('capability state transition conflicted; transaction must roll back');
764
+ return { ok: true, outcome, consumed: null, remaining: null };
765
+ });
766
+ },
767
+ async reconcileSpend({ capabilityId, operationId, actionDigest, evidenceDigest, outcome = 'executed', now = Date.now } = {}) {
768
+ validateOperationId(operationId);
769
+ if (typeof actionDigest !== 'string' || !ACTION_DIGEST_RE.test(actionDigest)
770
+ || typeof evidenceDigest !== 'string' || !ACTION_DIGEST_RE.test(evidenceDigest)
771
+ || outcome !== 'executed') {
772
+ return { ok: false, reason: 'capability_reconciliation_evidence_invalid' };
773
+ }
774
+ const at = nowMs(now);
775
+ return transaction(async (query) => {
776
+ const operationResult = await query(CAPABILITY_SQL.readOperation, [operationId]);
777
+ const operation = operationResult?.rows?.[0];
778
+ if (!operation || operation.capability_id !== capabilityId)
779
+ return { ok: false, reason: 'capability_operation_not_found' };
780
+ if (operation.status !== 'committed' || operation.outcome !== 'indeterminate')
781
+ return { ok: false, reason: 'capability_operation_not_indeterminate' };
782
+ if (operation.action_digest !== actionDigest)
783
+ return { ok: false, reason: 'capability_reconciliation_action_mismatch' };
784
+ if (operation.reconciliation_outcome) {
785
+ return operation.reconciliation_outcome === outcome
786
+ && operation.reconciliation_evidence_digest === evidenceDigest
787
+ ? { ok: true, idempotent: true, outcome }
788
+ : { ok: false, reason: 'capability_reconciliation_conflict' };
789
+ }
790
+ const updated = await query(CAPABILITY_SQL.reconcileOperation, [
791
+ operationId,
792
+ capabilityId,
793
+ outcome,
794
+ evidenceDigest,
795
+ new Date(at).toISOString(),
796
+ ]);
797
+ if (updated?.rowCount !== 1)
798
+ throw new Error('capability reconciliation transition conflicted; transaction must roll back');
799
+ return { ok: true, idempotent: false, outcome };
800
+ });
801
+ },
802
+ };
803
+ }
804
+ function verifySecret(capability, secret) {
805
+ const normalized = digestSecret(secret);
806
+ return equalHash(capability.secret_hash, normalized.hash);
807
+ }
808
+ function capabilityAmount(action, capability, verifiedAction = action) {
809
+ const amount = validateAmount(action?.amount, 'action.amount');
810
+ const currency = validateCurrency(action?.currency);
811
+ if (currency !== capability.budget.currency)
812
+ throw new TypeError('capability action currency does not match the budget');
813
+ const verifiedAmount = Number.isSafeInteger(verifiedAction?.amount)
814
+ ? verifiedAction.amount
815
+ : verifiedAction?.amount_usd;
816
+ if (verifiedAmount !== amount || verifiedAction?.currency !== currency) {
817
+ throw new TypeError('capability budget projection does not match the verified action');
818
+ }
819
+ if (amount <= 0)
820
+ throw new TypeError('capability action amount must be greater than zero');
821
+ return { amount, currency };
822
+ }
823
+ /**
824
+ * Execute one spend under a capability. The base EP receipt is checked on
825
+ * every spend with consumptionMode=none; the capability store is the replay
826
+ * and budget authority. The external function is entered only after the
827
+ * atomic reservation succeeds. `action` is the budget projection; the
828
+ * external function receives only a clone of the exact verified
829
+ * `observedAction ?? action`. Any exception after entry permanently commits
830
+ * the reserved amount as indeterminate.
831
+ *
832
+ * @param {object} [options]
833
+ * @param {object} [options.capabilityReceipt]
834
+ * @param {Buffer|string} [options.secret]
835
+ * @param {{amount:number,currency:string}} [options.action]
836
+ * @param {any} [options.store]
837
+ * @param {Function} [options.executeAction]
838
+ * @param {any} [options.gate]
839
+ * @param {object} [options.selector]
840
+ * @param {object|null} [options.observedAction]
841
+ * @param {string[]} [options.trustedIssuerKeys]
842
+ * @param {Function|null} [options.verifyBaseReceipt]
843
+ * @param {Function|null} [options.resolveCaid]
844
+ * @param {string|null} [options.operationId]
845
+ * @param {number|(() => number)} [options.now]
846
+ * @param {boolean} [options.thresholdSecretVerified]
847
+ */
848
+ export async function executeWithCapability({ capabilityReceipt, secret, action, store, executeAction, gate = null, selector = {}, observedAction = null, trustedIssuerKeys = [], verifyBaseReceipt = null, resolveCaid = null, operationId = null, now = Date.now, thresholdSecretVerified = false, } = {}) {
849
+ const verified = verifyCapabilityReceipt(capabilityReceipt, { trustedIssuerKeys });
850
+ if (!verified.ok)
851
+ return { ok: false, reason: verified.reason };
852
+ if ((verified.capability.threshold.m !== 1 || verified.capability.threshold.n !== 1) && thresholdSecretVerified !== true)
853
+ return { ok: false, reason: 'threshold_shares_required' };
854
+ if (!verifySecret(verified.capability, secret))
855
+ return { ok: false, reason: 'invalid_secret' };
856
+ if (!store || typeof store.reserveSpend !== 'function' || typeof store.commitSpend !== 'function')
857
+ return { ok: false, reason: 'capability_store_required' };
858
+ if (typeof executeAction !== 'function')
859
+ throw new TypeError('executeWithCapability requires executeAction');
860
+ try {
861
+ validateOperationId(operationId);
862
+ }
863
+ catch {
864
+ return { ok: false, reason: 'capability_operation_id_required' };
865
+ }
866
+ let immutableAction;
867
+ let scope;
868
+ try {
869
+ immutableAction = deepFreeze(structuredClone(observedAction ?? action));
870
+ scope = verifyCapabilityScope(verified.capability, immutableAction, operationId, { resolveCaid });
871
+ }
872
+ catch {
873
+ return { ok: false, reason: 'capability_action_invalid' };
874
+ }
875
+ if (!scope.ok)
876
+ return { ok: false, reason: scope.reason, scope };
877
+ let authorization = null;
878
+ if (gate && typeof gate.check === 'function') {
879
+ authorization = await gate.check({
880
+ selector,
881
+ receipt: verified.receipt,
882
+ observedAction: immutableAction,
883
+ consumptionMode: 'none',
884
+ capability: { capabilityReceipt, action: immutableAction, operationId },
885
+ });
886
+ if (!authorization?.allow)
887
+ return { ok: false, reason: 'base_receipt_rejected', authorization };
888
+ }
889
+ else if (typeof verifyBaseReceipt === 'function') {
890
+ const result = await verifyBaseReceipt(verified.receipt, {
891
+ action: immutableAction,
892
+ selector,
893
+ observedAction: immutableAction,
894
+ scope,
895
+ });
896
+ if (result !== true && result?.ok !== true)
897
+ return { ok: false, reason: 'base_receipt_rejected', authorization: result };
898
+ }
899
+ else {
900
+ return { ok: false, reason: 'base_receipt_verifier_required' };
901
+ }
902
+ let spend;
903
+ try {
904
+ // `action` is the budget projection used by Gate integrations; it must
905
+ // match the verified action and can never reach the effect.
906
+ spend = capabilityAmount(action, verified.capability, immutableAction);
907
+ }
908
+ catch (error) {
909
+ return { ok: false, reason: error?.message || 'capability_action_invalid', authorization };
910
+ }
911
+ const reserved = await store.reserveSpend({
912
+ capabilityId: verified.capability.id,
913
+ capabilityFingerprint: capabilityEnvelopeFingerprint(capabilityReceipt),
914
+ operationId,
915
+ actionDigest: scope.action_digest,
916
+ amount: spend.amount,
917
+ currency: spend.currency,
918
+ now,
919
+ });
920
+ if (!reserved?.ok)
921
+ return { ok: false, reason: reserved?.reason || 'capability_reservation_refused', authorization };
922
+ try {
923
+ const result = await executeAction(structuredClone(immutableAction), {
924
+ capabilityReceipt,
925
+ authorization,
926
+ operation_id: operationId,
927
+ action_digest: scope.action_digest,
928
+ ...(scope.caid ? { caid: scope.caid } : {}),
929
+ observed_action: immutableAction,
930
+ reservation: reserved,
931
+ });
932
+ const committed = await store.commitSpend({ capabilityId: verified.capability.id, operationId, reservationToken: reserved.reservation_token, outcome: 'executed', now });
933
+ if (!committed?.ok)
934
+ return { ok: false, reason: 'capability_commit_indeterminate', authorization, result, operation_id: operationId };
935
+ return {
936
+ ok: true,
937
+ result,
938
+ authorization,
939
+ operation_id: operationId,
940
+ action_digest: scope.action_digest,
941
+ ...(scope.caid ? { caid: scope.caid } : {}),
942
+ remaining: committed.remaining,
943
+ };
944
+ }
945
+ catch (error) {
946
+ const committed = await store.commitSpend({ capabilityId: verified.capability.id, operationId, reservationToken: reserved.reservation_token, outcome: 'indeterminate', now }).catch(() => ({ ok: false }));
947
+ return {
948
+ ok: false,
949
+ reason: committed.ok ? 'effect_indeterminate' : 'capability_commit_indeterminate',
950
+ authorization,
951
+ operation_id: operationId,
952
+ action_digest: scope.action_digest,
953
+ ...(scope.caid ? { caid: scope.caid } : {}),
954
+ };
955
+ }
956
+ }
957
+ /**
958
+ * Execute a capability requiring m-of-n Shamir shares.
959
+ * @param {Record<string, any>} [args] capabilityReceipt, shares, and executeWithCapability passthrough options
960
+ */
961
+ export async function executeWithThreshold({ capabilityReceipt, shares, ...options } = {}) {
962
+ const verified = verifyCapabilityReceipt(capabilityReceipt, { trustedIssuerKeys: options.trustedIssuerKeys || [] });
963
+ if (!verified.ok)
964
+ return { ok: false, reason: verified.reason };
965
+ try {
966
+ const secret = reconstructCapabilitySecret(shares, verified.capability.threshold);
967
+ return executeWithCapability({ ...options, capabilityReceipt, secret, thresholdSecretVerified: true });
968
+ }
969
+ catch (error) {
970
+ return { ok: false, reason: error?.message === 'insufficient capability shares' ? 'insufficient_shares' : 'invalid_shares' };
971
+ }
972
+ }
973
+ /**
974
+ * Authentically reconcile a committed indeterminate capability operation.
975
+ * The generic path records only a proven `executed` outcome and never restores
976
+ * budget. A deployment that wants to prove the effect boundary was not crossed
977
+ * needs a separate, action-specific negative-evidence profile.
978
+ *
979
+ * @param {object} [options]
980
+ * @param {any} [options.store]
981
+ * @param {string} [options.capabilityId]
982
+ * @param {string} [options.operationId]
983
+ * @param {object} [options.action]
984
+ * @param {object} [options.evidence]
985
+ * @param {Function} [options.verifyEvidence]
986
+ * @param {number|(() => number)} [options.now]
987
+ */
988
+ export async function reconcileCapabilityOperation({ store, capabilityId, operationId, action, evidence, verifyEvidence, now = Date.now, } = {}) {
989
+ if (!store || typeof store.reconcileSpend !== 'function')
990
+ return { ok: false, reason: 'capability_reconciliation_store_required' };
991
+ try {
992
+ validateCapabilityId(capabilityId);
993
+ validateOperationId(operationId);
994
+ }
995
+ catch {
996
+ return { ok: false, reason: 'capability_reconciliation_operation_invalid' };
997
+ }
998
+ if (typeof verifyEvidence !== 'function')
999
+ return { ok: false, reason: 'capability_reconciliation_verifier_required' };
1000
+ let actionDigest;
1001
+ let verified;
1002
+ try {
1003
+ const immutableAction = structuredClone(action);
1004
+ actionDigest = capabilityActionDigest(immutableAction);
1005
+ verified = await verifyEvidence(structuredClone(evidence), {
1006
+ capability_id: capabilityId,
1007
+ operation_id: operationId,
1008
+ action: immutableAction,
1009
+ action_digest: actionDigest,
1010
+ });
1011
+ }
1012
+ catch {
1013
+ return { ok: false, reason: 'capability_reconciliation_evidence_rejected' };
1014
+ }
1015
+ if (!isRecord(verified) || verified.valid !== true
1016
+ || verified.outcome !== 'executed'
1017
+ || verified.action_digest !== actionDigest
1018
+ || typeof verified.evidence_digest !== 'string'
1019
+ || !ACTION_DIGEST_RE.test(verified.evidence_digest)) {
1020
+ return { ok: false, reason: 'capability_reconciliation_evidence_rejected' };
1021
+ }
1022
+ const result = await store.reconcileSpend({
1023
+ capabilityId,
1024
+ operationId,
1025
+ actionDigest,
1026
+ evidenceDigest: verified.evidence_digest,
1027
+ outcome: 'executed',
1028
+ now,
1029
+ });
1030
+ return result?.ok
1031
+ ? { ok: true, outcome: 'executed', action_digest: actionDigest, evidence_digest: verified.evidence_digest, idempotent: result.idempotent === true }
1032
+ : { ok: false, reason: result?.reason || 'capability_reconciliation_refused' };
1033
+ }
1034
+ /**
1035
+ * Issue a bounded child capability from a parent capability.
1036
+ *
1037
+ * Delegation is issuer-authorized metadata plus an atomic parent spend. The
1038
+ * parent budget is committed as `delegated` before the child is registered;
1039
+ * if child registration fails, the safe result is an orphaned child issuance
1040
+ * that must be reconciled, never a child with unbacked budget.
1041
+ *
1042
+ * @param {object} [options]
1043
+ * @param {object} [options.parentCapabilityReceipt]
1044
+ * @param {Buffer|string} [options.parentSecret]
1045
+ * @param {KeyMaterial} [options.issuerPrivateKey]
1046
+ * @param {CapabilityBudget} [options.budget]
1047
+ * @param {string|number} [options.expiry]
1048
+ * @param {{m:number,n:number}} [options.threshold]
1049
+ * @param {object|null} [options.scope]
1050
+ * @param {string} [options.delegateId]
1051
+ * @param {string} [options.capabilityId]
1052
+ * @param {Buffer|string} [options.secret]
1053
+ * @param {any} [options.store]
1054
+ * @param {string[]} [options.trustedIssuerKeys]
1055
+ * @param {string|null} [options.operationId]
1056
+ * @param {number|(() => number)} [options.now]
1057
+ */
1058
+ export async function delegateCapabilityReceipt({ parentCapabilityReceipt, parentSecret, issuerPrivateKey, budget, expiry, threshold = { m: 1, n: 1 }, scope = null, delegateId, capabilityId = randomUUID(), secret = randomBytes(HASH_BYTES), store, trustedIssuerKeys = [], operationId = null, now = Date.now, } = {}) {
1059
+ const verified = verifyCapabilityReceipt(parentCapabilityReceipt, { trustedIssuerKeys });
1060
+ if (!verified.ok)
1061
+ return { ok: false, reason: verified.reason };
1062
+ if (!verifySecret(verified.capability, parentSecret))
1063
+ return { ok: false, reason: 'invalid_parent_secret' };
1064
+ if (!store || typeof store.reserveSpend !== 'function' || typeof store.commitSpend !== 'function' || typeof store.registerCapability !== 'function') {
1065
+ return { ok: false, reason: 'capability_store_required' };
1066
+ }
1067
+ if (typeof delegateId !== 'string' || delegateId.length === 0)
1068
+ return { ok: false, reason: 'delegate_id_required' };
1069
+ try {
1070
+ const childId = validateCapabilityId(String(capabilityId));
1071
+ if (!isRecord(budget))
1072
+ throw new TypeError('capability budget is required');
1073
+ const childAmount = validateAmount(budget.amount, 'budget.amount');
1074
+ const currency = validateCurrency(budget.currency);
1075
+ if (childAmount <= 0)
1076
+ throw new TypeError('delegated capability budget must be greater than zero');
1077
+ if (currency !== verified.capability.budget.currency)
1078
+ throw new TypeError('delegated capability currency does not match the parent budget');
1079
+ const childExpiry = validateExpiry(expiry);
1080
+ const parentExpiry = validateExpiry(verified.capability.expiry);
1081
+ if (Date.parse(childExpiry) > Date.parse(parentExpiry))
1082
+ return { ok: false, reason: 'delegated_capability_expiry_exceeds_parent' };
1083
+ const parentScope = normalizeCapabilityScope(verified.capability.scope);
1084
+ const childScope = normalizeCapabilityScope(scope ?? parentScope);
1085
+ const parentMembers = parentScope.profile === CAPABILITY_CAID_SCOPE_PROFILE ? parentScope.caids : parentScope.action_digests;
1086
+ const childMembers = childScope.profile === CAPABILITY_CAID_SCOPE_PROFILE ? childScope.caids : childScope.action_digests;
1087
+ if (childScope.profile !== parentScope.profile
1088
+ || childScope.operation_id_field !== parentScope.operation_id_field
1089
+ || childMembers.some((member) => !parentMembers.includes(member))) {
1090
+ return { ok: false, reason: 'delegated_capability_scope_broadened' };
1091
+ }
1092
+ const parentOperationId = validateOperationId(operationId || `delegation:${childId}`);
1093
+ const child = mintCapabilityReceipt(verified.receipt, {
1094
+ issuerPrivateKey,
1095
+ budget: { amount: childAmount, currency },
1096
+ expiry: childExpiry,
1097
+ threshold,
1098
+ scope: childScope,
1099
+ capabilityId: childId,
1100
+ secret,
1101
+ delegationChain: [
1102
+ ...verified.capability.delegation_chain,
1103
+ {
1104
+ delegation_id: parentOperationId,
1105
+ parent_capability_id: verified.capability.id,
1106
+ delegate_id: delegateId,
1107
+ amount: childAmount,
1108
+ currency,
1109
+ issued_at: new Date(nowMs(now)).toISOString(),
1110
+ },
1111
+ ],
1112
+ });
1113
+ const reserved = await store.reserveSpend({
1114
+ capabilityId: verified.capability.id,
1115
+ capabilityFingerprint: capabilityEnvelopeFingerprint(parentCapabilityReceipt),
1116
+ operationId: parentOperationId,
1117
+ actionDigest: capabilityActionDigest({
1118
+ action_type: 'capability.delegate',
1119
+ operation_id: parentOperationId,
1120
+ parent_capability_id: verified.capability.id,
1121
+ child_capability_id: childId,
1122
+ child_capability_fingerprint: capabilityEnvelopeFingerprint(child.capabilityReceipt),
1123
+ }),
1124
+ amount: childAmount,
1125
+ currency,
1126
+ now,
1127
+ });
1128
+ if (!reserved?.ok)
1129
+ return { ok: false, reason: reserved?.reason || 'parent_delegation_refused' };
1130
+ const committed = await store.commitSpend({
1131
+ capabilityId: verified.capability.id,
1132
+ operationId: parentOperationId,
1133
+ reservationToken: reserved.reservation_token,
1134
+ outcome: 'delegated',
1135
+ now,
1136
+ });
1137
+ if (!committed?.ok)
1138
+ return { ok: false, reason: 'parent_delegation_commit_indeterminate', operation_id: parentOperationId };
1139
+ const registered = await store.registerCapability(child.capabilityReceipt);
1140
+ if (!registered)
1141
+ return { ok: false, reason: 'child_registration_failed', operation_id: parentOperationId };
1142
+ return {
1143
+ ok: true,
1144
+ capabilityReceipt: child.capabilityReceipt,
1145
+ secret: child.secret,
1146
+ shares: child.shares,
1147
+ operation_id: parentOperationId,
1148
+ remaining: committed.remaining,
1149
+ };
1150
+ }
1151
+ catch (error) {
1152
+ return { ok: false, reason: error?.message || 'delegation_invalid' };
1153
+ }
1154
+ }
1155
+ export default {
1156
+ CAPABILITY_RECEIPT_VERSION,
1157
+ CAPABILITY_STATE_VERSION,
1158
+ CAPABILITY_SHARE_VERSION,
1159
+ CAPABILITY_SCOPE_PROFILE,
1160
+ CAPABILITY_CAID_SCOPE_PROFILE,
1161
+ CAPABILITY_STATE_DDL,
1162
+ CAPABILITY_SQL,
1163
+ capabilityBaseReceiptDigest,
1164
+ capabilityActionDigest,
1165
+ verifyCapabilityScope,
1166
+ mintCapabilityReceipt,
1167
+ verifyCapabilityReceipt,
1168
+ splitCapabilitySecret,
1169
+ reconstructCapabilitySecret,
1170
+ createMemoryCapabilityStore,
1171
+ createPostgresCapabilityStore,
1172
+ executeWithCapability,
1173
+ executeWithThreshold,
1174
+ reconcileCapabilityOperation,
1175
+ delegateCapabilityReceipt,
1176
+ };
1177
+ //# sourceMappingURL=capability-receipt.js.map