@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.
- package/README.md +43 -51
- package/dist/capability/invocation-capability.d.ts +2 -2
- package/dist/capability/probe-capability.d.ts +225 -0
- package/dist/capability/probe-capability.d.ts.map +1 -0
- package/dist/capability/probe-capability.js +248 -0
- package/dist/capability/probe-capability.js.map +1 -0
- package/dist/cli/args.d.ts +11 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +23 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/commands/claim.d.ts +32 -0
- package/dist/cli/commands/claim.d.ts.map +1 -1
- package/dist/cli/commands/claim.js +116 -28
- package/dist/cli/commands/claim.js.map +1 -1
- package/dist/cli/commands/close.d.ts.map +1 -1
- package/dist/cli/commands/close.js +35 -3
- package/dist/cli/commands/close.js.map +1 -1
- package/dist/cli/commands/invoke.d.ts +2 -0
- package/dist/cli/commands/invoke.d.ts.map +1 -1
- package/dist/cli/commands/invoke.js +2 -2
- package/dist/cli/commands/invoke.js.map +1 -1
- package/dist/cli/commands/mcp.d.ts.map +1 -1
- package/dist/cli/commands/mcp.js +15 -2
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/open.d.ts.map +1 -1
- package/dist/cli/commands/open.js +58 -4
- package/dist/cli/commands/open.js.map +1 -1
- package/dist/cli/commands/pay.d.ts +34 -0
- package/dist/cli/commands/pay.d.ts.map +1 -0
- package/dist/cli/commands/pay.js +479 -0
- package/dist/cli/commands/pay.js.map +1 -0
- package/dist/cli/commands/reclaim-unverified.d.ts.map +1 -1
- package/dist/cli/commands/reclaim-unverified.js +3 -2
- package/dist/cli/commands/reclaim-unverified.js.map +1 -1
- package/dist/cli/commands/reclaim.d.ts +23 -2
- package/dist/cli/commands/reclaim.d.ts.map +1 -1
- package/dist/cli/commands/reclaim.js +204 -20
- package/dist/cli/commands/reclaim.js.map +1 -1
- package/dist/cli/commands/refund.d.ts +31 -0
- package/dist/cli/commands/refund.d.ts.map +1 -0
- package/dist/cli/commands/refund.js +397 -0
- package/dist/cli/commands/refund.js.map +1 -0
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +21 -2
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/streams.js +8 -12
- package/dist/cli/commands/streams.js.map +1 -1
- package/dist/cli/commands/watch.d.ts.map +1 -1
- package/dist/cli/commands/watch.js +4 -1
- package/dist/cli/commands/watch.js.map +1 -1
- package/dist/cli/config.d.ts +44 -26
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +88 -38
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/context.d.ts +19 -0
- package/dist/cli/context.d.ts.map +1 -1
- package/dist/cli/deps.d.ts +53 -0
- package/dist/cli/deps.d.ts.map +1 -1
- package/dist/cli/deps.js +51 -1
- package/dist/cli/deps.js.map +1 -1
- package/dist/cli/errors.d.ts.map +1 -1
- package/dist/cli/errors.js +21 -1
- package/dist/cli/errors.js.map +1 -1
- package/dist/cli/escape-hatch.d.ts +6 -4
- package/dist/cli/escape-hatch.d.ts.map +1 -1
- package/dist/cli/escape-hatch.js +6 -2
- package/dist/cli/escape-hatch.js.map +1 -1
- package/dist/cli/listings.d.ts +18 -0
- package/dist/cli/listings.d.ts.map +1 -1
- package/dist/cli/listings.js +22 -12
- package/dist/cli/listings.js.map +1 -1
- package/dist/cli/preflight.d.ts +20 -14
- package/dist/cli/preflight.d.ts.map +1 -1
- package/dist/cli/preflight.js +34 -20
- package/dist/cli/preflight.js.map +1 -1
- package/dist/cli/privy.d.ts.map +1 -1
- package/dist/cli/privy.js +4 -2
- package/dist/cli/privy.js.map +1 -1
- package/dist/cli/progress.d.ts +13 -1
- package/dist/cli/progress.d.ts.map +1 -1
- package/dist/cli/progress.js +7 -1
- package/dist/cli/progress.js.map +1 -1
- package/dist/cli/reclaimability.d.ts +82 -0
- package/dist/cli/reclaimability.d.ts.map +1 -0
- package/dist/cli/reclaimability.js +80 -0
- package/dist/cli/reclaimability.js.map +1 -0
- package/dist/cli/run.d.ts +2 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +62 -21
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/scan-range.d.ts +2 -2
- package/dist/cli/scan-range.js +2 -2
- package/dist/cli/status.d.ts +106 -1
- package/dist/cli/status.d.ts.map +1 -1
- package/dist/cli/status.js +325 -44
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/wallet.d.ts +2 -2
- package/dist/cli/wallet.d.ts.map +1 -1
- package/dist/cli/wallet.js +29 -5
- package/dist/cli/wallet.js.map +1 -1
- package/dist/discovery/rate-band.d.ts +74 -0
- package/dist/discovery/rate-band.d.ts.map +1 -0
- package/dist/discovery/rate-band.js +114 -0
- package/dist/discovery/rate-band.js.map +1 -0
- package/dist/escrow-generation.d.ts +81 -0
- package/dist/escrow-generation.d.ts.map +1 -0
- package/dist/escrow-generation.js +76 -0
- package/dist/escrow-generation.js.map +1 -0
- package/dist/frameworks/agentkit.d.ts +8 -4
- package/dist/frameworks/agentkit.d.ts.map +1 -1
- package/dist/frameworks/agentkit.js +27 -10
- package/dist/frameworks/agentkit.js.map +1 -1
- package/dist/frameworks/eliza.d.ts +3 -3
- package/dist/frameworks/eliza.d.ts.map +1 -1
- package/dist/frameworks/eliza.js +15 -6
- package/dist/frameworks/eliza.js.map +1 -1
- package/dist/frameworks/langchain.d.ts.map +1 -1
- package/dist/frameworks/langchain.js +8 -0
- package/dist/frameworks/langchain.js.map +1 -1
- package/dist/gateway/caller-auth-gateway.d.ts +83 -31
- package/dist/gateway/caller-auth-gateway.d.ts.map +1 -1
- package/dist/gateway/caller-auth-gateway.js +180 -27
- package/dist/gateway/caller-auth-gateway.js.map +1 -1
- package/dist/gateway/http-server.d.ts +128 -44
- package/dist/gateway/http-server.d.ts.map +1 -1
- package/dist/gateway/http-server.js +506 -155
- package/dist/gateway/http-server.js.map +1 -1
- package/dist/gateway/probe-auth.d.ts +112 -0
- package/dist/gateway/probe-auth.d.ts.map +1 -0
- package/dist/gateway/probe-auth.js +127 -0
- package/dist/gateway/probe-auth.js.map +1 -0
- package/dist/gateway/signed-rate-bounds.d.ts +51 -0
- package/dist/gateway/signed-rate-bounds.d.ts.map +1 -0
- package/dist/gateway/signed-rate-bounds.js +94 -0
- package/dist/gateway/signed-rate-bounds.js.map +1 -0
- package/dist/gateway/x402-opener-mode.d.ts +252 -0
- package/dist/gateway/x402-opener-mode.d.ts.map +1 -0
- package/dist/gateway/x402-opener-mode.js +772 -0
- package/dist/gateway/x402-opener-mode.js.map +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -5
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +64 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +247 -37
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/x402-tools.d.ts +109 -0
- package/dist/mcp/x402-tools.d.ts.map +1 -0
- package/dist/mcp/x402-tools.js +554 -0
- package/dist/mcp/x402-tools.js.map +1 -0
- package/dist/network.d.ts +119 -0
- package/dist/network.d.ts.map +1 -0
- package/dist/network.js +199 -0
- package/dist/network.js.map +1 -0
- package/dist/sdk/client.d.ts +13 -0
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/client.js +15 -0
- package/dist/sdk/client.js.map +1 -1
- package/dist/sdk/invoke.d.ts +4 -0
- package/dist/sdk/invoke.d.ts.map +1 -1
- package/dist/sdk/invoke.js +2 -5
- package/dist/sdk/invoke.js.map +1 -1
- package/dist/wallet/autonomous-wallet-broker.d.ts +3 -0
- package/dist/wallet/autonomous-wallet-broker.d.ts.map +1 -1
- package/dist/wallet/autonomous-wallet-broker.js +27 -23
- package/dist/wallet/autonomous-wallet-broker.js.map +1 -1
- package/dist/wallet/autonomous-wallet-protocol.d.ts +8 -2
- package/dist/wallet/autonomous-wallet-protocol.d.ts.map +1 -1
- package/dist/wallet/autonomous-wallet-protocol.js +9 -2
- package/dist/wallet/autonomous-wallet-protocol.js.map +1 -1
- package/dist/wallet/autonomous-wallet.d.ts.map +1 -1
- package/dist/wallet/autonomous-wallet.js +3 -2
- package/dist/wallet/autonomous-wallet.js.map +1 -1
- package/dist/wallet/broker-escrows.d.ts +222 -0
- package/dist/wallet/broker-escrows.d.ts.map +1 -0
- package/dist/wallet/broker-escrows.js +36 -0
- package/dist/wallet/broker-escrows.js.map +1 -0
- package/dist/wallet/lifecycle.d.ts +21 -5
- package/dist/wallet/lifecycle.d.ts.map +1 -1
- package/dist/wallet/lifecycle.js +32 -12
- package/dist/wallet/lifecycle.js.map +1 -1
- package/dist/wallet/privy-broker-server.d.ts.map +1 -1
- package/dist/wallet/privy-broker-server.js +31 -26
- package/dist/wallet/privy-broker-server.js.map +1 -1
- package/dist/wallet/privy-session-broker.d.ts +14 -5
- package/dist/wallet/privy-session-broker.d.ts.map +1 -1
- package/dist/wallet/privy-session-broker.js +29 -17
- package/dist/wallet/privy-session-broker.js.map +1 -1
- package/dist/wallet/privy-session-provider.d.ts +3 -1
- package/dist/wallet/privy-session-provider.d.ts.map +1 -1
- package/dist/wallet/privy-session-provider.js +5 -3
- package/dist/wallet/privy-session-provider.js.map +1 -1
- package/dist/wallet/provider.d.ts.map +1 -1
- package/dist/wallet/provider.js +15 -8
- package/dist/wallet/provider.js.map +1 -1
- package/dist/x402/facilitator.d.ts +3 -30
- package/dist/x402/facilitator.d.ts.map +1 -1
- package/dist/x402/facilitator.js +0 -27
- package/dist/x402/facilitator.js.map +1 -1
- package/dist/x402-buyer/guarded-fetch.d.ts +51 -0
- package/dist/x402-buyer/guarded-fetch.d.ts.map +1 -0
- package/dist/x402-buyer/guarded-fetch.js +147 -0
- package/dist/x402-buyer/guarded-fetch.js.map +1 -0
- package/dist/x402-buyer/index.d.ts +10 -0
- package/dist/x402-buyer/index.d.ts.map +1 -0
- package/dist/x402-buyer/index.js +10 -0
- package/dist/x402-buyer/index.js.map +1 -0
- package/dist/x402-buyer/pay.d.ts +157 -0
- package/dist/x402-buyer/pay.d.ts.map +1 -0
- package/dist/x402-buyer/pay.js +366 -0
- package/dist/x402-buyer/pay.js.map +1 -0
- package/dist/x402-opener/abi.d.ts +684 -0
- package/dist/x402-opener/abi.d.ts.map +1 -0
- package/dist/x402-opener/abi.js +880 -0
- package/dist/x402-opener/abi.js.map +1 -0
- package/dist/x402-opener/facilitator-entry.d.ts +3 -0
- package/dist/x402-opener/facilitator-entry.d.ts.map +1 -0
- package/dist/x402-opener/facilitator-entry.js +40 -0
- package/dist/x402-opener/facilitator-entry.js.map +1 -0
- package/dist/x402-opener/facilitator-server.d.ts +209 -0
- package/dist/x402-opener/facilitator-server.d.ts.map +1 -0
- package/dist/x402-opener/facilitator-server.js +762 -0
- package/dist/x402-opener/facilitator-server.js.map +1 -0
- package/dist/x402-opener/index.d.ts +14 -0
- package/dist/x402-opener/index.d.ts.map +1 -0
- package/dist/x402-opener/index.js +14 -0
- package/dist/x402-opener/index.js.map +1 -0
- package/dist/x402-opener/listing-resolver.d.ts +55 -0
- package/dist/x402-opener/listing-resolver.d.ts.map +1 -0
- package/dist/x402-opener/listing-resolver.js +125 -0
- package/dist/x402-opener/listing-resolver.js.map +1 -0
- package/dist/x402-opener/refund-relay.d.ts +266 -0
- package/dist/x402-opener/refund-relay.d.ts.map +1 -0
- package/dist/x402-opener/refund-relay.js +1341 -0
- package/dist/x402-opener/refund-relay.js.map +1 -0
- package/dist/x402-opener/relayer-queue.d.ts +14 -0
- package/dist/x402-opener/relayer-queue.d.ts.map +1 -0
- package/dist/x402-opener/relayer-queue.js +18 -0
- package/dist/x402-opener/relayer-queue.js.map +1 -0
- package/dist/x402-opener/scheme.d.ts +296 -0
- package/dist/x402-opener/scheme.d.ts.map +1 -0
- package/dist/x402-opener/scheme.js +1035 -0
- package/dist/x402-opener/scheme.js.map +1 -0
- package/dist/x402-opener/terms.d.ts +165 -0
- package/dist/x402-opener/terms.d.ts.map +1 -0
- package/dist/x402-opener/terms.js +304 -0
- package/dist/x402-opener/terms.js.map +1 -0
- package/dist/zktls/reclaim.d.ts +2 -2
- package/dist/zktls/t2-delivery-proof.d.ts +6 -6
- package/docs/agent-layer.md +3 -2
- package/docs/autonomous-privy-wallet.md +188 -0
- package/docs/cli.md +265 -34
- package/docs/langchain.md +5 -0
- package/docs/mcp-reference.md +91 -5
- package/docs/probe-capability.md +191 -0
- package/docs/quickstart.md +39 -4
- package/docs/threat-model.md +20 -0
- package/docs/x402.md +766 -593
- package/package.json +19 -17
- package/src/capability/probe-capability.ts +355 -0
- package/src/cli/args.ts +29 -0
- package/src/cli/commands/claim.ts +168 -33
- package/src/cli/commands/close.ts +36 -3
- package/src/cli/commands/invoke.ts +5 -2
- package/src/cli/commands/mcp.ts +18 -2
- package/src/cli/commands/open.ts +75 -4
- package/src/cli/commands/pay.ts +584 -0
- package/src/cli/commands/reclaim-unverified.ts +8 -2
- package/src/cli/commands/reclaim.ts +277 -19
- package/src/cli/commands/refund.ts +475 -0
- package/src/cli/commands/status.ts +28 -2
- package/src/cli/commands/streams.ts +10 -14
- package/src/cli/commands/watch.ts +11 -1
- package/src/cli/config.ts +139 -52
- package/src/cli/context.ts +16 -0
- package/src/cli/deps.ts +111 -1
- package/src/cli/errors.ts +24 -1
- package/src/cli/escape-hatch.ts +19 -5
- package/src/cli/listings.ts +43 -10
- package/src/cli/preflight.ts +36 -21
- package/src/cli/privy.ts +4 -2
- package/src/cli/progress.ts +21 -2
- package/src/cli/reclaimability.ts +154 -0
- package/src/cli/run.ts +77 -21
- package/src/cli/scan-range.ts +2 -2
- package/src/cli/status.ts +454 -44
- package/src/cli/wallet.ts +45 -8
- package/src/discovery/rate-band.ts +172 -0
- package/src/escrow-generation.ts +135 -0
- package/src/frameworks/agentkit.ts +45 -11
- package/src/frameworks/eliza.ts +27 -5
- package/src/frameworks/langchain.ts +14 -0
- package/src/gateway/caller-auth-gateway.ts +320 -68
- package/src/gateway/http-server.ts +717 -220
- package/src/gateway/probe-auth.ts +191 -0
- package/src/gateway/signed-rate-bounds.ts +164 -0
- package/src/gateway/x402-opener-mode.ts +1171 -0
- package/src/index.ts +82 -19
- package/src/mcp/server.ts +351 -42
- package/src/mcp/x402-tools.ts +785 -0
- package/src/network.ts +302 -0
- package/src/sdk/client.ts +25 -0
- package/src/sdk/invoke.ts +6 -5
- package/src/wallet/autonomous-wallet-broker.ts +46 -20
- package/src/wallet/autonomous-wallet-protocol.ts +11 -2
- package/src/wallet/autonomous-wallet.ts +4 -2
- package/src/wallet/broker-escrows.ts +66 -0
- package/src/wallet/lifecycle.ts +53 -18
- package/src/wallet/privy-broker-server.ts +35 -25
- package/src/wallet/privy-session-broker.ts +58 -25
- package/src/wallet/privy-session-provider.ts +7 -3
- package/src/wallet/provider.ts +20 -15
- package/src/x402/facilitator.ts +3 -43
- package/src/x402-buyer/guarded-fetch.ts +213 -0
- package/src/x402-buyer/index.ts +9 -0
- package/src/x402-buyer/pay.ts +555 -0
- package/src/x402-opener/abi.ts +881 -0
- package/src/x402-opener/facilitator-entry.ts +48 -0
- package/src/x402-opener/facilitator-server.ts +1114 -0
- package/src/x402-opener/index.ts +13 -0
- package/src/x402-opener/listing-resolver.ts +202 -0
- package/src/x402-opener/refund-relay.ts +1873 -0
- package/src/x402-opener/relayer-queue.ts +18 -0
- package/src/x402-opener/scheme.ts +1551 -0
- package/src/x402-opener/terms.ts +453 -0
- package/dist/gateway/x402-mode.d.ts +0 -123
- package/dist/gateway/x402-mode.d.ts.map +0 -1
- package/dist/gateway/x402-mode.js +0 -212
- package/dist/gateway/x402-mode.js.map +0 -1
- package/dist/x402/delivery-oracle.d.ts +0 -108
- package/dist/x402/delivery-oracle.d.ts.map +0 -1
- package/dist/x402/delivery-oracle.js +0 -174
- package/dist/x402/delivery-oracle.js.map +0 -1
- package/dist/x402/facilitator-server-entry.d.ts +0 -3
- package/dist/x402/facilitator-server-entry.d.ts.map +0 -1
- package/dist/x402/facilitator-server-entry.js +0 -27
- package/dist/x402/facilitator-server-entry.js.map +0 -1
- package/dist/x402/facilitator-server.d.ts +0 -73
- package/dist/x402/facilitator-server.d.ts.map +0 -1
- package/dist/x402/facilitator-server.js +0 -305
- package/dist/x402/facilitator-server.js.map +0 -1
- package/dist/x402/payment-requirements.d.ts +0 -106
- package/dist/x402/payment-requirements.d.ts.map +0 -1
- package/dist/x402/payment-requirements.js +0 -138
- package/dist/x402/payment-requirements.js.map +0 -1
- package/dist/x402/relayer.d.ts +0 -41
- package/dist/x402/relayer.d.ts.map +0 -1
- package/dist/x402/relayer.js +0 -64
- package/dist/x402/relayer.js.map +0 -1
- package/dist/x402/resource-index.d.ts +0 -75
- package/dist/x402/resource-index.d.ts.map +0 -1
- package/dist/x402/resource-index.js +0 -126
- package/dist/x402/resource-index.js.map +0 -1
- package/dist/x402/verified-delivery-facilitator.d.ts +0 -285
- package/dist/x402/verified-delivery-facilitator.d.ts.map +0 -1
- package/dist/x402/verified-delivery-facilitator.js +0 -423
- package/dist/x402/verified-delivery-facilitator.js.map +0 -1
- package/src/gateway/x402-mode.ts +0 -350
- package/src/x402/delivery-oracle.ts +0 -285
- package/src/x402/facilitator-server-entry.ts +0 -35
- package/src/x402/facilitator-server.ts +0 -445
- package/src/x402/payment-requirements.ts +0 -231
- package/src/x402/relayer.ts +0 -122
- package/src/x402/resource-index.ts +0 -228
- package/src/x402/verified-delivery-facilitator.ts +0 -744
|
@@ -16,14 +16,28 @@ import {
|
|
|
16
16
|
type CallerAuthDenialReason,
|
|
17
17
|
} from "./caller-auth-gateway.js";
|
|
18
18
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
DEFAULT_MAX_PROBES_PER_MINUTE,
|
|
20
|
+
DEFAULT_ORACLE_SIGNER_CACHE_SECONDS,
|
|
21
|
+
} from "./probe-auth.js";
|
|
22
|
+
import { PROBE_MARKER_HEADER_NAME } from "../capability/probe-capability.js";
|
|
23
|
+
import { networkForCaip2, networkOpenerFactories } from "@absol-labs/sdk";
|
|
24
|
+
import {
|
|
25
|
+
chainIdFromNetwork,
|
|
26
|
+
assertSchemaDeclaration,
|
|
27
|
+
assertValidPrice,
|
|
28
|
+
assertValidTerms,
|
|
29
|
+
assertValidUpfrontTerms,
|
|
30
|
+
isHttpLink,
|
|
31
|
+
type OpenerTerms,
|
|
32
|
+
type X402SchemaDeclaration,
|
|
33
|
+
} from "../x402-opener/terms.js";
|
|
34
|
+
import type {
|
|
35
|
+
GatewayX402OpenerMode,
|
|
36
|
+
X402GatewayResponseReason,
|
|
37
|
+
X402UpstreamResult,
|
|
38
|
+
} from "./x402-opener-mode.js";
|
|
39
|
+
import { loadSignedRateBoundsFile } from "./signed-rate-bounds.js";
|
|
40
|
+
import { defaultNetwork, requireNetworkForChainId } from "../network.js";
|
|
27
41
|
|
|
28
42
|
/**
|
|
29
43
|
* Reference standalone reverse-proxy gateway server (metrik-agent#63).
|
|
@@ -60,11 +74,14 @@ export interface CallerAuthGatewayServerConfig {
|
|
|
60
74
|
/** Extra request-timeout for the upstream fetch, in ms. Default 30000. */
|
|
61
75
|
readonly upstreamTimeoutMs?: number;
|
|
62
76
|
/**
|
|
63
|
-
*
|
|
64
|
-
* x402
|
|
65
|
-
*
|
|
77
|
+
* Optional x402 v2 mode (metrik-agent#116): an uncapabilitied request gets a
|
|
78
|
+
* standard x402 `402` whose `payTo` is the listing's escrow opener, and a
|
|
79
|
+
* `PAYMENT-SIGNATURE` is verified, served, and settled into a
|
|
80
|
+
* `StreamEscrowV2` stream. OFF when unset — the server is then byte for byte
|
|
81
|
+
* the capability-only gateway (`test/gateway-http-baseline.test.ts`).
|
|
82
|
+
* Build it with `buildGatewayX402OpenerMode`.
|
|
66
83
|
*/
|
|
67
|
-
readonly
|
|
84
|
+
readonly x402Opener?: GatewayX402OpenerMode;
|
|
68
85
|
}
|
|
69
86
|
|
|
70
87
|
export interface CallerAuthGatewayServer {
|
|
@@ -86,7 +103,7 @@ export function createCallerAuthGatewayServer(
|
|
|
86
103
|
config.gateway,
|
|
87
104
|
upstreamBase,
|
|
88
105
|
upstreamTimeoutMs,
|
|
89
|
-
config.
|
|
106
|
+
config.x402Opener,
|
|
90
107
|
).catch((error) => {
|
|
91
108
|
logError("unhandled request error", error);
|
|
92
109
|
if (!res.headersSent) {
|
|
@@ -122,12 +139,37 @@ async function handleRequest(
|
|
|
122
139
|
gateway: CallerAuthGateway,
|
|
123
140
|
upstreamBase: URL,
|
|
124
141
|
upstreamTimeoutMs: number,
|
|
125
|
-
|
|
142
|
+
x402Opener: GatewayX402OpenerMode | undefined,
|
|
126
143
|
): Promise<void> {
|
|
127
144
|
const requestUrl = new URL(req.url ?? "/", "http://localhost");
|
|
128
145
|
const method = req.method ?? "GET";
|
|
129
146
|
const headers = normalizeIncomingHeaders(req.headers);
|
|
130
147
|
|
|
148
|
+
if (x402Opener !== undefined) {
|
|
149
|
+
const route = routeX402(headers, gateway);
|
|
150
|
+
if (route === "ambiguous-probe-mode") {
|
|
151
|
+
writeJsonError(res, 400, "ambiguous-probe-mode");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (route === "ambiguous-capability-and-payment") {
|
|
155
|
+
writeJsonError(res, 400, "ambiguous-capability-and-payment");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (route === "x402") {
|
|
159
|
+
await handleX402Request(
|
|
160
|
+
req,
|
|
161
|
+
res,
|
|
162
|
+
x402Opener,
|
|
163
|
+
requestUrl,
|
|
164
|
+
method,
|
|
165
|
+
headers,
|
|
166
|
+
upstreamBase,
|
|
167
|
+
upstreamTimeoutMs,
|
|
168
|
+
);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
131
173
|
const decision = await gateway.authorize({
|
|
132
174
|
headers,
|
|
133
175
|
method,
|
|
@@ -135,14 +177,6 @@ async function handleRequest(
|
|
|
135
177
|
});
|
|
136
178
|
|
|
137
179
|
if (!decision.authorized) {
|
|
138
|
-
// The x402 challenge is a PROTOCOL body, not Metrik's `{authorized,reason}`
|
|
139
|
-
// shape: `x402-fetch` parses `{x402Version, accepts}` and nothing else.
|
|
140
|
-
if (decision.x402PaymentRequired !== undefined) {
|
|
141
|
-
res
|
|
142
|
-
.writeHead(402, { "Content-Type": "application/json" })
|
|
143
|
-
.end(JSON.stringify(decision.x402PaymentRequired));
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
180
|
writeJsonError(
|
|
147
181
|
res,
|
|
148
182
|
decision.httpStatus ?? 403,
|
|
@@ -164,28 +198,23 @@ async function handleRequest(
|
|
|
164
198
|
upstreamHeaders.set(key, value);
|
|
165
199
|
}
|
|
166
200
|
|
|
201
|
+
// The probe marker is gateway-asserted, never client-asserted: strip any
|
|
202
|
+
// inbound copy first, so a paying caller cannot mark its own billable request
|
|
203
|
+
// as verification traffic and slip past the seller's metering.
|
|
204
|
+
upstreamHeaders.delete(PROBE_MARKER_HEADER_NAME);
|
|
205
|
+
if (decision.probe !== undefined) {
|
|
206
|
+
// Advisory marker so the seller's OWN metering can exclude verification
|
|
207
|
+
// traffic from billable usage. The gateway itself charges nothing for it.
|
|
208
|
+
upstreamHeaders.set(PROBE_MARKER_HEADER_NAME, "1");
|
|
209
|
+
}
|
|
210
|
+
|
|
167
211
|
const upstreamUrl = new URL(
|
|
168
212
|
requestUrl.pathname + requestUrl.search,
|
|
169
213
|
upstreamBase,
|
|
170
214
|
);
|
|
171
215
|
|
|
172
|
-
// Bound the upstream by the payment's OWN deadline, not just by the server's
|
|
173
|
-
// timeout. Settlement happens after the upstream returns, so an upstream that
|
|
174
|
-
// outlives the signed authorization would produce its side effects and then
|
|
175
|
-
// be unsettleable — the caller would get the work for free (never the response
|
|
176
|
-
// body, which is only flushed after settlement, but the side effects are
|
|
177
|
-
// real). Aborting early turns that into an ordinary 502.
|
|
178
|
-
const effectiveTimeoutMs = boundedUpstreamTimeoutMs(
|
|
179
|
-
upstreamTimeoutMs,
|
|
180
|
-
decision.x402Payment?.validBefore,
|
|
181
|
-
);
|
|
182
|
-
if (effectiveTimeoutMs <= 0) {
|
|
183
|
-
writeJsonError(res, 402, "x402-authorization-too-short");
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
216
|
const controller = new AbortController();
|
|
188
|
-
const timeout = setTimeout(() => controller.abort(),
|
|
217
|
+
const timeout = setTimeout(() => controller.abort(), upstreamTimeoutMs);
|
|
189
218
|
try {
|
|
190
219
|
const upstreamResponse = await fetch(upstreamUrl, {
|
|
191
220
|
method,
|
|
@@ -197,22 +226,6 @@ async function handleRequest(
|
|
|
197
226
|
const buffer = Buffer.from(await upstreamResponse.arrayBuffer());
|
|
198
227
|
const responseHeaders = headersToObject(upstreamResponse.headers);
|
|
199
228
|
|
|
200
|
-
// Settle AFTER the upstream produced a response and BEFORE that response is
|
|
201
|
-
// flushed. Buyer-favouring on both edges: a call that never produced a
|
|
202
|
-
// response is never charged for, and a payment that cannot be settled
|
|
203
|
-
// returns `402` instead of handing over the goods for free.
|
|
204
|
-
if (decision.x402Payment !== undefined) {
|
|
205
|
-
const settled = await settleX402(decision, x402);
|
|
206
|
-
if (settled === undefined) {
|
|
207
|
-
writeJsonError(res, 402, "x402-settlement-failed");
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
responseHeaders[X402_PAYMENT_RESPONSE_HEADER] =
|
|
211
|
-
encodeSettleResponseHeader(settled);
|
|
212
|
-
responseHeaders["access-control-expose-headers"] =
|
|
213
|
-
X402_PAYMENT_RESPONSE_HEADER;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
229
|
res.writeHead(upstreamResponse.status, responseHeaders);
|
|
217
230
|
res.end(buffer);
|
|
218
231
|
} catch (error) {
|
|
@@ -223,64 +236,127 @@ async function handleRequest(
|
|
|
223
236
|
}
|
|
224
237
|
}
|
|
225
238
|
|
|
239
|
+
/** x402 v2 request header (base64 `PaymentPayload`). */
|
|
240
|
+
const PAYMENT_SIGNATURE_HEADER = "payment-signature";
|
|
241
|
+
/** x402 v1 request header. Never forwarded upstream in x402 mode either. */
|
|
242
|
+
const LEGACY_PAYMENT_HEADER = "x-payment";
|
|
243
|
+
|
|
244
|
+
type X402Route =
|
|
245
|
+
| "x402"
|
|
246
|
+
| "capability-or-probe"
|
|
247
|
+
| "ambiguous-probe-mode"
|
|
248
|
+
| "ambiguous-capability-and-payment";
|
|
249
|
+
|
|
226
250
|
/**
|
|
227
|
-
*
|
|
228
|
-
*
|
|
251
|
+
* Decides which path an x402-mode request takes. A payment is never mixed with
|
|
252
|
+
* a capability or a probe: both-present is a `400`, not a guess.
|
|
229
253
|
*/
|
|
230
|
-
|
|
254
|
+
function routeX402(
|
|
255
|
+
headers: Record<string, string | undefined>,
|
|
256
|
+
gateway: CallerAuthGateway,
|
|
257
|
+
): X402Route {
|
|
258
|
+
const present = (name: string) => {
|
|
259
|
+
const value = headers[name];
|
|
260
|
+
return value !== undefined && value.trim().length > 0;
|
|
261
|
+
};
|
|
262
|
+
const hasPayment = present(PAYMENT_SIGNATURE_HEADER);
|
|
263
|
+
const hasCapability = present(gateway.capabilityHeaderName);
|
|
264
|
+
const hasProbe = present(gateway.probeCapabilityHeaderName);
|
|
265
|
+
if (hasProbe && hasPayment) return "ambiguous-probe-mode";
|
|
266
|
+
if (hasCapability && hasPayment) return "ambiguous-capability-and-payment";
|
|
267
|
+
if (hasPayment) return "x402";
|
|
268
|
+
if (!hasCapability && !hasProbe) return "x402";
|
|
269
|
+
return "capability-or-probe";
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The x402 v2 path. The protocol logic (verify → upstream → settle-before-body)
|
|
274
|
+
* lives with the mode in `./x402-opener-mode.ts`, so this module — and the
|
|
275
|
+
* package's root barrel — never loads `@x402/*` for a gateway that does not
|
|
276
|
+
* enable it. This function only supplies the transport.
|
|
277
|
+
*/
|
|
278
|
+
async function handleX402Request(
|
|
279
|
+
req: IncomingMessage,
|
|
280
|
+
res: ServerResponse,
|
|
281
|
+
mode: GatewayX402OpenerMode,
|
|
282
|
+
requestUrl: URL,
|
|
283
|
+
method: string,
|
|
284
|
+
headers: Record<string, string | undefined>,
|
|
285
|
+
upstreamBase: URL,
|
|
231
286
|
upstreamTimeoutMs: number,
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
287
|
+
): Promise<void> {
|
|
288
|
+
await mode.handle({
|
|
289
|
+
requestUrl,
|
|
290
|
+
method,
|
|
291
|
+
headers,
|
|
292
|
+
callUpstream: () =>
|
|
293
|
+
callUpstream(
|
|
294
|
+
req,
|
|
295
|
+
method,
|
|
296
|
+
headers,
|
|
297
|
+
requestUrl,
|
|
298
|
+
upstreamBase,
|
|
299
|
+
upstreamTimeoutMs,
|
|
300
|
+
),
|
|
301
|
+
writeJson: (status, reason) => writeJsonError(res, status, reason),
|
|
302
|
+
write: (status, responseHeaders, body) => {
|
|
303
|
+
if (res.headersSent) {
|
|
304
|
+
res.end();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
res.writeHead(status, responseHeaders);
|
|
308
|
+
res.end(body);
|
|
309
|
+
},
|
|
310
|
+
logError,
|
|
311
|
+
});
|
|
244
312
|
}
|
|
245
313
|
|
|
246
|
-
/**
|
|
247
|
-
async function
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
314
|
+
/** Buffers one upstream call. Payment headers are never forwarded. */
|
|
315
|
+
async function callUpstream(
|
|
316
|
+
req: IncomingMessage,
|
|
317
|
+
method: string,
|
|
318
|
+
headers: Record<string, string | undefined>,
|
|
319
|
+
requestUrl: URL,
|
|
320
|
+
upstreamBase: URL,
|
|
321
|
+
upstreamTimeoutMs: number,
|
|
322
|
+
): Promise<X402UpstreamResult> {
|
|
323
|
+
let body: Buffer | undefined;
|
|
324
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
325
|
+
body = await readBody(req);
|
|
256
326
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
"x402 settlement",
|
|
262
|
-
new Error(
|
|
263
|
-
"gateway accepted an x402 payment but the server has no x402 facilitator configured",
|
|
264
|
-
),
|
|
265
|
-
);
|
|
266
|
-
return undefined;
|
|
327
|
+
const upstreamHeaders = new Headers();
|
|
328
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
329
|
+
if (value === undefined || HOP_BY_HOP_HEADERS.has(key)) continue;
|
|
330
|
+
upstreamHeaders.set(key, value);
|
|
267
331
|
}
|
|
332
|
+
// A signed payment authorization must never reach the upstream: anyone who
|
|
333
|
+
// holds it can submit it to USDC directly, which strands the payer's funds
|
|
334
|
+
// in the opener. The probe marker is gateway-asserted only.
|
|
335
|
+
upstreamHeaders.delete(PAYMENT_SIGNATURE_HEADER);
|
|
336
|
+
upstreamHeaders.delete(LEGACY_PAYMENT_HEADER);
|
|
337
|
+
upstreamHeaders.delete(PROBE_MARKER_HEADER_NAME);
|
|
338
|
+
|
|
339
|
+
const upstreamUrl = new URL(
|
|
340
|
+
requestUrl.pathname + requestUrl.search,
|
|
341
|
+
upstreamBase,
|
|
342
|
+
);
|
|
343
|
+
const controller = new AbortController();
|
|
344
|
+
const timeout = setTimeout(() => controller.abort(), upstreamTimeoutMs);
|
|
268
345
|
try {
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return undefined;
|
|
346
|
+
const response = await fetch(upstreamUrl, {
|
|
347
|
+
method,
|
|
348
|
+
headers: upstreamHeaders,
|
|
349
|
+
...(body === undefined ? {} : { body }),
|
|
350
|
+
signal: controller.signal,
|
|
351
|
+
});
|
|
352
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
353
|
+
return {
|
|
354
|
+
status: response.status,
|
|
355
|
+
headers: headersToObject(response.headers),
|
|
356
|
+
body: buffer,
|
|
357
|
+
};
|
|
358
|
+
} finally {
|
|
359
|
+
clearTimeout(timeout);
|
|
284
360
|
}
|
|
285
361
|
}
|
|
286
362
|
|
|
@@ -328,8 +404,8 @@ function writeJsonError(
|
|
|
328
404
|
| "unauthorized"
|
|
329
405
|
| "internal-error"
|
|
330
406
|
| "upstream-unavailable"
|
|
331
|
-
| "
|
|
332
|
-
|
|
|
407
|
+
| "ambiguous-capability-and-payment"
|
|
408
|
+
| X402GatewayResponseReason,
|
|
333
409
|
): void {
|
|
334
410
|
if (res.headersSent) {
|
|
335
411
|
res.end();
|
|
@@ -361,22 +437,57 @@ export interface CallerAuthGatewayEnvConfig {
|
|
|
361
437
|
readonly corroboratingRpcUrls: readonly string[];
|
|
362
438
|
/** Freshness bound for on-chain stream state, in seconds. */
|
|
363
439
|
readonly maxStateStalenessSeconds: number;
|
|
364
|
-
/** Set only when
|
|
365
|
-
readonly
|
|
440
|
+
/** Set only when oracle probe verification is enabled. */
|
|
441
|
+
readonly probe?: CallerAuthGatewayProbeEnvConfig;
|
|
442
|
+
/** Set only when the x402 v2 opener mode is enabled. */
|
|
443
|
+
readonly x402Opener?: CallerAuthGatewayX402OpenerEnvConfig;
|
|
444
|
+
/**
|
|
445
|
+
* Path of the operator-signed descriptor(s) whose rate band the capability
|
|
446
|
+
* path enforces (`METRIK_GATEWAY_SIGNED_DESCRIPTOR_FILE`). Loaded and
|
|
447
|
+
* verified at startup by {@link startCallerAuthGatewayServerFromEnv}.
|
|
448
|
+
*/
|
|
449
|
+
readonly signedDescriptorFile?: string;
|
|
366
450
|
}
|
|
367
451
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
readonly pricePerCallBaseUnits: bigint;
|
|
371
|
-
/** A facilitator exposing `/verify` and `/settle` — e.g. Metrik's own. */
|
|
452
|
+
/** Env view of the gateway's x402 v2 opener mode (see {@link parseGatewayX402OpenerEnvConfig}). */
|
|
453
|
+
export interface CallerAuthGatewayX402OpenerEnvConfig {
|
|
372
454
|
readonly facilitatorUrl: string;
|
|
373
|
-
|
|
374
|
-
readonly
|
|
375
|
-
|
|
455
|
+
readonly facilitatorToken: string;
|
|
456
|
+
readonly network: `${string}:${string}`;
|
|
457
|
+
readonly factoryAddress: Address;
|
|
458
|
+
readonly asset: Address;
|
|
459
|
+
readonly assetName: string;
|
|
460
|
+
readonly assetVersion: string;
|
|
461
|
+
readonly terms: OpenerTerms;
|
|
462
|
+
readonly price: bigint;
|
|
463
|
+
/**
|
|
464
|
+
* `METRIK_GATEWAY_X402V2_UPFRONT_AMOUNT`: selects the upfront opener kind
|
|
465
|
+
* (then `factoryAddress` is the upfront factory). Undefined = stream kind.
|
|
466
|
+
*/
|
|
467
|
+
readonly upfrontAmount?: bigint;
|
|
468
|
+
/** The registry's implementation of the upfront factory, when it records one. */
|
|
469
|
+
readonly upfrontImplementation?: Address;
|
|
470
|
+
readonly maxTimeoutSeconds: number;
|
|
471
|
+
/** Per-request facilitator HTTP timeout, ms. */
|
|
472
|
+
readonly facilitatorTimeoutMs: number;
|
|
473
|
+
readonly resourceBaseUrl?: string;
|
|
376
474
|
readonly registryUrl?: string;
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
readonly
|
|
475
|
+
/** Public base URL of the facilitator's refund relay (default: facilitatorUrl). */
|
|
476
|
+
readonly refundBaseUrl?: string;
|
|
477
|
+
readonly docsUrl?: string;
|
|
478
|
+
/** Token decimals for the 402 description (default 6). */
|
|
479
|
+
readonly assetDecimals?: number;
|
|
480
|
+
/** One-sentence service description, prefixed to the fixed disclosure. */
|
|
481
|
+
readonly description?: string;
|
|
482
|
+
/** Optional input/output schema, declared as `extensions.bazaar`. */
|
|
483
|
+
readonly schemaDeclaration?: X402SchemaDeclaration;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export interface CallerAuthGatewayProbeEnvConfig {
|
|
487
|
+
/** Free-probe budget per minute. */
|
|
488
|
+
readonly maxProbesPerMinute: number;
|
|
489
|
+
/** Freshness bound for the cached on-chain oracle signer set, in seconds. */
|
|
490
|
+
readonly signerCacheSeconds: number;
|
|
380
491
|
}
|
|
381
492
|
|
|
382
493
|
/**
|
|
@@ -395,8 +506,14 @@ export interface CallerAuthGatewayX402EnvConfig {
|
|
|
395
506
|
* - `METRIK_GATEWAY_MAX_STATE_STALENESS_SECONDS` (optional) - reject stream
|
|
396
507
|
* state observed more than N seconds behind the wall clock. Default
|
|
397
508
|
* {@link DEFAULT_MAX_STATE_STALENESS_SECONDS}.
|
|
398
|
-
* - `METRIK_GATEWAY_CHAIN_ID` (optional) - default
|
|
509
|
+
* - `METRIK_GATEWAY_CHAIN_ID` (optional) - default the registry's default
|
|
510
|
+
* network. Must be a network the SDK registry knows.
|
|
399
511
|
* - `METRIK_GATEWAY_HEADER_NAME` (optional) - default `x-metrik-capability`.
|
|
512
|
+
* - `METRIK_GATEWAY_SIGNED_DESCRIPTOR_FILE` (optional, recommended) - JSON file
|
|
513
|
+
* holding this service's operator-signed descriptor (or an array, one per
|
|
514
|
+
* served serviceRef). Verified at startup; the capability path then refuses
|
|
515
|
+
* any stream whose on-chain `ratePerSecond` is outside the signed
|
|
516
|
+
* `minRatePerSecond`/`maxRatePerSecond` (`stream-rate-out-of-bounds`).
|
|
400
517
|
* - `PORT` / `METRIK_GATEWAY_PORT` (optional) - default `8787`.
|
|
401
518
|
* - `METRIK_GATEWAY_HOST` (optional) - default `0.0.0.0`.
|
|
402
519
|
*
|
|
@@ -429,11 +546,15 @@ export function parseCallerAuthGatewayEnvConfig(
|
|
|
429
546
|
// eslint-disable-next-line no-new -- validates the URL is well-formed.
|
|
430
547
|
new URL(upstreamUrl);
|
|
431
548
|
|
|
432
|
-
const chainIdRaw =
|
|
549
|
+
const chainIdRaw =
|
|
550
|
+
env.METRIK_GATEWAY_CHAIN_ID ?? String(defaultNetwork().chain.id);
|
|
433
551
|
const chainId = Number(chainIdRaw);
|
|
434
552
|
if (!Number.isInteger(chainId) || chainId <= 0) {
|
|
435
553
|
throw new Error(`invalid METRIK_GATEWAY_CHAIN_ID: ${chainIdRaw}`);
|
|
436
554
|
}
|
|
555
|
+
// The stream reader is built from the registry entry for this chain, so an
|
|
556
|
+
// unknown chain is refused at startup rather than at the first request.
|
|
557
|
+
requireNetworkForChainId(chainId, {}, "METRIK_GATEWAY_CHAIN_ID");
|
|
437
558
|
|
|
438
559
|
const portRaw = env.METRIK_GATEWAY_PORT ?? env.PORT ?? "8787";
|
|
439
560
|
const port = Number(portRaw);
|
|
@@ -481,7 +602,14 @@ export function parseCallerAuthGatewayEnvConfig(
|
|
|
481
602
|
);
|
|
482
603
|
}
|
|
483
604
|
|
|
484
|
-
const
|
|
605
|
+
const signedDescriptorFile =
|
|
606
|
+
env.METRIK_GATEWAY_SIGNED_DESCRIPTOR_FILE?.trim() || undefined;
|
|
607
|
+
|
|
608
|
+
const probe = parseGatewayProbeEnvConfig(env);
|
|
609
|
+
const x402Opener = parseGatewayX402OpenerEnvConfig(env, {
|
|
610
|
+
chainId,
|
|
611
|
+
serviceRef: serviceRef as Hex,
|
|
612
|
+
});
|
|
485
613
|
|
|
486
614
|
return {
|
|
487
615
|
host,
|
|
@@ -497,133 +625,387 @@ export function parseCallerAuthGatewayEnvConfig(
|
|
|
497
625
|
corroboratingRpcUrls,
|
|
498
626
|
maxStateStalenessSeconds,
|
|
499
627
|
...(headerName === undefined ? {} : { headerName }),
|
|
500
|
-
...(
|
|
628
|
+
...(probe === undefined ? {} : { probe }),
|
|
629
|
+
...(x402Opener === undefined ? {} : { x402Opener }),
|
|
630
|
+
...(signedDescriptorFile === undefined ? {} : { signedDescriptorFile }),
|
|
501
631
|
};
|
|
502
632
|
}
|
|
503
633
|
|
|
504
634
|
/**
|
|
505
|
-
* x402 mode is OFF unless `
|
|
506
|
-
* on,
|
|
507
|
-
* `402`
|
|
635
|
+
* The x402 v2 opener mode is OFF unless `METRIK_GATEWAY_X402V2_OPENER` is
|
|
636
|
+
* truthy. When on, an uncapabilitied request is answered with a standard x402
|
|
637
|
+
* v2 `402` whose `payTo` is the opener for this listing's terms, and payments
|
|
638
|
+
* settle into a `StreamEscrowV2` stream through the Metrik opener facilitator.
|
|
639
|
+
* The listing is the gateway's primary `METRIK_GATEWAY_SERVICE_REF`.
|
|
508
640
|
*
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
641
|
+
* These names deliberately differ from the removed v1 mode's variables, so a
|
|
642
|
+
* leftover v1 environment can never switch a payment mode on by accident.
|
|
643
|
+
*
|
|
644
|
+
* - `METRIK_GATEWAY_X402V2_OPENER` — `1`/`true`/`yes`/`on` to enable.
|
|
645
|
+
* - `METRIK_GATEWAY_X402V2_FACILITATOR_URL` (required) — the Metrik opener facilitator.
|
|
646
|
+
* - `METRIK_GATEWAY_X402V2_FACILITATOR_TOKEN` (required) — its `/settle` bearer token.
|
|
647
|
+
* - `METRIK_GATEWAY_X402V2_FACTORY_ADDRESS` — the `X402StreamOpenerFactory`; default the
|
|
648
|
+
* network's registered factory (required where the registry has none).
|
|
649
|
+
* - `METRIK_GATEWAY_X402V2_OPERATOR` (required) — must be the signed listing's operator.
|
|
650
|
+
* - `METRIK_GATEWAY_X402V2_RATE_PER_SECOND` (required) — USDC base units per second.
|
|
651
|
+
* - `METRIK_GATEWAY_X402V2_MAX_DURATION_SECONDS` (required).
|
|
652
|
+
* - `METRIK_GATEWAY_X402V2_PRICE` (required) — base units per paid request (= deposit),
|
|
653
|
+
* `0 < price <= rate * maxDuration`.
|
|
654
|
+
* - `METRIK_GATEWAY_X402V2_UPFRONT_AMOUNT` — base units; when SET the listing
|
|
655
|
+
* quotes the UPFRONT ("minimum charge") opener instead: `amount = price`,
|
|
656
|
+
* `payTo = upfrontFactory.predict(6 terms)`; `upfrontAmount` is paid to the
|
|
657
|
+
* seller at settlement and is NOT refundable, only `price - upfrontAmount`
|
|
658
|
+
* (`<= rate * maxDuration`) is metered and refundable. `= PRICE` is
|
|
659
|
+
* upfront-only (no stream). Validated with the factory's rules at startup.
|
|
660
|
+
* - `METRIK_GATEWAY_X402V2_UPFRONT_FACTORY_ADDRESS` — the
|
|
661
|
+
* `X402UpfrontStreamOpenerFactory`; default the network registry's (only
|
|
662
|
+
* with `_UPFRONT_AMOUNT`). `_FACTORY_ADDRESS` names the STREAM factory and is
|
|
663
|
+
* refused together with `_UPFRONT_AMOUNT`.
|
|
664
|
+
* - `METRIK_GATEWAY_X402V2_NETWORK` — default `eip155:<METRIK_GATEWAY_CHAIN_ID>`.
|
|
665
|
+
* - `METRIK_GATEWAY_X402V2_ASSET` — default the network's registered USDC.
|
|
666
|
+
* - `METRIK_GATEWAY_X402V2_ASSET_NAME` / `_ASSET_VERSION` — EIP-712 domain, default the
|
|
667
|
+
* registered USDC's own EIP-712 name/version.
|
|
668
|
+
* - `METRIK_GATEWAY_X402V2_MAX_TIMEOUT_SECONDS` — authorization validity, default 300.
|
|
669
|
+
* - `METRIK_GATEWAY_X402V2_FACILITATOR_TIMEOUT_SECONDS` — per-request facilitator
|
|
670
|
+
* timeout, default 120; must exceed the facilitator's receipt wait by 30 s.
|
|
671
|
+
* - `METRIK_GATEWAY_X402V2_RESOURCE_BASE_URL` — public origin for the 402 `resource.url`.
|
|
672
|
+
* - `METRIK_GATEWAY_X402V2_REGISTRY_URL` — registry used to prove the listing.
|
|
673
|
+
* - `METRIK_GATEWAY_X402V2_REFUND_BASE_URL` — public base URL of the facilitator's
|
|
674
|
+
* gasless refund relay, advertised in the 402; default the facilitator URL. Its
|
|
675
|
+
* `/supported` must show the opener marker with `refundRelay: true`.
|
|
676
|
+
* - `METRIK_GATEWAY_X402V2_DOCS_URL` — docs link in the 402; default docs/x402.md on GitHub.
|
|
677
|
+
* - `METRIK_GATEWAY_X402V2_ASSET_DECIMALS` — decimals for the 402 description; default 6.
|
|
678
|
+
* - `METRIK_GATEWAY_X402V2_DESCRIPTION` — one sentence on what the service does,
|
|
679
|
+
* prefixed to the fixed disclosure of what the payment buys (at most 300 chars).
|
|
680
|
+
* - `METRIK_GATEWAY_X402V2_SCHEMA` — optional JSON `{info: {input, output}, schema}`
|
|
681
|
+
* declared in the 402 as `extensions.bazaar` (declared only, never registered).
|
|
513
682
|
*/
|
|
514
|
-
export function
|
|
515
|
-
env: NodeJS.ProcessEnv
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
683
|
+
export function parseGatewayX402OpenerEnvConfig(
|
|
684
|
+
env: NodeJS.ProcessEnv,
|
|
685
|
+
context: { readonly chainId: number; readonly serviceRef: Hex },
|
|
686
|
+
): CallerAuthGatewayX402OpenerEnvConfig | undefined {
|
|
687
|
+
const raw = env.METRIK_GATEWAY_X402V2_OPENER?.trim().toLowerCase();
|
|
688
|
+
if (
|
|
689
|
+
raw === undefined ||
|
|
690
|
+
raw.length === 0 ||
|
|
691
|
+
["0", "false", "no", "off"].includes(raw)
|
|
692
|
+
) {
|
|
519
693
|
return undefined;
|
|
520
694
|
}
|
|
521
|
-
|
|
522
|
-
try {
|
|
523
|
-
pricePerCallBaseUnits = BigInt(rawPrice);
|
|
524
|
-
} catch {
|
|
695
|
+
if (!["1", "true", "yes", "on"].includes(raw)) {
|
|
525
696
|
throw new Error(
|
|
526
|
-
`invalid
|
|
697
|
+
`invalid METRIK_GATEWAY_X402V2_OPENER: ${raw} (expected a boolean)`,
|
|
527
698
|
);
|
|
528
699
|
}
|
|
529
|
-
|
|
530
|
-
|
|
700
|
+
const facilitatorUrl = requireEnv(
|
|
701
|
+
env,
|
|
702
|
+
"METRIK_GATEWAY_X402V2_FACILITATOR_URL",
|
|
703
|
+
).trim();
|
|
704
|
+
// eslint-disable-next-line no-new -- validates the URL is well-formed.
|
|
705
|
+
new URL(facilitatorUrl);
|
|
706
|
+
const facilitatorToken = requireEnv(
|
|
707
|
+
env,
|
|
708
|
+
"METRIK_GATEWAY_X402V2_FACILITATOR_TOKEN",
|
|
709
|
+
).trim();
|
|
710
|
+
const network = (env.METRIK_GATEWAY_X402V2_NETWORK?.trim() ||
|
|
711
|
+
`eip155:${String(context.chainId)}`) as `${string}:${string}`;
|
|
712
|
+
if (chainIdFromNetwork(network) !== context.chainId) {
|
|
713
|
+
throw new Error(
|
|
714
|
+
`METRIK_GATEWAY_X402V2_NETWORK ${network} does not match METRIK_GATEWAY_CHAIN_ID ${context.chainId}`,
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
// Defaults come from the registry entry for THIS network only.
|
|
718
|
+
const registered = networkForCaip2(network);
|
|
719
|
+
const factoryRaw = env.METRIK_GATEWAY_X402V2_FACTORY_ADDRESS?.trim();
|
|
720
|
+
const upfrontRaw = env.METRIK_GATEWAY_X402V2_UPFRONT_AMOUNT?.trim();
|
|
721
|
+
const upfrontFactoryRaw =
|
|
722
|
+
env.METRIK_GATEWAY_X402V2_UPFRONT_FACTORY_ADDRESS?.trim();
|
|
723
|
+
const upfrontMode = upfrontRaw !== undefined && upfrontRaw.length > 0;
|
|
724
|
+
if (
|
|
725
|
+
!upfrontMode &&
|
|
726
|
+
upfrontFactoryRaw !== undefined &&
|
|
727
|
+
upfrontFactoryRaw.length > 0
|
|
728
|
+
) {
|
|
729
|
+
throw new Error(
|
|
730
|
+
"METRIK_GATEWAY_X402V2_UPFRONT_FACTORY_ADDRESS needs METRIK_GATEWAY_X402V2_UPFRONT_AMOUNT (it selects the upfront opener)",
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
let factoryAddress: Address;
|
|
734
|
+
let upfrontImplementation: Address | undefined;
|
|
735
|
+
if (upfrontMode) {
|
|
736
|
+
if (factoryRaw !== undefined && factoryRaw.length > 0) {
|
|
737
|
+
throw new Error(
|
|
738
|
+
"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",
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
const registeredUpfront =
|
|
742
|
+
registered === undefined
|
|
743
|
+
? []
|
|
744
|
+
: networkOpenerFactories(registered).filter(
|
|
745
|
+
(entry) => entry.kind === "upfront",
|
|
746
|
+
);
|
|
747
|
+
if (upfrontFactoryRaw === undefined || upfrontFactoryRaw.length === 0) {
|
|
748
|
+
const current = registeredUpfront.find((entry) => !entry.legacy);
|
|
749
|
+
if (current === undefined) {
|
|
750
|
+
throw new Error(
|
|
751
|
+
`METRIK_GATEWAY_X402V2_UPFRONT_FACTORY_ADDRESS is required on ${network}: the network registry has no upfront opener factory for it`,
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
factoryAddress = current.address;
|
|
755
|
+
upfrontImplementation = current.implementation ?? undefined;
|
|
756
|
+
} else if (isAddress(upfrontFactoryRaw)) {
|
|
757
|
+
factoryAddress = upfrontFactoryRaw;
|
|
758
|
+
upfrontImplementation =
|
|
759
|
+
registeredUpfront.find(
|
|
760
|
+
(entry) =>
|
|
761
|
+
entry.address.toLowerCase() === upfrontFactoryRaw.toLowerCase(),
|
|
762
|
+
)?.implementation ?? undefined;
|
|
763
|
+
} else {
|
|
764
|
+
throw new Error(
|
|
765
|
+
"METRIK_GATEWAY_X402V2_UPFRONT_FACTORY_ADDRESS must be an EVM address",
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
} else if (factoryRaw === undefined || factoryRaw.length === 0) {
|
|
769
|
+
if (registered?.openerFactory == null) {
|
|
770
|
+
throw new Error(
|
|
771
|
+
`METRIK_GATEWAY_X402V2_FACTORY_ADDRESS is required on ${network}: the network registry has no opener factory for it`,
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
factoryAddress = registered.openerFactory.address;
|
|
775
|
+
} else if (isAddress(factoryRaw)) {
|
|
776
|
+
factoryAddress = factoryRaw;
|
|
777
|
+
} else {
|
|
778
|
+
throw new Error(
|
|
779
|
+
"METRIK_GATEWAY_X402V2_FACTORY_ADDRESS must be an EVM address",
|
|
780
|
+
);
|
|
531
781
|
}
|
|
532
|
-
const
|
|
533
|
-
|
|
782
|
+
const operator = requireEnv(env, "METRIK_GATEWAY_X402V2_OPERATOR").trim();
|
|
783
|
+
if (!isAddress(operator)) {
|
|
784
|
+
throw new Error("METRIK_GATEWAY_X402V2_OPERATOR must be an EVM address");
|
|
785
|
+
}
|
|
786
|
+
const assetRaw = env.METRIK_GATEWAY_X402V2_ASSET?.trim();
|
|
787
|
+
let asset: Address;
|
|
788
|
+
if (assetRaw === undefined || assetRaw.length === 0) {
|
|
789
|
+
if (registered === undefined) {
|
|
790
|
+
throw new Error(
|
|
791
|
+
`METRIK_GATEWAY_X402V2_ASSET is required on ${network}: the network registry does not know it`,
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
asset = registered.usdc.address;
|
|
795
|
+
} else if (isAddress(assetRaw)) {
|
|
796
|
+
asset = assetRaw;
|
|
797
|
+
} else {
|
|
798
|
+
throw new Error("METRIK_GATEWAY_X402V2_ASSET must be an EVM address");
|
|
799
|
+
}
|
|
800
|
+
const terms: OpenerTerms = {
|
|
801
|
+
operator,
|
|
802
|
+
serviceRef: context.serviceRef,
|
|
803
|
+
ratePerSecond: requireUintEnv(env, "METRIK_GATEWAY_X402V2_RATE_PER_SECOND"),
|
|
804
|
+
maxDuration: requireUintEnv(
|
|
805
|
+
env,
|
|
806
|
+
"METRIK_GATEWAY_X402V2_MAX_DURATION_SECONDS",
|
|
807
|
+
),
|
|
808
|
+
};
|
|
809
|
+
const price = requireUintEnv(env, "METRIK_GATEWAY_X402V2_PRICE");
|
|
810
|
+
let upfrontAmount: bigint | undefined;
|
|
811
|
+
if (upfrontMode) {
|
|
812
|
+
upfrontAmount = requireUintEnv(env, "METRIK_GATEWAY_X402V2_UPFRONT_AMOUNT");
|
|
813
|
+
// Startup validation, fail closed: the upfront factory's own validateTerms
|
|
814
|
+
// rules, 0 < price, upfrontAmount <= price, and the metered part
|
|
815
|
+
// (price - upfrontAmount) <= rate * maxDuration.
|
|
816
|
+
assertValidUpfrontTerms({ ...terms, price, upfrontAmount });
|
|
817
|
+
} else {
|
|
818
|
+
// Startup validation: rate/duration > 0 and 0 < price <= rate * maxDuration.
|
|
819
|
+
assertValidTerms(terms);
|
|
820
|
+
assertValidPrice(price, terms);
|
|
821
|
+
}
|
|
822
|
+
const maxTimeoutSeconds = requirePositiveIntEnv(
|
|
534
823
|
env,
|
|
535
|
-
"
|
|
824
|
+
"METRIK_GATEWAY_X402V2_MAX_TIMEOUT_SECONDS",
|
|
825
|
+
300,
|
|
536
826
|
);
|
|
537
|
-
|
|
827
|
+
const facilitatorTimeoutSeconds = requirePositiveIntEnv(
|
|
828
|
+
env,
|
|
829
|
+
"METRIK_GATEWAY_X402V2_FACILITATOR_TIMEOUT_SECONDS",
|
|
830
|
+
120,
|
|
831
|
+
);
|
|
832
|
+
const resourceBaseUrl = env.METRIK_GATEWAY_X402V2_RESOURCE_BASE_URL?.trim();
|
|
833
|
+
if (resourceBaseUrl !== undefined && resourceBaseUrl.length > 0) {
|
|
834
|
+
// eslint-disable-next-line no-new -- validates the URL is well-formed.
|
|
835
|
+
new URL(resourceBaseUrl);
|
|
836
|
+
}
|
|
837
|
+
const registryUrl = env.METRIK_GATEWAY_X402V2_REGISTRY_URL?.trim();
|
|
838
|
+
if (registryUrl !== undefined && registryUrl.length > 0) {
|
|
538
839
|
// eslint-disable-next-line no-new -- validates the URL is well-formed.
|
|
539
|
-
new URL(
|
|
540
|
-
}
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
840
|
+
new URL(registryUrl);
|
|
841
|
+
}
|
|
842
|
+
const optionalLink = (name: string): string | undefined => {
|
|
843
|
+
const value = env[name]?.trim();
|
|
844
|
+
if (value === undefined || value.length === 0) return undefined;
|
|
845
|
+
if (!isHttpLink(value)) {
|
|
846
|
+
throw new Error(`${name} must be an absolute http(s) URL`);
|
|
847
|
+
}
|
|
848
|
+
return value.replace(/\/+$/, "");
|
|
849
|
+
};
|
|
850
|
+
const refundBaseUrl = optionalLink("METRIK_GATEWAY_X402V2_REFUND_BASE_URL");
|
|
851
|
+
const docsUrl = optionalLink("METRIK_GATEWAY_X402V2_DOCS_URL");
|
|
852
|
+
const decimalsRaw = env.METRIK_GATEWAY_X402V2_ASSET_DECIMALS?.trim();
|
|
853
|
+
let assetDecimals: number | undefined;
|
|
854
|
+
if (decimalsRaw !== undefined && decimalsRaw.length > 0) {
|
|
855
|
+
assetDecimals = Number(decimalsRaw);
|
|
856
|
+
if (
|
|
857
|
+
!/^\d+$/.test(decimalsRaw) ||
|
|
858
|
+
!Number.isInteger(assetDecimals) ||
|
|
859
|
+
assetDecimals > 36
|
|
860
|
+
) {
|
|
548
861
|
throw new Error(
|
|
549
|
-
`invalid
|
|
862
|
+
`invalid METRIK_GATEWAY_X402V2_ASSET_DECIMALS: ${decimalsRaw} (expected an integer 0-36)`,
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
const description = env.METRIK_GATEWAY_X402V2_DESCRIPTION?.trim();
|
|
867
|
+
if (description !== undefined && description.length > 300) {
|
|
868
|
+
throw new Error(
|
|
869
|
+
"METRIK_GATEWAY_X402V2_DESCRIPTION must be at most 300 characters",
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
const schemaRaw = env.METRIK_GATEWAY_X402V2_SCHEMA?.trim();
|
|
873
|
+
let schemaDeclaration: X402SchemaDeclaration | undefined;
|
|
874
|
+
if (schemaRaw !== undefined && schemaRaw.length > 0) {
|
|
875
|
+
try {
|
|
876
|
+
const parsed: unknown = JSON.parse(schemaRaw);
|
|
877
|
+
assertSchemaDeclaration(parsed);
|
|
878
|
+
schemaDeclaration = parsed;
|
|
879
|
+
} catch (error) {
|
|
880
|
+
throw new Error(
|
|
881
|
+
`invalid METRIK_GATEWAY_X402V2_SCHEMA: ${error instanceof Error ? error.message : String(error)}`,
|
|
550
882
|
);
|
|
551
883
|
}
|
|
552
884
|
}
|
|
553
|
-
|
|
554
885
|
return {
|
|
555
|
-
pricePerCallBaseUnits,
|
|
556
886
|
facilitatorUrl,
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
887
|
+
facilitatorToken,
|
|
888
|
+
network,
|
|
889
|
+
factoryAddress,
|
|
890
|
+
asset,
|
|
891
|
+
assetName: requireAssetDomainField(
|
|
892
|
+
env.METRIK_GATEWAY_X402V2_ASSET_NAME,
|
|
893
|
+
registered?.usdc.eip712.name,
|
|
894
|
+
"METRIK_GATEWAY_X402V2_ASSET_NAME",
|
|
895
|
+
network,
|
|
896
|
+
),
|
|
897
|
+
assetVersion: requireAssetDomainField(
|
|
898
|
+
env.METRIK_GATEWAY_X402V2_ASSET_VERSION,
|
|
899
|
+
registered?.usdc.eip712.version,
|
|
900
|
+
"METRIK_GATEWAY_X402V2_ASSET_VERSION",
|
|
901
|
+
network,
|
|
902
|
+
),
|
|
903
|
+
terms,
|
|
904
|
+
price,
|
|
905
|
+
...(upfrontAmount === undefined ? {} : { upfrontAmount }),
|
|
906
|
+
...(upfrontImplementation === undefined ? {} : { upfrontImplementation }),
|
|
907
|
+
maxTimeoutSeconds,
|
|
908
|
+
facilitatorTimeoutMs: facilitatorTimeoutSeconds * 1000,
|
|
909
|
+
...(resourceBaseUrl === undefined || resourceBaseUrl.length === 0
|
|
910
|
+
? {}
|
|
911
|
+
: { resourceBaseUrl }),
|
|
912
|
+
...(registryUrl === undefined || registryUrl.length === 0
|
|
913
|
+
? {}
|
|
914
|
+
: { registryUrl }),
|
|
915
|
+
...(refundBaseUrl === undefined ? {} : { refundBaseUrl }),
|
|
916
|
+
...(docsUrl === undefined ? {} : { docsUrl }),
|
|
917
|
+
...(assetDecimals === undefined ? {} : { assetDecimals }),
|
|
918
|
+
...(description === undefined || description.length === 0
|
|
561
919
|
? {}
|
|
562
|
-
: {
|
|
920
|
+
: { description }),
|
|
921
|
+
...(schemaDeclaration === undefined ? {} : { schemaDeclaration }),
|
|
563
922
|
};
|
|
564
923
|
}
|
|
565
924
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
925
|
+
function requireAssetDomainField(
|
|
926
|
+
raw: string | undefined,
|
|
927
|
+
registered: string | undefined,
|
|
928
|
+
name: string,
|
|
929
|
+
network: string,
|
|
930
|
+
): string {
|
|
931
|
+
const value = raw?.trim();
|
|
932
|
+
if (value !== undefined && value.length > 0) return value;
|
|
933
|
+
if (registered === undefined) {
|
|
934
|
+
throw new Error(
|
|
935
|
+
`${name} is required on ${network}: the network registry does not know the asset's EIP-712 domain`,
|
|
572
936
|
);
|
|
573
|
-
this.name = "UnresolvedListingPayeeError";
|
|
574
937
|
}
|
|
938
|
+
return registered;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function requireUintEnv(env: NodeJS.ProcessEnv, name: string): bigint {
|
|
942
|
+
const raw = requireEnv(env, name).trim();
|
|
943
|
+
if (!/^(0|[1-9]\d*)$/.test(raw)) {
|
|
944
|
+
throw new Error(`invalid ${name}: ${raw} (expected a base-10 integer)`);
|
|
945
|
+
}
|
|
946
|
+
return BigInt(raw);
|
|
575
947
|
}
|
|
576
948
|
|
|
577
949
|
/**
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
950
|
+
* Oracle probe verification is OFF unless `METRIK_GATEWAY_PROBE_CAPABILITY` is
|
|
951
|
+
* truthy. Turning it on lets the neutral verifier exercise this gateway's PAID
|
|
952
|
+
* route with an oracle-signed, single-use, short-lived `ProbeCapability`
|
|
953
|
+
* instead of only its health route.
|
|
954
|
+
*
|
|
955
|
+
* There is NO credential to configure — authority is an EIP-712 signature by an
|
|
956
|
+
* address in the escrow's public `getOracleSigners()` set, read from the same
|
|
957
|
+
* `METRIK_GATEWAY_RPC_URL` the stream check already uses.
|
|
958
|
+
*
|
|
959
|
+
* - `METRIK_GATEWAY_PROBE_CAPABILITY` — `1`/`true`/`yes`/`on` to enable.
|
|
960
|
+
* - `METRIK_GATEWAY_PROBE_MAX_PER_MINUTE` (optional) — free-probe budget.
|
|
961
|
+
* Default {@link DEFAULT_MAX_PROBES_PER_MINUTE}.
|
|
962
|
+
* - `METRIK_GATEWAY_PROBE_SIGNER_CACHE_SECONDS` (optional) — how long the
|
|
963
|
+
* on-chain signer set is cached. Default
|
|
964
|
+
* {@link DEFAULT_ORACLE_SIGNER_CACHE_SECONDS}.
|
|
582
965
|
*/
|
|
583
|
-
export
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
966
|
+
export function parseGatewayProbeEnvConfig(
|
|
967
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
968
|
+
): CallerAuthGatewayProbeEnvConfig | undefined {
|
|
969
|
+
const raw = env.METRIK_GATEWAY_PROBE_CAPABILITY?.trim().toLowerCase();
|
|
970
|
+
if (raw === undefined || raw.length === 0) {
|
|
971
|
+
return undefined;
|
|
972
|
+
}
|
|
973
|
+
if (!["1", "true", "yes", "on"].includes(raw)) {
|
|
974
|
+
if (["0", "false", "no", "off"].includes(raw)) {
|
|
975
|
+
return undefined;
|
|
976
|
+
}
|
|
977
|
+
throw new Error(
|
|
978
|
+
`invalid METRIK_GATEWAY_PROBE_CAPABILITY: ${raw} (expected a boolean)`,
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const maxProbesPerMinute = requirePositiveIntEnv(
|
|
983
|
+
env,
|
|
984
|
+
"METRIK_GATEWAY_PROBE_MAX_PER_MINUTE",
|
|
985
|
+
DEFAULT_MAX_PROBES_PER_MINUTE,
|
|
589
986
|
);
|
|
590
|
-
const
|
|
591
|
-
|
|
987
|
+
const signerCacheSeconds = requirePositiveIntEnv(
|
|
988
|
+
env,
|
|
989
|
+
"METRIK_GATEWAY_PROBE_SIGNER_CACHE_SECONDS",
|
|
990
|
+
DEFAULT_ORACLE_SIGNER_CACHE_SECONDS,
|
|
592
991
|
);
|
|
593
|
-
|
|
594
|
-
throw new UnresolvedListingPayeeError(serviceRef);
|
|
595
|
-
}
|
|
596
|
-
return { serviceRef: match.serviceRef, operator: match.operator };
|
|
992
|
+
return { maxProbesPerMinute, signerCacheSeconds };
|
|
597
993
|
}
|
|
598
994
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
995
|
+
function requirePositiveIntEnv(
|
|
996
|
+
env: NodeJS.ProcessEnv,
|
|
997
|
+
name: string,
|
|
998
|
+
fallback: number,
|
|
999
|
+
): number {
|
|
1000
|
+
const raw = env[name]?.trim();
|
|
1001
|
+
if (raw === undefined || raw.length === 0) {
|
|
1002
|
+
return fallback;
|
|
605
1003
|
}
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
return
|
|
611
|
-
quote: createListingPriceQuoter({
|
|
612
|
-
listing,
|
|
613
|
-
resourceBaseUrl: config.x402.resourceBaseUrl,
|
|
614
|
-
pricePerCallBaseUnits: config.x402.pricePerCallBaseUnits,
|
|
615
|
-
chainId: config.chainId,
|
|
616
|
-
...(config.x402.maxTimeoutSeconds === undefined
|
|
617
|
-
? {}
|
|
618
|
-
: { maxTimeoutSeconds: config.x402.maxTimeoutSeconds }),
|
|
619
|
-
}),
|
|
620
|
-
facilitator: createHttpFacilitatorClient({
|
|
621
|
-
url: config.x402.facilitatorUrl,
|
|
622
|
-
...(config.x402.facilitatorToken === undefined
|
|
623
|
-
? {}
|
|
624
|
-
: { token: config.x402.facilitatorToken }),
|
|
625
|
-
}),
|
|
626
|
-
};
|
|
1004
|
+
const value = Number(raw);
|
|
1005
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
1006
|
+
throw new Error(`invalid ${name}: ${raw} (expected a positive integer)`);
|
|
1007
|
+
}
|
|
1008
|
+
return value;
|
|
627
1009
|
}
|
|
628
1010
|
|
|
629
1011
|
function requireEnv(env: NodeJS.ProcessEnv, name: string): string {
|
|
@@ -639,8 +1021,14 @@ export async function startCallerAuthGatewayServerFromEnv(
|
|
|
639
1021
|
env: NodeJS.ProcessEnv = process.env,
|
|
640
1022
|
): Promise<CallerAuthGatewayServer & { readonly boundPort: number }> {
|
|
641
1023
|
const config = parseCallerAuthGatewayEnvConfig(env);
|
|
642
|
-
|
|
1024
|
+
// Fail closed at startup: a configured descriptor that does not verify is a
|
|
1025
|
+
// refusal to start, never a silently unenforced price band.
|
|
1026
|
+
const signedRateBounds =
|
|
1027
|
+
config.signedDescriptorFile === undefined
|
|
1028
|
+
? undefined
|
|
1029
|
+
: await loadSignedRateBoundsFile(config.signedDescriptorFile);
|
|
643
1030
|
const gateway = new CallerAuthGateway({
|
|
1031
|
+
...(signedRateBounds === undefined ? {} : { signedRateBounds }),
|
|
644
1032
|
escrowAddress: config.escrowAddress,
|
|
645
1033
|
rpcUrl: config.rpcUrl,
|
|
646
1034
|
serviceRef: config.serviceRef,
|
|
@@ -651,8 +1039,32 @@ export async function startCallerAuthGatewayServerFromEnv(
|
|
|
651
1039
|
...(config.headerName === undefined
|
|
652
1040
|
? {}
|
|
653
1041
|
: { headerName: config.headerName }),
|
|
654
|
-
...(
|
|
1042
|
+
...(config.probe === undefined
|
|
1043
|
+
? {}
|
|
1044
|
+
: {
|
|
1045
|
+
probe: {
|
|
1046
|
+
maxProbesPerMinute: config.probe.maxProbesPerMinute,
|
|
1047
|
+
signerCacheSeconds: config.probe.signerCacheSeconds,
|
|
1048
|
+
},
|
|
1049
|
+
}),
|
|
655
1050
|
});
|
|
1051
|
+
if (signedRateBounds === undefined) {
|
|
1052
|
+
console.error(
|
|
1053
|
+
"[metrik-caller-auth-gateway] WARNING: no METRIK_GATEWAY_SIGNED_DESCRIPTOR_FILE — the " +
|
|
1054
|
+
"operator-signed minRatePerSecond/maxRatePerSecond are NOT enforced here. The escrow " +
|
|
1055
|
+
"accepts any non-zero rate, so a buyer who opens a stream directly against it can be " +
|
|
1056
|
+
"served below your signed price (metrik-contracts#41).",
|
|
1057
|
+
);
|
|
1058
|
+
} else {
|
|
1059
|
+
console.error(
|
|
1060
|
+
`[metrik-caller-auth-gateway] enforcing operator-signed rate bounds for ${signedRateBounds
|
|
1061
|
+
.map(
|
|
1062
|
+
(bounds) =>
|
|
1063
|
+
`${bounds.serviceRef} [${bounds.minRatePerSecond ?? "-"}, ${bounds.maxRatePerSecond ?? "-"}]`,
|
|
1064
|
+
)
|
|
1065
|
+
.join(", ")}`,
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
656
1068
|
if (config.corroboratingRpcUrls.length === 0) {
|
|
657
1069
|
console.error(
|
|
658
1070
|
"[metrik-caller-auth-gateway] WARNING: no METRIK_GATEWAY_CORROBORATING_RPC_URLS " +
|
|
@@ -662,18 +1074,103 @@ export async function startCallerAuthGatewayServerFromEnv(
|
|
|
662
1074
|
"one INDEPENDENT provider to close that window.",
|
|
663
1075
|
);
|
|
664
1076
|
}
|
|
665
|
-
if (
|
|
1077
|
+
if (config.probe !== undefined) {
|
|
666
1078
|
console.error(
|
|
667
|
-
"[metrik-caller-auth-gateway]
|
|
668
|
-
|
|
669
|
-
`
|
|
670
|
-
|
|
1079
|
+
"[metrik-caller-auth-gateway] oracle probe verification ENABLED: a request carrying a " +
|
|
1080
|
+
"valid EIP-712 ProbeCapability signed by an address in this escrow's on-chain " +
|
|
1081
|
+
`getOracleSigners() set is proxied FREE, unmetered and unbilled, at most ` +
|
|
1082
|
+
`${config.probe.maxProbesPerMinute} per minute. This is what lets the neutral ` +
|
|
1083
|
+
"verifier check this gateway's PAID route instead of only its health route. " +
|
|
1084
|
+
"Leaving it off is safe — the oracle then reports this listing's paid route as " +
|
|
1085
|
+
"UNVERIFIABLE rather than as passing.",
|
|
671
1086
|
);
|
|
1087
|
+
} else {
|
|
1088
|
+
console.error(
|
|
1089
|
+
"[metrik-caller-auth-gateway] oracle probe verification is OFF " +
|
|
1090
|
+
"(METRIK_GATEWAY_PROBE_CAPABILITY unset). The verifier can only reach this service's " +
|
|
1091
|
+
"health route, so paid-route delivery here is UNVERIFIED.",
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
let x402Opener: GatewayX402OpenerMode | undefined;
|
|
1095
|
+
if (config.x402Opener !== undefined) {
|
|
1096
|
+
// Loaded only when enabled, so a capability-only gateway never imports @x402/*.
|
|
1097
|
+
const { buildGatewayX402OpenerMode } =
|
|
1098
|
+
await import("./x402-opener-mode.js");
|
|
1099
|
+
x402Opener = await buildGatewayX402OpenerMode(
|
|
1100
|
+
{
|
|
1101
|
+
facilitatorUrl: config.x402Opener.facilitatorUrl,
|
|
1102
|
+
facilitatorToken: config.x402Opener.facilitatorToken,
|
|
1103
|
+
network: config.x402Opener.network,
|
|
1104
|
+
factoryAddress: config.x402Opener.factoryAddress,
|
|
1105
|
+
escrowAddress: config.escrowAddress,
|
|
1106
|
+
asset: config.x402Opener.asset,
|
|
1107
|
+
assetName: config.x402Opener.assetName,
|
|
1108
|
+
assetVersion: config.x402Opener.assetVersion,
|
|
1109
|
+
terms: config.x402Opener.terms,
|
|
1110
|
+
price: config.x402Opener.price,
|
|
1111
|
+
...(config.x402Opener.upfrontAmount === undefined
|
|
1112
|
+
? {}
|
|
1113
|
+
: { upfrontAmount: config.x402Opener.upfrontAmount }),
|
|
1114
|
+
...(config.x402Opener.upfrontImplementation === undefined
|
|
1115
|
+
? {}
|
|
1116
|
+
: { upfrontImplementation: config.x402Opener.upfrontImplementation }),
|
|
1117
|
+
maxTimeoutSeconds: config.x402Opener.maxTimeoutSeconds,
|
|
1118
|
+
facilitatorTimeoutMs: config.x402Opener.facilitatorTimeoutMs,
|
|
1119
|
+
...(config.x402Opener.resourceBaseUrl === undefined
|
|
1120
|
+
? {}
|
|
1121
|
+
: { resourceBaseUrl: config.x402Opener.resourceBaseUrl }),
|
|
1122
|
+
...(config.x402Opener.registryUrl === undefined
|
|
1123
|
+
? {}
|
|
1124
|
+
: { registryUrl: config.x402Opener.registryUrl }),
|
|
1125
|
+
...(config.x402Opener.refundBaseUrl === undefined
|
|
1126
|
+
? {}
|
|
1127
|
+
: { refundBaseUrl: config.x402Opener.refundBaseUrl }),
|
|
1128
|
+
...(config.x402Opener.docsUrl === undefined
|
|
1129
|
+
? {}
|
|
1130
|
+
: { docsUrl: config.x402Opener.docsUrl }),
|
|
1131
|
+
...(config.x402Opener.assetDecimals === undefined
|
|
1132
|
+
? {}
|
|
1133
|
+
: { assetDecimals: config.x402Opener.assetDecimals }),
|
|
1134
|
+
...(config.x402Opener.description === undefined
|
|
1135
|
+
? {}
|
|
1136
|
+
: { description: config.x402Opener.description }),
|
|
1137
|
+
...(config.x402Opener.schemaDeclaration === undefined
|
|
1138
|
+
? {}
|
|
1139
|
+
: { schemaDeclaration: config.x402Opener.schemaDeclaration }),
|
|
1140
|
+
},
|
|
1141
|
+
{ rpcUrl: config.rpcUrl },
|
|
1142
|
+
);
|
|
1143
|
+
const upfront = x402Opener.upfrontAmount;
|
|
1144
|
+
console.error(
|
|
1145
|
+
"[metrik-caller-auth-gateway] x402 v2 opener mode ENABLED: a request with neither a " +
|
|
1146
|
+
"capability nor PAYMENT-SIGNATURE gets a 402 quoting payTo " +
|
|
1147
|
+
`${x402Opener.payTo} (the ${x402Opener.openerKind} opener for serviceRef ${config.serviceRef}), price ` +
|
|
1148
|
+
`${config.x402Opener.price} base units` +
|
|
1149
|
+
(upfront === null
|
|
1150
|
+
? " (all of it escrowed and metered)"
|
|
1151
|
+
: upfront === config.x402Opener.price
|
|
1152
|
+
? " (UPFRONT-ONLY: all of it a non-refundable per-call charge; no stream, nothing to refund)"
|
|
1153
|
+
: ` (${upfront} a non-refundable per-call charge, ${config.x402Opener.price - upfront} escrowed, metered and refundable)`) +
|
|
1154
|
+
`. Payments settle via the Metrik opener facilitator at ${config.x402Opener.facilitatorUrl}, and only after the upstream answers < 400. ` +
|
|
1155
|
+
`Refunds (no ETH needed, never automatic): ${x402Opener.refundBaseUrl}/streams/:streamId.`,
|
|
1156
|
+
);
|
|
1157
|
+
const refundHost = new URL(x402Opener.refundBaseUrl).hostname;
|
|
1158
|
+
if (
|
|
1159
|
+
refundHost === "localhost" ||
|
|
1160
|
+
refundHost === "[::1]" ||
|
|
1161
|
+
refundHost.startsWith("127.")
|
|
1162
|
+
) {
|
|
1163
|
+
console.error(
|
|
1164
|
+
"[metrik-caller-auth-gateway] WARNING: the 402 advertises a loopback refund URL " +
|
|
1165
|
+
`(${x402Opener.refundBaseUrl}), which payers cannot reach. Set ` +
|
|
1166
|
+
"METRIK_GATEWAY_X402V2_REFUND_BASE_URL to the facilitator's public origin.",
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
672
1169
|
}
|
|
673
1170
|
const server = createCallerAuthGatewayServer({
|
|
674
1171
|
gateway,
|
|
675
1172
|
upstreamUrl: config.upstreamUrl,
|
|
676
|
-
...(
|
|
1173
|
+
...(x402Opener === undefined ? {} : { x402Opener }),
|
|
677
1174
|
});
|
|
678
1175
|
const { port } = await server.listen(config.port, config.host);
|
|
679
1176
|
return Object.assign(server, { boundPort: port });
|