feat: add X402 payment enforcement for HTTP routes

- Updated the HTTP route handler to support X402 payment configuration. - Introduced new X402Config struct to encapsulate payment-related settings. - Enhanced newHTTPRouteHandler to accept tunnel identity and metadata. - Implemented newX402HTTPRouteHandler to manage payment middleware for routes. - Updated tests to accommodate changes in HTTP route handling.

Kim committed May 26, 2026 at 18:39 UTC dbbc5b23204d91002f97f844b70d9b4066da6dc0
10 files changed +433 -17
cmd/portal-tunnel/README.md
+48
@@ -72,6 +72,26 @@ connections. Because the tunnel process parses HTTP in this mode, this is the
72 right mode for HTTP-specific behavior such as path routing, response header
73 policy, redirect rewriting, and cookie path remapping.
74
75 +Use x402 when a routed HTTP endpoint should require payment before the upstream
76 +app receives the request:
77 +
78 +```text
79 +portal expose 3000 --name paid-api \
80 + --description "Paid API" \
81 + --x402-network eip155:8453 \
82 + --x402-price "$0.001" \
83 + --x402-resource /
84 +```
85 +
86 +When `--x402-*` is used with a positional target, the CLI runs routed HTTP mode
87 +internally as `--http-route /=<target>`. `--x402-pay-to` defaults to the tunnel
88 +identity address; set it explicitly when payments should be received by another
89 +wallet. The x402 paywall uses tunnel metadata: `--name` for the app name,
90 +`--description` for the resource description, and `--thumbnail` for the app
91 +logo. `--x402-resource` defaults to the matched HTTP route prefix and controls
92 +the resource path advertised in the x402 payment requirement. x402 is not
93 +available in raw TCP or UDP modes.
94 +
95 Use dedicated raw TCP mode for non-HTTP services that need a public TCP port:
96
97 ```text
@@ -172,6 +192,17 @@ Common flags:
192 --owner Service owner metadata
193 --hide Hide service from relay listing screens
194 --http-route HTTP route mapping in PATH=UPSTREAM form; repeatable
195 +--x402-network x402 payment network, such as eip155:8453
196 +--x402-price x402 route price, such as $0.001
197 +--x402-pay-to x402 recipient address; defaults to the tunnel identity address
198 +--x402-facilitator-url
199 + x402 facilitator URL; defaults to the SDK default
200 +--x402-resource x402 protected resource/root path; defaults to the HTTP route prefix
201 +--x402-mime-type x402 protected resource MIME type
202 +--x402-testnet Render the x402 paywall in testnet mode
203 +--x402-max-timeout x402 max payment timeout seconds advertised to clients
204 +--x402-payment-timeout
205 + x402 middleware verify/settle timeout seconds
206 --tcp Request a dedicated raw TCP port on the relay
207 --udp Enable public UDP relay in addition to the default stream path
208 --udp-addr Local UDP target; defaults to the primary target when --udp is enabled
@@ -275,6 +306,23 @@ http_routes = [
306 { prefix = "/api", upstream = "http://127.0.0.1:3001" },
307 { prefix = "/", upstream = "http://127.0.0.1:5173" },
308 ]
309 +
310 +[[tunnels]]
311 +id = "paid-api"
312 +name = "paid-api"
313 +description = "Paid API"
314 +relays = ["https://portal.example.com"]
315 +discovery = false
316 +
317 +[[tunnels.http_routes]]
318 +prefix = "/"
319 +upstream = "http://127.0.0.1:3000"
320 +
321 +[tunnels.http_routes.x402]
322 +network = "eip155:8453"
323 +price = "$0.001"
324 +pay_to = "identity"
325 +resource = "/"
326 ```
327
328 ## Install Behavior
cmd/portal-tunnel/agent/config.go
+27 -2
@@ -13,6 +13,7 @@ import (
13 "github.com/knadh/koanf/v2"
14
15 "github.com/gosuda/portal-tunnel/v2/cmd/portal-tunnel/agent/service"
16 + "github.com/gosuda/portal-tunnel/v2/types"
17 "github.com/gosuda/portal-tunnel/v2/utils"
18 )
19
@@ -62,8 +63,9 @@ type TunnelConfig struct {
63 }
64
65 type HTTPRouteConfig struct {
65 - Prefix string `koanf:"prefix"`
66 - Upstream string `koanf:"upstream"`
66 + Prefix string `koanf:"prefix"`
67 + Upstream string `koanf:"upstream"`
68 + X402 *types.X402Config `koanf:"x402"`
69 }
70
71 func LoadExistingConfig(path string) (Config, error) {
@@ -177,6 +179,9 @@ func tunnelConfigDocumentMap(cfg TunnelConfig) map[string]any {
179 routeMap := make(map[string]any)
180 addStringDocumentField(routeMap, "prefix", route.Prefix)
181 addStringDocumentField(routeMap, "upstream", route.Upstream)
182 + if route.X402 != nil && !route.X402.Empty() {
183 + routeMap["x402"] = x402ConfigDocumentMap(*route.X402)
184 + }
185 routes = append(routes, routeMap)
186 }
187 out["http_routes"] = routes
@@ -214,6 +219,26 @@ func tunnelConfigDocumentMap(cfg TunnelConfig) map[string]any {
219 return out
220 }
221
222 +func x402ConfigDocumentMap(cfg types.X402Config) map[string]any {
223 + out := make(map[string]any)
224 + addStringDocumentField(out, "network", cfg.Network)
225 + addStringDocumentField(out, "price", cfg.Price)
226 + addStringDocumentField(out, "pay_to", cfg.PayTo)
227 + addStringDocumentField(out, "facilitator_url", cfg.FacilitatorURL)
228 + addStringDocumentField(out, "resource", cfg.Resource)
229 + addStringDocumentField(out, "mime_type", cfg.MimeType)
230 + if cfg.Testnet {
231 + out["testnet"] = cfg.Testnet
232 + }
233 + if cfg.MaxTimeoutSeconds != 0 {
234 + out["max_timeout_seconds"] = cfg.MaxTimeoutSeconds
235 + }
236 + if cfg.PaymentTimeoutSecs != 0 {
237 + out["payment_timeout_seconds"] = cfg.PaymentTimeoutSecs
238 + }
239 + return out
240 +}
241 +
242 func addStringDocumentField(out map[string]any, key, value string) {
243 if strings.TrimSpace(value) != "" {
244 out[key] = value
cmd/portal-tunnel/agent/manager.go
+1
@@ -686,6 +686,7 @@ func (t *managedTunnel) runOnce(ctx context.Context) error {
686 routes = append(routes, sdk.HTTPRoute{
687 Prefix: route.Prefix,
688 Upstream: route.Upstream,
689 + X402: route.X402,
690 })
691 }
692 err = exposure.RunHTTPRoutes(ctx, routes, "")
cmd/portal-tunnel/main.go
+72 -5
@@ -61,6 +61,7 @@ type exposeFlags struct {
61 hide bool
62 targetAddr string
63 httpRoutes []string
64 + x402 exposeX402Flags
65 udp bool
66 udpAddr string
67 tcp bool
@@ -69,6 +70,18 @@ type exposeFlags struct {
70 metricsAddr string
71 }
72
73 +type exposeX402Flags struct {
74 + network string
75 + price string
76 + payTo string
77 + facilitator string
78 + resource string
79 + mimeType string
80 + testnet bool
81 + maxTimeout int
82 + paymentTimeout int
83 +}
84 +
85 func runExposeCommand(args []string) error {
86 installer.StartUpdateCheck(types.ReleaseVersion)
87
@@ -88,6 +101,7 @@ func runExposeCommand(args []string) error {
101 utils.StringFlag(fs, &flags.thumbnail, "thumbnail", "", "Service thumbnail URL metadata")
102 utils.BoolFlag(fs, &flags.hide, "hide", false, "Hide service from relay listing screens")
103 utils.RepeatedStringFlag(fs, &flags.httpRoutes, "http-route", "HTTP route mapping in PATH=UPSTREAM form; repeat to aggregate multiple local HTTP services behind one public URL")
104 + flags.x402.bind(fs)
105 utils.BoolFlagEnv(fs, &flags.udp, "udp", false, "Enable public UDP relay in addition to the default TCP relay", "UDP_ENABLED")
106 utils.StringFlagEnv(fs, &flags.udpAddr, "udp-addr", "", "Local UDP target address for relayed datagrams (host:port or port only); defaults to the target when --udp is enabled", "UDP_ADDR")
107 utils.BoolFlagEnv(fs, &flags.tcp, "tcp", false, "Request a dedicated TCP port on the relay for raw TCP services (no TLS; e.g., Minecraft, game servers)", "TCP_ENABLED")
@@ -108,16 +122,28 @@ func runExposeCommand(args []string) error {
122 printExposeUsage(os.Stderr)
123 return err
124 }
125 + x402Config, err := flags.x402.config()
126 + if err != nil {
127 + printExposeUsage(os.Stderr)
128 + return err
129 + }
130 + httpRouteInputs := append([]string(nil), flags.httpRoutes...)
131 + if x402Config != nil && flags.targetAddr != "" && len(httpRouteInputs) == 0 {
132 + httpRouteInputs = []string{"/=" + flags.targetAddr}
133 + }
134 switch {
112 - case flags.targetAddr == "" && len(flags.httpRoutes) == 0:
135 + case flags.targetAddr == "" && len(httpRouteInputs) == 0:
136 printExposeUsage(os.Stderr)
137 return errors.New("target or at least one --http-route is required")
138 case flags.targetAddr != "" && len(flags.httpRoutes) > 0:
139 printExposeUsage(os.Stderr)
140 return errors.New("target cannot be combined with --http-route")
118 - case len(flags.httpRoutes) > 0 && flags.udp:
141 + case len(httpRouteInputs) > 0 && flags.udp:
142 printExposeUsage(os.Stderr)
143 return errors.New("--udp cannot be combined with --http-route")
144 + case x402Config != nil && flags.tcp:
145 + printExposeUsage(os.Stderr)
146 + return errors.New("--x402 cannot be combined with --tcp")
147 }
148
149 ctx, stop := utils.SignalContext()
@@ -164,9 +190,9 @@ func runExposeCommand(args []string) error {
190 if err != nil {
191 return fmt.Errorf("failed to start relays: %w", err)
192 }
167 - if len(flags.httpRoutes) > 0 {
168 - httpRoutes := make([]sdk.HTTPRoute, 0, len(flags.httpRoutes))
169 - for _, raw := range flags.httpRoutes {
193 + if len(httpRouteInputs) > 0 {
194 + httpRoutes := make([]sdk.HTTPRoute, 0, len(httpRouteInputs))
195 + for _, raw := range httpRouteInputs {
196 prefix, upstream, ok := strings.Cut(raw, "=")
197 if !ok {
198 return fmt.Errorf("--http-route %q: expected PATH=UPSTREAM", raw)
@@ -174,6 +200,7 @@ func runExposeCommand(args []string) error {
200 httpRoutes = append(httpRoutes, sdk.HTTPRoute{
201 Prefix: strings.TrimSpace(prefix),
202 Upstream: strings.TrimSpace(upstream),
203 + X402: x402Config,
204 })
205 }
206
@@ -183,6 +210,46 @@ func runExposeCommand(args []string) error {
210 return sdk.ProxyExposure(ctx, exposure)
211 }
212
213 +func (f *exposeX402Flags) bind(fs *flag.FlagSet) {
214 + utils.StringFlag(fs, &f.network, "x402-network", "", "x402 payment network, such as eip155:8453")
215 + utils.StringFlag(fs, &f.price, "x402-price", "", "x402 route price, such as $0.001")
216 + utils.StringFlag(fs, &f.payTo, "x402-pay-to", "", "x402 recipient address; empty uses the tunnel identity address")
217 + utils.StringFlag(fs, &f.facilitator, "x402-facilitator-url", "", "x402 facilitator URL; empty uses the SDK default")
218 + utils.StringFlag(fs, &f.resource, "x402-resource", "", "x402 protected resource/root path; defaults to the HTTP route prefix")
219 + utils.StringFlag(fs, &f.mimeType, "x402-mime-type", "", "x402 protected resource MIME type")
220 + utils.BoolFlag(fs, &f.testnet, "x402-testnet", false, "Render the x402 paywall in testnet mode")
221 + fs.IntVar(&f.maxTimeout, "x402-max-timeout", 0, "x402 max payment timeout seconds advertised to clients")
222 + fs.IntVar(&f.paymentTimeout, "x402-payment-timeout", 0, "x402 middleware verify/settle timeout seconds")
223 +}
224 +
225 +func (f exposeX402Flags) config() (*types.X402Config, error) {
226 + cfg := &types.X402Config{
227 + Network: f.network,
228 + Price: f.price,
229 + PayTo: f.payTo,
230 + FacilitatorURL: f.facilitator,
231 + Resource: f.resource,
232 + MimeType: f.mimeType,
233 + Testnet: f.testnet,
234 + MaxTimeoutSeconds: f.maxTimeout,
235 + PaymentTimeoutSecs: f.paymentTimeout,
236 + }
237 + if cfg.Empty() {
238 + return nil, nil
239 + }
240 + switch {
241 + case strings.TrimSpace(cfg.Network) == "":
242 + return nil, errors.New("--x402-network is required when x402 is enabled")
243 + case strings.TrimSpace(cfg.Price) == "":
244 + return nil, errors.New("--x402-price is required when x402 is enabled")
245 + case cfg.MaxTimeoutSeconds < 0:
246 + return nil, errors.New("--x402-max-timeout cannot be negative")
247 + case cfg.PaymentTimeoutSecs < 0:
248 + return nil, errors.New("--x402-payment-timeout cannot be negative")
249 + }
250 + return cfg, nil
251 +}
252 +
253 func runUpdateCommand(args []string) error {
254 var version string
255 fs := utils.NewFlagSet("update", printUpdateUsage)
go.mod
+18 -1
@@ -30,6 +30,7 @@ require (
30 github.com/rs/zerolog v1.34.0
31 github.com/spruceid/siwe-go v0.2.1
32 github.com/vultr/govultr/v3 v3.30.0
33 + github.com/x402-foundation/x402/go v0.0.0-20260526081544-8cf020c5335e
34 golang.org/x/crypto v0.50.0
35 golang.org/x/net v0.53.0
36 golang.org/x/oauth2 v0.36.0
@@ -42,7 +43,9 @@ require (
43 require (
44 cloud.google.com/go/auth v0.20.0 // indirect
45 cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
46 + github.com/Microsoft/go-winio v0.6.2 // indirect
47 github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect
48 + github.com/StackExchange/wmi v1.2.1 // indirect
49 github.com/atotto/clipboard v0.1.4 // indirect
50 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect
51 github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
@@ -57,6 +60,7 @@ require (
60 github.com/aws/smithy-go v1.24.2 // indirect
61 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
62 github.com/beorn7/perks v1.0.1 // indirect
63 + github.com/bits-and-blooms/bitset v1.24.4 // indirect
64 github.com/cenkalti/backoff/v5 v5.0.3 // indirect
65 github.com/cespare/xxhash/v2 v2.3.0 // indirect
66 github.com/charmbracelet/colorprofile v0.4.1 // indirect
@@ -66,13 +70,18 @@ require (
70 github.com/clipperhouse/displaywidth v0.9.0 // indirect
71 github.com/clipperhouse/stringish v0.1.1 // indirect
72 github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
73 + github.com/consensys/gnark-crypto v0.18.1 // indirect
74 + github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect
75 github.com/dchest/uniuri v1.2.0 // indirect
76 + github.com/deckarep/golang-set/v2 v2.6.0 // indirect
77 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
78 + github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect
79 github.com/ethereum/go-ethereum v1.17.1 // indirect
80 github.com/felixge/httpsnoop v1.0.4 // indirect
81 github.com/fsnotify/fsnotify v1.9.0 // indirect
82 github.com/go-logr/logr v1.4.3 // indirect
83 github.com/go-logr/stdr v1.2.2 // indirect
84 + github.com/go-ole/go-ole v1.3.0 // indirect
85 github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
86 github.com/google/btree v1.1.2 // indirect
87 github.com/google/go-querystring v1.2.0 // indirect
@@ -80,12 +89,13 @@ require (
89 github.com/google/uuid v1.6.0 // indirect
90 github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
91 github.com/googleapis/gax-go/v2 v2.21.0 // indirect
92 + github.com/gorilla/websocket v1.4.2 // indirect
93 github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
94 github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
95 github.com/holiman/uint256 v1.3.2 // indirect
96 github.com/knadh/koanf/maps v0.1.2 // indirect
97 github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
88 - github.com/mattn/go-colorable v0.1.13 // indirect
98 + github.com/mattn/go-colorable v0.1.14 // indirect
99 github.com/mattn/go-isatty v0.0.21 // indirect
100 github.com/mattn/go-localereader v0.0.1 // indirect
101 github.com/mattn/go-runewidth v0.0.19 // indirect
@@ -100,6 +110,13 @@ require (
110 github.com/prometheus/procfs v0.16.1 // indirect
111 github.com/relvacode/iso8601 v1.1.1-0.20210511065120-b30b151cc433 // indirect
112 github.com/rivo/uniseg v0.4.7 // indirect
113 + github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
114 + github.com/supranational/blst v0.3.16 // indirect
115 + github.com/tklauser/go-sysconf v0.3.12 // indirect
116 + github.com/tklauser/numcpus v0.6.1 // indirect
117 + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
118 + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
119 + github.com/xeipuuv/gojsonschema v1.2.0 // indirect
120 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
121 github.com/ysmood/fetchup v0.2.3 // indirect
122 github.com/ysmood/goob v0.4.0 // indirect
go.sum
+132 -1
@@ -4,8 +4,16 @@ cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIi
4 cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
5 cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
6 cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
7 +github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
8 +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
9 +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
10 +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
11 github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU=
12 github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
13 +github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
14 +github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
15 +github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
16 +github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
17 github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
18 github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
19 github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
@@ -44,6 +52,8 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE
52 github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
53 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
54 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
55 +github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
56 +github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
57 github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
58 github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
59 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@@ -68,25 +78,58 @@ github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfa
78 github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
79 github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
80 github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
81 +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
82 +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8=
83 +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4=
84 +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M=
85 +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
86 +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
87 +github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw=
88 +github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo=
89 +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
90 +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
91 +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
92 +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
93 +github.com/consensys/gnark-crypto v0.18.1 h1:RyLV6UhPRoYYzaFnPQA4qK3DyuDgkTgskDdoGqFt3fI=
94 +github.com/consensys/gnark-crypto v0.18.1/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c=
95 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
96 +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
97 +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
98 +github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg=
99 +github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI=
100 +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
101 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
102 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
103 +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=
104 +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc=
105 github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g=
106 github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY=
107 +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
108 +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
109 github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
110 github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
111 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
112 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
113 +github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A=
114 +github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
115 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
116 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
117 +github.com/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn27fRjSls=
118 +github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw=
119 +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk=
120 +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8=
121 github.com/ethereum/go-ethereum v1.17.1 h1:IjlQDjgxg2uL+GzPRkygGULPMLzcYWncEI7wbaizvho=
122 github.com/ethereum/go-ethereum v1.17.1/go.mod h1:7UWOVHL7K3b8RfVRea022btnzLCaanwHtBuH1jUCH/I=
123 github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
124 github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
125 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
126 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
127 +github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY=
128 +github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg=
129 github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
130 github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
131 +github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
132 +github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
133 github.com/go-acme/lego/v4 v4.34.0 h1:oRsIuPJ4ORX7ufviXvelUpBSez2XxeKGwo5pNG9BVeY=
134 github.com/go-acme/lego/v4 v4.34.0/go.mod h1:gsmdlx/ZS6OUeXbOj0U+VnCLLfEFj4WCYRkcGpZw+pc=
135 github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
@@ -96,13 +139,24 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
139 github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
140 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
141 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
142 +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
143 +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
144 +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
145 github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA=
146 github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg=
147 github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
148 github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
149 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
150 +github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
151 +github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
152 +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
153 +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
154 +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
155 +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
156 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
157 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
158 +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
159 +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
160 github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
161 github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
162 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@@ -110,6 +164,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
164 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
165 github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
166 github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
167 +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
168 +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
169 github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
170 github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
171 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -118,8 +174,16 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA
174 github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
175 github.com/googleapis/gax-go/v2 v2.21.0 h1:h45NjjzEO3faG9Lg/cFrBh2PgegVVgzqKzuZl/wMbiI=
176 github.com/googleapis/gax-go/v2 v2.21.0/go.mod h1:But/NJU6TnZsrLai/xBAQLLz+Hc7fHZJt/hsCz3Fih4=
177 +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
178 +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
179 github.com/gosuda/keyless_tls v0.0.2-0.20260507061030-5128be6b5008 h1:KuP/5VlPJwqZNyAV5U60C/j8Pc5O8ENkWPTgP7mEvj0=
180 github.com/gosuda/keyless_tls v0.0.2-0.20260507061030-5128be6b5008/go.mod h1:BOhUZgiAAQzxKO3QcC4fCXgd/+lqxgIu1OyIYTqtta8=
181 +github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac=
182 +github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc=
183 +github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
184 +github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
185 +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE=
186 +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0=
187 github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
188 github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
189 github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
@@ -130,10 +194,20 @@ github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8
194 github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
195 github.com/hetznercloud/hcloud-go/v2 v2.40.0 h1:fuP7khfiDQAIXdKyQq7f3LnnOjyZg0PXTafXjUKkqIA=
196 github.com/hetznercloud/hcloud-go/v2 v2.40.0/go.mod h1:ANz38eerXjPv00dm9dckKhttOGtYeeGmjjvwL5e6c5E=
197 +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330=
198 +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg=
199 +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao=
200 +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA=
201 github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
202 github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
203 +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
204 +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
205 +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
206 +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
207 github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
208 github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
209 +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
210 +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
211 github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
212 github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
213 github.com/knadh/koanf/parsers/toml/v2 v2.2.0 h1:2nV7tHYJ5OZy2BynQ4mOJ6k5bDqbbCzRERLUKBytz3A=
@@ -148,10 +222,13 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
222 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
223 github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
224 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
225 +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4=
226 +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
227 github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
228 github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
153 -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
229 github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
230 +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
231 +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
232 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
233 github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
234 github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
@@ -162,8 +239,14 @@ github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byF
239 github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
240 github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
241 github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
242 +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
243 +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
244 github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
245 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
246 +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
247 +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
248 +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A=
249 +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
250 github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
251 github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
252 github.com/montanaflynn/stats v0.9.0 h1:tsBJ0RXwph9BmAuFoCmqGv6e8xa0MENQ8m0ptKq29mQ=
@@ -178,7 +261,19 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
261 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
262 github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
263 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
264 +github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
265 +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
266 +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
267 +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
268 +github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0=
269 +github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ=
270 +github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c=
271 +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
272 +github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
273 +github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
274 +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
275 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
276 +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
277 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
278 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
279 github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
@@ -197,17 +292,45 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
292 github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
293 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
294 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
295 +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
296 +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
297 github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
298 github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
299 github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
300 +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
301 +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
302 +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU=
303 +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
304 github.com/spruceid/siwe-go v0.2.1 h1:BroySys6CyUzeyNppTseEOT/w56xTdOfcmECTI7rnuc=
305 github.com/spruceid/siwe-go v0.2.1/go.mod h1:MHpHbptGsM3lHth2L8quhZ9ipiwST8zsJH1CjWpeO1k=
306 +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
307 +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
308 github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
309 github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
310 +github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE=
311 +github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
312 +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
313 +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
314 +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
315 +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
316 +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
317 +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
318 +github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
319 +github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
320 github.com/vultr/govultr/v3 v3.30.0 h1:kTeDJ+5or6g4CQJmD6Kmz4R63B18poNZ8RP87r9LZdg=
321 github.com/vultr/govultr/v3 v3.30.0/go.mod h1:2zyUw9yADQaGwKnwDesmIOlBNLrm7edsCfWHFJpWKf8=
322 +github.com/x402-foundation/x402/go v0.0.0-20260526081544-8cf020c5335e h1:0N+e1CjhzAqm6CKn9zhimQqtpjrTjwbx5+IMvcgyXHM=
323 +github.com/x402-foundation/x402/go v0.0.0-20260526081544-8cf020c5335e/go.mod h1:58Cdk20g83eAI3QvxAiQJze7qWUgkjCj9uZlPb4M4HM=
324 +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
325 +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
326 +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
327 +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
328 +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
329 +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
330 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
331 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
332 +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
333 +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
334 github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
335 github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
336 github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ=
@@ -258,9 +381,13 @@ golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
381 golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
382 golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
383 golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
384 +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
385 golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
386 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
387 +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
388 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
389 +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
390 +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
391 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
392 golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
393 golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
@@ -291,6 +418,10 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
418 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
419 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
420 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
421 +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
422 +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
423 +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
424 +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
425 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
426 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
427 gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI=
sdk/expose.go
+2 -1
@@ -520,7 +520,8 @@ func (e *Exposure) WaitDatagramReady(ctx context.Context) ([]string, error) {
520
521 // RunHTTPRoutes serves path-routed HTTP upstreams through the exposure.
522 func (e *Exposure) RunHTTPRoutes(ctx context.Context, routes []HTTPRoute, localAddr string) error {
523 - handler, err := newHTTPRouteHandler(routes)
523 + cfg := e.Config()
524 + handler, err := newHTTPRouteHandler(routes, cfg.Identity, cfg.Metadata)
525 if err != nil {
526 return err
527 }
sdk/http.go
+103 -4
@@ -15,13 +15,27 @@ import (
15 "strconv"
16 "strings"
17 "sync"
18 + "time"
19
20 "github.com/andybalholm/brotli"
21 "github.com/rs/zerolog/log"
22 + x402 "github.com/x402-foundation/x402/go"
23 + x402http "github.com/x402-foundation/x402/go/http"
24 + x402nethttp "github.com/x402-foundation/x402/go/http/nethttp"
25 + evmserver "github.com/x402-foundation/x402/go/mechanisms/evm/exact/server"
26
27 + "github.com/gosuda/portal-tunnel/v2/portal/identity"
28 + "github.com/gosuda/portal-tunnel/v2/types"
29 "github.com/gosuda/portal-tunnel/v2/utils"
30 )
31
32 +const (
33 + defaultX402Scheme = "exact"
34 + defaultX402PaymentTimeout = 30 * time.Second
35 + defaultX402RouteDescription = "Portal protected route"
36 + defaultX402PayToIdentityValue = "identity"
37 +)
38 +
39 func RunHTTP(ctx context.Context, relayListener net.Listener, handler http.Handler, localAddr string) error {
40 if relayListener == nil && localAddr == "" {
41 return errors.New("relay listener or local address is required")
@@ -131,6 +145,8 @@ type HTTPRoute struct {
145 Prefix string
146 // Upstream is the target HTTP URL, or a loopback host:port shorthand.
147 Upstream string
148 + // X402 enables payment enforcement for this public route.
149 + X402 *types.X402Config
150 }
151
152 type httpRoute struct {
@@ -140,10 +156,10 @@ type httpRoute struct {
156 upstreamPath string
157 upstreamPathSlash string
158 upstreamDomain string
143 - proxy *httputil.ReverseProxy
159 + handler http.Handler
160 }
161
146 -func newHTTPRouteHandler(routeConfigs []HTTPRoute) (http.Handler, error) {
162 +func newHTTPRouteHandler(routeConfigs []HTTPRoute, tunnelIdentity types.Identity, metadata types.LeaseMetadata) (http.Handler, error) {
163 if len(routeConfigs) == 0 {
164 return nil, errors.New("at least one http route is required")
165 }
@@ -159,7 +175,14 @@ func newHTTPRouteHandler(routeConfigs []HTTPRoute) (http.Handler, error) {
175 return nil, fmt.Errorf("duplicate http route prefix %q", route.prefix)
176 }
177 seen[route.prefix] = struct{}{}
162 - route.proxy = route.newReverseProxy()
178 + var handler http.Handler = route.newReverseProxy()
179 + if routeConfig.X402 != nil && !routeConfig.X402.Empty() {
180 + handler, err = newX402HTTPRouteHandler(route, handler, *routeConfig.X402, tunnelIdentity, metadata)
181 + if err != nil {
182 + return nil, err
183 + }
184 + }
185 + route.handler = handler
186 routes = append(routes, route)
187 }
188
@@ -177,7 +200,7 @@ func newHTTPRouteHandler(routeConfigs []HTTPRoute) (http.Handler, error) {
200 }
201 for _, route := range routes {
202 if route.prefix == "/" || p == route.prefix || strings.HasPrefix(p, route.prefixSlash) {
180 - route.proxy.ServeHTTP(w, r)
203 + route.handler.ServeHTTP(w, r)
204 return
205 }
206 }
@@ -398,6 +421,82 @@ func (r *httpRoute) upstreamPathToPublic(raw string) string {
421 return r.prefix + rest
422 }
423
424 +func newX402HTTPRouteHandler(route *httpRoute, next http.Handler, cfg types.X402Config, tunnelIdentity types.Identity, metadata types.LeaseMetadata) (http.Handler, error) {
425 + network := strings.TrimSpace(cfg.Network)
426 + if network == "" {
427 + return nil, fmt.Errorf("http route %q x402 network is required", route.prefix)
428 + }
429 + price := strings.TrimSpace(cfg.Price)
430 + if price == "" {
431 + return nil, fmt.Errorf("http route %q x402 price is required", route.prefix)
432 + }
433 + payTo := strings.TrimSpace(cfg.PayTo)
434 + if payTo == "" || strings.EqualFold(payTo, defaultX402PayToIdentityValue) {
435 + payTo = strings.TrimSpace(tunnelIdentity.Address)
436 + }
437 + if payTo == "" {
438 + return nil, fmt.Errorf("http route %q x402 pay_to is required", route.prefix)
439 + }
440 + payTo, err := identity.NormalizeEVMAddress(payTo)
441 + if err != nil {
442 + return nil, fmt.Errorf("http route %q x402 pay_to: %w", route.prefix, err)
443 + }
444 + if cfg.PaymentTimeoutSecs < 0 {
445 + return nil, errors.New("x402 payment_timeout_seconds cannot be negative")
446 + }
447 + if cfg.MaxTimeoutSeconds < 0 {
448 + return nil, errors.New("x402 max_timeout_seconds cannot be negative")
449 + }
450 +
451 + timeout := defaultX402PaymentTimeout
452 + if cfg.PaymentTimeoutSecs > 0 {
453 + timeout = time.Duration(cfg.PaymentTimeoutSecs) * time.Second
454 + }
455 + resource := strings.TrimSpace(cfg.Resource)
456 + if resource == "" {
457 + resource = route.prefix
458 + }
459 + description := strings.TrimSpace(metadata.Description)
460 + if description == "" {
461 + description = defaultX402RouteDescription
462 + }
463 + middleware := x402nethttp.X402Payment(x402nethttp.Config{
464 + Routes: x402http.RoutesConfig{
465 + "*": x402http.RouteConfig{
466 + Accepts: []x402http.PaymentOption{
467 + {
468 + Scheme: defaultX402Scheme,
469 + PayTo: payTo,
470 + Price: x402.Price(price),
471 + Network: x402.Network(network),
472 + MaxTimeoutSeconds: cfg.MaxTimeoutSeconds,
473 + },
474 + },
475 + Resource: resource,
476 + Description: description,
477 + MimeType: strings.TrimSpace(cfg.MimeType),
478 + },
479 + },
480 + Facilitator: x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{
481 + URL: strings.TrimSpace(cfg.FacilitatorURL),
482 + }),
483 + Schemes: []x402nethttp.SchemeConfig{
484 + {
485 + Network: x402.Network("eip155:*"),
486 + Server: evmserver.NewExactEvmScheme(),
487 + },
488 + },
489 + PaywallConfig: &x402http.PaywallConfig{
490 + AppName: strings.TrimSpace(tunnelIdentity.Name),
491 + AppLogo: strings.TrimSpace(metadata.Thumbnail),
492 + Testnet: cfg.Testnet,
493 + },
494 + SyncFacilitatorOnStart: true,
495 + Timeout: timeout,
496 + })
497 + return middleware(next), nil
498 +}
499 +
500 func serveCompressedHTTP(handler http.Handler, w http.ResponseWriter, r *http.Request) {
501 if handler == nil {
502 http.NotFound(w, r)
sdk/http_test.go
+5 -3
@@ -5,6 +5,8 @@ import (
5 "net/http/httptest"
6 "strings"
7 "testing"
8 +
9 + "github.com/gosuda/portal-tunnel/v2/types"
10 )
11
12 func TestServeCompressedHTTPChoosesAcceptedEncoding(t *testing.T) {
@@ -144,7 +146,7 @@ func TestHTTPRoutesUseLongestPrefix(t *testing.T) {
146 handler, err := newHTTPRouteHandler([]HTTPRoute{
147 {Prefix: "/", Upstream: rootServer.URL},
148 {Prefix: "/api", Upstream: apiServer.URL},
147 - })
149 + }, types.Identity{}, types.LeaseMetadata{})
150 if err != nil {
151 t.Fatalf("newHTTPRouteHandler() error = %v", err)
152 }
@@ -175,7 +177,7 @@ func TestHTTPRoutesRewriteResponseHeaders(t *testing.T) {
177
178 handler, err := newHTTPRouteHandler([]HTTPRoute{
179 {Prefix: "/app", Upstream: upstreamURL + "/base"},
178 - })
180 + }, types.Identity{}, types.LeaseMetadata{})
181 if err != nil {
182 t.Fatalf("newHTTPRouteHandler() error = %v", err)
183 }
@@ -199,7 +201,7 @@ func TestHTTPRoutesRejectDuplicateNormalizedPrefixes(t *testing.T) {
201 _, err := newHTTPRouteHandler([]HTTPRoute{
202 {Prefix: "/api", Upstream: "127.0.0.1:3001"},
203 {Prefix: "/api/", Upstream: "127.0.0.1:3002"},
202 - })
204 + }, types.Identity{}, types.LeaseMetadata{})
205 if err == nil {
206 t.Fatal("newHTTPRouteHandler() error = nil, want duplicate prefix error")
207 }
types/x402.go new
+25
@@ -0,0 +1,25 @@
1 +package types
2 +
3 +type X402Config struct {
4 + Network string `json:"network,omitempty" koanf:"network"`
5 + Price string `json:"price,omitempty" koanf:"price"`
6 + PayTo string `json:"pay_to,omitempty" koanf:"pay_to"`
7 + FacilitatorURL string `json:"facilitator_url,omitempty" koanf:"facilitator_url"`
8 + Resource string `json:"resource,omitempty" koanf:"resource"`
9 + MimeType string `json:"mime_type,omitempty" koanf:"mime_type"`
10 + Testnet bool `json:"testnet,omitempty" koanf:"testnet"`
11 + MaxTimeoutSeconds int `json:"max_timeout_seconds,omitempty" koanf:"max_timeout_seconds"`
12 + PaymentTimeoutSecs int `json:"payment_timeout_seconds,omitempty" koanf:"payment_timeout_seconds"`
13 +}
14 +
15 +func (c X402Config) Empty() bool {
16 + return c.Network == "" &&
17 + c.Price == "" &&
18 + c.PayTo == "" &&
19 + c.FacilitatorURL == "" &&
20 + c.Resource == "" &&
21 + c.MimeType == "" &&
22 + !c.Testnet &&
23 + c.MaxTimeoutSeconds == 0 &&
24 + c.PaymentTimeoutSecs == 0
25 +}