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
+}