@absol-labs/agent 0.11.0 → 0.12.0-rc.2

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 (366) hide show
  1. package/README.md +43 -51
  2. package/dist/capability/invocation-capability.d.ts +2 -2
  3. package/dist/capability/probe-capability.d.ts +225 -0
  4. package/dist/capability/probe-capability.d.ts.map +1 -0
  5. package/dist/capability/probe-capability.js +248 -0
  6. package/dist/capability/probe-capability.js.map +1 -0
  7. package/dist/cli/args.d.ts +11 -0
  8. package/dist/cli/args.d.ts.map +1 -1
  9. package/dist/cli/args.js +23 -0
  10. package/dist/cli/args.js.map +1 -1
  11. package/dist/cli/commands/claim.d.ts +32 -0
  12. package/dist/cli/commands/claim.d.ts.map +1 -1
  13. package/dist/cli/commands/claim.js +116 -28
  14. package/dist/cli/commands/claim.js.map +1 -1
  15. package/dist/cli/commands/close.d.ts.map +1 -1
  16. package/dist/cli/commands/close.js +35 -3
  17. package/dist/cli/commands/close.js.map +1 -1
  18. package/dist/cli/commands/invoke.d.ts +2 -0
  19. package/dist/cli/commands/invoke.d.ts.map +1 -1
  20. package/dist/cli/commands/invoke.js +2 -2
  21. package/dist/cli/commands/invoke.js.map +1 -1
  22. package/dist/cli/commands/mcp.d.ts.map +1 -1
  23. package/dist/cli/commands/mcp.js +15 -2
  24. package/dist/cli/commands/mcp.js.map +1 -1
  25. package/dist/cli/commands/open.d.ts.map +1 -1
  26. package/dist/cli/commands/open.js +58 -4
  27. package/dist/cli/commands/open.js.map +1 -1
  28. package/dist/cli/commands/pay.d.ts +34 -0
  29. package/dist/cli/commands/pay.d.ts.map +1 -0
  30. package/dist/cli/commands/pay.js +479 -0
  31. package/dist/cli/commands/pay.js.map +1 -0
  32. package/dist/cli/commands/reclaim-unverified.d.ts.map +1 -1
  33. package/dist/cli/commands/reclaim-unverified.js +3 -2
  34. package/dist/cli/commands/reclaim-unverified.js.map +1 -1
  35. package/dist/cli/commands/reclaim.d.ts +23 -2
  36. package/dist/cli/commands/reclaim.d.ts.map +1 -1
  37. package/dist/cli/commands/reclaim.js +204 -20
  38. package/dist/cli/commands/reclaim.js.map +1 -1
  39. package/dist/cli/commands/refund.d.ts +31 -0
  40. package/dist/cli/commands/refund.d.ts.map +1 -0
  41. package/dist/cli/commands/refund.js +397 -0
  42. package/dist/cli/commands/refund.js.map +1 -0
  43. package/dist/cli/commands/status.d.ts.map +1 -1
  44. package/dist/cli/commands/status.js +21 -2
  45. package/dist/cli/commands/status.js.map +1 -1
  46. package/dist/cli/commands/streams.js +8 -12
  47. package/dist/cli/commands/streams.js.map +1 -1
  48. package/dist/cli/commands/watch.d.ts.map +1 -1
  49. package/dist/cli/commands/watch.js +4 -1
  50. package/dist/cli/commands/watch.js.map +1 -1
  51. package/dist/cli/config.d.ts +44 -26
  52. package/dist/cli/config.d.ts.map +1 -1
  53. package/dist/cli/config.js +88 -38
  54. package/dist/cli/config.js.map +1 -1
  55. package/dist/cli/context.d.ts +19 -0
  56. package/dist/cli/context.d.ts.map +1 -1
  57. package/dist/cli/deps.d.ts +53 -0
  58. package/dist/cli/deps.d.ts.map +1 -1
  59. package/dist/cli/deps.js +51 -1
  60. package/dist/cli/deps.js.map +1 -1
  61. package/dist/cli/errors.d.ts.map +1 -1
  62. package/dist/cli/errors.js +21 -1
  63. package/dist/cli/errors.js.map +1 -1
  64. package/dist/cli/escape-hatch.d.ts +6 -4
  65. package/dist/cli/escape-hatch.d.ts.map +1 -1
  66. package/dist/cli/escape-hatch.js +6 -2
  67. package/dist/cli/escape-hatch.js.map +1 -1
  68. package/dist/cli/listings.d.ts +18 -0
  69. package/dist/cli/listings.d.ts.map +1 -1
  70. package/dist/cli/listings.js +22 -12
  71. package/dist/cli/listings.js.map +1 -1
  72. package/dist/cli/preflight.d.ts +20 -14
  73. package/dist/cli/preflight.d.ts.map +1 -1
  74. package/dist/cli/preflight.js +34 -20
  75. package/dist/cli/preflight.js.map +1 -1
  76. package/dist/cli/privy.d.ts.map +1 -1
  77. package/dist/cli/privy.js +4 -2
  78. package/dist/cli/privy.js.map +1 -1
  79. package/dist/cli/progress.d.ts +13 -1
  80. package/dist/cli/progress.d.ts.map +1 -1
  81. package/dist/cli/progress.js +7 -1
  82. package/dist/cli/progress.js.map +1 -1
  83. package/dist/cli/reclaimability.d.ts +82 -0
  84. package/dist/cli/reclaimability.d.ts.map +1 -0
  85. package/dist/cli/reclaimability.js +80 -0
  86. package/dist/cli/reclaimability.js.map +1 -0
  87. package/dist/cli/run.d.ts +2 -1
  88. package/dist/cli/run.d.ts.map +1 -1
  89. package/dist/cli/run.js +62 -21
  90. package/dist/cli/run.js.map +1 -1
  91. package/dist/cli/scan-range.d.ts +2 -2
  92. package/dist/cli/scan-range.js +2 -2
  93. package/dist/cli/status.d.ts +106 -1
  94. package/dist/cli/status.d.ts.map +1 -1
  95. package/dist/cli/status.js +325 -44
  96. package/dist/cli/status.js.map +1 -1
  97. package/dist/cli/wallet.d.ts +2 -2
  98. package/dist/cli/wallet.d.ts.map +1 -1
  99. package/dist/cli/wallet.js +29 -5
  100. package/dist/cli/wallet.js.map +1 -1
  101. package/dist/discovery/rate-band.d.ts +74 -0
  102. package/dist/discovery/rate-band.d.ts.map +1 -0
  103. package/dist/discovery/rate-band.js +114 -0
  104. package/dist/discovery/rate-band.js.map +1 -0
  105. package/dist/escrow-generation.d.ts +81 -0
  106. package/dist/escrow-generation.d.ts.map +1 -0
  107. package/dist/escrow-generation.js +76 -0
  108. package/dist/escrow-generation.js.map +1 -0
  109. package/dist/frameworks/agentkit.d.ts +8 -4
  110. package/dist/frameworks/agentkit.d.ts.map +1 -1
  111. package/dist/frameworks/agentkit.js +27 -10
  112. package/dist/frameworks/agentkit.js.map +1 -1
  113. package/dist/frameworks/eliza.d.ts +3 -3
  114. package/dist/frameworks/eliza.d.ts.map +1 -1
  115. package/dist/frameworks/eliza.js +15 -6
  116. package/dist/frameworks/eliza.js.map +1 -1
  117. package/dist/frameworks/langchain.d.ts.map +1 -1
  118. package/dist/frameworks/langchain.js +8 -0
  119. package/dist/frameworks/langchain.js.map +1 -1
  120. package/dist/gateway/caller-auth-gateway.d.ts +83 -31
  121. package/dist/gateway/caller-auth-gateway.d.ts.map +1 -1
  122. package/dist/gateway/caller-auth-gateway.js +180 -27
  123. package/dist/gateway/caller-auth-gateway.js.map +1 -1
  124. package/dist/gateway/http-server.d.ts +128 -44
  125. package/dist/gateway/http-server.d.ts.map +1 -1
  126. package/dist/gateway/http-server.js +506 -155
  127. package/dist/gateway/http-server.js.map +1 -1
  128. package/dist/gateway/probe-auth.d.ts +112 -0
  129. package/dist/gateway/probe-auth.d.ts.map +1 -0
  130. package/dist/gateway/probe-auth.js +127 -0
  131. package/dist/gateway/probe-auth.js.map +1 -0
  132. package/dist/gateway/signed-rate-bounds.d.ts +51 -0
  133. package/dist/gateway/signed-rate-bounds.d.ts.map +1 -0
  134. package/dist/gateway/signed-rate-bounds.js +94 -0
  135. package/dist/gateway/signed-rate-bounds.js.map +1 -0
  136. package/dist/gateway/x402-opener-mode.d.ts +252 -0
  137. package/dist/gateway/x402-opener-mode.d.ts.map +1 -0
  138. package/dist/gateway/x402-opener-mode.js +772 -0
  139. package/dist/gateway/x402-opener-mode.js.map +1 -0
  140. package/dist/index.d.ts +6 -2
  141. package/dist/index.d.ts.map +1 -1
  142. package/dist/index.js +10 -5
  143. package/dist/index.js.map +1 -1
  144. package/dist/mcp/server.d.ts +64 -1
  145. package/dist/mcp/server.d.ts.map +1 -1
  146. package/dist/mcp/server.js +247 -37
  147. package/dist/mcp/server.js.map +1 -1
  148. package/dist/mcp/x402-tools.d.ts +109 -0
  149. package/dist/mcp/x402-tools.d.ts.map +1 -0
  150. package/dist/mcp/x402-tools.js +554 -0
  151. package/dist/mcp/x402-tools.js.map +1 -0
  152. package/dist/network.d.ts +119 -0
  153. package/dist/network.d.ts.map +1 -0
  154. package/dist/network.js +199 -0
  155. package/dist/network.js.map +1 -0
  156. package/dist/sdk/client.d.ts +13 -0
  157. package/dist/sdk/client.d.ts.map +1 -1
  158. package/dist/sdk/client.js +15 -0
  159. package/dist/sdk/client.js.map +1 -1
  160. package/dist/sdk/invoke.d.ts +4 -0
  161. package/dist/sdk/invoke.d.ts.map +1 -1
  162. package/dist/sdk/invoke.js +2 -5
  163. package/dist/sdk/invoke.js.map +1 -1
  164. package/dist/wallet/autonomous-wallet-broker.d.ts +3 -0
  165. package/dist/wallet/autonomous-wallet-broker.d.ts.map +1 -1
  166. package/dist/wallet/autonomous-wallet-broker.js +27 -23
  167. package/dist/wallet/autonomous-wallet-broker.js.map +1 -1
  168. package/dist/wallet/autonomous-wallet-protocol.d.ts +8 -2
  169. package/dist/wallet/autonomous-wallet-protocol.d.ts.map +1 -1
  170. package/dist/wallet/autonomous-wallet-protocol.js +9 -2
  171. package/dist/wallet/autonomous-wallet-protocol.js.map +1 -1
  172. package/dist/wallet/autonomous-wallet.d.ts.map +1 -1
  173. package/dist/wallet/autonomous-wallet.js +3 -2
  174. package/dist/wallet/autonomous-wallet.js.map +1 -1
  175. package/dist/wallet/broker-escrows.d.ts +222 -0
  176. package/dist/wallet/broker-escrows.d.ts.map +1 -0
  177. package/dist/wallet/broker-escrows.js +36 -0
  178. package/dist/wallet/broker-escrows.js.map +1 -0
  179. package/dist/wallet/lifecycle.d.ts +21 -5
  180. package/dist/wallet/lifecycle.d.ts.map +1 -1
  181. package/dist/wallet/lifecycle.js +32 -12
  182. package/dist/wallet/lifecycle.js.map +1 -1
  183. package/dist/wallet/privy-broker-server.d.ts.map +1 -1
  184. package/dist/wallet/privy-broker-server.js +31 -26
  185. package/dist/wallet/privy-broker-server.js.map +1 -1
  186. package/dist/wallet/privy-session-broker.d.ts +14 -5
  187. package/dist/wallet/privy-session-broker.d.ts.map +1 -1
  188. package/dist/wallet/privy-session-broker.js +29 -17
  189. package/dist/wallet/privy-session-broker.js.map +1 -1
  190. package/dist/wallet/privy-session-provider.d.ts +3 -1
  191. package/dist/wallet/privy-session-provider.d.ts.map +1 -1
  192. package/dist/wallet/privy-session-provider.js +5 -3
  193. package/dist/wallet/privy-session-provider.js.map +1 -1
  194. package/dist/wallet/provider.d.ts.map +1 -1
  195. package/dist/wallet/provider.js +15 -8
  196. package/dist/wallet/provider.js.map +1 -1
  197. package/dist/x402/facilitator.d.ts +3 -30
  198. package/dist/x402/facilitator.d.ts.map +1 -1
  199. package/dist/x402/facilitator.js +0 -27
  200. package/dist/x402/facilitator.js.map +1 -1
  201. package/dist/x402-buyer/guarded-fetch.d.ts +51 -0
  202. package/dist/x402-buyer/guarded-fetch.d.ts.map +1 -0
  203. package/dist/x402-buyer/guarded-fetch.js +147 -0
  204. package/dist/x402-buyer/guarded-fetch.js.map +1 -0
  205. package/dist/x402-buyer/index.d.ts +10 -0
  206. package/dist/x402-buyer/index.d.ts.map +1 -0
  207. package/dist/x402-buyer/index.js +10 -0
  208. package/dist/x402-buyer/index.js.map +1 -0
  209. package/dist/x402-buyer/pay.d.ts +157 -0
  210. package/dist/x402-buyer/pay.d.ts.map +1 -0
  211. package/dist/x402-buyer/pay.js +366 -0
  212. package/dist/x402-buyer/pay.js.map +1 -0
  213. package/dist/x402-opener/abi.d.ts +684 -0
  214. package/dist/x402-opener/abi.d.ts.map +1 -0
  215. package/dist/x402-opener/abi.js +880 -0
  216. package/dist/x402-opener/abi.js.map +1 -0
  217. package/dist/x402-opener/facilitator-entry.d.ts +3 -0
  218. package/dist/x402-opener/facilitator-entry.d.ts.map +1 -0
  219. package/dist/x402-opener/facilitator-entry.js +40 -0
  220. package/dist/x402-opener/facilitator-entry.js.map +1 -0
  221. package/dist/x402-opener/facilitator-server.d.ts +209 -0
  222. package/dist/x402-opener/facilitator-server.d.ts.map +1 -0
  223. package/dist/x402-opener/facilitator-server.js +762 -0
  224. package/dist/x402-opener/facilitator-server.js.map +1 -0
  225. package/dist/x402-opener/index.d.ts +14 -0
  226. package/dist/x402-opener/index.d.ts.map +1 -0
  227. package/dist/x402-opener/index.js +14 -0
  228. package/dist/x402-opener/index.js.map +1 -0
  229. package/dist/x402-opener/listing-resolver.d.ts +55 -0
  230. package/dist/x402-opener/listing-resolver.d.ts.map +1 -0
  231. package/dist/x402-opener/listing-resolver.js +125 -0
  232. package/dist/x402-opener/listing-resolver.js.map +1 -0
  233. package/dist/x402-opener/refund-relay.d.ts +266 -0
  234. package/dist/x402-opener/refund-relay.d.ts.map +1 -0
  235. package/dist/x402-opener/refund-relay.js +1341 -0
  236. package/dist/x402-opener/refund-relay.js.map +1 -0
  237. package/dist/x402-opener/relayer-queue.d.ts +14 -0
  238. package/dist/x402-opener/relayer-queue.d.ts.map +1 -0
  239. package/dist/x402-opener/relayer-queue.js +18 -0
  240. package/dist/x402-opener/relayer-queue.js.map +1 -0
  241. package/dist/x402-opener/scheme.d.ts +296 -0
  242. package/dist/x402-opener/scheme.d.ts.map +1 -0
  243. package/dist/x402-opener/scheme.js +1035 -0
  244. package/dist/x402-opener/scheme.js.map +1 -0
  245. package/dist/x402-opener/terms.d.ts +165 -0
  246. package/dist/x402-opener/terms.d.ts.map +1 -0
  247. package/dist/x402-opener/terms.js +304 -0
  248. package/dist/x402-opener/terms.js.map +1 -0
  249. package/dist/zktls/reclaim.d.ts +2 -2
  250. package/dist/zktls/t2-delivery-proof.d.ts +6 -6
  251. package/docs/agent-layer.md +3 -2
  252. package/docs/autonomous-privy-wallet.md +188 -0
  253. package/docs/cli.md +265 -34
  254. package/docs/langchain.md +5 -0
  255. package/docs/mcp-reference.md +91 -5
  256. package/docs/probe-capability.md +191 -0
  257. package/docs/quickstart.md +39 -4
  258. package/docs/threat-model.md +20 -0
  259. package/docs/x402.md +766 -593
  260. package/package.json +19 -17
  261. package/src/capability/probe-capability.ts +355 -0
  262. package/src/cli/args.ts +29 -0
  263. package/src/cli/commands/claim.ts +168 -33
  264. package/src/cli/commands/close.ts +36 -3
  265. package/src/cli/commands/invoke.ts +5 -2
  266. package/src/cli/commands/mcp.ts +18 -2
  267. package/src/cli/commands/open.ts +75 -4
  268. package/src/cli/commands/pay.ts +584 -0
  269. package/src/cli/commands/reclaim-unverified.ts +8 -2
  270. package/src/cli/commands/reclaim.ts +277 -19
  271. package/src/cli/commands/refund.ts +475 -0
  272. package/src/cli/commands/status.ts +28 -2
  273. package/src/cli/commands/streams.ts +10 -14
  274. package/src/cli/commands/watch.ts +11 -1
  275. package/src/cli/config.ts +139 -52
  276. package/src/cli/context.ts +16 -0
  277. package/src/cli/deps.ts +111 -1
  278. package/src/cli/errors.ts +24 -1
  279. package/src/cli/escape-hatch.ts +19 -5
  280. package/src/cli/listings.ts +43 -10
  281. package/src/cli/preflight.ts +36 -21
  282. package/src/cli/privy.ts +4 -2
  283. package/src/cli/progress.ts +21 -2
  284. package/src/cli/reclaimability.ts +154 -0
  285. package/src/cli/run.ts +77 -21
  286. package/src/cli/scan-range.ts +2 -2
  287. package/src/cli/status.ts +454 -44
  288. package/src/cli/wallet.ts +45 -8
  289. package/src/discovery/rate-band.ts +172 -0
  290. package/src/escrow-generation.ts +135 -0
  291. package/src/frameworks/agentkit.ts +45 -11
  292. package/src/frameworks/eliza.ts +27 -5
  293. package/src/frameworks/langchain.ts +14 -0
  294. package/src/gateway/caller-auth-gateway.ts +320 -68
  295. package/src/gateway/http-server.ts +717 -220
  296. package/src/gateway/probe-auth.ts +191 -0
  297. package/src/gateway/signed-rate-bounds.ts +164 -0
  298. package/src/gateway/x402-opener-mode.ts +1171 -0
  299. package/src/index.ts +82 -19
  300. package/src/mcp/server.ts +351 -42
  301. package/src/mcp/x402-tools.ts +785 -0
  302. package/src/network.ts +302 -0
  303. package/src/sdk/client.ts +25 -0
  304. package/src/sdk/invoke.ts +6 -5
  305. package/src/wallet/autonomous-wallet-broker.ts +46 -20
  306. package/src/wallet/autonomous-wallet-protocol.ts +11 -2
  307. package/src/wallet/autonomous-wallet.ts +4 -2
  308. package/src/wallet/broker-escrows.ts +66 -0
  309. package/src/wallet/lifecycle.ts +53 -18
  310. package/src/wallet/privy-broker-server.ts +35 -25
  311. package/src/wallet/privy-session-broker.ts +58 -25
  312. package/src/wallet/privy-session-provider.ts +7 -3
  313. package/src/wallet/provider.ts +20 -15
  314. package/src/x402/facilitator.ts +3 -43
  315. package/src/x402-buyer/guarded-fetch.ts +213 -0
  316. package/src/x402-buyer/index.ts +9 -0
  317. package/src/x402-buyer/pay.ts +555 -0
  318. package/src/x402-opener/abi.ts +881 -0
  319. package/src/x402-opener/facilitator-entry.ts +48 -0
  320. package/src/x402-opener/facilitator-server.ts +1114 -0
  321. package/src/x402-opener/index.ts +13 -0
  322. package/src/x402-opener/listing-resolver.ts +202 -0
  323. package/src/x402-opener/refund-relay.ts +1873 -0
  324. package/src/x402-opener/relayer-queue.ts +18 -0
  325. package/src/x402-opener/scheme.ts +1551 -0
  326. package/src/x402-opener/terms.ts +453 -0
  327. package/dist/gateway/x402-mode.d.ts +0 -123
  328. package/dist/gateway/x402-mode.d.ts.map +0 -1
  329. package/dist/gateway/x402-mode.js +0 -212
  330. package/dist/gateway/x402-mode.js.map +0 -1
  331. package/dist/x402/delivery-oracle.d.ts +0 -108
  332. package/dist/x402/delivery-oracle.d.ts.map +0 -1
  333. package/dist/x402/delivery-oracle.js +0 -174
  334. package/dist/x402/delivery-oracle.js.map +0 -1
  335. package/dist/x402/facilitator-server-entry.d.ts +0 -3
  336. package/dist/x402/facilitator-server-entry.d.ts.map +0 -1
  337. package/dist/x402/facilitator-server-entry.js +0 -27
  338. package/dist/x402/facilitator-server-entry.js.map +0 -1
  339. package/dist/x402/facilitator-server.d.ts +0 -73
  340. package/dist/x402/facilitator-server.d.ts.map +0 -1
  341. package/dist/x402/facilitator-server.js +0 -305
  342. package/dist/x402/facilitator-server.js.map +0 -1
  343. package/dist/x402/payment-requirements.d.ts +0 -106
  344. package/dist/x402/payment-requirements.d.ts.map +0 -1
  345. package/dist/x402/payment-requirements.js +0 -138
  346. package/dist/x402/payment-requirements.js.map +0 -1
  347. package/dist/x402/relayer.d.ts +0 -41
  348. package/dist/x402/relayer.d.ts.map +0 -1
  349. package/dist/x402/relayer.js +0 -64
  350. package/dist/x402/relayer.js.map +0 -1
  351. package/dist/x402/resource-index.d.ts +0 -75
  352. package/dist/x402/resource-index.d.ts.map +0 -1
  353. package/dist/x402/resource-index.js +0 -126
  354. package/dist/x402/resource-index.js.map +0 -1
  355. package/dist/x402/verified-delivery-facilitator.d.ts +0 -285
  356. package/dist/x402/verified-delivery-facilitator.d.ts.map +0 -1
  357. package/dist/x402/verified-delivery-facilitator.js +0 -423
  358. package/dist/x402/verified-delivery-facilitator.js.map +0 -1
  359. package/src/gateway/x402-mode.ts +0 -350
  360. package/src/x402/delivery-oracle.ts +0 -285
  361. package/src/x402/facilitator-server-entry.ts +0 -35
  362. package/src/x402/facilitator-server.ts +0 -445
  363. package/src/x402/payment-requirements.ts +0 -231
  364. package/src/x402/relayer.ts +0 -122
  365. package/src/x402/resource-index.ts +0 -228
  366. package/src/x402/verified-delivery-facilitator.ts +0 -744
@@ -1,8 +1,12 @@
1
1
  import { createServer, } from "node:http";
2
2
  import { isAddress, isHex } from "viem";
3
3
  import { CallerAuthGateway, DEFAULT_MAX_STATE_STALENESS_SECONDS, MAX_CORROBORATING_STREAM_READERS, } from "./caller-auth-gateway.js";
4
- import { createHttpFacilitatorClient, createListingPriceQuoter, encodeSettleResponseHeader, X402_PAYMENT_RESPONSE_HEADER, X402_SETTLEMENT_HEADROOM_SECONDS, } from "./x402-mode.js";
5
- import { discoverServices } from "../discovery/registry.js";
4
+ import { DEFAULT_MAX_PROBES_PER_MINUTE, DEFAULT_ORACLE_SIGNER_CACHE_SECONDS, } from "./probe-auth.js";
5
+ import { PROBE_MARKER_HEADER_NAME } from "../capability/probe-capability.js";
6
+ import { networkForCaip2, networkOpenerFactories } from "@absol-labs/sdk";
7
+ import { chainIdFromNetwork, assertSchemaDeclaration, assertValidPrice, assertValidTerms, assertValidUpfrontTerms, isHttpLink, } from "../x402-opener/terms.js";
8
+ import { loadSignedRateBoundsFile } from "./signed-rate-bounds.js";
9
+ import { defaultNetwork, requireNetworkForChainId } from "../network.js";
6
10
  /**
7
11
  * Reference standalone reverse-proxy gateway server (metrik-agent#63).
8
12
  *
@@ -32,7 +36,7 @@ export function createCallerAuthGatewayServer(config) {
32
36
  const upstreamBase = new URL(config.upstreamUrl);
33
37
  const upstreamTimeoutMs = config.upstreamTimeoutMs ?? 30_000;
34
38
  const httpServer = createServer((req, res) => {
35
- handleRequest(req, res, config.gateway, upstreamBase, upstreamTimeoutMs, config.x402).catch((error) => {
39
+ handleRequest(req, res, config.gateway, upstreamBase, upstreamTimeoutMs, config.x402Opener).catch((error) => {
36
40
  logError("unhandled request error", error);
37
41
  if (!res.headersSent) {
38
42
  writeJsonError(res, 500, "internal-error");
@@ -58,24 +62,31 @@ export function createCallerAuthGatewayServer(config) {
58
62
  }),
59
63
  };
60
64
  }
61
- async function handleRequest(req, res, gateway, upstreamBase, upstreamTimeoutMs, x402) {
65
+ async function handleRequest(req, res, gateway, upstreamBase, upstreamTimeoutMs, x402Opener) {
62
66
  const requestUrl = new URL(req.url ?? "/", "http://localhost");
63
67
  const method = req.method ?? "GET";
64
68
  const headers = normalizeIncomingHeaders(req.headers);
69
+ if (x402Opener !== undefined) {
70
+ const route = routeX402(headers, gateway);
71
+ if (route === "ambiguous-probe-mode") {
72
+ writeJsonError(res, 400, "ambiguous-probe-mode");
73
+ return;
74
+ }
75
+ if (route === "ambiguous-capability-and-payment") {
76
+ writeJsonError(res, 400, "ambiguous-capability-and-payment");
77
+ return;
78
+ }
79
+ if (route === "x402") {
80
+ await handleX402Request(req, res, x402Opener, requestUrl, method, headers, upstreamBase, upstreamTimeoutMs);
81
+ return;
82
+ }
83
+ }
65
84
  const decision = await gateway.authorize({
66
85
  headers,
67
86
  method,
68
87
  path: requestUrl.pathname,
69
88
  });
70
89
  if (!decision.authorized) {
71
- // The x402 challenge is a PROTOCOL body, not Metrik's `{authorized,reason}`
72
- // shape: `x402-fetch` parses `{x402Version, accepts}` and nothing else.
73
- if (decision.x402PaymentRequired !== undefined) {
74
- res
75
- .writeHead(402, { "Content-Type": "application/json" })
76
- .end(JSON.stringify(decision.x402PaymentRequired));
77
- return;
78
- }
79
90
  writeJsonError(res, decision.httpStatus ?? 403, decision.reason ?? "unauthorized");
80
91
  return;
81
92
  }
@@ -90,20 +101,18 @@ async function handleRequest(req, res, gateway, upstreamBase, upstreamTimeoutMs,
90
101
  }
91
102
  upstreamHeaders.set(key, value);
92
103
  }
93
- const upstreamUrl = new URL(requestUrl.pathname + requestUrl.search, upstreamBase);
94
- // Bound the upstream by the payment's OWN deadline, not just by the server's
95
- // timeout. Settlement happens after the upstream returns, so an upstream that
96
- // outlives the signed authorization would produce its side effects and then
97
- // be unsettleable — the caller would get the work for free (never the response
98
- // body, which is only flushed after settlement, but the side effects are
99
- // real). Aborting early turns that into an ordinary 502.
100
- const effectiveTimeoutMs = boundedUpstreamTimeoutMs(upstreamTimeoutMs, decision.x402Payment?.validBefore);
101
- if (effectiveTimeoutMs <= 0) {
102
- writeJsonError(res, 402, "x402-authorization-too-short");
103
- return;
104
+ // The probe marker is gateway-asserted, never client-asserted: strip any
105
+ // inbound copy first, so a paying caller cannot mark its own billable request
106
+ // as verification traffic and slip past the seller's metering.
107
+ upstreamHeaders.delete(PROBE_MARKER_HEADER_NAME);
108
+ if (decision.probe !== undefined) {
109
+ // Advisory marker so the seller's OWN metering can exclude verification
110
+ // traffic from billable usage. The gateway itself charges nothing for it.
111
+ upstreamHeaders.set(PROBE_MARKER_HEADER_NAME, "1");
104
112
  }
113
+ const upstreamUrl = new URL(requestUrl.pathname + requestUrl.search, upstreamBase);
105
114
  const controller = new AbortController();
106
- const timeout = setTimeout(() => controller.abort(), effectiveTimeoutMs);
115
+ const timeout = setTimeout(() => controller.abort(), upstreamTimeoutMs);
107
116
  try {
108
117
  const upstreamResponse = await fetch(upstreamUrl, {
109
118
  method,
@@ -113,21 +122,6 @@ async function handleRequest(req, res, gateway, upstreamBase, upstreamTimeoutMs,
113
122
  });
114
123
  const buffer = Buffer.from(await upstreamResponse.arrayBuffer());
115
124
  const responseHeaders = headersToObject(upstreamResponse.headers);
116
- // Settle AFTER the upstream produced a response and BEFORE that response is
117
- // flushed. Buyer-favouring on both edges: a call that never produced a
118
- // response is never charged for, and a payment that cannot be settled
119
- // returns `402` instead of handing over the goods for free.
120
- if (decision.x402Payment !== undefined) {
121
- const settled = await settleX402(decision, x402);
122
- if (settled === undefined) {
123
- writeJsonError(res, 402, "x402-settlement-failed");
124
- return;
125
- }
126
- responseHeaders[X402_PAYMENT_RESPONSE_HEADER] =
127
- encodeSettleResponseHeader(settled);
128
- responseHeaders["access-control-expose-headers"] =
129
- X402_PAYMENT_RESPONSE_HEADER;
130
- }
131
125
  res.writeHead(upstreamResponse.status, responseHeaders);
132
126
  res.end(buffer);
133
127
  }
@@ -139,43 +133,93 @@ async function handleRequest(req, res, gateway, upstreamBase, upstreamTimeoutMs,
139
133
  clearTimeout(timeout);
140
134
  }
141
135
  }
136
+ /** x402 v2 request header (base64 `PaymentPayload`). */
137
+ const PAYMENT_SIGNATURE_HEADER = "payment-signature";
138
+ /** x402 v1 request header. Never forwarded upstream in x402 mode either. */
139
+ const LEGACY_PAYMENT_HEADER = "x-payment";
142
140
  /**
143
- * The upstream budget, capped so the signed authorization is still settleable
144
- * when the call returns. Returns <= 0 when there is no usable window at all.
141
+ * Decides which path an x402-mode request takes. A payment is never mixed with
142
+ * a capability or a probe: both-present is a `400`, not a guess.
145
143
  */
146
- export function boundedUpstreamTimeoutMs(upstreamTimeoutMs, validBeforeSeconds, nowSeconds = Math.floor(Date.now() / 1000)) {
147
- if (validBeforeSeconds === undefined) {
148
- return upstreamTimeoutMs;
149
- }
150
- const remainingSeconds = validBeforeSeconds - nowSeconds - X402_SETTLEMENT_HEADROOM_SECONDS;
151
- if (remainingSeconds <= 0) {
152
- return 0;
153
- }
154
- return Math.min(upstreamTimeoutMs, remainingSeconds * 1000);
144
+ function routeX402(headers, gateway) {
145
+ const present = (name) => {
146
+ const value = headers[name];
147
+ return value !== undefined && value.trim().length > 0;
148
+ };
149
+ const hasPayment = present(PAYMENT_SIGNATURE_HEADER);
150
+ const hasCapability = present(gateway.capabilityHeaderName);
151
+ const hasProbe = present(gateway.probeCapabilityHeaderName);
152
+ if (hasProbe && hasPayment)
153
+ return "ambiguous-probe-mode";
154
+ if (hasCapability && hasPayment)
155
+ return "ambiguous-capability-and-payment";
156
+ if (hasPayment)
157
+ return "x402";
158
+ if (!hasCapability && !hasProbe)
159
+ return "x402";
160
+ return "capability-or-probe";
155
161
  }
156
- /** Returns the settlement result, or `undefined` if nothing was settled. */
157
- async function settleX402(decision, x402) {
158
- const payment = decision.x402Payment;
159
- if (payment === undefined) {
160
- return undefined;
162
+ /**
163
+ * The x402 v2 path. The protocol logic (verify → upstream → settle-before-body)
164
+ * lives with the mode in `./x402-opener-mode.ts`, so this module — and the
165
+ * package's root barrel — never loads `@x402/*` for a gateway that does not
166
+ * enable it. This function only supplies the transport.
167
+ */
168
+ async function handleX402Request(req, res, mode, requestUrl, method, headers, upstreamBase, upstreamTimeoutMs) {
169
+ await mode.handle({
170
+ requestUrl,
171
+ method,
172
+ headers,
173
+ callUpstream: () => callUpstream(req, method, headers, requestUrl, upstreamBase, upstreamTimeoutMs),
174
+ writeJson: (status, reason) => writeJsonError(res, status, reason),
175
+ write: (status, responseHeaders, body) => {
176
+ if (res.headersSent) {
177
+ res.end();
178
+ return;
179
+ }
180
+ res.writeHead(status, responseHeaders);
181
+ res.end(body);
182
+ },
183
+ logError,
184
+ });
185
+ }
186
+ /** Buffers one upstream call. Payment headers are never forwarded. */
187
+ async function callUpstream(req, method, headers, requestUrl, upstreamBase, upstreamTimeoutMs) {
188
+ let body;
189
+ if (method !== "GET" && method !== "HEAD") {
190
+ body = await readBody(req);
161
191
  }
162
- if (x402 === undefined) {
163
- // The gateway accepted an x402 payment but this server was given no
164
- // facilitator to settle it with. Refuse: serving here would be free service.
165
- logError("x402 settlement", new Error("gateway accepted an x402 payment but the server has no x402 facilitator configured"));
166
- return undefined;
192
+ const upstreamHeaders = new Headers();
193
+ for (const [key, value] of Object.entries(headers)) {
194
+ if (value === undefined || HOP_BY_HOP_HEADERS.has(key))
195
+ continue;
196
+ upstreamHeaders.set(key, value);
167
197
  }
198
+ // A signed payment authorization must never reach the upstream: anyone who
199
+ // holds it can submit it to USDC directly, which strands the payer's funds
200
+ // in the opener. The probe marker is gateway-asserted only.
201
+ upstreamHeaders.delete(PAYMENT_SIGNATURE_HEADER);
202
+ upstreamHeaders.delete(LEGACY_PAYMENT_HEADER);
203
+ upstreamHeaders.delete(PROBE_MARKER_HEADER_NAME);
204
+ const upstreamUrl = new URL(requestUrl.pathname + requestUrl.search, upstreamBase);
205
+ const controller = new AbortController();
206
+ const timeout = setTimeout(() => controller.abort(), upstreamTimeoutMs);
168
207
  try {
169
- const settled = await x402.facilitator.settle(payment.payload, payment.requirements);
170
- if (settled.success !== true) {
171
- logError("x402 settlement", new Error(settled.errorReason ?? "facilitator reported failure"));
172
- return undefined;
173
- }
174
- return settled;
208
+ const response = await fetch(upstreamUrl, {
209
+ method,
210
+ headers: upstreamHeaders,
211
+ ...(body === undefined ? {} : { body }),
212
+ signal: controller.signal,
213
+ });
214
+ const buffer = Buffer.from(await response.arrayBuffer());
215
+ return {
216
+ status: response.status,
217
+ headers: headersToObject(response.headers),
218
+ body: buffer,
219
+ };
175
220
  }
176
- catch (error) {
177
- logError("x402 settlement", error);
178
- return undefined;
221
+ finally {
222
+ clearTimeout(timeout);
179
223
  }
180
224
  }
181
225
  function normalizeIncomingHeaders(headers) {
@@ -240,8 +284,14 @@ function logError(context, error) {
240
284
  * - `METRIK_GATEWAY_MAX_STATE_STALENESS_SECONDS` (optional) - reject stream
241
285
  * state observed more than N seconds behind the wall clock. Default
242
286
  * {@link DEFAULT_MAX_STATE_STALENESS_SECONDS}.
243
- * - `METRIK_GATEWAY_CHAIN_ID` (optional) - default `84532` (Base Sepolia).
287
+ * - `METRIK_GATEWAY_CHAIN_ID` (optional) - default the registry's default
288
+ * network. Must be a network the SDK registry knows.
244
289
  * - `METRIK_GATEWAY_HEADER_NAME` (optional) - default `x-metrik-capability`.
290
+ * - `METRIK_GATEWAY_SIGNED_DESCRIPTOR_FILE` (optional, recommended) - JSON file
291
+ * holding this service's operator-signed descriptor (or an array, one per
292
+ * served serviceRef). Verified at startup; the capability path then refuses
293
+ * any stream whose on-chain `ratePerSecond` is outside the signed
294
+ * `minRatePerSecond`/`maxRatePerSecond` (`stream-rate-out-of-bounds`).
245
295
  * - `PORT` / `METRIK_GATEWAY_PORT` (optional) - default `8787`.
246
296
  * - `METRIK_GATEWAY_HOST` (optional) - default `0.0.0.0`.
247
297
  *
@@ -269,11 +319,14 @@ export function parseCallerAuthGatewayEnvConfig(env = process.env) {
269
319
  const upstreamUrl = requireEnv(env, "METRIK_GATEWAY_UPSTREAM_URL");
270
320
  // eslint-disable-next-line no-new -- validates the URL is well-formed.
271
321
  new URL(upstreamUrl);
272
- const chainIdRaw = env.METRIK_GATEWAY_CHAIN_ID ?? "84532";
322
+ const chainIdRaw = env.METRIK_GATEWAY_CHAIN_ID ?? String(defaultNetwork().chain.id);
273
323
  const chainId = Number(chainIdRaw);
274
324
  if (!Number.isInteger(chainId) || chainId <= 0) {
275
325
  throw new Error(`invalid METRIK_GATEWAY_CHAIN_ID: ${chainIdRaw}`);
276
326
  }
327
+ // The stream reader is built from the registry entry for this chain, so an
328
+ // unknown chain is refused at startup rather than at the first request.
329
+ requireNetworkForChainId(chainId, {}, "METRIK_GATEWAY_CHAIN_ID");
277
330
  const portRaw = env.METRIK_GATEWAY_PORT ?? env.PORT ?? "8787";
278
331
  const port = Number(portRaw);
279
332
  if (!Number.isInteger(port) || port < 0 || port > 65535) {
@@ -306,7 +359,12 @@ export function parseCallerAuthGatewayEnvConfig(env = process.env) {
306
359
  maxStateStalenessSeconds <= 0) {
307
360
  throw new Error(`invalid METRIK_GATEWAY_MAX_STATE_STALENESS_SECONDS: ${stalenessRaw}`);
308
361
  }
309
- const x402 = parseGatewayX402EnvConfig(env);
362
+ const signedDescriptorFile = env.METRIK_GATEWAY_SIGNED_DESCRIPTOR_FILE?.trim() || undefined;
363
+ const probe = parseGatewayProbeEnvConfig(env);
364
+ const x402Opener = parseGatewayX402OpenerEnvConfig(env, {
365
+ chainId,
366
+ serviceRef: serviceRef,
367
+ });
310
368
  return {
311
369
  host,
312
370
  port,
@@ -321,106 +379,302 @@ export function parseCallerAuthGatewayEnvConfig(env = process.env) {
321
379
  corroboratingRpcUrls,
322
380
  maxStateStalenessSeconds,
323
381
  ...(headerName === undefined ? {} : { headerName }),
324
- ...(x402 === undefined ? {} : { x402 }),
382
+ ...(probe === undefined ? {} : { probe }),
383
+ ...(x402Opener === undefined ? {} : { x402Opener }),
384
+ ...(signedDescriptorFile === undefined ? {} : { signedDescriptorFile }),
325
385
  };
326
386
  }
327
387
  /**
328
- * x402 mode is OFF unless `METRIK_GATEWAY_X402_PRICE_BASE_UNITS` is set. Once
329
- * on, every other x402 var is required: a half-configured paywall would answer
330
- * `402` with a quote it cannot settle.
388
+ * The x402 v2 opener mode is OFF unless `METRIK_GATEWAY_X402V2_OPENER` is
389
+ * truthy. When on, an uncapabilitied request is answered with a standard x402
390
+ * v2 `402` whose `payTo` is the opener for this listing's terms, and payments
391
+ * settle into a `StreamEscrowV2` stream through the Metrik opener facilitator.
392
+ * The listing is the gateway's primary `METRIK_GATEWAY_SERVICE_REF`.
393
+ *
394
+ * These names deliberately differ from the removed v1 mode's variables, so a
395
+ * leftover v1 environment can never switch a payment mode on by accident.
331
396
  *
332
- * Note what is NOT here: `payTo`. It is never configurable — it is resolved at
333
- * startup from the operator-signed descriptor for this gateway's `serviceRef`
334
- * (see {@link resolveSignedListingPayee}), and the gateway refuses to start if
335
- * that lookup fails.
397
+ * - `METRIK_GATEWAY_X402V2_OPENER` — `1`/`true`/`yes`/`on` to enable.
398
+ * - `METRIK_GATEWAY_X402V2_FACILITATOR_URL` (required) — the Metrik opener facilitator.
399
+ * - `METRIK_GATEWAY_X402V2_FACILITATOR_TOKEN` (required) — its `/settle` bearer token.
400
+ * - `METRIK_GATEWAY_X402V2_FACTORY_ADDRESS` — the `X402StreamOpenerFactory`; default the
401
+ * network's registered factory (required where the registry has none).
402
+ * - `METRIK_GATEWAY_X402V2_OPERATOR` (required) — must be the signed listing's operator.
403
+ * - `METRIK_GATEWAY_X402V2_RATE_PER_SECOND` (required) — USDC base units per second.
404
+ * - `METRIK_GATEWAY_X402V2_MAX_DURATION_SECONDS` (required).
405
+ * - `METRIK_GATEWAY_X402V2_PRICE` (required) — base units per paid request (= deposit),
406
+ * `0 < price <= rate * maxDuration`.
407
+ * - `METRIK_GATEWAY_X402V2_UPFRONT_AMOUNT` — base units; when SET the listing
408
+ * quotes the UPFRONT ("minimum charge") opener instead: `amount = price`,
409
+ * `payTo = upfrontFactory.predict(6 terms)`; `upfrontAmount` is paid to the
410
+ * seller at settlement and is NOT refundable, only `price - upfrontAmount`
411
+ * (`<= rate * maxDuration`) is metered and refundable. `= PRICE` is
412
+ * upfront-only (no stream). Validated with the factory's rules at startup.
413
+ * - `METRIK_GATEWAY_X402V2_UPFRONT_FACTORY_ADDRESS` — the
414
+ * `X402UpfrontStreamOpenerFactory`; default the network registry's (only
415
+ * with `_UPFRONT_AMOUNT`). `_FACTORY_ADDRESS` names the STREAM factory and is
416
+ * refused together with `_UPFRONT_AMOUNT`.
417
+ * - `METRIK_GATEWAY_X402V2_NETWORK` — default `eip155:<METRIK_GATEWAY_CHAIN_ID>`.
418
+ * - `METRIK_GATEWAY_X402V2_ASSET` — default the network's registered USDC.
419
+ * - `METRIK_GATEWAY_X402V2_ASSET_NAME` / `_ASSET_VERSION` — EIP-712 domain, default the
420
+ * registered USDC's own EIP-712 name/version.
421
+ * - `METRIK_GATEWAY_X402V2_MAX_TIMEOUT_SECONDS` — authorization validity, default 300.
422
+ * - `METRIK_GATEWAY_X402V2_FACILITATOR_TIMEOUT_SECONDS` — per-request facilitator
423
+ * timeout, default 120; must exceed the facilitator's receipt wait by 30 s.
424
+ * - `METRIK_GATEWAY_X402V2_RESOURCE_BASE_URL` — public origin for the 402 `resource.url`.
425
+ * - `METRIK_GATEWAY_X402V2_REGISTRY_URL` — registry used to prove the listing.
426
+ * - `METRIK_GATEWAY_X402V2_REFUND_BASE_URL` — public base URL of the facilitator's
427
+ * gasless refund relay, advertised in the 402; default the facilitator URL. Its
428
+ * `/supported` must show the opener marker with `refundRelay: true`.
429
+ * - `METRIK_GATEWAY_X402V2_DOCS_URL` — docs link in the 402; default docs/x402.md on GitHub.
430
+ * - `METRIK_GATEWAY_X402V2_ASSET_DECIMALS` — decimals for the 402 description; default 6.
431
+ * - `METRIK_GATEWAY_X402V2_DESCRIPTION` — one sentence on what the service does,
432
+ * prefixed to the fixed disclosure of what the payment buys (at most 300 chars).
433
+ * - `METRIK_GATEWAY_X402V2_SCHEMA` — optional JSON `{info: {input, output}, schema}`
434
+ * declared in the 402 as `extensions.bazaar` (declared only, never registered).
336
435
  */
337
- export function parseGatewayX402EnvConfig(env = process.env) {
338
- const rawPrice = env.METRIK_GATEWAY_X402_PRICE_BASE_UNITS?.trim();
339
- if (rawPrice === undefined || rawPrice.length === 0) {
436
+ export function parseGatewayX402OpenerEnvConfig(env, context) {
437
+ const raw = env.METRIK_GATEWAY_X402V2_OPENER?.trim().toLowerCase();
438
+ if (raw === undefined ||
439
+ raw.length === 0 ||
440
+ ["0", "false", "no", "off"].includes(raw)) {
340
441
  return undefined;
341
442
  }
342
- let pricePerCallBaseUnits;
343
- try {
344
- pricePerCallBaseUnits = BigInt(rawPrice);
443
+ if (!["1", "true", "yes", "on"].includes(raw)) {
444
+ throw new Error(`invalid METRIK_GATEWAY_X402V2_OPENER: ${raw} (expected a boolean)`);
445
+ }
446
+ const facilitatorUrl = requireEnv(env, "METRIK_GATEWAY_X402V2_FACILITATOR_URL").trim();
447
+ // eslint-disable-next-line no-new -- validates the URL is well-formed.
448
+ new URL(facilitatorUrl);
449
+ const facilitatorToken = requireEnv(env, "METRIK_GATEWAY_X402V2_FACILITATOR_TOKEN").trim();
450
+ const network = (env.METRIK_GATEWAY_X402V2_NETWORK?.trim() ||
451
+ `eip155:${String(context.chainId)}`);
452
+ if (chainIdFromNetwork(network) !== context.chainId) {
453
+ throw new Error(`METRIK_GATEWAY_X402V2_NETWORK ${network} does not match METRIK_GATEWAY_CHAIN_ID ${context.chainId}`);
454
+ }
455
+ // Defaults come from the registry entry for THIS network only.
456
+ const registered = networkForCaip2(network);
457
+ const factoryRaw = env.METRIK_GATEWAY_X402V2_FACTORY_ADDRESS?.trim();
458
+ const upfrontRaw = env.METRIK_GATEWAY_X402V2_UPFRONT_AMOUNT?.trim();
459
+ const upfrontFactoryRaw = env.METRIK_GATEWAY_X402V2_UPFRONT_FACTORY_ADDRESS?.trim();
460
+ const upfrontMode = upfrontRaw !== undefined && upfrontRaw.length > 0;
461
+ if (!upfrontMode &&
462
+ upfrontFactoryRaw !== undefined &&
463
+ upfrontFactoryRaw.length > 0) {
464
+ throw new Error("METRIK_GATEWAY_X402V2_UPFRONT_FACTORY_ADDRESS needs METRIK_GATEWAY_X402V2_UPFRONT_AMOUNT (it selects the upfront opener)");
465
+ }
466
+ let factoryAddress;
467
+ let upfrontImplementation;
468
+ if (upfrontMode) {
469
+ if (factoryRaw !== undefined && factoryRaw.length > 0) {
470
+ throw new Error("METRIK_GATEWAY_X402V2_FACTORY_ADDRESS names the STREAM opener factory; with METRIK_GATEWAY_X402V2_UPFRONT_AMOUNT set the listing quotes the upfront factory — set METRIK_GATEWAY_X402V2_UPFRONT_FACTORY_ADDRESS (or nothing, for the registry's) instead");
471
+ }
472
+ const registeredUpfront = registered === undefined
473
+ ? []
474
+ : networkOpenerFactories(registered).filter((entry) => entry.kind === "upfront");
475
+ if (upfrontFactoryRaw === undefined || upfrontFactoryRaw.length === 0) {
476
+ const current = registeredUpfront.find((entry) => !entry.legacy);
477
+ if (current === undefined) {
478
+ throw new Error(`METRIK_GATEWAY_X402V2_UPFRONT_FACTORY_ADDRESS is required on ${network}: the network registry has no upfront opener factory for it`);
479
+ }
480
+ factoryAddress = current.address;
481
+ upfrontImplementation = current.implementation ?? undefined;
482
+ }
483
+ else if (isAddress(upfrontFactoryRaw)) {
484
+ factoryAddress = upfrontFactoryRaw;
485
+ upfrontImplementation =
486
+ registeredUpfront.find((entry) => entry.address.toLowerCase() === upfrontFactoryRaw.toLowerCase())?.implementation ?? undefined;
487
+ }
488
+ else {
489
+ throw new Error("METRIK_GATEWAY_X402V2_UPFRONT_FACTORY_ADDRESS must be an EVM address");
490
+ }
491
+ }
492
+ else if (factoryRaw === undefined || factoryRaw.length === 0) {
493
+ if (registered?.openerFactory == null) {
494
+ throw new Error(`METRIK_GATEWAY_X402V2_FACTORY_ADDRESS is required on ${network}: the network registry has no opener factory for it`);
495
+ }
496
+ factoryAddress = registered.openerFactory.address;
345
497
  }
346
- catch {
347
- throw new Error(`invalid METRIK_GATEWAY_X402_PRICE_BASE_UNITS: ${rawPrice} (integer base units, e.g. 1000 = 0.001 USDC)`);
498
+ else if (isAddress(factoryRaw)) {
499
+ factoryAddress = factoryRaw;
348
500
  }
349
- if (pricePerCallBaseUnits <= 0n) {
350
- throw new Error("METRIK_GATEWAY_X402_PRICE_BASE_UNITS must be positive");
501
+ else {
502
+ throw new Error("METRIK_GATEWAY_X402V2_FACTORY_ADDRESS must be an EVM address");
503
+ }
504
+ const operator = requireEnv(env, "METRIK_GATEWAY_X402V2_OPERATOR").trim();
505
+ if (!isAddress(operator)) {
506
+ throw new Error("METRIK_GATEWAY_X402V2_OPERATOR must be an EVM address");
507
+ }
508
+ const assetRaw = env.METRIK_GATEWAY_X402V2_ASSET?.trim();
509
+ let asset;
510
+ if (assetRaw === undefined || assetRaw.length === 0) {
511
+ if (registered === undefined) {
512
+ throw new Error(`METRIK_GATEWAY_X402V2_ASSET is required on ${network}: the network registry does not know it`);
513
+ }
514
+ asset = registered.usdc.address;
515
+ }
516
+ else if (isAddress(assetRaw)) {
517
+ asset = assetRaw;
518
+ }
519
+ else {
520
+ throw new Error("METRIK_GATEWAY_X402V2_ASSET must be an EVM address");
521
+ }
522
+ const terms = {
523
+ operator,
524
+ serviceRef: context.serviceRef,
525
+ ratePerSecond: requireUintEnv(env, "METRIK_GATEWAY_X402V2_RATE_PER_SECOND"),
526
+ maxDuration: requireUintEnv(env, "METRIK_GATEWAY_X402V2_MAX_DURATION_SECONDS"),
527
+ };
528
+ const price = requireUintEnv(env, "METRIK_GATEWAY_X402V2_PRICE");
529
+ let upfrontAmount;
530
+ if (upfrontMode) {
531
+ upfrontAmount = requireUintEnv(env, "METRIK_GATEWAY_X402V2_UPFRONT_AMOUNT");
532
+ // Startup validation, fail closed: the upfront factory's own validateTerms
533
+ // rules, 0 < price, upfrontAmount <= price, and the metered part
534
+ // (price - upfrontAmount) <= rate * maxDuration.
535
+ assertValidUpfrontTerms({ ...terms, price, upfrontAmount });
536
+ }
537
+ else {
538
+ // Startup validation: rate/duration > 0 and 0 < price <= rate * maxDuration.
539
+ assertValidTerms(terms);
540
+ assertValidPrice(price, terms);
541
+ }
542
+ const maxTimeoutSeconds = requirePositiveIntEnv(env, "METRIK_GATEWAY_X402V2_MAX_TIMEOUT_SECONDS", 300);
543
+ const facilitatorTimeoutSeconds = requirePositiveIntEnv(env, "METRIK_GATEWAY_X402V2_FACILITATOR_TIMEOUT_SECONDS", 120);
544
+ const resourceBaseUrl = env.METRIK_GATEWAY_X402V2_RESOURCE_BASE_URL?.trim();
545
+ if (resourceBaseUrl !== undefined && resourceBaseUrl.length > 0) {
546
+ // eslint-disable-next-line no-new -- validates the URL is well-formed.
547
+ new URL(resourceBaseUrl);
351
548
  }
352
- const facilitatorUrl = requireEnv(env, "METRIK_GATEWAY_X402_FACILITATOR_URL");
353
- const resourceBaseUrl = requireEnv(env, "METRIK_GATEWAY_X402_RESOURCE_BASE_URL");
354
- for (const candidate of [facilitatorUrl, resourceBaseUrl]) {
549
+ const registryUrl = env.METRIK_GATEWAY_X402V2_REGISTRY_URL?.trim();
550
+ if (registryUrl !== undefined && registryUrl.length > 0) {
355
551
  // eslint-disable-next-line no-new -- validates the URL is well-formed.
356
- new URL(candidate);
357
- }
358
- const registryUrl = env.METRIK_GATEWAY_X402_REGISTRY_URL;
359
- const facilitatorToken = env.METRIK_GATEWAY_X402_FACILITATOR_TOKEN?.trim();
360
- const rawTimeout = env.METRIK_GATEWAY_X402_MAX_TIMEOUT_SECONDS;
361
- let maxTimeoutSeconds;
362
- if (rawTimeout !== undefined && rawTimeout.trim().length > 0) {
363
- maxTimeoutSeconds = Number(rawTimeout);
364
- if (!Number.isInteger(maxTimeoutSeconds) || maxTimeoutSeconds <= 0) {
365
- throw new Error(`invalid METRIK_GATEWAY_X402_MAX_TIMEOUT_SECONDS: ${rawTimeout}`);
552
+ new URL(registryUrl);
553
+ }
554
+ const optionalLink = (name) => {
555
+ const value = env[name]?.trim();
556
+ if (value === undefined || value.length === 0)
557
+ return undefined;
558
+ if (!isHttpLink(value)) {
559
+ throw new Error(`${name} must be an absolute http(s) URL`);
560
+ }
561
+ return value.replace(/\/+$/, "");
562
+ };
563
+ const refundBaseUrl = optionalLink("METRIK_GATEWAY_X402V2_REFUND_BASE_URL");
564
+ const docsUrl = optionalLink("METRIK_GATEWAY_X402V2_DOCS_URL");
565
+ const decimalsRaw = env.METRIK_GATEWAY_X402V2_ASSET_DECIMALS?.trim();
566
+ let assetDecimals;
567
+ if (decimalsRaw !== undefined && decimalsRaw.length > 0) {
568
+ assetDecimals = Number(decimalsRaw);
569
+ if (!/^\d+$/.test(decimalsRaw) ||
570
+ !Number.isInteger(assetDecimals) ||
571
+ assetDecimals > 36) {
572
+ throw new Error(`invalid METRIK_GATEWAY_X402V2_ASSET_DECIMALS: ${decimalsRaw} (expected an integer 0-36)`);
573
+ }
574
+ }
575
+ const description = env.METRIK_GATEWAY_X402V2_DESCRIPTION?.trim();
576
+ if (description !== undefined && description.length > 300) {
577
+ throw new Error("METRIK_GATEWAY_X402V2_DESCRIPTION must be at most 300 characters");
578
+ }
579
+ const schemaRaw = env.METRIK_GATEWAY_X402V2_SCHEMA?.trim();
580
+ let schemaDeclaration;
581
+ if (schemaRaw !== undefined && schemaRaw.length > 0) {
582
+ try {
583
+ const parsed = JSON.parse(schemaRaw);
584
+ assertSchemaDeclaration(parsed);
585
+ schemaDeclaration = parsed;
586
+ }
587
+ catch (error) {
588
+ throw new Error(`invalid METRIK_GATEWAY_X402V2_SCHEMA: ${error instanceof Error ? error.message : String(error)}`);
366
589
  }
367
590
  }
368
591
  return {
369
- pricePerCallBaseUnits,
370
592
  facilitatorUrl,
371
- resourceBaseUrl,
372
- ...(registryUrl === undefined ? {} : { registryUrl }),
373
- ...(maxTimeoutSeconds === undefined ? {} : { maxTimeoutSeconds }),
374
- ...(facilitatorToken === undefined || facilitatorToken.length === 0
593
+ facilitatorToken,
594
+ network,
595
+ factoryAddress,
596
+ asset,
597
+ assetName: requireAssetDomainField(env.METRIK_GATEWAY_X402V2_ASSET_NAME, registered?.usdc.eip712.name, "METRIK_GATEWAY_X402V2_ASSET_NAME", network),
598
+ assetVersion: requireAssetDomainField(env.METRIK_GATEWAY_X402V2_ASSET_VERSION, registered?.usdc.eip712.version, "METRIK_GATEWAY_X402V2_ASSET_VERSION", network),
599
+ terms,
600
+ price,
601
+ ...(upfrontAmount === undefined ? {} : { upfrontAmount }),
602
+ ...(upfrontImplementation === undefined ? {} : { upfrontImplementation }),
603
+ maxTimeoutSeconds,
604
+ facilitatorTimeoutMs: facilitatorTimeoutSeconds * 1000,
605
+ ...(resourceBaseUrl === undefined || resourceBaseUrl.length === 0
606
+ ? {}
607
+ : { resourceBaseUrl }),
608
+ ...(registryUrl === undefined || registryUrl.length === 0
375
609
  ? {}
376
- : { facilitatorToken }),
610
+ : { registryUrl }),
611
+ ...(refundBaseUrl === undefined ? {} : { refundBaseUrl }),
612
+ ...(docsUrl === undefined ? {} : { docsUrl }),
613
+ ...(assetDecimals === undefined ? {} : { assetDecimals }),
614
+ ...(description === undefined || description.length === 0
615
+ ? {}
616
+ : { description }),
617
+ ...(schemaDeclaration === undefined ? {} : { schemaDeclaration }),
377
618
  };
378
619
  }
379
- export class UnresolvedListingPayeeError extends Error {
380
- constructor(serviceRef) {
381
- super(`no operator-signed listing found for serviceRef ${serviceRef}. ` +
382
- "x402 mode refuses to start: payTo is derived from the signed descriptor and " +
383
- "is never taken from configuration, so an unresolved listing has no payee.");
384
- this.name = "UnresolvedListingPayeeError";
620
+ function requireAssetDomainField(raw, registered, name, network) {
621
+ const value = raw?.trim();
622
+ if (value !== undefined && value.length > 0)
623
+ return value;
624
+ if (registered === undefined) {
625
+ throw new Error(`${name} is required on ${network}: the network registry does not know the asset's EIP-712 domain`);
626
+ }
627
+ return registered;
628
+ }
629
+ function requireUintEnv(env, name) {
630
+ const raw = requireEnv(env, name).trim();
631
+ if (!/^(0|[1-9]\d*)$/.test(raw)) {
632
+ throw new Error(`invalid ${name}: ${raw} (expected a base-10 integer)`);
385
633
  }
634
+ return BigInt(raw);
386
635
  }
387
636
  /**
388
- * Resolves `{serviceRef, operator}` from the registry. `discoverServices`
389
- * re-derives `serviceRef` from the signed content and recovers the operator's
390
- * EIP-712 signature, dropping anything that fails — so what comes back here is
391
- * signed, not merely served.
637
+ * Oracle probe verification is OFF unless `METRIK_GATEWAY_PROBE_CAPABILITY` is
638
+ * truthy. Turning it on lets the neutral verifier exercise this gateway's PAID
639
+ * route with an oracle-signed, single-use, short-lived `ProbeCapability`
640
+ * instead of only its health route.
641
+ *
642
+ * There is NO credential to configure — authority is an EIP-712 signature by an
643
+ * address in the escrow's public `getOracleSigners()` set, read from the same
644
+ * `METRIK_GATEWAY_RPC_URL` the stream check already uses.
645
+ *
646
+ * - `METRIK_GATEWAY_PROBE_CAPABILITY` — `1`/`true`/`yes`/`on` to enable.
647
+ * - `METRIK_GATEWAY_PROBE_MAX_PER_MINUTE` (optional) — free-probe budget.
648
+ * Default {@link DEFAULT_MAX_PROBES_PER_MINUTE}.
649
+ * - `METRIK_GATEWAY_PROBE_SIGNER_CACHE_SECONDS` (optional) — how long the
650
+ * on-chain signer set is cached. Default
651
+ * {@link DEFAULT_ORACLE_SIGNER_CACHE_SECONDS}.
392
652
  */
393
- export async function resolveSignedListingPayee(serviceRef, registryUrl) {
394
- const listings = await discoverServices(registryUrl === undefined ? {} : { registryUrl });
395
- const match = listings.find((listing) => listing.serviceRef.toLowerCase() === serviceRef.toLowerCase());
396
- if (match === undefined) {
397
- throw new UnresolvedListingPayeeError(serviceRef);
653
+ export function parseGatewayProbeEnvConfig(env = process.env) {
654
+ const raw = env.METRIK_GATEWAY_PROBE_CAPABILITY?.trim().toLowerCase();
655
+ if (raw === undefined || raw.length === 0) {
656
+ return undefined;
398
657
  }
399
- return { serviceRef: match.serviceRef, operator: match.operator };
658
+ if (!["1", "true", "yes", "on"].includes(raw)) {
659
+ if (["0", "false", "no", "off"].includes(raw)) {
660
+ return undefined;
661
+ }
662
+ throw new Error(`invalid METRIK_GATEWAY_PROBE_CAPABILITY: ${raw} (expected a boolean)`);
663
+ }
664
+ const maxProbesPerMinute = requirePositiveIntEnv(env, "METRIK_GATEWAY_PROBE_MAX_PER_MINUTE", DEFAULT_MAX_PROBES_PER_MINUTE);
665
+ const signerCacheSeconds = requirePositiveIntEnv(env, "METRIK_GATEWAY_PROBE_SIGNER_CACHE_SECONDS", DEFAULT_ORACLE_SIGNER_CACHE_SECONDS);
666
+ return { maxProbesPerMinute, signerCacheSeconds };
400
667
  }
401
- /** Builds the gateway's x402 mode, deriving `payTo` from the signed listing. */
402
- export async function buildGatewayX402Mode(config) {
403
- if (config.x402 === undefined) {
404
- return undefined;
668
+ function requirePositiveIntEnv(env, name, fallback) {
669
+ const raw = env[name]?.trim();
670
+ if (raw === undefined || raw.length === 0) {
671
+ return fallback;
405
672
  }
406
- const listing = await resolveSignedListingPayee(config.serviceRef, config.x402.registryUrl);
407
- return {
408
- quote: createListingPriceQuoter({
409
- listing,
410
- resourceBaseUrl: config.x402.resourceBaseUrl,
411
- pricePerCallBaseUnits: config.x402.pricePerCallBaseUnits,
412
- chainId: config.chainId,
413
- ...(config.x402.maxTimeoutSeconds === undefined
414
- ? {}
415
- : { maxTimeoutSeconds: config.x402.maxTimeoutSeconds }),
416
- }),
417
- facilitator: createHttpFacilitatorClient({
418
- url: config.x402.facilitatorUrl,
419
- ...(config.x402.facilitatorToken === undefined
420
- ? {}
421
- : { token: config.x402.facilitatorToken }),
422
- }),
423
- };
673
+ const value = Number(raw);
674
+ if (!Number.isInteger(value) || value <= 0) {
675
+ throw new Error(`invalid ${name}: ${raw} (expected a positive integer)`);
676
+ }
677
+ return value;
424
678
  }
425
679
  function requireEnv(env, name) {
426
680
  const value = env[name];
@@ -432,8 +686,13 @@ function requireEnv(env, name) {
432
686
  /** Builds and starts the reference gateway server entirely from the environment. */
433
687
  export async function startCallerAuthGatewayServerFromEnv(env = process.env) {
434
688
  const config = parseCallerAuthGatewayEnvConfig(env);
435
- const x402 = await buildGatewayX402Mode(config);
689
+ // Fail closed at startup: a configured descriptor that does not verify is a
690
+ // refusal to start, never a silently unenforced price band.
691
+ const signedRateBounds = config.signedDescriptorFile === undefined
692
+ ? undefined
693
+ : await loadSignedRateBoundsFile(config.signedDescriptorFile);
436
694
  const gateway = new CallerAuthGateway({
695
+ ...(signedRateBounds === undefined ? {} : { signedRateBounds }),
437
696
  escrowAddress: config.escrowAddress,
438
697
  rpcUrl: config.rpcUrl,
439
698
  serviceRef: config.serviceRef,
@@ -444,8 +703,26 @@ export async function startCallerAuthGatewayServerFromEnv(env = process.env) {
444
703
  ...(config.headerName === undefined
445
704
  ? {}
446
705
  : { headerName: config.headerName }),
447
- ...(x402 === undefined ? {} : { x402 }),
706
+ ...(config.probe === undefined
707
+ ? {}
708
+ : {
709
+ probe: {
710
+ maxProbesPerMinute: config.probe.maxProbesPerMinute,
711
+ signerCacheSeconds: config.probe.signerCacheSeconds,
712
+ },
713
+ }),
448
714
  });
715
+ if (signedRateBounds === undefined) {
716
+ console.error("[metrik-caller-auth-gateway] WARNING: no METRIK_GATEWAY_SIGNED_DESCRIPTOR_FILE — the " +
717
+ "operator-signed minRatePerSecond/maxRatePerSecond are NOT enforced here. The escrow " +
718
+ "accepts any non-zero rate, so a buyer who opens a stream directly against it can be " +
719
+ "served below your signed price (metrik-contracts#41).");
720
+ }
721
+ else {
722
+ console.error(`[metrik-caller-auth-gateway] enforcing operator-signed rate bounds for ${signedRateBounds
723
+ .map((bounds) => `${bounds.serviceRef} [${bounds.minRatePerSecond ?? "-"}, ${bounds.maxRatePerSecond ?? "-"}]`)
724
+ .join(", ")}`);
725
+ }
449
726
  if (config.corroboratingRpcUrls.length === 0) {
450
727
  console.error("[metrik-caller-auth-gateway] WARNING: no METRIK_GATEWAY_CORROBORATING_RPC_URLS " +
451
728
  "configured — stream state is authorized from a single RPC view. A stale view " +
@@ -453,16 +730,90 @@ export async function startCallerAuthGatewayServerFromEnv(env = process.env) {
453
730
  `${config.maxStateStalenessSeconds}s by the freshness check). Configure at least ` +
454
731
  "one INDEPENDENT provider to close that window.");
455
732
  }
456
- if (x402 !== undefined && config.x402 !== undefined) {
457
- console.error("[metrik-caller-auth-gateway] standard x402 mode ENABLED: callers with no Metrik " +
458
- `capability are quoted ${config.x402.pricePerCallBaseUnits} base units per call, ` +
459
- `settled through ${config.x402.facilitatorUrl}. This is a per-call payment with NO ` +
460
- "escrow and NO recoverability — the stream path remains the only recoverable one.");
733
+ if (config.probe !== undefined) {
734
+ console.error("[metrik-caller-auth-gateway] oracle probe verification ENABLED: a request carrying a " +
735
+ "valid EIP-712 ProbeCapability signed by an address in this escrow's on-chain " +
736
+ `getOracleSigners() set is proxied FREE, unmetered and unbilled, at most ` +
737
+ `${config.probe.maxProbesPerMinute} per minute. This is what lets the neutral ` +
738
+ "verifier check this gateway's PAID route instead of only its health route. " +
739
+ "Leaving it off is safe — the oracle then reports this listing's paid route as " +
740
+ "UNVERIFIABLE rather than as passing.");
741
+ }
742
+ else {
743
+ console.error("[metrik-caller-auth-gateway] oracle probe verification is OFF " +
744
+ "(METRIK_GATEWAY_PROBE_CAPABILITY unset). The verifier can only reach this service's " +
745
+ "health route, so paid-route delivery here is UNVERIFIED.");
746
+ }
747
+ let x402Opener;
748
+ if (config.x402Opener !== undefined) {
749
+ // Loaded only when enabled, so a capability-only gateway never imports @x402/*.
750
+ const { buildGatewayX402OpenerMode } = await import("./x402-opener-mode.js");
751
+ x402Opener = await buildGatewayX402OpenerMode({
752
+ facilitatorUrl: config.x402Opener.facilitatorUrl,
753
+ facilitatorToken: config.x402Opener.facilitatorToken,
754
+ network: config.x402Opener.network,
755
+ factoryAddress: config.x402Opener.factoryAddress,
756
+ escrowAddress: config.escrowAddress,
757
+ asset: config.x402Opener.asset,
758
+ assetName: config.x402Opener.assetName,
759
+ assetVersion: config.x402Opener.assetVersion,
760
+ terms: config.x402Opener.terms,
761
+ price: config.x402Opener.price,
762
+ ...(config.x402Opener.upfrontAmount === undefined
763
+ ? {}
764
+ : { upfrontAmount: config.x402Opener.upfrontAmount }),
765
+ ...(config.x402Opener.upfrontImplementation === undefined
766
+ ? {}
767
+ : { upfrontImplementation: config.x402Opener.upfrontImplementation }),
768
+ maxTimeoutSeconds: config.x402Opener.maxTimeoutSeconds,
769
+ facilitatorTimeoutMs: config.x402Opener.facilitatorTimeoutMs,
770
+ ...(config.x402Opener.resourceBaseUrl === undefined
771
+ ? {}
772
+ : { resourceBaseUrl: config.x402Opener.resourceBaseUrl }),
773
+ ...(config.x402Opener.registryUrl === undefined
774
+ ? {}
775
+ : { registryUrl: config.x402Opener.registryUrl }),
776
+ ...(config.x402Opener.refundBaseUrl === undefined
777
+ ? {}
778
+ : { refundBaseUrl: config.x402Opener.refundBaseUrl }),
779
+ ...(config.x402Opener.docsUrl === undefined
780
+ ? {}
781
+ : { docsUrl: config.x402Opener.docsUrl }),
782
+ ...(config.x402Opener.assetDecimals === undefined
783
+ ? {}
784
+ : { assetDecimals: config.x402Opener.assetDecimals }),
785
+ ...(config.x402Opener.description === undefined
786
+ ? {}
787
+ : { description: config.x402Opener.description }),
788
+ ...(config.x402Opener.schemaDeclaration === undefined
789
+ ? {}
790
+ : { schemaDeclaration: config.x402Opener.schemaDeclaration }),
791
+ }, { rpcUrl: config.rpcUrl });
792
+ const upfront = x402Opener.upfrontAmount;
793
+ console.error("[metrik-caller-auth-gateway] x402 v2 opener mode ENABLED: a request with neither a " +
794
+ "capability nor PAYMENT-SIGNATURE gets a 402 quoting payTo " +
795
+ `${x402Opener.payTo} (the ${x402Opener.openerKind} opener for serviceRef ${config.serviceRef}), price ` +
796
+ `${config.x402Opener.price} base units` +
797
+ (upfront === null
798
+ ? " (all of it escrowed and metered)"
799
+ : upfront === config.x402Opener.price
800
+ ? " (UPFRONT-ONLY: all of it a non-refundable per-call charge; no stream, nothing to refund)"
801
+ : ` (${upfront} a non-refundable per-call charge, ${config.x402Opener.price - upfront} escrowed, metered and refundable)`) +
802
+ `. Payments settle via the Metrik opener facilitator at ${config.x402Opener.facilitatorUrl}, and only after the upstream answers < 400. ` +
803
+ `Refunds (no ETH needed, never automatic): ${x402Opener.refundBaseUrl}/streams/:streamId.`);
804
+ const refundHost = new URL(x402Opener.refundBaseUrl).hostname;
805
+ if (refundHost === "localhost" ||
806
+ refundHost === "[::1]" ||
807
+ refundHost.startsWith("127.")) {
808
+ console.error("[metrik-caller-auth-gateway] WARNING: the 402 advertises a loopback refund URL " +
809
+ `(${x402Opener.refundBaseUrl}), which payers cannot reach. Set ` +
810
+ "METRIK_GATEWAY_X402V2_REFUND_BASE_URL to the facilitator's public origin.");
811
+ }
461
812
  }
462
813
  const server = createCallerAuthGatewayServer({
463
814
  gateway,
464
815
  upstreamUrl: config.upstreamUrl,
465
- ...(x402 === undefined ? {} : { x402 }),
816
+ ...(x402Opener === undefined ? {} : { x402Opener }),
466
817
  });
467
818
  const { port } = await server.listen(config.port, config.host);
468
819
  return Object.assign(server, { boundPort: port });