@emilia-protocol/gate 0.10.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +272 -0
  3. package/action-control-manifest.js +3 -268
  4. package/action-escrow-custodian.js +4 -0
  5. package/action-escrow-evidence.js +4 -0
  6. package/action-escrow-package.js +4 -0
  7. package/action-escrow-postgres.js +4 -0
  8. package/action-escrow-state.js +4 -0
  9. package/action-escrow-verifiers.js +4 -0
  10. package/action-escrow.js +4 -0
  11. package/action-packs.js +3 -140
  12. package/adapters/_kit.js +3 -104
  13. package/adapters/aws.js +3 -108
  14. package/adapters/cloudflare.js +3 -54
  15. package/adapters/gcp.js +3 -70
  16. package/adapters/github.js +3 -109
  17. package/adapters/jira.js +3 -55
  18. package/adapters/k8s.js +3 -70
  19. package/adapters/linear.js +3 -55
  20. package/adapters/salesforce.js +3 -55
  21. package/adapters/stripe.js +3 -81
  22. package/adapters/supabase.js +3 -121
  23. package/adapters/terraform.js +3 -59
  24. package/adapters/vercel.js +3 -84
  25. package/aec-execution.js +3 -312
  26. package/breakglass.js +3 -582
  27. package/capability-receipt.js +4 -0
  28. package/cf1-conformance.js +3 -106
  29. package/challenge-store.js +3 -69
  30. package/control-plane.js +3 -340
  31. package/coverage.js +3 -721
  32. package/demo.mjs +1 -1
  33. package/deployment-attestation.js +3 -247
  34. package/dist/action-control-manifest.d.ts +82 -0
  35. package/dist/action-control-manifest.d.ts.map +1 -0
  36. package/dist/action-control-manifest.js +422 -0
  37. package/dist/action-control-manifest.js.map +1 -0
  38. package/dist/action-escrow-custodian.d.ts +74 -0
  39. package/dist/action-escrow-custodian.d.ts.map +1 -0
  40. package/dist/action-escrow-custodian.js +363 -0
  41. package/dist/action-escrow-custodian.js.map +1 -0
  42. package/dist/action-escrow-evidence.d.ts +130 -0
  43. package/dist/action-escrow-evidence.d.ts.map +1 -0
  44. package/dist/action-escrow-evidence.js +1012 -0
  45. package/dist/action-escrow-evidence.js.map +1 -0
  46. package/dist/action-escrow-package.d.ts +10 -0
  47. package/dist/action-escrow-package.d.ts.map +1 -0
  48. package/dist/action-escrow-package.js +1583 -0
  49. package/dist/action-escrow-package.js.map +1 -0
  50. package/dist/action-escrow-postgres.d.ts +62 -0
  51. package/dist/action-escrow-postgres.d.ts.map +1 -0
  52. package/dist/action-escrow-postgres.js +339 -0
  53. package/dist/action-escrow-postgres.js.map +1 -0
  54. package/dist/action-escrow-state.d.ts +141 -0
  55. package/dist/action-escrow-state.d.ts.map +1 -0
  56. package/dist/action-escrow-state.js +413 -0
  57. package/dist/action-escrow-state.js.map +1 -0
  58. package/dist/action-escrow-verifiers.d.ts +65 -0
  59. package/dist/action-escrow-verifiers.d.ts.map +1 -0
  60. package/dist/action-escrow-verifiers.js +421 -0
  61. package/dist/action-escrow-verifiers.js.map +1 -0
  62. package/dist/action-escrow.d.ts +112 -0
  63. package/dist/action-escrow.d.ts.map +1 -0
  64. package/dist/action-escrow.js +2661 -0
  65. package/dist/action-escrow.js.map +1 -0
  66. package/dist/action-packs.d.ts +54 -0
  67. package/dist/action-packs.d.ts.map +1 -0
  68. package/dist/action-packs.js +143 -0
  69. package/dist/action-packs.js.map +1 -0
  70. package/dist/adapters/_kit.d.ts +50 -0
  71. package/dist/adapters/_kit.d.ts.map +1 -0
  72. package/dist/adapters/_kit.js +98 -0
  73. package/dist/adapters/_kit.js.map +1 -0
  74. package/dist/adapters/aws.d.ts +158 -0
  75. package/dist/adapters/aws.d.ts.map +1 -0
  76. package/dist/adapters/aws.js +136 -0
  77. package/dist/adapters/aws.js.map +1 -0
  78. package/dist/adapters/cloudflare.d.ts +116 -0
  79. package/dist/adapters/cloudflare.d.ts.map +1 -0
  80. package/dist/adapters/cloudflare.js +75 -0
  81. package/dist/adapters/cloudflare.js.map +1 -0
  82. package/dist/adapters/gcp.d.ts +139 -0
  83. package/dist/adapters/gcp.d.ts.map +1 -0
  84. package/dist/adapters/gcp.js +69 -0
  85. package/dist/adapters/gcp.js.map +1 -0
  86. package/dist/adapters/github.d.ts +123 -0
  87. package/dist/adapters/github.d.ts.map +1 -0
  88. package/dist/adapters/github.js +105 -0
  89. package/dist/adapters/github.js.map +1 -0
  90. package/dist/adapters/jira.d.ts +109 -0
  91. package/dist/adapters/jira.d.ts.map +1 -0
  92. package/dist/adapters/jira.js +54 -0
  93. package/dist/adapters/jira.js.map +1 -0
  94. package/dist/adapters/k8s.d.ts +139 -0
  95. package/dist/adapters/k8s.d.ts.map +1 -0
  96. package/dist/adapters/k8s.js +69 -0
  97. package/dist/adapters/k8s.js.map +1 -0
  98. package/dist/adapters/linear.d.ts +109 -0
  99. package/dist/adapters/linear.d.ts.map +1 -0
  100. package/dist/adapters/linear.js +54 -0
  101. package/dist/adapters/linear.js.map +1 -0
  102. package/dist/adapters/salesforce.d.ts +109 -0
  103. package/dist/adapters/salesforce.d.ts.map +1 -0
  104. package/dist/adapters/salesforce.js +54 -0
  105. package/dist/adapters/salesforce.js.map +1 -0
  106. package/dist/adapters/stripe.d.ts +116 -0
  107. package/dist/adapters/stripe.d.ts.map +1 -0
  108. package/dist/adapters/stripe.js +77 -0
  109. package/dist/adapters/stripe.js.map +1 -0
  110. package/dist/adapters/supabase.d.ts +127 -0
  111. package/dist/adapters/supabase.d.ts.map +1 -0
  112. package/dist/adapters/supabase.js +112 -0
  113. package/dist/adapters/supabase.js.map +1 -0
  114. package/dist/adapters/terraform.d.ts +109 -0
  115. package/dist/adapters/terraform.d.ts.map +1 -0
  116. package/dist/adapters/terraform.js +58 -0
  117. package/dist/adapters/terraform.js.map +1 -0
  118. package/dist/adapters/vercel.d.ts +114 -0
  119. package/dist/adapters/vercel.d.ts.map +1 -0
  120. package/dist/adapters/vercel.js +81 -0
  121. package/dist/adapters/vercel.js.map +1 -0
  122. package/dist/aec-execution.d.ts +107 -0
  123. package/dist/aec-execution.d.ts.map +1 -0
  124. package/dist/aec-execution.js +334 -0
  125. package/dist/aec-execution.js.map +1 -0
  126. package/dist/breakglass.d.ts +205 -0
  127. package/dist/breakglass.d.ts.map +1 -0
  128. package/dist/breakglass.js +627 -0
  129. package/dist/breakglass.js.map +1 -0
  130. package/dist/capability-receipt.d.ts +601 -0
  131. package/dist/capability-receipt.d.ts.map +1 -0
  132. package/dist/capability-receipt.js +1177 -0
  133. package/dist/capability-receipt.js.map +1 -0
  134. package/dist/cf1-conformance.d.ts +83 -0
  135. package/dist/cf1-conformance.d.ts.map +1 -0
  136. package/dist/cf1-conformance.js +94 -0
  137. package/dist/cf1-conformance.js.map +1 -0
  138. package/dist/challenge-store.d.ts +20 -0
  139. package/dist/challenge-store.d.ts.map +1 -0
  140. package/dist/challenge-store.js +62 -0
  141. package/dist/challenge-store.js.map +1 -0
  142. package/dist/control-plane.d.ts +136 -0
  143. package/dist/control-plane.d.ts.map +1 -0
  144. package/dist/control-plane.js +349 -0
  145. package/dist/control-plane.js.map +1 -0
  146. package/dist/coverage.d.ts +156 -0
  147. package/dist/coverage.d.ts.map +1 -0
  148. package/dist/coverage.js +768 -0
  149. package/dist/coverage.js.map +1 -0
  150. package/dist/deployment-attestation.d.ts +93 -0
  151. package/dist/deployment-attestation.d.ts.map +1 -0
  152. package/dist/deployment-attestation.js +262 -0
  153. package/dist/deployment-attestation.js.map +1 -0
  154. package/dist/eg1-conformance.d.ts +286 -0
  155. package/dist/eg1-conformance.d.ts.map +1 -0
  156. package/dist/eg1-conformance.js +379 -0
  157. package/dist/eg1-conformance.js.map +1 -0
  158. package/dist/enterprise.d.ts +88 -0
  159. package/dist/enterprise.d.ts.map +1 -0
  160. package/dist/enterprise.js +194 -0
  161. package/dist/enterprise.js.map +1 -0
  162. package/dist/evidence-postgres.d.ts +123 -0
  163. package/dist/evidence-postgres.d.ts.map +1 -0
  164. package/dist/evidence-postgres.js +343 -0
  165. package/dist/evidence-postgres.js.map +1 -0
  166. package/dist/evidence.d.ts +113 -0
  167. package/dist/evidence.d.ts.map +1 -0
  168. package/dist/evidence.js +369 -0
  169. package/dist/evidence.js.map +1 -0
  170. package/dist/execution-binding.d.ts +49 -0
  171. package/dist/execution-binding.d.ts.map +1 -0
  172. package/dist/execution-binding.js +243 -0
  173. package/dist/execution-binding.js.map +1 -0
  174. package/dist/formal-runtime-map.d.ts +42 -0
  175. package/dist/formal-runtime-map.d.ts.map +1 -0
  176. package/dist/formal-runtime-map.js +23 -0
  177. package/dist/formal-runtime-map.js.map +1 -0
  178. package/dist/index.d.ts +1002 -0
  179. package/dist/index.d.ts.map +1 -0
  180. package/dist/index.js +1736 -0
  181. package/dist/index.js.map +1 -0
  182. package/dist/key-registry.d.ts +60 -0
  183. package/dist/key-registry.d.ts.map +1 -0
  184. package/dist/key-registry.js +144 -0
  185. package/dist/key-registry.js.map +1 -0
  186. package/dist/mcp.d.ts +53 -0
  187. package/dist/mcp.d.ts.map +1 -0
  188. package/dist/mcp.js +112 -0
  189. package/dist/mcp.js.map +1 -0
  190. package/dist/metering.d.ts +80 -0
  191. package/dist/metering.d.ts.map +1 -0
  192. package/dist/metering.js +227 -0
  193. package/dist/metering.js.map +1 -0
  194. package/dist/metrics.d.ts +68 -0
  195. package/dist/metrics.d.ts.map +1 -0
  196. package/dist/metrics.js +242 -0
  197. package/dist/metrics.js.map +1 -0
  198. package/dist/network-witness.d.ts +167 -0
  199. package/dist/network-witness.d.ts.map +1 -0
  200. package/dist/network-witness.js +520 -0
  201. package/dist/network-witness.js.map +1 -0
  202. package/dist/protocol-state.d.ts +141 -0
  203. package/dist/protocol-state.d.ts.map +1 -0
  204. package/dist/protocol-state.js +106 -0
  205. package/dist/protocol-state.js.map +1 -0
  206. package/dist/receipt-program.d.ts +59 -0
  207. package/dist/receipt-program.d.ts.map +1 -0
  208. package/dist/receipt-program.js +1053 -0
  209. package/dist/receipt-program.js.map +1 -0
  210. package/dist/reliance-kernel.d.ts +66 -0
  211. package/dist/reliance-kernel.d.ts.map +1 -0
  212. package/dist/reliance-kernel.js +103 -0
  213. package/dist/reliance-kernel.js.map +1 -0
  214. package/dist/reliance-packet.d.ts +67 -0
  215. package/dist/reliance-packet.d.ts.map +1 -0
  216. package/dist/reliance-packet.js +176 -0
  217. package/dist/reliance-packet.js.map +1 -0
  218. package/dist/remedy-program-postgres.d.ts +37 -0
  219. package/dist/remedy-program-postgres.d.ts.map +1 -0
  220. package/dist/remedy-program-postgres.js +291 -0
  221. package/dist/remedy-program-postgres.js.map +1 -0
  222. package/dist/remedy-program-receipt.d.ts +97 -0
  223. package/dist/remedy-program-receipt.d.ts.map +1 -0
  224. package/dist/remedy-program-receipt.js +817 -0
  225. package/dist/remedy-program-receipt.js.map +1 -0
  226. package/dist/remedy-program.d.ts +76 -0
  227. package/dist/remedy-program.d.ts.map +1 -0
  228. package/dist/remedy-program.js +1346 -0
  229. package/dist/remedy-program.js.map +1 -0
  230. package/dist/reports/art14.d.ts +101 -0
  231. package/dist/reports/art14.d.ts.map +1 -0
  232. package/dist/reports/art14.js +340 -0
  233. package/dist/reports/art14.js.map +1 -0
  234. package/dist/reports/assurance-package.d.ts +197 -0
  235. package/dist/reports/assurance-package.d.ts.map +1 -0
  236. package/dist/reports/assurance-package.js +313 -0
  237. package/dist/reports/assurance-package.js.map +1 -0
  238. package/dist/reports/auditor-workpaper.d.ts +194 -0
  239. package/dist/reports/auditor-workpaper.d.ts.map +1 -0
  240. package/dist/reports/auditor-workpaper.js +545 -0
  241. package/dist/reports/auditor-workpaper.js.map +1 -0
  242. package/dist/reports/external-verification.d.ts +76 -0
  243. package/dist/reports/external-verification.d.ts.map +1 -0
  244. package/dist/reports/external-verification.js +209 -0
  245. package/dist/reports/external-verification.js.map +1 -0
  246. package/dist/reports/reperform.d.ts +122 -0
  247. package/dist/reports/reperform.d.ts.map +1 -0
  248. package/dist/reports/reperform.js +432 -0
  249. package/dist/reports/reperform.js.map +1 -0
  250. package/dist/reports/underwriter.d.ts +127 -0
  251. package/dist/reports/underwriter.d.ts.map +1 -0
  252. package/dist/reports/underwriter.js +348 -0
  253. package/dist/reports/underwriter.js.map +1 -0
  254. package/dist/retention.d.ts +88 -0
  255. package/dist/retention.d.ts.map +1 -0
  256. package/dist/retention.js +90 -0
  257. package/dist/retention.js.map +1 -0
  258. package/dist/roster.d.ts +113 -0
  259. package/dist/roster.d.ts.map +1 -0
  260. package/dist/roster.js +261 -0
  261. package/dist/roster.js.map +1 -0
  262. package/dist/runtime-monitor.d.ts +132 -0
  263. package/dist/runtime-monitor.d.ts.map +1 -0
  264. package/dist/runtime-monitor.js +197 -0
  265. package/dist/runtime-monitor.js.map +1 -0
  266. package/dist/settlement.d.ts +68 -0
  267. package/dist/settlement.d.ts.map +1 -0
  268. package/dist/settlement.js +306 -0
  269. package/dist/settlement.js.map +1 -0
  270. package/dist/siem.d.ts +261 -0
  271. package/dist/siem.d.ts.map +1 -0
  272. package/dist/siem.js +247 -0
  273. package/dist/siem.js.map +1 -0
  274. package/dist/store-postgres.d.ts +107 -0
  275. package/dist/store-postgres.d.ts.map +1 -0
  276. package/dist/store-postgres.js +152 -0
  277. package/dist/store-postgres.js.map +1 -0
  278. package/dist/store.d.ts +95 -0
  279. package/dist/store.d.ts.map +1 -0
  280. package/dist/store.js +202 -0
  281. package/dist/store.js.map +1 -0
  282. package/dist/strict-json.d.ts +14 -0
  283. package/dist/strict-json.d.ts.map +1 -0
  284. package/dist/strict-json.js +132 -0
  285. package/dist/strict-json.js.map +1 -0
  286. package/dist/trust-program-adapters.d.ts +122 -0
  287. package/dist/trust-program-adapters.d.ts.map +1 -0
  288. package/dist/trust-program-adapters.js +802 -0
  289. package/dist/trust-program-adapters.js.map +1 -0
  290. package/dist/trust-program-postgres.d.ts +41 -0
  291. package/dist/trust-program-postgres.d.ts.map +1 -0
  292. package/dist/trust-program-postgres.js +346 -0
  293. package/dist/trust-program-postgres.js.map +1 -0
  294. package/dist/trust-program-revocation.d.ts +206 -0
  295. package/dist/trust-program-revocation.d.ts.map +1 -0
  296. package/dist/trust-program-revocation.js +600 -0
  297. package/dist/trust-program-revocation.js.map +1 -0
  298. package/dist/trust-program.d.ts +197 -0
  299. package/dist/trust-program.d.ts.map +1 -0
  300. package/dist/trust-program.js +1351 -0
  301. package/dist/trust-program.js.map +1 -0
  302. package/dist/witness-postgres.d.ts +43 -0
  303. package/dist/witness-postgres.d.ts.map +1 -0
  304. package/dist/witness-postgres.js +92 -0
  305. package/dist/witness-postgres.js.map +1 -0
  306. package/dist/zk-range-proof.d.ts +78 -0
  307. package/dist/zk-range-proof.d.ts.map +1 -0
  308. package/dist/zk-range-proof.js +228 -0
  309. package/dist/zk-range-proof.js.map +1 -0
  310. package/eg1-conformance.js +3 -404
  311. package/enterprise.js +3 -180
  312. package/ep-assure.mjs +1 -0
  313. package/evidence-postgres.js +3 -356
  314. package/evidence.js +4 -360
  315. package/execution-binding.js +3 -212
  316. package/formal-runtime-map.js +4 -0
  317. package/index.js +4 -1249
  318. package/key-registry.js +3 -134
  319. package/mcp.js +3 -109
  320. package/metering.js +3 -226
  321. package/metrics.js +3 -231
  322. package/network-witness.js +3 -499
  323. package/package.json +47 -4
  324. package/receipt-program.js +4 -0
  325. package/reliance-kernel.js +3 -112
  326. package/reliance-packet.js +3 -175
  327. package/remedy-program-postgres.js +4 -0
  328. package/remedy-program-receipt.js +4 -0
  329. package/remedy-program.js +4 -0
  330. package/reports/art14.js +0 -0
  331. package/reports/assurance-package.js +2 -312
  332. package/reports/auditor-workpaper.js +3 -537
  333. package/reports/external-verification.js +3 -222
  334. package/reports/reperform.js +3 -424
  335. package/reports/underwriter.js +3 -339
  336. package/retention.js +3 -84
  337. package/roster.js +3 -264
  338. package/runtime-monitor.js +4 -0
  339. package/settlement.js +3 -299
  340. package/siem.js +3 -235
  341. package/src/action-control-manifest.ts +424 -0
  342. package/src/action-escrow-custodian.ts +427 -0
  343. package/src/action-escrow-evidence.ts +1178 -0
  344. package/src/action-escrow-package.ts +1821 -0
  345. package/src/action-escrow-postgres.ts +366 -0
  346. package/src/action-escrow-state.ts +498 -0
  347. package/src/action-escrow-verifiers.ts +484 -0
  348. package/src/action-escrow.ts +3039 -0
  349. package/src/action-packs.ts +170 -0
  350. package/src/adapters/_kit.ts +117 -0
  351. package/src/adapters/aws.ts +142 -0
  352. package/src/adapters/cloudflare.ts +78 -0
  353. package/src/adapters/gcp.ts +71 -0
  354. package/src/adapters/github.ts +110 -0
  355. package/src/adapters/jira.ts +56 -0
  356. package/src/adapters/k8s.ts +71 -0
  357. package/src/adapters/linear.ts +56 -0
  358. package/src/adapters/salesforce.ts +56 -0
  359. package/src/adapters/stripe.ts +82 -0
  360. package/src/adapters/supabase.ts +122 -0
  361. package/src/adapters/terraform.ts +60 -0
  362. package/src/adapters/vercel.ts +85 -0
  363. package/src/aec-execution.ts +354 -0
  364. package/src/breakglass.ts +647 -0
  365. package/src/capability-receipt.ts +1258 -0
  366. package/src/cf1-conformance.ts +120 -0
  367. package/src/challenge-store.ts +70 -0
  368. package/src/control-plane.ts +374 -0
  369. package/src/coverage.ts +832 -0
  370. package/src/deployment-attestation.ts +259 -0
  371. package/src/eg1-conformance.ts +471 -0
  372. package/src/enterprise.ts +195 -0
  373. package/src/evidence-postgres.ts +374 -0
  374. package/src/evidence.ts +367 -0
  375. package/src/execution-binding.ts +236 -0
  376. package/src/formal-runtime-map.ts +24 -0
  377. package/src/index.ts +2122 -0
  378. package/src/key-registry.ts +150 -0
  379. package/src/mcp.ts +120 -0
  380. package/src/metering.ts +237 -0
  381. package/src/metrics.ts +232 -0
  382. package/src/network-witness.ts +585 -0
  383. package/src/protocol-state.ts +143 -0
  384. package/src/protocol-state.typecheck.ts +94 -0
  385. package/src/receipt-program.ts +1105 -0
  386. package/src/reliance-kernel.ts +119 -0
  387. package/src/reliance-packet.ts +206 -0
  388. package/src/remedy-program-postgres.ts +367 -0
  389. package/src/remedy-program-receipt.ts +891 -0
  390. package/src/remedy-program.ts +1388 -0
  391. package/src/reports/art14.ts +361 -0
  392. package/src/reports/assurance-package.ts +330 -0
  393. package/src/reports/auditor-workpaper.ts +558 -0
  394. package/src/reports/external-verification.ts +232 -0
  395. package/src/reports/reperform.ts +441 -0
  396. package/src/reports/underwriter.ts +356 -0
  397. package/src/retention.ts +100 -0
  398. package/src/roster.ts +273 -0
  399. package/src/runtime-monitor.ts +217 -0
  400. package/src/settlement.ts +313 -0
  401. package/src/siem.ts +247 -0
  402. package/src/store-postgres.ts +167 -0
  403. package/src/store.ts +214 -0
  404. package/src/strict-json.ts +115 -0
  405. package/src/trust-program-adapters.ts +847 -0
  406. package/src/trust-program-postgres.ts +429 -0
  407. package/src/trust-program-revocation.ts +751 -0
  408. package/src/trust-program.ts +1460 -0
  409. package/src/witness-postgres.ts +106 -0
  410. package/src/zk-range-proof.ts +250 -0
  411. package/store-postgres.js +3 -161
  412. package/store.js +4 -198
  413. package/strict-json.js +3 -85
  414. package/trust-program-adapters.js +4 -0
  415. package/trust-program-postgres.js +4 -0
  416. package/trust-program-revocation.js +4 -0
  417. package/trust-program.js +4 -0
  418. package/witness-postgres.js +3 -96
  419. package/zk-range-proof.js +4 -0
@@ -0,0 +1,167 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * EMILIA Gate — reference DURABLE consumption backend: Postgres
4
+ * (EP-GATE-PG-CONSUMPTION-v2).
5
+ *
6
+ * Replay defense that survives restarts. This module implements the backend
7
+ * contract consumed by `createDurableConsumptionStore` in ./store.js:
8
+ *
9
+ * backend = {
10
+ * async addIfAbsent(key, value, { ttlSeconds }?) : boolean // true iff inserted
11
+ * async compareAndSet(key, expected, replacement, { ttlSeconds }?) : boolean
12
+ * async deleteIfValue(key, expected) : boolean
13
+ * async has(key) : boolean
14
+ * }
15
+ *
16
+ * plus a `cleanupExpired(now)` garbage-collection statement.
17
+ *
18
+ * Each transition is one SQL statement. `addIfAbsent` decides first use;
19
+ * `compareAndSet` and `deleteIfValue` bind commit/release to the opaque owner of
20
+ * the current reservation. There is no read-then-write interval in which a
21
+ * delayed worker can overwrite or remove a newer worker's reservation.
22
+ *
23
+ * FAIL-CLOSED CONTRACT: if the injected `query` THROWS (connection down,
24
+ * timeout, constraint other than the PK, ...) the error PROPAGATES to the
25
+ * caller — nothing here catches it. The gate must refuse an action when
26
+ * one-time-use cannot be proven; a backend error is NEVER treated as
27
+ * not-consumed (which would admit replays during an outage) and never treated
28
+ * as consumed-silently (which would mask the outage as a replay verdict).
29
+ * Likewise, an EXPIRED row that cleanup has not yet removed still refuses:
30
+ * TTL expiry is garbage collection, never a grant — the gate already rejects
31
+ * stale receipts on freshness, so nothing is lost by refusing conservatively.
32
+ *
33
+ * Deterministic by construction: the pg-style `query(text, params)` function
34
+ * and the clock (`now`) are both injected, so tests run against an in-memory
35
+ * fake with real ON CONFLICT semantics — no network, no database.
36
+ */
37
+
38
+ export const PG_CONSUMPTION_VERSION = 'EP-GATE-PG-CONSUMPTION-v2';
39
+
40
+ /** Single consumption table. Timestamps are epoch milliseconds (BIGINT) so the
41
+ * injected JS clock maps 1:1 onto column values with no timezone ambiguity. */
42
+ export const CONSUMPTION_TABLE = 'ep_gate_consumption';
43
+
44
+ export const CONSUMPTION_TABLE_DDL = `CREATE TABLE IF NOT EXISTS ${CONSUMPTION_TABLE} (
45
+ consumption_key TEXT PRIMARY KEY,
46
+ state TEXT NOT NULL,
47
+ consumed_at BIGINT NOT NULL,
48
+ expires_at BIGINT
49
+ );
50
+ CREATE INDEX IF NOT EXISTS ${CONSUMPTION_TABLE}_expires_idx
51
+ ON ${CONSUMPTION_TABLE} (expires_at) WHERE expires_at IS NOT NULL;`;
52
+
53
+ /** The exact statements the backend issues — exported for transparency and so
54
+ * a fake client can implement them without parsing SQL. */
55
+ export const CONSUMPTION_SQL = {
56
+ health: `SELECT
57
+ to_regclass('public.${CONSUMPTION_TABLE}') IS NOT NULL AS table_ready,
58
+ CASE WHEN to_regclass('public.${CONSUMPTION_TABLE}') IS NULL THEN FALSE
59
+ ELSE has_table_privilege(current_user, to_regclass('public.${CONSUMPTION_TABLE}'), 'SELECT,INSERT,UPDATE,DELETE') END AS can_use`,
60
+ /** $1 key, $2 state, $3 consumed_at ms, $4 expires_at ms|null. rowCount 1 = consumed, 0 = replay. */
61
+ addIfAbsent: `INSERT INTO ${CONSUMPTION_TABLE} (consumption_key, state, consumed_at, expires_at) `
62
+ + 'VALUES ($1, $2, $3, $4) ON CONFLICT (consumption_key) DO NOTHING',
63
+ /** $1 key, $2 expected state, $3 replacement, $4 consumed_at ms, $5 expires_at ms|null. */
64
+ compareAndSet: `UPDATE ${CONSUMPTION_TABLE} SET state = $3, consumed_at = $4, expires_at = $5 `
65
+ + 'WHERE consumption_key = $1 AND state = $2',
66
+ /** $1 key, $2 expected state. */
67
+ deleteIfValue: `DELETE FROM ${CONSUMPTION_TABLE} WHERE consumption_key = $1 AND state = $2`,
68
+ /** $1 key. Any row — even an expired one — counts as consumed until cleaned. */
69
+ has: `SELECT 1 FROM ${CONSUMPTION_TABLE} WHERE consumption_key = $1`,
70
+ /** $1 now ms. Removes ONLY rows whose TTL has elapsed; NULL expires_at never expires. */
71
+ cleanupExpired: `DELETE FROM ${CONSUMPTION_TABLE} WHERE state LIKE 'committed:%' `
72
+ + 'AND expires_at IS NOT NULL AND expires_at <= $1',
73
+ };
74
+
75
+ /**
76
+ * Create the Postgres consumption backend.
77
+ * @param {{ query?: (text: string, params: any[]) => Promise<{ rowCount: number, rows?: any[] }>, now?: number|function }} [o]
78
+ * `query` is a pg-style query function (e.g. `pool.query.bind(pool)`). Injected
79
+ * so tests need no database. MUST throw on failure — errors propagate (fail
80
+ * closed). `now` is the injected clock (ms or () => ms).
81
+ * @returns backend conforming to createDurableConsumptionStore, plus cleanupExpired(now).
82
+ */
83
+ export function createPostgresBackend({
84
+ query,
85
+ now = Date.now,
86
+ }: {
87
+ query?: (text: string, params: any[]) => Promise<{ rowCount: number; rows?: any[] }>;
88
+ now?: number | (() => number);
89
+ } = {}) {
90
+ if (typeof query !== 'function') {
91
+ throw new Error('createPostgresBackend: query must be an async (text, params) => { rowCount } function '
92
+ + '(e.g. pg pool.query). It must THROW on failure — a backend error must never look like a verdict.');
93
+ }
94
+ let lastObservedNow = Number.NEGATIVE_INFINITY;
95
+ const checkedNow = (candidate) => {
96
+ if (!Number.isSafeInteger(candidate) || candidate < 0) {
97
+ throw new Error('consumption clock must return a non-negative safe-integer epoch millisecond');
98
+ }
99
+ if (candidate < lastObservedNow) {
100
+ throw new Error(`consumption clock regression refused: ${candidate} < ${lastObservedNow}`);
101
+ }
102
+ lastObservedNow = candidate;
103
+ return candidate;
104
+ };
105
+ const nowMs = () => checkedNow(typeof now === 'function' ? now() : now);
106
+ const expiryFor = (opt) => {
107
+ const ttl = Number(opt?.ttlSeconds);
108
+ return Number.isFinite(ttl) && ttl > 0 ? nowMs() + ttl * 1000 : null;
109
+ };
110
+
111
+ return {
112
+ durable: true,
113
+ async health() {
114
+ const res = await query(CONSUMPTION_SQL.health, []);
115
+ if (!res || res.rowCount !== 1 || !Array.isArray(res.rows) || res.rows.length !== 1) {
116
+ throw new Error('consumption health: malformed Postgres result');
117
+ }
118
+ return {
119
+ ok: res.rows[0].table_ready === true && res.rows[0].can_use === true,
120
+ version: PG_CONSUMPTION_VERSION,
121
+ };
122
+ },
123
+ /** True iff THIS call inserted the row — the atomic consumed-vs-replay decision. */
124
+ async addIfAbsent(key, value, opt) {
125
+ const res = await query(CONSUMPTION_SQL.addIfAbsent, [key, value, nowMs(), expiryFor(opt)]);
126
+ // Fail closed on a malformed driver result: without a numeric rowCount we
127
+ // cannot PROVE first-use, so refuse loudly rather than guess.
128
+ if (!res || typeof res.rowCount !== 'number') {
129
+ throw new Error('addIfAbsent: query result has no numeric rowCount — cannot prove one-time use');
130
+ }
131
+ return res.rowCount === 1;
132
+ },
133
+
134
+ /** Ownership-fenced reserved -> committed transition. */
135
+ async compareAndSet(key, expected, replacement, opt) {
136
+ const res = await query(CONSUMPTION_SQL.compareAndSet, [key, expected, replacement, nowMs(), expiryFor(opt)]);
137
+ if (!res || typeof res.rowCount !== 'number') {
138
+ throw new Error('compareAndSet: query result has no numeric rowCount — cannot prove reservation ownership');
139
+ }
140
+ return res.rowCount === 1;
141
+ },
142
+
143
+ /** Remove only the reservation owned by the caller. */
144
+ async deleteIfValue(key, expected) {
145
+ const res = await query(CONSUMPTION_SQL.deleteIfValue, [key, expected]);
146
+ if (!res || typeof res.rowCount !== 'number') {
147
+ throw new Error('deleteIfValue: query result has no numeric rowCount — cannot prove reservation ownership');
148
+ }
149
+ return res.rowCount === 1;
150
+ },
151
+
152
+ /** Present = consumed. Expired-but-uncleaned rows still count (conservative). */
153
+ async has(key) {
154
+ const res = await query(CONSUMPTION_SQL.has, [key]);
155
+ return (res?.rows?.length ?? res?.rowCount ?? 0) > 0;
156
+ },
157
+
158
+ /** Garbage-collect rows whose TTL elapsed. Returns the number removed. */
159
+ async cleanupExpired(at) {
160
+ const cleanupAt = at === undefined ? nowMs() : checkedNow(at);
161
+ const res = await query(CONSUMPTION_SQL.cleanupExpired, [cleanupAt]);
162
+ return res?.rowCount ?? 0;
163
+ },
164
+ };
165
+ }
166
+
167
+ export default { createPostgresBackend, CONSUMPTION_TABLE_DDL, CONSUMPTION_TABLE, CONSUMPTION_SQL, PG_CONSUMPTION_VERSION };
package/src/store.ts ADDED
@@ -0,0 +1,214 @@
1
+ /**
2
+ * @emilia-protocol/gate — consumption store (replay defense).
3
+ * @license Apache-2.0
4
+ *
5
+ * A receipt authorizes ONE action, once. The gate consumes a receipt's
6
+ * identifier the first time it is used; any later presentation of the same
7
+ * receipt is a replay and is refused. The in-memory store is an explicit
8
+ * test/demo opt-in; security-bearing gates use the durable contract below.
9
+ */
10
+ export class MemoryConsumptionStore {
11
+ durable: boolean;
12
+ ownershipFenced: boolean;
13
+ permanentConsumption: boolean;
14
+ seen: Set<string>;
15
+ reserved: Set<string>;
16
+
17
+ constructor() {
18
+ this.durable = false;
19
+ this.ownershipFenced = false;
20
+ this.permanentConsumption = false;
21
+ this.seen = new Set();
22
+ this.reserved = new Set();
23
+ }
24
+
25
+ /** Returns true the FIRST time a key is seen, false on every replay. */
26
+ async consume(key) {
27
+ if (this.seen.has(key) || this.reserved.has(key)) return false;
28
+ this.seen.add(key);
29
+ return true;
30
+ }
31
+
32
+ /** Reserve an id while an action is in flight; blocks concurrent replay. */
33
+ async reserve(key) {
34
+ if (this.seen.has(key) || this.reserved.has(key)) return false;
35
+ this.reserved.add(key);
36
+ return true;
37
+ }
38
+
39
+ /** Commit a reserved id after an external-effect attempt begins. */
40
+ async commit(key) {
41
+ this.reserved.delete(key);
42
+ this.seen.add(key);
43
+ return true;
44
+ }
45
+
46
+ /** Release only when the caller can prove the external effect never began. */
47
+ async release(key) {
48
+ this.reserved.delete(key);
49
+ return true;
50
+ }
51
+
52
+ async has(key) {
53
+ return this.seen.has(key) || this.reserved.has(key);
54
+ }
55
+
56
+ get size() {
57
+ return this.seen.size;
58
+ }
59
+ }
60
+
61
+ export const DURABLE_CONSUMPTION_VERSION = 'EP-GATE-DURABLE-CONSUMPTION-v2';
62
+
63
+ /** Capability contract required by security-bearing Gate execution paths. */
64
+ export function isSecureConsumptionStore(store) {
65
+ if (!store || typeof store !== 'object') return false;
66
+ return store.durable === true
67
+ && store.ownershipFenced === true
68
+ && store.permanentConsumption === true
69
+ && typeof store.consume === 'function'
70
+ && typeof store.reserve === 'function'
71
+ && typeof store.commit === 'function';
72
+ }
73
+
74
+ const COMMITTED_VALUE = 'committed:v2';
75
+ const RESERVED_PREFIX = 'reserved:v2:';
76
+
77
+ function defaultReservationToken() {
78
+ if (typeof globalThis.crypto?.randomUUID !== 'function') {
79
+ throw new Error('secure crypto.randomUUID() is required for durable reservation fencing');
80
+ }
81
+ return globalThis.crypto.randomUUID();
82
+ }
83
+
84
+ /**
85
+ * Production custody for replay defense: a durable consumption store backed by
86
+ * any shared key-value backend (Redis, Postgres, DynamoDB, ...), so a receipt
87
+ * consumed on one pod/lambda cannot be replayed on another.
88
+ *
89
+ * The backend MUST provide atomic insert-if-absent plus atomic conditional
90
+ * transition and delete. Together they make replay defense and reservation
91
+ * ownership sound under concurrency:
92
+ *
93
+ * backend = {
94
+ * async addIfAbsent(key, value): boolean
95
+ * async compareAndSet(key, expected, replacement): boolean
96
+ * async deleteIfValue(key, expected): boolean
97
+ * async has(key): boolean
98
+ * }
99
+ *
100
+ * State per receipt id is a single ownership-fenced value. A reservation is
101
+ * `reserved:v2:<random token>` and only the store instance holding that token
102
+ * can commit or release it. This prevents a delayed worker from deleting or
103
+ * committing a newer worker's reservation after timeout/failover.
104
+ *
105
+ * Reservations deliberately receive NO TTL. A crash after an external effect
106
+ * has begun is an indeterminate outcome, and automatically reopening the key
107
+ * would permit a duplicate effect. Operators must reconcile abandoned
108
+ * reservations. `ttlSeconds` applies only after a value is committed, when the
109
+ * receipt's own freshness window independently prevents reuse.
110
+ */
111
+ /**
112
+ * @param {any} backend
113
+ * @param {{ ttlSeconds?: number, reservationTokenFactory?: () => string }} [options]
114
+ */
115
+ export function createDurableConsumptionStore(
116
+ backend,
117
+ { ttlSeconds, reservationTokenFactory = defaultReservationToken }: {
118
+ ttlSeconds?: number | null;
119
+ reservationTokenFactory?: () => string;
120
+ } = {},
121
+ ) {
122
+ for (const m of ['addIfAbsent', 'compareAndSet', 'deleteIfValue', 'has']) {
123
+ if (typeof backend?.[m] !== 'function') {
124
+ throw new Error(`createDurableConsumptionStore: backend must implement async ${m}(). `
125
+ + 'addIfAbsent and conditional transitions MUST be atomic or replay defense is not fleet-safe.');
126
+ }
127
+ }
128
+ if (typeof reservationTokenFactory !== 'function') {
129
+ throw new Error('createDurableConsumptionStore: reservationTokenFactory must be a function');
130
+ }
131
+ if (ttlSeconds !== undefined && ttlSeconds !== null
132
+ && (!Number.isSafeInteger(ttlSeconds) || ttlSeconds <= 0)) {
133
+ throw new Error('createDurableConsumptionStore: ttlSeconds must be a positive safe integer when supplied');
134
+ }
135
+ const opt = ttlSeconds ? { ttlSeconds } : undefined;
136
+ const ownedReservations = new Map();
137
+
138
+ function ownedValue(key) {
139
+ const token = ownedReservations.get(key);
140
+ if (!token) {
141
+ throw new Error(`durable consumption transition refused: this store does not own reservation ${key}`);
142
+ }
143
+ return `${RESERVED_PREFIX}${token}`;
144
+ }
145
+
146
+ return {
147
+ durable: backend.durable === true,
148
+ ownershipFenced: true,
149
+ permanentConsumption: ttlSeconds === undefined || ttlSeconds === null,
150
+ retentionSeconds: ttlSeconds ?? null,
151
+ async health() {
152
+ if (typeof backend.health !== 'function') return { ok: false, reason: 'backend_health_unavailable' };
153
+ return backend.health();
154
+ },
155
+ async reserve(key) {
156
+ const token = reservationTokenFactory();
157
+ if (typeof token !== 'string' || token.length < 16) {
158
+ throw new Error('reservationTokenFactory must return an unpredictable string of at least 16 characters');
159
+ }
160
+ const inserted = (await backend.addIfAbsent(key, `${RESERVED_PREFIX}${token}`)) === true;
161
+ if (inserted) ownedReservations.set(key, token);
162
+ return inserted;
163
+ },
164
+ async commit(key) {
165
+ const expected = ownedValue(key);
166
+ const changed = await backend.compareAndSet(key, expected, COMMITTED_VALUE, opt);
167
+ if (changed !== true) {
168
+ throw new Error(`durable consumption commit refused: reservation ownership was lost for ${key}`);
169
+ }
170
+ ownedReservations.delete(key);
171
+ return true;
172
+ },
173
+ async release(key) {
174
+ const expected = ownedValue(key);
175
+ const deleted = await backend.deleteIfValue(key, expected);
176
+ ownedReservations.delete(key);
177
+ if (deleted !== true) {
178
+ throw new Error(`durable consumption release refused: reservation ownership was lost for ${key}`);
179
+ }
180
+ return true;
181
+ },
182
+ async consume(key) { return (await backend.addIfAbsent(key, COMMITTED_VALUE, opt)) === true; },
183
+ async has(key) { return (await backend.has(key)) === true; },
184
+ };
185
+ }
186
+
187
+ /** In-memory reference backend (for tests/single-process). addIfAbsent is atomic in a single thread. */
188
+ export function createMemoryBackend() {
189
+ const map = new Map();
190
+ return {
191
+ durable: false,
192
+ async addIfAbsent(key, value) { if (map.has(key)) return false; map.set(key, value); return true; },
193
+ async compareAndSet(key, expected, replacement) {
194
+ if (map.get(key) !== expected) return false;
195
+ map.set(key, replacement);
196
+ return true;
197
+ },
198
+ async deleteIfValue(key, expected) {
199
+ if (map.get(key) !== expected) return false;
200
+ return map.delete(key);
201
+ },
202
+ async has(key) { return map.has(key); },
203
+ async get(key) { return map.get(key); },
204
+ get size() { return map.size; },
205
+ };
206
+ }
207
+
208
+ export default {
209
+ MemoryConsumptionStore,
210
+ createDurableConsumptionStore,
211
+ createMemoryBackend,
212
+ isSecureConsumptionStore,
213
+ DURABLE_CONSUMPTION_VERSION,
214
+ };
@@ -0,0 +1,115 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Duplicate-name and Unicode-scalar gate for signed nested JSON such as
3
+ // WebAuthn clientDataJSON. JSON.parse remains the syntax gate.
4
+
5
+ export const MAX_JSON_DEPTH = 64;
6
+
7
+ function hasUnpairedUtf16Surrogate(value) {
8
+ for (let index = 0; index < value.length; index += 1) {
9
+ const code = value.charCodeAt(index);
10
+ if (code >= 0xd800 && code <= 0xdbff) {
11
+ const next = value.charCodeAt(index + 1);
12
+ if (next < 0xdc00 || next > 0xdfff) return true;
13
+ index += 1;
14
+ } else if (code >= 0xdc00 && code <= 0xdfff) {
15
+ return true;
16
+ }
17
+ }
18
+ return false;
19
+ }
20
+
21
+ type JsonObjectFrame = { object: true; keys: Set<string>; expectsKey: boolean };
22
+ type JsonArrayFrame = { object: false };
23
+ type JsonFrame = JsonObjectFrame | JsonArrayFrame;
24
+
25
+ export function strictJsonGate(raw) {
26
+ if (typeof raw !== 'string') return { ok: false, reason: 'JSON input must be text' };
27
+ if (hasUnpairedUtf16Surrogate(raw)) {
28
+ return { ok: false, reason: 'unpaired Unicode surrogate' };
29
+ }
30
+ try { JSON.parse(raw); } catch { return { ok: false, reason: 'invalid JSON syntax' }; }
31
+ let index = 0;
32
+ const stack: JsonFrame[] = [];
33
+ let reason: string | null = null;
34
+ const escapes = { '"': '"', '\\': '\\', '/': '/', b: '\b', f: '\f', n: '\n', r: '\r', t: '\t' };
35
+
36
+ function readString() {
37
+ index += 1;
38
+ let output = '';
39
+ while (index < raw.length) {
40
+ const character = raw[index];
41
+ if (character === '"') { index += 1; return output; }
42
+ if (character !== '\\') { output += character; index += 1; continue; }
43
+ const escape = raw[index + 1];
44
+ if (escape !== 'u') {
45
+ output += escapes[escape] ?? '';
46
+ index += 2;
47
+ continue;
48
+ }
49
+ const first = Number.parseInt(raw.slice(index + 2, index + 6), 16);
50
+ index += 6;
51
+ if (first >= 0xd800 && first <= 0xdbff) {
52
+ if (raw[index] === '\\' && raw[index + 1] === 'u') {
53
+ const second = Number.parseInt(raw.slice(index + 2, index + 6), 16);
54
+ if (second >= 0xdc00 && second <= 0xdfff) {
55
+ output += String.fromCharCode(first, second);
56
+ index += 6;
57
+ continue;
58
+ }
59
+ }
60
+ reason = 'unpaired high surrogate escape';
61
+ return null;
62
+ }
63
+ if (first >= 0xdc00 && first <= 0xdfff) {
64
+ reason = 'unpaired low surrogate escape';
65
+ return null;
66
+ }
67
+ output += String.fromCharCode(first);
68
+ }
69
+ reason = 'unterminated string';
70
+ return null;
71
+ }
72
+
73
+ while (index < raw.length) {
74
+ const character = raw[index];
75
+ if (character === '{') {
76
+ stack.push({ object: true, keys: new Set<string>(), expectsKey: true });
77
+ if (stack.length > MAX_JSON_DEPTH) return { ok: false, reason: `nesting depth exceeds ${MAX_JSON_DEPTH}` };
78
+ index += 1;
79
+ } else if (character === '[') {
80
+ stack.push({ object: false });
81
+ if (stack.length > MAX_JSON_DEPTH) return { ok: false, reason: `nesting depth exceeds ${MAX_JSON_DEPTH}` };
82
+ index += 1;
83
+ } else if (character === '}' || character === ']') {
84
+ stack.pop();
85
+ index += 1;
86
+ } else if (character === ',') {
87
+ const top = stack.at(-1);
88
+ if (top?.object) top.expectsKey = true;
89
+ index += 1;
90
+ } else if (character === '"') {
91
+ const top = stack.at(-1);
92
+ const isKey = Boolean(top?.object && top.expectsKey);
93
+ const value = readString();
94
+ if (reason) return { ok: false, reason };
95
+ if (isKey) {
96
+ // `isKey` is only true when `top?.object && top.expectsKey` held above,
97
+ // which guarantees `top` is a defined object-frame here; narrow the
98
+ // type for the compiler without altering the runtime reference.
99
+ const frame = top as JsonObjectFrame;
100
+ // `readString()` only ever returns null on a path that also sets
101
+ // `reason`, and the `if (reason) return` above already exited in
102
+ // that case, so `value` is guaranteed to be a string here.
103
+ const key = value as string;
104
+ if (frame.keys.has(key)) return { ok: false, reason: 'duplicate object member name' };
105
+ frame.keys.add(key);
106
+ frame.expectsKey = false;
107
+ }
108
+ } else {
109
+ index += 1;
110
+ }
111
+ }
112
+ return { ok: true };
113
+ }
114
+
115
+ export default { strictJsonGate, MAX_JSON_DEPTH };