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

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 (350) hide show
  1. package/README.md +42 -50
  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.js +1 -1
  12. package/dist/cli/commands/claim.js.map +1 -1
  13. package/dist/cli/commands/close.d.ts.map +1 -1
  14. package/dist/cli/commands/close.js +35 -3
  15. package/dist/cli/commands/close.js.map +1 -1
  16. package/dist/cli/commands/invoke.d.ts +2 -0
  17. package/dist/cli/commands/invoke.d.ts.map +1 -1
  18. package/dist/cli/commands/invoke.js +2 -2
  19. package/dist/cli/commands/invoke.js.map +1 -1
  20. package/dist/cli/commands/mcp.d.ts.map +1 -1
  21. package/dist/cli/commands/mcp.js +15 -2
  22. package/dist/cli/commands/mcp.js.map +1 -1
  23. package/dist/cli/commands/open.d.ts.map +1 -1
  24. package/dist/cli/commands/open.js +58 -4
  25. package/dist/cli/commands/open.js.map +1 -1
  26. package/dist/cli/commands/pay.d.ts +19 -0
  27. package/dist/cli/commands/pay.d.ts.map +1 -0
  28. package/dist/cli/commands/pay.js +355 -0
  29. package/dist/cli/commands/pay.js.map +1 -0
  30. package/dist/cli/commands/reclaim-unverified.d.ts.map +1 -1
  31. package/dist/cli/commands/reclaim-unverified.js +3 -2
  32. package/dist/cli/commands/reclaim-unverified.js.map +1 -1
  33. package/dist/cli/commands/reclaim.d.ts +23 -2
  34. package/dist/cli/commands/reclaim.d.ts.map +1 -1
  35. package/dist/cli/commands/reclaim.js +184 -16
  36. package/dist/cli/commands/reclaim.js.map +1 -1
  37. package/dist/cli/commands/refund.d.ts +13 -0
  38. package/dist/cli/commands/refund.d.ts.map +1 -0
  39. package/dist/cli/commands/refund.js +224 -0
  40. package/dist/cli/commands/refund.js.map +1 -0
  41. package/dist/cli/commands/status.d.ts.map +1 -1
  42. package/dist/cli/commands/status.js +10 -1
  43. package/dist/cli/commands/status.js.map +1 -1
  44. package/dist/cli/commands/streams.js +8 -12
  45. package/dist/cli/commands/streams.js.map +1 -1
  46. package/dist/cli/commands/watch.d.ts.map +1 -1
  47. package/dist/cli/commands/watch.js +2 -1
  48. package/dist/cli/commands/watch.js.map +1 -1
  49. package/dist/cli/config.d.ts +44 -26
  50. package/dist/cli/config.d.ts.map +1 -1
  51. package/dist/cli/config.js +88 -38
  52. package/dist/cli/config.js.map +1 -1
  53. package/dist/cli/deps.d.ts +33 -0
  54. package/dist/cli/deps.d.ts.map +1 -1
  55. package/dist/cli/deps.js +37 -1
  56. package/dist/cli/deps.js.map +1 -1
  57. package/dist/cli/errors.d.ts.map +1 -1
  58. package/dist/cli/errors.js +21 -1
  59. package/dist/cli/errors.js.map +1 -1
  60. package/dist/cli/escape-hatch.d.ts +6 -4
  61. package/dist/cli/escape-hatch.d.ts.map +1 -1
  62. package/dist/cli/escape-hatch.js +6 -2
  63. package/dist/cli/escape-hatch.js.map +1 -1
  64. package/dist/cli/listings.d.ts +18 -0
  65. package/dist/cli/listings.d.ts.map +1 -1
  66. package/dist/cli/listings.js +22 -12
  67. package/dist/cli/listings.js.map +1 -1
  68. package/dist/cli/preflight.d.ts +20 -14
  69. package/dist/cli/preflight.d.ts.map +1 -1
  70. package/dist/cli/preflight.js +34 -20
  71. package/dist/cli/preflight.js.map +1 -1
  72. package/dist/cli/privy.d.ts.map +1 -1
  73. package/dist/cli/privy.js +4 -2
  74. package/dist/cli/privy.js.map +1 -1
  75. package/dist/cli/reclaimability.d.ts +82 -0
  76. package/dist/cli/reclaimability.d.ts.map +1 -0
  77. package/dist/cli/reclaimability.js +80 -0
  78. package/dist/cli/reclaimability.js.map +1 -0
  79. package/dist/cli/run.d.ts +2 -1
  80. package/dist/cli/run.d.ts.map +1 -1
  81. package/dist/cli/run.js +40 -11
  82. package/dist/cli/run.js.map +1 -1
  83. package/dist/cli/scan-range.d.ts +2 -2
  84. package/dist/cli/scan-range.js +2 -2
  85. package/dist/cli/status.d.ts +3 -1
  86. package/dist/cli/status.d.ts.map +1 -1
  87. package/dist/cli/status.js +15 -3
  88. package/dist/cli/status.js.map +1 -1
  89. package/dist/cli/wallet.d.ts +1 -1
  90. package/dist/cli/wallet.js +2 -2
  91. package/dist/cli/wallet.js.map +1 -1
  92. package/dist/discovery/rate-band.d.ts +74 -0
  93. package/dist/discovery/rate-band.d.ts.map +1 -0
  94. package/dist/discovery/rate-band.js +114 -0
  95. package/dist/discovery/rate-band.js.map +1 -0
  96. package/dist/escrow-generation.d.ts +81 -0
  97. package/dist/escrow-generation.d.ts.map +1 -0
  98. package/dist/escrow-generation.js +76 -0
  99. package/dist/escrow-generation.js.map +1 -0
  100. package/dist/frameworks/agentkit.d.ts +8 -4
  101. package/dist/frameworks/agentkit.d.ts.map +1 -1
  102. package/dist/frameworks/agentkit.js +27 -10
  103. package/dist/frameworks/agentkit.js.map +1 -1
  104. package/dist/frameworks/eliza.d.ts +3 -3
  105. package/dist/frameworks/eliza.d.ts.map +1 -1
  106. package/dist/frameworks/eliza.js +15 -6
  107. package/dist/frameworks/eliza.js.map +1 -1
  108. package/dist/frameworks/langchain.d.ts.map +1 -1
  109. package/dist/frameworks/langchain.js +8 -0
  110. package/dist/frameworks/langchain.js.map +1 -1
  111. package/dist/gateway/caller-auth-gateway.d.ts +83 -31
  112. package/dist/gateway/caller-auth-gateway.d.ts.map +1 -1
  113. package/dist/gateway/caller-auth-gateway.js +180 -27
  114. package/dist/gateway/caller-auth-gateway.js.map +1 -1
  115. package/dist/gateway/http-server.d.ts +111 -44
  116. package/dist/gateway/http-server.d.ts.map +1 -1
  117. package/dist/gateway/http-server.js +439 -155
  118. package/dist/gateway/http-server.js.map +1 -1
  119. package/dist/gateway/probe-auth.d.ts +112 -0
  120. package/dist/gateway/probe-auth.d.ts.map +1 -0
  121. package/dist/gateway/probe-auth.js +127 -0
  122. package/dist/gateway/probe-auth.js.map +1 -0
  123. package/dist/gateway/signed-rate-bounds.d.ts +51 -0
  124. package/dist/gateway/signed-rate-bounds.d.ts.map +1 -0
  125. package/dist/gateway/signed-rate-bounds.js +94 -0
  126. package/dist/gateway/signed-rate-bounds.js.map +1 -0
  127. package/dist/gateway/x402-opener-mode.d.ts +215 -0
  128. package/dist/gateway/x402-opener-mode.d.ts.map +1 -0
  129. package/dist/gateway/x402-opener-mode.js +639 -0
  130. package/dist/gateway/x402-opener-mode.js.map +1 -0
  131. package/dist/index.d.ts +6 -2
  132. package/dist/index.d.ts.map +1 -1
  133. package/dist/index.js +10 -5
  134. package/dist/index.js.map +1 -1
  135. package/dist/mcp/server.d.ts +52 -1
  136. package/dist/mcp/server.d.ts.map +1 -1
  137. package/dist/mcp/server.js +228 -37
  138. package/dist/mcp/server.js.map +1 -1
  139. package/dist/mcp/x402-tools.d.ts +104 -0
  140. package/dist/mcp/x402-tools.d.ts.map +1 -0
  141. package/dist/mcp/x402-tools.js +434 -0
  142. package/dist/mcp/x402-tools.js.map +1 -0
  143. package/dist/network.d.ts +119 -0
  144. package/dist/network.d.ts.map +1 -0
  145. package/dist/network.js +199 -0
  146. package/dist/network.js.map +1 -0
  147. package/dist/sdk/invoke.d.ts +4 -0
  148. package/dist/sdk/invoke.d.ts.map +1 -1
  149. package/dist/sdk/invoke.js +2 -5
  150. package/dist/sdk/invoke.js.map +1 -1
  151. package/dist/wallet/autonomous-wallet-broker.d.ts +3 -0
  152. package/dist/wallet/autonomous-wallet-broker.d.ts.map +1 -1
  153. package/dist/wallet/autonomous-wallet-broker.js +27 -23
  154. package/dist/wallet/autonomous-wallet-broker.js.map +1 -1
  155. package/dist/wallet/autonomous-wallet-protocol.d.ts +8 -2
  156. package/dist/wallet/autonomous-wallet-protocol.d.ts.map +1 -1
  157. package/dist/wallet/autonomous-wallet-protocol.js +9 -2
  158. package/dist/wallet/autonomous-wallet-protocol.js.map +1 -1
  159. package/dist/wallet/autonomous-wallet.d.ts.map +1 -1
  160. package/dist/wallet/autonomous-wallet.js +3 -2
  161. package/dist/wallet/autonomous-wallet.js.map +1 -1
  162. package/dist/wallet/broker-escrows.d.ts +222 -0
  163. package/dist/wallet/broker-escrows.d.ts.map +1 -0
  164. package/dist/wallet/broker-escrows.js +36 -0
  165. package/dist/wallet/broker-escrows.js.map +1 -0
  166. package/dist/wallet/lifecycle.d.ts +21 -5
  167. package/dist/wallet/lifecycle.d.ts.map +1 -1
  168. package/dist/wallet/lifecycle.js +32 -12
  169. package/dist/wallet/lifecycle.js.map +1 -1
  170. package/dist/wallet/privy-broker-server.d.ts.map +1 -1
  171. package/dist/wallet/privy-broker-server.js +31 -26
  172. package/dist/wallet/privy-broker-server.js.map +1 -1
  173. package/dist/wallet/privy-session-broker.d.ts +14 -5
  174. package/dist/wallet/privy-session-broker.d.ts.map +1 -1
  175. package/dist/wallet/privy-session-broker.js +29 -17
  176. package/dist/wallet/privy-session-broker.js.map +1 -1
  177. package/dist/wallet/privy-session-provider.d.ts +3 -1
  178. package/dist/wallet/privy-session-provider.d.ts.map +1 -1
  179. package/dist/wallet/privy-session-provider.js +5 -3
  180. package/dist/wallet/privy-session-provider.js.map +1 -1
  181. package/dist/wallet/provider.d.ts.map +1 -1
  182. package/dist/wallet/provider.js +15 -8
  183. package/dist/wallet/provider.js.map +1 -1
  184. package/dist/x402/facilitator.d.ts +3 -30
  185. package/dist/x402/facilitator.d.ts.map +1 -1
  186. package/dist/x402/facilitator.js +0 -27
  187. package/dist/x402/facilitator.js.map +1 -1
  188. package/dist/x402-buyer/guarded-fetch.d.ts +51 -0
  189. package/dist/x402-buyer/guarded-fetch.d.ts.map +1 -0
  190. package/dist/x402-buyer/guarded-fetch.js +147 -0
  191. package/dist/x402-buyer/guarded-fetch.js.map +1 -0
  192. package/dist/x402-buyer/index.d.ts +10 -0
  193. package/dist/x402-buyer/index.d.ts.map +1 -0
  194. package/dist/x402-buyer/index.js +10 -0
  195. package/dist/x402-buyer/index.js.map +1 -0
  196. package/dist/x402-buyer/pay.d.ts +135 -0
  197. package/dist/x402-buyer/pay.d.ts.map +1 -0
  198. package/dist/x402-buyer/pay.js +336 -0
  199. package/dist/x402-buyer/pay.js.map +1 -0
  200. package/dist/x402-opener/abi.d.ts +684 -0
  201. package/dist/x402-opener/abi.d.ts.map +1 -0
  202. package/dist/x402-opener/abi.js +880 -0
  203. package/dist/x402-opener/abi.js.map +1 -0
  204. package/dist/x402-opener/facilitator-entry.d.ts +3 -0
  205. package/dist/x402-opener/facilitator-entry.d.ts.map +1 -0
  206. package/dist/x402-opener/facilitator-entry.js +37 -0
  207. package/dist/x402-opener/facilitator-entry.js.map +1 -0
  208. package/dist/x402-opener/facilitator-server.d.ts +180 -0
  209. package/dist/x402-opener/facilitator-server.d.ts.map +1 -0
  210. package/dist/x402-opener/facilitator-server.js +696 -0
  211. package/dist/x402-opener/facilitator-server.js.map +1 -0
  212. package/dist/x402-opener/index.d.ts +14 -0
  213. package/dist/x402-opener/index.d.ts.map +1 -0
  214. package/dist/x402-opener/index.js +14 -0
  215. package/dist/x402-opener/index.js.map +1 -0
  216. package/dist/x402-opener/listing-resolver.d.ts +55 -0
  217. package/dist/x402-opener/listing-resolver.d.ts.map +1 -0
  218. package/dist/x402-opener/listing-resolver.js +125 -0
  219. package/dist/x402-opener/listing-resolver.js.map +1 -0
  220. package/dist/x402-opener/refund-relay.d.ts +240 -0
  221. package/dist/x402-opener/refund-relay.d.ts.map +1 -0
  222. package/dist/x402-opener/refund-relay.js +1240 -0
  223. package/dist/x402-opener/refund-relay.js.map +1 -0
  224. package/dist/x402-opener/relayer-queue.d.ts +14 -0
  225. package/dist/x402-opener/relayer-queue.d.ts.map +1 -0
  226. package/dist/x402-opener/relayer-queue.js +18 -0
  227. package/dist/x402-opener/relayer-queue.js.map +1 -0
  228. package/dist/x402-opener/scheme.d.ts +217 -0
  229. package/dist/x402-opener/scheme.d.ts.map +1 -0
  230. package/dist/x402-opener/scheme.js +749 -0
  231. package/dist/x402-opener/scheme.js.map +1 -0
  232. package/dist/x402-opener/terms.d.ts +105 -0
  233. package/dist/x402-opener/terms.d.ts.map +1 -0
  234. package/dist/x402-opener/terms.js +212 -0
  235. package/dist/x402-opener/terms.js.map +1 -0
  236. package/dist/zktls/reclaim.d.ts +2 -2
  237. package/dist/zktls/t2-delivery-proof.d.ts +6 -6
  238. package/docs/agent-layer.md +3 -2
  239. package/docs/autonomous-privy-wallet.md +188 -0
  240. package/docs/cli.md +184 -31
  241. package/docs/langchain.md +5 -0
  242. package/docs/mcp-reference.md +75 -5
  243. package/docs/probe-capability.md +191 -0
  244. package/docs/quickstart.md +36 -1
  245. package/docs/threat-model.md +20 -0
  246. package/docs/x402.md +628 -595
  247. package/package.json +19 -17
  248. package/src/capability/probe-capability.ts +355 -0
  249. package/src/cli/args.ts +29 -0
  250. package/src/cli/commands/claim.ts +1 -1
  251. package/src/cli/commands/close.ts +36 -3
  252. package/src/cli/commands/invoke.ts +5 -2
  253. package/src/cli/commands/mcp.ts +18 -2
  254. package/src/cli/commands/open.ts +75 -4
  255. package/src/cli/commands/pay.ts +442 -0
  256. package/src/cli/commands/reclaim-unverified.ts +8 -2
  257. package/src/cli/commands/reclaim.ts +252 -14
  258. package/src/cli/commands/refund.ts +277 -0
  259. package/src/cli/commands/status.ts +17 -1
  260. package/src/cli/commands/streams.ts +10 -14
  261. package/src/cli/commands/watch.ts +7 -1
  262. package/src/cli/config.ts +139 -52
  263. package/src/cli/deps.ts +74 -1
  264. package/src/cli/errors.ts +24 -1
  265. package/src/cli/escape-hatch.ts +19 -5
  266. package/src/cli/listings.ts +43 -10
  267. package/src/cli/preflight.ts +36 -21
  268. package/src/cli/privy.ts +4 -2
  269. package/src/cli/reclaimability.ts +154 -0
  270. package/src/cli/run.ts +54 -11
  271. package/src/cli/scan-range.ts +2 -2
  272. package/src/cli/status.ts +14 -1
  273. package/src/cli/wallet.ts +2 -2
  274. package/src/discovery/rate-band.ts +172 -0
  275. package/src/escrow-generation.ts +135 -0
  276. package/src/frameworks/agentkit.ts +45 -11
  277. package/src/frameworks/eliza.ts +27 -5
  278. package/src/frameworks/langchain.ts +14 -0
  279. package/src/gateway/caller-auth-gateway.ts +320 -68
  280. package/src/gateway/http-server.ts +629 -220
  281. package/src/gateway/probe-auth.ts +191 -0
  282. package/src/gateway/signed-rate-bounds.ts +164 -0
  283. package/src/gateway/x402-opener-mode.ts +974 -0
  284. package/src/index.ts +82 -19
  285. package/src/mcp/server.ts +319 -42
  286. package/src/mcp/x402-tools.ts +621 -0
  287. package/src/network.ts +302 -0
  288. package/src/sdk/invoke.ts +6 -5
  289. package/src/wallet/autonomous-wallet-broker.ts +46 -20
  290. package/src/wallet/autonomous-wallet-protocol.ts +11 -2
  291. package/src/wallet/autonomous-wallet.ts +4 -2
  292. package/src/wallet/broker-escrows.ts +66 -0
  293. package/src/wallet/lifecycle.ts +53 -18
  294. package/src/wallet/privy-broker-server.ts +35 -25
  295. package/src/wallet/privy-session-broker.ts +58 -25
  296. package/src/wallet/privy-session-provider.ts +7 -3
  297. package/src/wallet/provider.ts +20 -15
  298. package/src/x402/facilitator.ts +3 -43
  299. package/src/x402-buyer/guarded-fetch.ts +213 -0
  300. package/src/x402-buyer/index.ts +9 -0
  301. package/src/x402-buyer/pay.ts +517 -0
  302. package/src/x402-opener/abi.ts +881 -0
  303. package/src/x402-opener/facilitator-entry.ts +45 -0
  304. package/src/x402-opener/facilitator-server.ts +1014 -0
  305. package/src/x402-opener/index.ts +13 -0
  306. package/src/x402-opener/listing-resolver.ts +202 -0
  307. package/src/x402-opener/refund-relay.ts +1743 -0
  308. package/src/x402-opener/relayer-queue.ts +18 -0
  309. package/src/x402-opener/scheme.ts +1133 -0
  310. package/src/x402-opener/terms.ts +300 -0
  311. package/dist/gateway/x402-mode.d.ts +0 -123
  312. package/dist/gateway/x402-mode.d.ts.map +0 -1
  313. package/dist/gateway/x402-mode.js +0 -212
  314. package/dist/gateway/x402-mode.js.map +0 -1
  315. package/dist/x402/delivery-oracle.d.ts +0 -108
  316. package/dist/x402/delivery-oracle.d.ts.map +0 -1
  317. package/dist/x402/delivery-oracle.js +0 -174
  318. package/dist/x402/delivery-oracle.js.map +0 -1
  319. package/dist/x402/facilitator-server-entry.d.ts +0 -3
  320. package/dist/x402/facilitator-server-entry.d.ts.map +0 -1
  321. package/dist/x402/facilitator-server-entry.js +0 -27
  322. package/dist/x402/facilitator-server-entry.js.map +0 -1
  323. package/dist/x402/facilitator-server.d.ts +0 -73
  324. package/dist/x402/facilitator-server.d.ts.map +0 -1
  325. package/dist/x402/facilitator-server.js +0 -305
  326. package/dist/x402/facilitator-server.js.map +0 -1
  327. package/dist/x402/payment-requirements.d.ts +0 -106
  328. package/dist/x402/payment-requirements.d.ts.map +0 -1
  329. package/dist/x402/payment-requirements.js +0 -138
  330. package/dist/x402/payment-requirements.js.map +0 -1
  331. package/dist/x402/relayer.d.ts +0 -41
  332. package/dist/x402/relayer.d.ts.map +0 -1
  333. package/dist/x402/relayer.js +0 -64
  334. package/dist/x402/relayer.js.map +0 -1
  335. package/dist/x402/resource-index.d.ts +0 -75
  336. package/dist/x402/resource-index.d.ts.map +0 -1
  337. package/dist/x402/resource-index.js +0 -126
  338. package/dist/x402/resource-index.js.map +0 -1
  339. package/dist/x402/verified-delivery-facilitator.d.ts +0 -285
  340. package/dist/x402/verified-delivery-facilitator.d.ts.map +0 -1
  341. package/dist/x402/verified-delivery-facilitator.js +0 -423
  342. package/dist/x402/verified-delivery-facilitator.js.map +0 -1
  343. package/src/gateway/x402-mode.ts +0 -350
  344. package/src/x402/delivery-oracle.ts +0 -285
  345. package/src/x402/facilitator-server-entry.ts +0 -35
  346. package/src/x402/facilitator-server.ts +0 -445
  347. package/src/x402/payment-requirements.ts +0 -231
  348. package/src/x402/relayer.ts +0 -122
  349. package/src/x402/resource-index.ts +0 -228
  350. package/src/x402/verified-delivery-facilitator.ts +0 -744
@@ -0,0 +1,696 @@
1
+ import { timingSafeEqual, createHash } from "node:crypto";
2
+ import { createServer, } from "node:http";
3
+ import { networkForCaip2 } from "@absol-labs/sdk";
4
+ import { x402Facilitator } from "@x402/core/facilitator";
5
+ import { createPublicClient, createWalletClient, getAddress, http, isAddress, isHex, } from "viem";
6
+ import { privateKeyToAccount } from "viem/accounts";
7
+ import { DEFAULT_METRIK_PUBLIC_REGISTRY_URL } from "../discovery/registry.js";
8
+ import { DEFAULT_LISTING_CACHE_SECONDS, RegistryListingResolver, } from "./listing-resolver.js";
9
+ import { DEFAULT_REFUND_READ_CACHE_SECONDS, DEFAULT_REFUND_SUBMISSIONS_PER_MINUTE, DEFAULT_REFUND_SUBMISSIONS_PER_STREAM_PER_MINUTE, OpenerRefundRelay, } from "./refund-relay.js";
10
+ import { RelayerQueue } from "./relayer-queue.js";
11
+ import { MetrikOpenerExactScheme, } from "./scheme.js";
12
+ import { chainIdFromNetwork } from "./terms.js";
13
+ import { chainForChainId, defaultNetwork } from "../network.js";
14
+ /**
15
+ * The Metrik x402 v2 opener facilitator as an HTTP service
16
+ * (`/verify`, `/settle`, `/supported`), built on the official
17
+ * `@x402/core` `x402Facilitator` with {@link MetrikOpenerExactScheme} as its
18
+ * only registered scheme — so it can only ever settle into the escrow opener.
19
+ *
20
+ * - `/settle` spends relayer gas and moves buyer funds, so it requires a bearer
21
+ * token (constant-time compared). `/verify` and `/supported` are read-only.
22
+ * - The relayer key comes from the environment only and is never logged.
23
+ * - OFF unless explicitly enabled; refuses to start without the relayer key,
24
+ * RPC, token and a factory whose on-chain wiring checks out.
25
+ */
26
+ /** Maximum accepted request body, bytes. A payment payload is well under 8 KiB. */
27
+ const MAX_BODY_BYTES = 64 * 1024;
28
+ /** Maximum refund-relay request body, bytes: `{deadline, signature}` is tiny. */
29
+ const MAX_REFUND_BODY_BYTES = 8 * 1024;
30
+ /** `/streams/:streamId` and `/streams/:streamId/:action`. */
31
+ const STREAMS_ROUTE = /^\/streams\/([^/]+)(?:\/([^/]+))?\/?$/;
32
+ class BodyTooLargeError extends Error {
33
+ }
34
+ export const MIN_AUTH_TOKEN_LENGTH = 32;
35
+ export const DEFAULT_SETTLE_RATE_LIMIT_PER_MINUTE = 60;
36
+ export const DEFAULT_REFUND_RATE_LIMIT_PER_IP_PER_MINUTE = 30;
37
+ export const DEFAULT_REFUND_GLOBAL_RATE_LIMIT_PER_MINUTE = 120;
38
+ /** Cap on remembered client IPs; beyond it new clients are refused (fail closed). */
39
+ const MAX_TRACKED_CLIENTS = 100_000;
40
+ export function createOpenerFacilitatorServer(config) {
41
+ if (config.authToken.length < MIN_AUTH_TOKEN_LENGTH) {
42
+ throw new Error(`authToken must be at least ${MIN_AUTH_TOKEN_LENGTH} characters`);
43
+ }
44
+ const logger = config.logger ?? console;
45
+ const tokenDigest = digest(config.authToken);
46
+ const requireAuthOnVerify = config.requireAuthOnVerify ?? true;
47
+ const settleLimit = config.settleRateLimitPerMinute ?? DEFAULT_SETTLE_RATE_LIMIT_PER_MINUTE;
48
+ if (!Number.isInteger(settleLimit) || settleLimit <= 0) {
49
+ throw new Error("settleRateLimitPerMinute must be a positive integer");
50
+ }
51
+ const nowMs = config.nowMs ?? (() => Date.now());
52
+ // Fixed one-minute windows, keyed by the token's digest (never the token).
53
+ const settleWindows = new Map();
54
+ const allowSettle = (key) => {
55
+ const now = nowMs();
56
+ const window = settleWindows.get(key);
57
+ if (window === undefined || now - window.start >= 60_000) {
58
+ settleWindows.set(key, { start: now, count: 1 });
59
+ return true;
60
+ }
61
+ if (window.count >= settleLimit)
62
+ return false;
63
+ window.count += 1;
64
+ return true;
65
+ };
66
+ const refundLimit = config.refundRateLimitPerIpPerMinute ??
67
+ DEFAULT_REFUND_RATE_LIMIT_PER_IP_PER_MINUTE;
68
+ if (!Number.isInteger(refundLimit) || refundLimit <= 0) {
69
+ throw new Error("refundRateLimitPerIpPerMinute must be a positive integer");
70
+ }
71
+ const globalRefundLimit = config.refundGlobalRateLimitPerMinute ??
72
+ DEFAULT_REFUND_GLOBAL_RATE_LIMIT_PER_MINUTE;
73
+ if (!Number.isInteger(globalRefundLimit) || globalRefundLimit <= 0) {
74
+ throw new Error("refundGlobalRateLimitPerMinute must be a positive integer");
75
+ }
76
+ const globalRefundWindow = { start: -Infinity, count: 0 };
77
+ /** Seconds until the global window resets, or 0 when the request is allowed. */
78
+ const takeGlobalRefundBudget = () => {
79
+ const now = nowMs();
80
+ if (now - globalRefundWindow.start >= 60_000) {
81
+ globalRefundWindow.start = now;
82
+ globalRefundWindow.count = 1;
83
+ return 0;
84
+ }
85
+ if (globalRefundWindow.count >= globalRefundLimit) {
86
+ return Math.max(1, Math.ceil((globalRefundWindow.start + 60_000 - now) / 1000));
87
+ }
88
+ globalRefundWindow.count += 1;
89
+ return 0;
90
+ };
91
+ const refundWindows = new Map();
92
+ /** Seconds until the client's window resets, or 0 when the request is allowed. */
93
+ const takeRefundBudget = (client) => {
94
+ const now = nowMs();
95
+ const window = refundWindows.get(client);
96
+ if (window === undefined || now - window.start >= 60_000) {
97
+ if (window === undefined && refundWindows.size >= MAX_TRACKED_CLIENTS) {
98
+ for (const [key, value] of refundWindows) {
99
+ if (now - value.start >= 60_000)
100
+ refundWindows.delete(key);
101
+ }
102
+ if (refundWindows.size >= MAX_TRACKED_CLIENTS)
103
+ return 60;
104
+ }
105
+ refundWindows.set(client, { start: now, count: 1 });
106
+ return 0;
107
+ }
108
+ if (window.count >= refundLimit) {
109
+ return Math.max(1, Math.ceil((window.start + 60_000 - now) / 1000));
110
+ }
111
+ window.count += 1;
112
+ return 0;
113
+ };
114
+ const clientKey = (req) => {
115
+ if (config.trustProxy === true) {
116
+ const header = req.headers["x-forwarded-for"];
117
+ const value = Array.isArray(header) ? header.at(-1) : header;
118
+ const hop = value?.split(",").at(-1)?.trim();
119
+ if (hop !== undefined && hop.length > 0 && hop.length <= 64)
120
+ return hop;
121
+ }
122
+ return req.socket.remoteAddress ?? "unknown";
123
+ };
124
+ const authorized = (req) => {
125
+ const header = req.headers.authorization;
126
+ if (typeof header !== "string" || !header.startsWith("Bearer ")) {
127
+ return false;
128
+ }
129
+ // Compare fixed-length digests so neither length nor content leaks.
130
+ return timingSafeEqual(digest(header.slice("Bearer ".length)), tokenDigest);
131
+ };
132
+ const httpServer = createServer((req, res) => {
133
+ handle(req, res).catch((error) => {
134
+ logger.error(`[metrik-opener-facilitator] unhandled error: ${error instanceof Error ? error.message : String(error)}`);
135
+ if (!res.headersSent) {
136
+ json(res, 500, { error: "internal-error" });
137
+ }
138
+ else {
139
+ res.end();
140
+ }
141
+ });
142
+ });
143
+ async function handle(req, res) {
144
+ const url = new URL(req.url ?? "/", "http://localhost");
145
+ const method = req.method ?? "GET";
146
+ if (url.pathname === "/health" && method === "GET") {
147
+ json(res, 200, { ok: true });
148
+ return;
149
+ }
150
+ if (url.pathname === "/supported" && method === "GET") {
151
+ json(res, 200, config.facilitator.getSupported());
152
+ return;
153
+ }
154
+ const streamsRoute = STREAMS_ROUTE.exec(url.pathname);
155
+ if (streamsRoute !== null && config.refundRelay !== undefined) {
156
+ let streamId;
157
+ try {
158
+ streamId = decodeURIComponent(streamsRoute[1]);
159
+ }
160
+ catch {
161
+ json(res, 400, { ok: false, error: "invalid-stream-id" });
162
+ return;
163
+ }
164
+ await handleRefund(req, res, config.refundRelay, method, streamId, streamsRoute[2]);
165
+ return;
166
+ }
167
+ if ((url.pathname === "/verify" || url.pathname === "/settle") &&
168
+ method === "POST") {
169
+ const isSettle = url.pathname === "/settle";
170
+ if ((isSettle || requireAuthOnVerify) && !authorized(req)) {
171
+ json(res, 401, { error: "unauthorized" });
172
+ return;
173
+ }
174
+ if (isSettle && !allowSettle(tokenDigest.toString("hex"))) {
175
+ json(res, 429, { error: "rate-limited" });
176
+ return;
177
+ }
178
+ let body;
179
+ try {
180
+ body = JSON.parse((await readBody(req)).toString("utf8"));
181
+ }
182
+ catch {
183
+ json(res, 400, { error: "invalid-json" });
184
+ return;
185
+ }
186
+ const request = body;
187
+ if (typeof request !== "object" ||
188
+ request === null ||
189
+ typeof request.paymentPayload !== "object" ||
190
+ request.paymentPayload === null ||
191
+ typeof request.paymentRequirements !== "object" ||
192
+ request.paymentRequirements === null) {
193
+ json(res, 400, {
194
+ error: "paymentPayload and paymentRequirements are required",
195
+ });
196
+ return;
197
+ }
198
+ const payload = request.paymentPayload;
199
+ const requirements = request.paymentRequirements;
200
+ if (isSettle) {
201
+ try {
202
+ json(res, 200, await config.facilitator.settle(payload, requirements));
203
+ }
204
+ catch (error) {
205
+ json(res, 200, {
206
+ success: false,
207
+ errorReason: "metrik_opener_settle_error",
208
+ errorMessage: error instanceof Error ? error.message : String(error),
209
+ transaction: "",
210
+ network: requirements.network ?? "",
211
+ });
212
+ }
213
+ return;
214
+ }
215
+ try {
216
+ json(res, 200, await config.facilitator.verify(payload, requirements));
217
+ }
218
+ catch (error) {
219
+ json(res, 200, {
220
+ isValid: false,
221
+ invalidReason: "metrik_opener_verify_error",
222
+ invalidMessage: error instanceof Error ? error.message : String(error),
223
+ });
224
+ }
225
+ return;
226
+ }
227
+ json(res, 404, { error: "not-found" });
228
+ }
229
+ async function handleRefund(req, res, relay, method, streamId, action) {
230
+ const route = action === undefined
231
+ ? "status"
232
+ : action === "close"
233
+ ? "close"
234
+ : action === "reclaim"
235
+ ? "reclaim"
236
+ : action === "reclaim-unverified"
237
+ ? "reclaimUnverified"
238
+ : undefined;
239
+ if (route === undefined) {
240
+ json(res, 404, { error: "not-found" });
241
+ return;
242
+ }
243
+ if ((route === "status") !== (method === "GET")) {
244
+ res.setHeader("Allow", route === "status" ? "GET" : "POST");
245
+ json(res, 405, { error: "method-not-allowed" });
246
+ return;
247
+ }
248
+ // Per client first (one noisy client cannot drain the global budget
249
+ // beyond its own share), then the IP-independent global cap.
250
+ let retryAfter = takeRefundBudget(clientKey(req));
251
+ if (retryAfter === 0)
252
+ retryAfter = takeGlobalRefundBudget();
253
+ if (retryAfter > 0) {
254
+ res.setHeader("Retry-After", String(retryAfter));
255
+ json(res, 429, { ok: false, error: "rate-limited" });
256
+ return;
257
+ }
258
+ let result;
259
+ if (route === "status") {
260
+ result = await relay.status(streamId);
261
+ }
262
+ else if (route === "reclaim") {
263
+ // Permissionless: no body is needed, and any body is ignored.
264
+ await readBody(req, MAX_REFUND_BODY_BYTES).catch(() => undefined);
265
+ result = await relay.reclaim(streamId);
266
+ }
267
+ else {
268
+ let body;
269
+ try {
270
+ body = JSON.parse((await readBody(req, MAX_REFUND_BODY_BYTES)).toString("utf8"));
271
+ }
272
+ catch (error) {
273
+ if (error instanceof BodyTooLargeError) {
274
+ json(res, 413, { ok: false, error: "body-too-large" });
275
+ }
276
+ else {
277
+ json(res, 400, { ok: false, error: "invalid-json" });
278
+ }
279
+ return;
280
+ }
281
+ result =
282
+ route === "close"
283
+ ? await relay.close(streamId, body)
284
+ : await relay.reclaimUnverified(streamId, body);
285
+ }
286
+ if (result.retryAfterSeconds !== undefined) {
287
+ res.setHeader("Retry-After", String(result.retryAfterSeconds));
288
+ }
289
+ json(res, result.status, result.body);
290
+ }
291
+ return {
292
+ httpServer,
293
+ listen: (port, host = "127.0.0.1") => new Promise((resolve, reject) => {
294
+ const onError = (error) => reject(error);
295
+ httpServer.once("error", onError);
296
+ httpServer.listen(port, host, () => {
297
+ httpServer.removeListener("error", onError);
298
+ const address = httpServer.address();
299
+ resolve({ port: address.port, host });
300
+ });
301
+ }),
302
+ close: () => new Promise((resolve, reject) => {
303
+ httpServer.close((error) => (error ? reject(error) : resolve()));
304
+ }),
305
+ };
306
+ }
307
+ function digest(value) {
308
+ return createHash("sha256").update(value, "utf8").digest();
309
+ }
310
+ function json(res, status, body) {
311
+ res
312
+ .writeHead(status, {
313
+ "Content-Type": "application/json",
314
+ "Cache-Control": "no-store",
315
+ })
316
+ .end(JSON.stringify(body, (_key, value) => typeof value === "bigint" ? value.toString() : value));
317
+ }
318
+ async function readBody(req, limit = MAX_BODY_BYTES) {
319
+ const chunks = [];
320
+ let size = 0;
321
+ for await (const chunk of req) {
322
+ const buffer = chunk;
323
+ size += buffer.length;
324
+ if (size > limit) {
325
+ throw new BodyTooLargeError("request body too large");
326
+ }
327
+ chunks.push(buffer);
328
+ }
329
+ return Buffer.concat(chunks);
330
+ }
331
+ /**
332
+ * Builds an `x402Facilitator` whose ONLY scheme is {@link MetrikOpenerExactScheme}
333
+ * on one network, and proves the on-chain wiring (chain id, factory code,
334
+ * factory → escrow, factory token allowlisted) before returning.
335
+ */
336
+ export async function buildOpenerFacilitator(options) {
337
+ const chainId = chainIdFromNetwork(options.network);
338
+ const chain = chainForChainId(chainId, options.rpcUrl);
339
+ const account = privateKeyToAccount(options.relayerPrivateKey);
340
+ const transport = http(options.rpcUrl);
341
+ const publicClient = createPublicClient({ chain, transport });
342
+ const walletClient = createWalletClient({ account, chain, transport });
343
+ // ONE queue for every transaction this relayer key sends.
344
+ const relayerQueue = new RelayerQueue();
345
+ const receiptTimeoutMs = options.receiptTimeoutMs;
346
+ const scheme = new MetrikOpenerExactScheme({
347
+ relayerQueue,
348
+ advertiseRefundRelay: options.refundRelay !== undefined,
349
+ network: options.network,
350
+ factoryAddress: options.factoryAddress,
351
+ escrowAddress: options.escrowAddress,
352
+ assetAllowlist: options.assetAllowlist,
353
+ publicClient,
354
+ walletClient,
355
+ listingResolver: options.listingResolver ??
356
+ new RegistryListingResolver({
357
+ registryUrl: options.registryUrl ?? DEFAULT_METRIK_PUBLIC_REGISTRY_URL,
358
+ cacheSeconds: options.listingCacheSeconds ?? DEFAULT_LISTING_CACHE_SECONDS,
359
+ }),
360
+ ...(options.receiptTimeoutMs === undefined
361
+ ? {}
362
+ : { receiptTimeoutMs: options.receiptTimeoutMs }),
363
+ ...(options.minAmount === undefined
364
+ ? {}
365
+ : { minAmount: options.minAmount }),
366
+ });
367
+ const binding = await scheme.init();
368
+ let refundRelay;
369
+ if (options.refundRelay !== undefined) {
370
+ const relay = options.refundRelay;
371
+ refundRelay = new OpenerRefundRelay({
372
+ network: options.network,
373
+ factoryAddress: options.factoryAddress,
374
+ escrowAddress: options.escrowAddress,
375
+ oracleUrl: relay.oracleUrl,
376
+ publicClient,
377
+ walletClient,
378
+ relayerQueue,
379
+ ...(receiptTimeoutMs === undefined ? {} : { receiptTimeoutMs }),
380
+ ...(relay.publicBaseUrl === undefined
381
+ ? {}
382
+ : { publicBaseUrl: relay.publicBaseUrl }),
383
+ ...(relay.submissionsPerStreamPerMinute === undefined
384
+ ? {}
385
+ : {
386
+ maxSubmissionsPerStreamPerMinute: relay.submissionsPerStreamPerMinute,
387
+ }),
388
+ ...(relay.submissionsPerMinute === undefined
389
+ ? {}
390
+ : { maxSubmissionsPerMinute: relay.submissionsPerMinute }),
391
+ ...(relay.oracleTimeoutMs === undefined
392
+ ? {}
393
+ : { oracleTimeoutMs: relay.oracleTimeoutMs }),
394
+ ...(relay.fetch === undefined ? {} : { fetch: relay.fetch }),
395
+ ...(relay.readCacheSeconds === undefined
396
+ ? {}
397
+ : { readCacheSeconds: relay.readCacheSeconds }),
398
+ });
399
+ await refundRelay.init();
400
+ }
401
+ const facilitator = new x402Facilitator().register(options.network, scheme);
402
+ return {
403
+ facilitator,
404
+ scheme,
405
+ binding,
406
+ ...(refundRelay === undefined ? {} : { refundRelay }),
407
+ };
408
+ }
409
+ /**
410
+ * Parses the facilitator's env contract. OFF unless
411
+ * `METRIK_OPENER_FACILITATOR_ENABLED` is truthy, and then every required
412
+ * variable must be present (fail closed):
413
+ *
414
+ * - `METRIK_OPENER_FACILITATOR_ENABLED` — `1`/`true`/`yes`/`on`.
415
+ * - `METRIK_OPENER_FACILITATOR_RPC_URL` (required).
416
+ * - `METRIK_OPENER_FACILITATOR_RELAYER_PRIVATE_KEY` (required) — pays gas only.
417
+ * - `METRIK_OPENER_FACILITATOR_AUTH_TOKEN` (required, >= 32 chars) — bearer on `/settle`.
418
+ * - `METRIK_OPENER_FACILITATOR_NETWORK` — CAIP-2 id; default the registry's
419
+ * default network. Factory, escrow, USDC and oracle default to THAT network's
420
+ * registry entry; on a network the registry does not know (or one with no
421
+ * opener factory) each must be given explicitly.
422
+ * - `METRIK_OPENER_FACILITATOR_FACTORY_ADDRESS` — default the network's registered factory.
423
+ * - `METRIK_OPENER_FACILITATOR_ESCROW_ADDRESS` — default the network's registered `StreamEscrowV2`.
424
+ * - `METRIK_OPENER_FACILITATOR_ASSETS` — comma-separated allowlist; default the network's USDC.
425
+ * - `METRIK_OPENER_FACILITATOR_REGISTRY_URL` — default the public listings registry.
426
+ * - `METRIK_OPENER_FACILITATOR_LISTING_CACHE_SECONDS` — default 60.
427
+ * - `METRIK_OPENER_FACILITATOR_RECEIPT_TIMEOUT_SECONDS` — default 60.
428
+ * - `METRIK_OPENER_FACILITATOR_REQUIRE_AUTH_ON_VERIFY` — default ON; `0` opts out.
429
+ * - `METRIK_OPENER_FACILITATOR_MIN_AMOUNT` — smallest payment, base units; default 10000 (0.01 USDC).
430
+ * - `METRIK_OPENER_FACILITATOR_SETTLE_RATE_LIMIT_PER_MINUTE` — per token; default 60.
431
+ * - `METRIK_OPENER_FACILITATOR_PORT` / `PORT` — default 8788.
432
+ * - `METRIK_OPENER_FACILITATOR_HOST` — default `127.0.0.1`.
433
+ *
434
+ * Gasless refund relay (metrik-agent#119), ON with the facilitator unless
435
+ * switched off. Its endpoints are public and unauthenticated by design:
436
+ *
437
+ * - `METRIK_OPENER_FACILITATOR_REFUND_RELAY` — default on; `0` switches it off
438
+ * (and a gateway, which promises refunds without ETH, then refuses to start).
439
+ * - `METRIK_OPENER_FACILITATOR_ORACLE_URL` — oracle serving
440
+ * `/checkpoint/proof`; default the network's registered oracle, required
441
+ * on a network the registry does not know.
442
+ * - `METRIK_OPENER_FACILITATOR_PUBLIC_URL` — public origin, for absolute links.
443
+ * - `METRIK_OPENER_FACILITATOR_REFUND_RATE_LIMIT_PER_IP_PER_MINUTE` — default 30.
444
+ * - `METRIK_OPENER_FACILITATOR_REFUND_GLOBAL_RATE_LIMIT_PER_MINUTE` — all clients; default 120.
445
+ * - `METRIK_OPENER_FACILITATOR_REFUND_READ_CACHE_SECONDS` — per-stream status/proof reuse; default 5 (0 = off).
446
+ * - `METRIK_OPENER_FACILITATOR_REFUND_SUBMISSIONS_PER_STREAM_PER_MINUTE` — relayed
447
+ * transactions per stream per minute; default 3.
448
+ * - `METRIK_OPENER_FACILITATOR_REFUND_SUBMISSIONS_PER_MINUTE` — all streams; default 30.
449
+ * - `METRIK_OPENER_FACILITATOR_TRUST_PROXY` — key the per-IP limit on the
450
+ * right-most `X-Forwarded-For` hop; default off. Enable only behind a proxy.
451
+ */
452
+ export function parseOpenerFacilitatorEnvConfig(env = process.env) {
453
+ if (!parseBoolean(env.METRIK_OPENER_FACILITATOR_ENABLED, "METRIK_OPENER_FACILITATOR_ENABLED")) {
454
+ return undefined;
455
+ }
456
+ const network = (env.METRIK_OPENER_FACILITATOR_NETWORK?.trim() ||
457
+ defaultNetwork().caip2);
458
+ // Validates the CAIP-2 shape (eip155:<chainId>) before anything else.
459
+ chainIdFromNetwork(network);
460
+ // Defaults come from the registry entry for THIS network, never from
461
+ // another network's: a factory is bound to one escrow on one chain.
462
+ const registered = networkForCaip2(network);
463
+ const rpcUrl = requireEnv(env, "METRIK_OPENER_FACILITATOR_RPC_URL");
464
+ // eslint-disable-next-line no-new -- validates the URL is well-formed.
465
+ new URL(rpcUrl);
466
+ const relayerPrivateKey = requireEnv(env, "METRIK_OPENER_FACILITATOR_RELAYER_PRIVATE_KEY");
467
+ if (!isHex(relayerPrivateKey, { strict: true }) ||
468
+ relayerPrivateKey.length !== 66) {
469
+ // Never echo the value.
470
+ throw new Error("METRIK_OPENER_FACILITATOR_RELAYER_PRIVATE_KEY must be a 32-byte 0x-hex private key");
471
+ }
472
+ const authToken = requireEnv(env, "METRIK_OPENER_FACILITATOR_AUTH_TOKEN");
473
+ if (authToken.length < MIN_AUTH_TOKEN_LENGTH) {
474
+ throw new Error(`METRIK_OPENER_FACILITATOR_AUTH_TOKEN must be at least ${MIN_AUTH_TOKEN_LENGTH} characters`);
475
+ }
476
+ const factoryRaw = env.METRIK_OPENER_FACILITATOR_FACTORY_ADDRESS?.trim();
477
+ let factoryAddress;
478
+ if (factoryRaw === undefined || factoryRaw.length === 0) {
479
+ if (registered?.openerFactory == null) {
480
+ throw new Error(`METRIK_OPENER_FACILITATOR_FACTORY_ADDRESS is required on ${network}: the network registry has no opener factory for it`);
481
+ }
482
+ factoryAddress = registered.openerFactory.address;
483
+ }
484
+ else {
485
+ factoryAddress = requireAddress(env, "METRIK_OPENER_FACILITATOR_FACTORY_ADDRESS");
486
+ }
487
+ const escrowRaw = env.METRIK_OPENER_FACILITATOR_ESCROW_ADDRESS?.trim();
488
+ let escrowAddress;
489
+ if (escrowRaw === undefined || escrowRaw.length === 0) {
490
+ if (registered === undefined) {
491
+ throw new Error(`METRIK_OPENER_FACILITATOR_ESCROW_ADDRESS is required on ${network}: the network registry does not know it`);
492
+ }
493
+ escrowAddress = registered.escrow.address;
494
+ }
495
+ else {
496
+ escrowAddress = requireAddress(env, "METRIK_OPENER_FACILITATOR_ESCROW_ADDRESS");
497
+ }
498
+ const assetsRaw = env.METRIK_OPENER_FACILITATOR_ASSETS?.trim();
499
+ let assetAllowlist;
500
+ if (assetsRaw === undefined || assetsRaw.length === 0) {
501
+ if (registered === undefined) {
502
+ throw new Error(`METRIK_OPENER_FACILITATOR_ASSETS is required on ${network}: the network registry does not know it`);
503
+ }
504
+ assetAllowlist = [registered.usdc.address];
505
+ }
506
+ else {
507
+ assetAllowlist = assetsRaw
508
+ .split(",")
509
+ .map((value) => value.trim())
510
+ .filter((value) => value.length > 0)
511
+ .map((value) => {
512
+ if (!isAddress(value)) {
513
+ throw new Error(`METRIK_OPENER_FACILITATOR_ASSETS entry is not an address: ${value}`);
514
+ }
515
+ return getAddress(value);
516
+ });
517
+ }
518
+ const registryUrl = env.METRIK_OPENER_FACILITATOR_REGISTRY_URL?.trim() ||
519
+ DEFAULT_METRIK_PUBLIC_REGISTRY_URL;
520
+ // eslint-disable-next-line no-new -- validates the URL is well-formed.
521
+ new URL(registryUrl);
522
+ const listingCacheSeconds = nonNegativeInt(env, "METRIK_OPENER_FACILITATOR_LISTING_CACHE_SECONDS", DEFAULT_LISTING_CACHE_SECONDS);
523
+ const receiptTimeoutSeconds = positiveInt(env, "METRIK_OPENER_FACILITATOR_RECEIPT_TIMEOUT_SECONDS", 60);
524
+ const portRaw = env.METRIK_OPENER_FACILITATOR_PORT ?? env.PORT ?? "8788";
525
+ const port = Number(portRaw);
526
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
527
+ throw new Error(`invalid port: ${portRaw}`);
528
+ }
529
+ const refundRelay = parseRefundRelayEnv(env, network);
530
+ return {
531
+ ...(refundRelay === undefined ? {} : { refundRelay }),
532
+ host: env.METRIK_OPENER_FACILITATOR_HOST?.trim() || "127.0.0.1",
533
+ port,
534
+ network,
535
+ rpcUrl,
536
+ relayerPrivateKey: relayerPrivateKey,
537
+ authToken,
538
+ requireAuthOnVerify: (env.METRIK_OPENER_FACILITATOR_REQUIRE_AUTH_ON_VERIFY?.trim() ?? "") ===
539
+ ""
540
+ ? true
541
+ : parseBoolean(env.METRIK_OPENER_FACILITATOR_REQUIRE_AUTH_ON_VERIFY, "METRIK_OPENER_FACILITATOR_REQUIRE_AUTH_ON_VERIFY"),
542
+ minAmount: BigInt(positiveInt(env, "METRIK_OPENER_FACILITATOR_MIN_AMOUNT", 10_000)),
543
+ settleRateLimitPerMinute: positiveInt(env, "METRIK_OPENER_FACILITATOR_SETTLE_RATE_LIMIT_PER_MINUTE", DEFAULT_SETTLE_RATE_LIMIT_PER_MINUTE),
544
+ factoryAddress,
545
+ escrowAddress,
546
+ assetAllowlist,
547
+ registryUrl,
548
+ listingCacheSeconds,
549
+ receiptTimeoutMs: receiptTimeoutSeconds * 1000,
550
+ };
551
+ }
552
+ function parseRefundRelayEnv(env, network) {
553
+ const raw = env.METRIK_OPENER_FACILITATOR_REFUND_RELAY?.trim() ?? "";
554
+ if (raw.length > 0 &&
555
+ !parseBoolean(raw, "METRIK_OPENER_FACILITATOR_REFUND_RELAY")) {
556
+ return undefined;
557
+ }
558
+ const oracleRaw = env.METRIK_OPENER_FACILITATOR_ORACLE_URL?.trim();
559
+ let oracleUrl;
560
+ if (oracleRaw === undefined || oracleRaw.length === 0) {
561
+ const registered = networkForCaip2(network);
562
+ if (registered === undefined) {
563
+ throw new Error(`METRIK_OPENER_FACILITATOR_ORACLE_URL is required on ${network}: the network registry does not know it (or set METRIK_OPENER_FACILITATOR_REFUND_RELAY=0)`);
564
+ }
565
+ oracleUrl = registered.oracleBaseUrl;
566
+ }
567
+ else {
568
+ oracleUrl = requireHttpUrl(oracleRaw, "METRIK_OPENER_FACILITATOR_ORACLE_URL");
569
+ }
570
+ const publicRaw = env.METRIK_OPENER_FACILITATOR_PUBLIC_URL?.trim();
571
+ const publicBaseUrl = publicRaw === undefined || publicRaw.length === 0
572
+ ? undefined
573
+ : requireHttpUrl(publicRaw, "METRIK_OPENER_FACILITATOR_PUBLIC_URL");
574
+ const trustRaw = env.METRIK_OPENER_FACILITATOR_TRUST_PROXY?.trim() ?? "";
575
+ return {
576
+ oracleUrl,
577
+ ...(publicBaseUrl === undefined ? {} : { publicBaseUrl }),
578
+ rateLimitPerIpPerMinute: positiveInt(env, "METRIK_OPENER_FACILITATOR_REFUND_RATE_LIMIT_PER_IP_PER_MINUTE", DEFAULT_REFUND_RATE_LIMIT_PER_IP_PER_MINUTE),
579
+ globalRateLimitPerMinute: positiveInt(env, "METRIK_OPENER_FACILITATOR_REFUND_GLOBAL_RATE_LIMIT_PER_MINUTE", DEFAULT_REFUND_GLOBAL_RATE_LIMIT_PER_MINUTE),
580
+ readCacheSeconds: nonNegativeInt(env, "METRIK_OPENER_FACILITATOR_REFUND_READ_CACHE_SECONDS", DEFAULT_REFUND_READ_CACHE_SECONDS),
581
+ submissionsPerStreamPerMinute: positiveInt(env, "METRIK_OPENER_FACILITATOR_REFUND_SUBMISSIONS_PER_STREAM_PER_MINUTE", DEFAULT_REFUND_SUBMISSIONS_PER_STREAM_PER_MINUTE),
582
+ submissionsPerMinute: positiveInt(env, "METRIK_OPENER_FACILITATOR_REFUND_SUBMISSIONS_PER_MINUTE", DEFAULT_REFUND_SUBMISSIONS_PER_MINUTE),
583
+ trustProxy: trustRaw.length > 0 &&
584
+ parseBoolean(trustRaw, "METRIK_OPENER_FACILITATOR_TRUST_PROXY"),
585
+ };
586
+ }
587
+ function requireHttpUrl(value, name) {
588
+ let url;
589
+ try {
590
+ url = new URL(value);
591
+ }
592
+ catch {
593
+ throw new Error(`${name} must be an absolute URL`);
594
+ }
595
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
596
+ throw new Error(`${name} must be an http(s) URL`);
597
+ }
598
+ return value.replace(/\/+$/, "");
599
+ }
600
+ /** Builds and starts the facilitator service entirely from the environment. */
601
+ export async function startOpenerFacilitatorServerFromEnv(env = process.env) {
602
+ const config = parseOpenerFacilitatorEnvConfig(env);
603
+ if (config === undefined) {
604
+ throw new Error("the Metrik x402 opener facilitator is OFF: set METRIK_OPENER_FACILITATOR_ENABLED=1 (plus its required variables) to run it");
605
+ }
606
+ const { facilitator, binding, refundRelay } = await buildOpenerFacilitator({
607
+ network: config.network,
608
+ rpcUrl: config.rpcUrl,
609
+ relayerPrivateKey: config.relayerPrivateKey,
610
+ factoryAddress: config.factoryAddress,
611
+ escrowAddress: config.escrowAddress,
612
+ assetAllowlist: config.assetAllowlist,
613
+ registryUrl: config.registryUrl,
614
+ listingCacheSeconds: config.listingCacheSeconds,
615
+ receiptTimeoutMs: config.receiptTimeoutMs,
616
+ minAmount: config.minAmount,
617
+ ...(config.refundRelay === undefined
618
+ ? {}
619
+ : {
620
+ refundRelay: {
621
+ oracleUrl: config.refundRelay.oracleUrl,
622
+ submissionsPerStreamPerMinute: config.refundRelay.submissionsPerStreamPerMinute,
623
+ submissionsPerMinute: config.refundRelay.submissionsPerMinute,
624
+ readCacheSeconds: config.refundRelay.readCacheSeconds,
625
+ ...(config.refundRelay.publicBaseUrl === undefined
626
+ ? {}
627
+ : { publicBaseUrl: config.refundRelay.publicBaseUrl }),
628
+ },
629
+ }),
630
+ });
631
+ const server = createOpenerFacilitatorServer({
632
+ facilitator,
633
+ authToken: config.authToken,
634
+ requireAuthOnVerify: config.requireAuthOnVerify,
635
+ settleRateLimitPerMinute: config.settleRateLimitPerMinute,
636
+ ...(refundRelay === undefined || config.refundRelay === undefined
637
+ ? {}
638
+ : {
639
+ refundRelay,
640
+ refundRateLimitPerIpPerMinute: config.refundRelay.rateLimitPerIpPerMinute,
641
+ refundGlobalRateLimitPerMinute: config.refundRelay.globalRateLimitPerMinute,
642
+ trustProxy: config.refundRelay.trustProxy,
643
+ }),
644
+ });
645
+ const { port } = await server.listen(config.port, config.host);
646
+ return Object.assign(server, {
647
+ boundPort: port,
648
+ binding,
649
+ refundRelayEnabled: refundRelay !== undefined,
650
+ });
651
+ }
652
+ function parseBoolean(raw, name) {
653
+ const value = raw?.trim().toLowerCase();
654
+ if (value === undefined || value.length === 0)
655
+ return false;
656
+ if (["1", "true", "yes", "on"].includes(value))
657
+ return true;
658
+ if (["0", "false", "no", "off"].includes(value))
659
+ return false;
660
+ throw new Error(`invalid ${name}: ${value} (expected a boolean)`);
661
+ }
662
+ function requireEnv(env, name) {
663
+ const value = env[name]?.trim();
664
+ if (value === undefined || value.length === 0) {
665
+ throw new Error(`${name} is required (fail-closed)`);
666
+ }
667
+ return value;
668
+ }
669
+ function requireAddress(env, name) {
670
+ const value = requireEnv(env, name);
671
+ if (!isAddress(value)) {
672
+ throw new Error(`${name} must be an EVM address`);
673
+ }
674
+ return getAddress(value);
675
+ }
676
+ function positiveInt(env, name, fallback) {
677
+ const raw = env[name]?.trim();
678
+ if (raw === undefined || raw.length === 0)
679
+ return fallback;
680
+ const value = Number(raw);
681
+ if (!Number.isInteger(value) || value <= 0) {
682
+ throw new Error(`invalid ${name}: ${raw} (expected a positive integer)`);
683
+ }
684
+ return value;
685
+ }
686
+ function nonNegativeInt(env, name, fallback) {
687
+ const raw = env[name]?.trim();
688
+ if (raw === undefined || raw.length === 0)
689
+ return fallback;
690
+ const value = Number(raw);
691
+ if (!Number.isInteger(value) || value < 0) {
692
+ throw new Error(`invalid ${name}: ${raw} (expected a non-negative integer)`);
693
+ }
694
+ return value;
695
+ }
696
+ //# sourceMappingURL=facilitator-server.js.map