refactor: remove x402 facilitator integration and related configurations
- Deleted x402 facilitator related code from api_server.go, server.go, and server_test.go. - Removed x402 configurations from ServerConfig and associated normalization logic. - Cleaned up related tests that were checking for x402 functionality. - Eliminated x402 related types and paths from types package. - Updated HTTP route handling to remove x402 payment enforcement.
rabbitprincess committed
Jun 3, 2026 at 17:54 UTC
43c4106b86d2d989c458b82cc635c272ded305b7
49 files changed
+261
-2619
.env.example
+2
-6
@@ -53,8 +53,8 @@ NJALLA_TOKEN=
53
# for DNSSEC and ENS TXT automation, even when certificate files are managed manually.
54
ENS_GASLESS_ENABLED=false
55
56
-# Admin/auth configuration. The relay identity wallet is always allowed.
57
-ADMIN_WALLETS=
56
+# Admin/auth configuration. Use a long random value for production relays.
57
+ADMIN_TOKEN=
58
# Enable when the relay is behind nginx/ingress/load balancers and should trust forwarded client IP headers.
59
# Optionally restrict which proxy source ranges may supply those headers; leave empty for default private/loopback proxy ranges.
60
TRUST_PROXY_HEADERS=true
@@ -68,7 +68,3 @@ LANDING_PAGE_ENABLED=false
68
# Used by the portal-api service. Requires the headless-shell sidecar.
69
# Leave empty to keep generated screenshots disabled. See docs/src/routes/deployment/+page.md.
70
# HEADLESS_SHELL_URL=ws://headless-shell:9222
71
-
72
-X402_FACILITATOR_ENABLED=false
73
-X402_NETWORK=eip155:84532
74
-X402_RPC_URL=https://base-sepolia-rpc.publicnode.com
README.md
-8
@@ -22,8 +22,6 @@ Most tunneling services (ngrok, Cloudflare Tunnel) terminate your TLS connection
22
23
- **No Accounts, No API Keys** — Authentication uses SIWE (Sign-In with Ethereum) with a locally generated secp256k1 key pair. No email, no registration, no vendor lock-in.
24
25
-- **x402 Payments** — Turn APIs, content, and local web apps into payable internet endpoints without centralized billing gateway.
26
-
25
## Comparison
26
27
| | Portal | ngrok | Cloudflare Tunnel | frp |
@@ -70,12 +68,6 @@ portal expose --name myapp \
68
--http-route /api=http://127.0.0.1:3001 \
69
--http-route /=http://127.0.0.1:5173
70
73
-# Require x402 payment before a local HTTP upstream receives traffic
74
-portal expose 3000 --name paid-api \
75
- --x402-facilitator-url https://portal.example.com/api/x402 \
76
- --x402-network eip155:8453 \
77
- --x402-price "$0.001"
78
-
71
# Raw TCP port (Minecraft, databases, SSH)
72
portal expose localhost:25565 --name minecraft --tcp
73
cmd/payment-app/handler.go
deleted
-257
@@ -1,257 +0,0 @@
1
-package main
2
-
3
-import (
4
- "embed"
5
- "html/template"
6
- "io/fs"
7
- "net/http"
8
- "strings"
9
-
10
- portalx402 "github.com/gosuda/portal-tunnel/v2/portal/x402"
11
- "github.com/gosuda/portal-tunnel/v2/types"
12
-)
13
-
14
-//go:embed static/index.html static/style.css
15
-var staticFiles embed.FS
16
-
17
-const paidPhotoPath = "/paid/photo"
18
-
19
-var (
20
- indexPage = template.Must(template.ParseFS(staticFiles, "static/index.html"))
21
- photoPage = template.Must(template.New("photo").Parse(`<!DOCTYPE html>
22
-<head>
23
- <meta charset="UTF-8">
24
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
25
- <title>{{.PageTitle}}</title>
26
- <meta name="description" content="{{.PageDescription}}">
27
- <meta property="og:type" content="website">
28
- <meta property="og:title" content="{{.PageTitle}}">
29
- <meta property="og:description" content="{{.PageDescription}}">
30
- <meta property="og:image" content="{{.OGImage}}">
31
- <meta property="og:url" content="{{.URL}}">
32
- <meta name="twitter:card" content="summary_large_image">
33
- <meta name="twitter:title" content="{{.PageTitle}}">
34
- <meta name="twitter:description" content="{{.PageDescription}}">
35
- <meta name="twitter:image" content="{{.OGImage}}">
36
- <style>
37
- * { box-sizing: border-box; }
38
- body {
39
- margin: 0;
40
- min-height: 100vh;
41
- display: flex;
42
- padding: 0;
43
- background: #f7f8fb;
44
- color: #182033;
45
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
46
- }
47
- main {
48
- display: grid;
49
- width: 100%;
50
- min-height: 100vh;
51
- grid-template-rows: minmax(0, 1fr) auto;
52
- overflow: hidden;
53
- background: #ffffff;
54
- }
55
- img {
56
- width: 100%;
57
- height: 100%;
58
- min-height: 0;
59
- display: block;
60
- object-fit: contain;
61
- background: #111827;
62
- }
63
- section {
64
- display: grid;
65
- gap: 10px;
66
- padding: 18px;
67
- border-top: 1px solid #e5eaf0;
68
- }
69
- .eyebrow {
70
- margin: 0;
71
- color: #0e7490;
72
- font-size: 12px;
73
- font-weight: 800;
74
- text-transform: uppercase;
75
- letter-spacing: 0;
76
- }
77
- h1 {
78
- margin: 0;
79
- color: #111827;
80
- font-size: 25px;
81
- line-height: 1.18;
82
- }
83
- p {
84
- margin: 0;
85
- color: #4b5565;
86
- font-size: 15px;
87
- line-height: 1.55;
88
- }
89
- dl {
90
- display: grid;
91
- grid-template-columns: repeat(3, 1fr);
92
- gap: 10px;
93
- margin: 4px 0 0;
94
- }
95
- div { min-width: 0; }
96
- dt {
97
- color: #667085;
98
- font-size: 12px;
99
- font-weight: 700;
100
- }
101
- dd {
102
- margin: 4px 0 0;
103
- overflow-wrap: anywhere;
104
- color: #111827;
105
- font-size: 13px;
106
- font-weight: 750;
107
- }
108
- @media (max-width: 640px) {
109
- section { padding: 14px; }
110
- dl { grid-template-columns: 1fr; }
111
- }
112
- </style>
113
-</head>
114
-<body>
115
- <main>
116
- <img src="{{.PhotoURL}}" alt="Unlocked protected image">
117
- <section>
118
- <p class="eyebrow">Payment complete</p>
119
- <h1>Image unlocked</h1>
120
- <p>The protected image is available after the {{.Price}} x402 settlement.</p>
121
- <dl>
122
- <div>
123
- <dt>Amount</dt>
124
- <dd>{{.Price}}</dd>
125
- </div>
126
- <div>
127
- <dt>Network</dt>
128
- <dd>{{.NetworkName}}</dd>
129
- </div>
130
- <div>
131
- <dt>Recipient</dt>
132
- <dd>{{.RecipientAddress}}</dd>
133
- </div>
134
- </dl>
135
- </section>
136
- </main>
137
-</body>
138
-`))
139
-)
140
-
141
-type paymentHandlerConfig struct {
142
- Identity types.Identity
143
- Metadata types.LeaseMetadata
144
- X402 types.X402Config
145
- PhotoURL string
146
-}
147
-
148
-type paymentPageData struct {
149
- PageTitle string
150
- PageDescription string
151
- URL string
152
- OGImage string
153
- ProtectedPath string
154
- Price string
155
- Network string
156
- NetworkName string
157
- PhotoURL string
158
- RecipientAddress string
159
-}
160
-
161
-func newHandler(cfg paymentHandlerConfig) (http.Handler, error) {
162
- staticFS, _ := fs.Sub(staticFiles, "static")
163
- pageData := newPaymentPageData(cfg)
164
-
165
- mux := http.NewServeMux()
166
- mux.Handle("/static/style.css", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
167
- mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
168
- if r.URL.Path != "/" {
169
- http.NotFound(w, r)
170
- return
171
- }
172
- data := pageData
173
- data.URL = requestURL(r)
174
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
175
- w.Header().Set("Cache-Control", "no-store")
176
- _ = indexPage.Execute(w, data)
177
- })
178
-
179
- photoHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
180
- data := pageData
181
- data.URL = requestURL(r)
182
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
183
- w.Header().Set("Cache-Control", "no-store")
184
- _ = photoPage.Execute(w, data)
185
- })
186
- routeX402 := cfg.X402
187
- routeX402.Price = pageData.Price
188
- if strings.TrimSpace(routeX402.Resource) == "" {
189
- routeX402.Resource = paidPhotoPath
190
- }
191
- if strings.TrimSpace(routeX402.MimeType) == "" {
192
- routeX402.MimeType = "text/html"
193
- }
194
- protectedPhoto, err := portalx402.NewHTTPRouteHandler(portalx402.HTTPRouteHandlerConfig{
195
- Prefix: paidPhotoPath,
196
- Next: photoHandler,
197
- X402: routeX402,
198
- TunnelIdentity: cfg.Identity,
199
- Metadata: cfg.Metadata,
200
- })
201
- if err != nil {
202
- return nil, err
203
- }
204
- mux.Handle(paidPhotoPath, protectedPhoto)
205
- mux.Handle(paidPhotoPath+"/", protectedPhoto)
206
-
207
- return mux, nil
208
-}
209
-
210
-func newPaymentPageData(cfg paymentHandlerConfig) paymentPageData {
211
- network := strings.TrimSpace(cfg.X402.Network)
212
- networkName := portalx402.NetworkDisplayName(network)
213
- if networkName == "" {
214
- networkName = network
215
- }
216
- recipient := strings.TrimSpace(cfg.X402.PayTo)
217
- if recipient == "" || strings.EqualFold(recipient, types.X402PayToIdentity) {
218
- recipient = cfg.Identity.Address
219
- }
220
- price := strings.TrimSpace(cfg.X402.Price)
221
- description := strings.TrimSpace(cfg.Metadata.Description)
222
- if description == "" {
223
- description = "Settle " + price + " with x402 and reveal the protected image."
224
- }
225
- return paymentPageData{
226
- PageTitle: "Portal Native Payment",
227
- PageDescription: description,
228
- OGImage: strings.TrimSpace(cfg.PhotoURL),
229
- ProtectedPath: paidPhotoPath,
230
- Price: price,
231
- Network: network,
232
- NetworkName: networkName,
233
- PhotoURL: strings.TrimSpace(cfg.PhotoURL),
234
- RecipientAddress: recipient,
235
- }
236
-}
237
-
238
-func requestURL(r *http.Request) string {
239
- if r == nil || r.URL == nil {
240
- return ""
241
- }
242
- scheme := "http"
243
- if r.TLS != nil {
244
- scheme = "https"
245
- }
246
- if forwardedProto := strings.TrimSpace(r.Header.Get("X-Forwarded-Proto")); forwardedProto != "" {
247
- scheme = strings.TrimSpace(strings.Split(forwardedProto, ",")[0])
248
- }
249
- host := r.Host
250
- if host == "" {
251
- host = r.Header.Get("Host")
252
- }
253
- if host == "" {
254
- return ""
255
- }
256
- return scheme + "://" + host + r.URL.Path
257
-}
cmd/payment-app/main.go
deleted
-188
@@ -1,188 +0,0 @@
1
-package main
2
-
3
-import (
4
- "context"
5
- "errors"
6
- "flag"
7
- "fmt"
8
- "io"
9
- "os"
10
- "strings"
11
-
12
- "github.com/rs/zerolog"
13
- "github.com/rs/zerolog/log"
14
-
15
- "github.com/gosuda/portal-tunnel/v2/sdk"
16
- "github.com/gosuda/portal-tunnel/v2/types"
17
- "github.com/gosuda/portal-tunnel/v2/utils"
18
-)
19
-
20
-const defaultPhotoURL = "https://image.s-h.day/generated/905a4835ad50.png"
21
-
22
-type paymentConfig struct {
23
- relayURLs string
24
- discovery bool
25
- banMITM bool
26
- identityPath string
27
- identityJSON string
28
- addr string
29
- name string
30
- desc string
31
- tags string
32
- owner string
33
- hide bool
34
- thumbnail string
35
- photoURL string
36
- maxActiveRelays int
37
- x402 types.X402Config
38
-}
39
-
40
-func main() {
41
- log.Logger = log.Output(zerolog.NewConsoleWriter())
42
- if err := run(os.Args[1:]); err != nil {
43
- log.Error().Err(err).Msg("payment app failed")
44
- os.Exit(1)
45
- }
46
-}
47
-
48
-func run(args []string) error {
49
- cfg := paymentConfig{}
50
- fs := utils.NewFlagSet("payment-app", printUsage)
51
-
52
- utils.StringFlagEnv(fs, &cfg.relayURLs, "relays", "https://gosunuts.xyz", "additional relay API URLs (comma-separated; scheme omitted defaults to https; merged with bootstrap relays when discovery is enabled)", "RELAYS")
53
- utils.BoolFlagEnv(fs, &cfg.discovery, "discovery", true, "include bootstrap relays and enable discovery", "DISCOVERY")
54
- utils.BoolFlagEnv(fs, &cfg.banMITM, "ban-mitm", false, "ban relay when the MITM self-probe detects TLS termination", "BAN_MITM")
55
- utils.StringFlagEnv(fs, &cfg.identityPath, "identity-path", "identity.json", "identity json file path", "IDENTITY_PATH")
56
- utils.StringFlagEnv(fs, &cfg.identityJSON, "identity-json", "", "identity json payload; overrides --identity-path contents and is persisted there when both are set", "IDENTITY_JSON")
57
- utils.IntFlagEnv(fs, &cfg.maxActiveRelays, "max-active-relays", 3, nil, "maximum number of auto-selected relays to keep connected; explicit --relays are always included", "MAX_ACTIVE_RELAYS")
58
- utils.StringFlag(fs, &cfg.addr, "addr", "127.0.0.1:8093", "local payment app HTTP listen address (host:port or URL; disable if empty)")
59
- utils.StringFlag(fs, &cfg.name, "name", "payment-app", "public hostname prefix (single DNS label)")
60
- utils.StringFlag(fs, &cfg.desc, "description", "Portal native x402 payment app", "lease description")
61
- utils.StringFlag(fs, &cfg.tags, "tags", "payment,x402,image,photo", "comma-separated lease tags")
62
- utils.StringFlag(fs, &cfg.owner, "owner", "PortalApp Developer", "lease owner")
63
- utils.StringFlag(fs, &cfg.thumbnail, "thumbnail", defaultPhotoURL, "lease thumbnail")
64
- utils.StringFlag(fs, &cfg.photoURL, "photo-url", defaultPhotoURL, "image URL revealed after payment")
65
- utils.BoolFlag(fs, &cfg.hide, "hide", false, "hide this lease from listings")
66
- utils.StringFlag(fs, &cfg.x402.FacilitatorURL, "x402-facilitator-url", "https://gosunuts.xyz/api/x402", "x402 facilitator URL, such as https://relay.example.com:4017/api/x402")
67
- utils.StringFlag(fs, &cfg.x402.Network, "x402-network", "eip155:84532", "x402 payment network, such as eip155:8453")
68
- utils.StringFlag(fs, &cfg.x402.Price, "x402-price", "$0.001", "x402 price for the protected image, such as $0.01")
69
- utils.StringFlag(fs, &cfg.x402.PayTo, "x402-pay-to", "", "x402 recipient address; empty uses the payment app identity address")
70
- fs.IntVar(&cfg.x402.MaxTimeoutSeconds, "x402-max-timeout", 0, "x402 max payment timeout seconds advertised to clients")
71
- fs.IntVar(&cfg.x402.PaymentTimeoutSecs, "x402-payment-timeout", 0, "x402 middleware verify/settle timeout seconds")
72
-
73
- if err := utils.ParseFlagSet(fs, args, printUsage); err != nil {
74
- if errors.Is(err, flag.ErrHelp) {
75
- return nil
76
- }
77
- return err
78
- }
79
- if err := utils.RequireNoArgs(fs.Args(), "payment-app"); err != nil {
80
- printUsage(os.Stderr)
81
- return err
82
- }
83
- normalizedName, err := utils.NormalizeDNSLabel(cfg.name)
84
- if err != nil {
85
- return fmt.Errorf("invalid --name value: %w", err)
86
- }
87
- cfg.name = normalizedName
88
- if err := validatePaymentConfig(cfg); err != nil {
89
- return err
90
- }
91
-
92
- ctx, stop := utils.SignalContext()
93
- defer stop()
94
-
95
- return runPaymentApp(ctx, cfg)
96
-}
97
-
98
-func validatePaymentConfig(cfg paymentConfig) error {
99
- switch {
100
- case strings.TrimSpace(cfg.x402.FacilitatorURL) == "":
101
- return errors.New("--x402-facilitator-url is required")
102
- case strings.TrimSpace(cfg.x402.Network) == "":
103
- return errors.New("--x402-network is required")
104
- case strings.TrimSpace(cfg.x402.Price) == "":
105
- return errors.New("--x402-price is required")
106
- case strings.TrimSpace(cfg.photoURL) == "":
107
- return errors.New("--photo-url is required")
108
- case cfg.x402.MaxTimeoutSeconds < 0:
109
- return errors.New("--x402-max-timeout cannot be negative")
110
- case cfg.x402.PaymentTimeoutSecs < 0:
111
- return errors.New("--x402-payment-timeout cannot be negative")
112
- default:
113
- return nil
114
- }
115
-}
116
-
117
-func runPaymentApp(ctx context.Context, cfg paymentConfig) error {
118
- metadata := types.LeaseMetadata{
119
- Description: cfg.desc,
120
- Tags: utils.SplitCSV(cfg.tags),
121
- Owner: cfg.owner,
122
- Thumbnail: cfg.thumbnail,
123
- Hide: cfg.hide,
124
- }
125
- exposure, err := sdk.Expose(ctx, sdk.ExposeConfig{
126
- RelayURLs: utils.SplitCSV(cfg.relayURLs),
127
- Discovery: cfg.discovery,
128
- Identity: types.Identity{Name: cfg.name},
129
- IdentityPath: cfg.identityPath,
130
- IdentityJSON: cfg.identityJSON,
131
- BanMITM: cfg.banMITM,
132
- MaxActiveRelays: cfg.maxActiveRelays,
133
- Metadata: metadata,
134
- })
135
- if err != nil {
136
- return fmt.Errorf("exposure listen error: %w", err)
137
- }
138
- defer exposure.Close()
139
-
140
- rawAddr := cfg.addr
141
- cfg.addr, err = utils.NormalizeTargetAddr(cfg.addr)
142
- if err != nil {
143
- return fmt.Errorf("invalid --addr value %q: %w", rawAddr, err)
144
- }
145
- if strings.TrimSpace(cfg.x402.PayTo) == "" {
146
- cfg.x402.PayTo = types.X402PayToIdentity
147
- }
148
- if strings.TrimSpace(cfg.x402.MimeType) == "" {
149
- cfg.x402.MimeType = "text/html"
150
- }
151
-
152
- handler, err := newHandler(paymentHandlerConfig{
153
- Identity: exposure.Identity(),
154
- Metadata: metadata,
155
- X402: cfg.x402,
156
- PhotoURL: cfg.photoURL,
157
- })
158
- if err != nil {
159
- return err
160
- }
161
-
162
- err = exposure.RunHTTP(ctx, handler, cfg.addr)
163
- if err != nil {
164
- if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
165
- err = nil
166
- }
167
- return err
168
- }
169
-
170
- if ctx.Err() != nil {
171
- log.Info().Msg("payment app shutting down")
172
- }
173
- log.Info().Msg("payment app shutdown complete")
174
- return nil
175
-}
176
-
177
-func printUsage(w io.Writer) {
178
- utils.WriteCommandUsage(w,
179
- []string{
180
- "payment-app [flags]",
181
- },
182
- []string{
183
- "payment-app",
184
- "payment-app --name paid-photo",
185
- "payment-app --x402-facilitator-url https://relay.example.com:4017/api/x402 --x402-network eip155:8453 --x402-price \"$0.01\"",
186
- },
187
- )
188
-}
cmd/payment-app/static/index.html
deleted
-109
@@ -1,109 +0,0 @@
1
-<!DOCTYPE html>
2
-<html lang="en">
3
-
4
-<head>
5
- <meta charset="UTF-8">
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
7
- <title>{{.PageTitle}}</title>
8
- <meta name="description" content="{{.PageDescription}}">
9
- <meta property="og:type" content="website">
10
- <meta property="og:title" content="{{.PageTitle}}">
11
- <meta property="og:description" content="{{.PageDescription}}">
12
- <meta property="og:image" content="{{.OGImage}}">
13
- <meta property="og:url" content="{{.URL}}">
14
- <meta name="twitter:card" content="summary_large_image">
15
- <meta name="twitter:title" content="{{.PageTitle}}">
16
- <meta name="twitter:description" content="{{.PageDescription}}">
17
- <meta name="twitter:image" content="{{.OGImage}}">
18
- <link rel="stylesheet" href="/static/style.css">
19
-</head>
20
-
21
-<body>
22
- <main class="shell">
23
- <section class="app-surface" aria-label="Payment app">
24
- <div class="summary">
25
- <div class="kicker">
26
- <span>Native x402</span>
27
- <span>{{.NetworkName}}</span>
28
- </div>
29
- <h1>Paid image delivery</h1>
30
- <p class="lede">Settle {{.Price}} and reveal the protected image in the viewer.</p>
31
-
32
- <dl class="facts">
33
- <div>
34
- <dt>Amount</dt>
35
- <dd>{{.Price}}</dd>
36
- </div>
37
- <div>
38
- <dt>Network</dt>
39
- <dd>{{.Network}}</dd>
40
- </div>
41
- <div>
42
- <dt>Recipient</dt>
43
- <dd>{{.RecipientAddress}}</dd>
44
- </div>
45
- </dl>
46
-
47
- <div class="actions">
48
- <button id="unlockButton" class="primary-button" type="button" data-protected-path="{{.ProtectedPath}}">
49
- Open checkout
50
- </button>
51
- <span id="status" class="status">Ready</span>
52
- </div>
53
- </div>
54
-
55
- <div id="viewer" class="preview" aria-label="Protected image viewer">
56
- <div class="preview-topbar">
57
- <div class="window-controls" aria-hidden="true">
58
- <span></span>
59
- <span></span>
60
- <span></span>
61
- </div>
62
- <span id="viewerTitle" class="viewer-title">Protected image</span>
63
- <button id="resetViewer" class="viewer-reset" type="button" hidden>Reset</button>
64
- </div>
65
- <div id="lockedPreview" class="preview-body">
66
- <div class="lock-mark">402</div>
67
- <div>
68
- <p class="preview-label">Protected image</p>
69
- <p class="preview-title">Checkout opens here</p>
70
- </div>
71
- </div>
72
- <iframe id="paymentFrame" class="payment-frame" title="x402 payment checkout" allow="clipboard-read; clipboard-write; payment; publickey-credentials-get" hidden></iframe>
73
- </div>
74
- </section>
75
- </main>
76
-
77
- <script>
78
- const unlockButton = document.getElementById('unlockButton');
79
- const viewer = document.getElementById('viewer');
80
- const lockedPreview = document.getElementById('lockedPreview');
81
- const paymentFrame = document.getElementById('paymentFrame');
82
- const resetViewer = document.getElementById('resetViewer');
83
- const viewerTitle = document.getElementById('viewerTitle');
84
- const statusEl = document.getElementById('status');
85
-
86
- unlockButton.addEventListener('click', () => {
87
- viewer.classList.add('is-active');
88
- lockedPreview.hidden = true;
89
- paymentFrame.hidden = false;
90
- resetViewer.hidden = false;
91
- viewerTitle.textContent = 'x402 checkout';
92
- statusEl.textContent = 'Checkout open';
93
- if (!paymentFrame.src) {
94
- paymentFrame.src = unlockButton.dataset.protectedPath;
95
- }
96
- });
97
-
98
- resetViewer.addEventListener('click', () => {
99
- viewer.classList.remove('is-active');
100
- lockedPreview.hidden = false;
101
- paymentFrame.hidden = true;
102
- resetViewer.hidden = true;
103
- viewerTitle.textContent = 'Protected image';
104
- statusEl.textContent = paymentFrame.src ? 'Checkout paused' : 'Ready';
105
- });
106
- </script>
107
-</body>
108
-
109
-</html>
cmd/payment-app/static/style.css
deleted
-351
@@ -1,351 +0,0 @@
1
-* {
2
- box-sizing: border-box;
3
-}
4
-
5
-body {
6
- margin: 0;
7
- min-height: 100vh;
8
- background: #f4f7f8;
9
- color: #182033;
10
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
11
-}
12
-
13
-button {
14
- font: inherit;
15
-}
16
-
17
-.shell {
18
- width: min(1120px, calc(100% - 32px));
19
- margin: 0 auto;
20
- padding: 42px 0;
21
-}
22
-
23
-.app-surface {
24
- display: grid;
25
- grid-template-columns: minmax(0, 0.88fr) minmax(360px, 1.12fr);
26
- gap: 24px;
27
- align-items: stretch;
28
-}
29
-
30
-.summary,
31
-.preview {
32
- border: 1px solid #d8e0e7;
33
- border-radius: 8px;
34
- background: #ffffff;
35
- box-shadow: 0 18px 38px rgba(20, 32, 46, 0.10);
36
-}
37
-
38
-.summary {
39
- display: flex;
40
- min-height: 500px;
41
- flex-direction: column;
42
- justify-content: space-between;
43
- padding: 28px;
44
-}
45
-
46
-.kicker {
47
- display: flex;
48
- flex-wrap: wrap;
49
- gap: 8px;
50
- margin-bottom: 24px;
51
-}
52
-
53
-.kicker span {
54
- border: 1px solid #c7d2dc;
55
- border-radius: 999px;
56
- padding: 5px 9px;
57
- color: #334155;
58
- font-size: 12px;
59
- font-weight: 750;
60
-}
61
-
62
-h1,
63
-h2,
64
-p,
65
-dl {
66
- margin: 0;
67
-}
68
-
69
-h1 {
70
- max-width: 10ch;
71
- color: #121826;
72
- font-size: 48px;
73
- line-height: 1.02;
74
- font-weight: 800;
75
-}
76
-
77
-.lede {
78
- max-width: 34ch;
79
- margin-top: 16px;
80
- color: #536171;
81
- font-size: 16px;
82
- line-height: 1.55;
83
-}
84
-
85
-.facts {
86
- display: grid;
87
- gap: 12px;
88
- margin-top: 28px;
89
-}
90
-
91
-.facts div {
92
- min-width: 0;
93
- border-top: 1px solid #e5ebf0;
94
- padding-top: 12px;
95
-}
96
-
97
-dt {
98
- color: #687586;
99
- font-size: 12px;
100
- font-weight: 750;
101
-}
102
-
103
-dd {
104
- margin: 4px 0 0;
105
- overflow-wrap: anywhere;
106
- color: #111827;
107
- font-size: 14px;
108
- font-weight: 760;
109
-}
110
-
111
-.actions {
112
- display: flex;
113
- flex-wrap: wrap;
114
- gap: 12px;
115
- align-items: center;
116
- margin-top: 30px;
117
-}
118
-
119
-.primary-button {
120
- min-height: 42px;
121
- border-radius: 6px;
122
- border: 1px solid transparent;
123
- padding: 0 16px;
124
- cursor: pointer;
125
- font-size: 14px;
126
- font-weight: 780;
127
- transition: background 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease;
128
-}
129
-
130
-.primary-button {
131
- background: #0e7490;
132
- color: #ffffff;
133
- box-shadow: 0 10px 22px rgba(14, 116, 144, 0.24);
134
-}
135
-
136
-.primary-button:hover {
137
- background: #155e75;
138
- transform: translateY(-1px);
139
-}
140
-
141
-.status {
142
- color: #667085;
143
- font-size: 13px;
144
- font-weight: 700;
145
-}
146
-
147
-.preview {
148
- position: relative;
149
- min-height: 500px;
150
- overflow: hidden;
151
- background:
152
- linear-gradient(140deg, rgba(14, 116, 144, 0.14), transparent 34%),
153
- linear-gradient(42deg, rgba(180, 83, 9, 0.17), transparent 42%),
154
- #edf2f6;
155
-}
156
-
157
-.preview.is-active {
158
- height: min(680px, calc(100vh - 84px));
159
- min-height: 560px;
160
- background: #ffffff;
161
-}
162
-
163
-.preview-topbar {
164
- display: flex;
165
- justify-content: space-between;
166
- gap: 12px;
167
- align-items: center;
168
- height: 42px;
169
- padding: 0 16px;
170
- border-bottom: 1px solid rgba(24, 32, 51, 0.10);
171
- background: rgba(255, 255, 255, 0.72);
172
-}
173
-
174
-.window-controls {
175
- display: flex;
176
- gap: 7px;
177
- align-items: center;
178
- min-width: 44px;
179
-}
180
-
181
-.window-controls span {
182
- width: 9px;
183
- height: 9px;
184
- border-radius: 999px;
185
- background: #94a3b8;
186
-}
187
-
188
-.window-controls span:nth-child(1) {
189
- background: #b45309;
190
-}
191
-
192
-.window-controls span:nth-child(2) {
193
- background: #0e7490;
194
-}
195
-
196
-.window-controls span:nth-child(3) {
197
- background: #475569;
198
-}
199
-
200
-.viewer-title {
201
- min-width: 0;
202
- overflow: hidden;
203
- color: #334155;
204
- font-size: 12px;
205
- font-weight: 800;
206
- text-overflow: ellipsis;
207
- white-space: nowrap;
208
-}
209
-
210
-.viewer-reset {
211
- min-width: 44px;
212
- border: 0;
213
- background: transparent;
214
- color: #0e7490;
215
- cursor: pointer;
216
- font-size: 12px;
217
- font-weight: 800;
218
-}
219
-
220
-.viewer-reset[hidden] {
221
- display: block;
222
- visibility: hidden;
223
-}
224
-
225
-.preview-body {
226
- position: absolute;
227
- inset: 42px 0 0;
228
- display: grid;
229
- place-items: center;
230
- gap: 18px;
231
- align-content: center;
232
- padding: 28px;
233
- text-align: center;
234
-}
235
-
236
-.preview-body::before {
237
- position: absolute;
238
- inset: 36px;
239
- content: "";
240
- border: 1px dashed rgba(71, 85, 105, 0.28);
241
- border-radius: 8px;
242
- background:
243
- repeating-linear-gradient(135deg, rgba(255, 255, 255, 0.34) 0 10px, transparent 10px 20px),
244
- rgba(255, 255, 255, 0.32);
245
-}
246
-
247
-.lock-mark {
248
- position: relative;
249
- z-index: 1;
250
- display: grid;
251
- width: 92px;
252
- height: 92px;
253
- place-items: center;
254
- border-radius: 8px;
255
- background: #121826;
256
- color: #f8fafc;
257
- font-size: 30px;
258
- font-weight: 850;
259
-}
260
-
261
-.preview-body > div:last-child {
262
- position: relative;
263
- z-index: 1;
264
-}
265
-
266
-.preview-label {
267
- color: #0e7490;
268
- font-size: 13px;
269
- font-weight: 800;
270
- text-transform: uppercase;
271
-}
272
-
273
-.preview-title {
274
- margin-top: 8px;
275
- color: #182033;
276
- font-size: 22px;
277
- font-weight: 800;
278
-}
279
-
280
-.preview-body[hidden],
281
-.payment-frame[hidden] {
282
- display: none;
283
-}
284
-
285
-.payment-frame {
286
- position: absolute;
287
- inset: 42px 0 0;
288
- display: block;
289
- width: 100%;
290
- height: calc(100% - 42px);
291
- border: 0;
292
- background: #f8fafc;
293
-}
294
-
295
-@media (max-width: 860px) {
296
- .shell {
297
- width: min(100% - 20px, 640px);
298
- padding: 18px 0;
299
- }
300
-
301
- .app-surface {
302
- grid-template-columns: 1fr;
303
- }
304
-
305
- .summary,
306
- .preview {
307
- min-height: auto;
308
- }
309
-
310
- h1 {
311
- max-width: none;
312
- font-size: 36px;
313
- }
314
-
315
- .preview {
316
- height: 360px;
317
- }
318
-
319
- .preview.is-active {
320
- height: min(660px, calc(100vh - 36px));
321
- min-height: 560px;
322
- }
323
-}
324
-
325
-@media (max-width: 520px) {
326
- .summary {
327
- padding: 20px;
328
- }
329
-
330
- .actions {
331
- flex-direction: column;
332
- align-items: stretch;
333
- }
334
-
335
- .primary-button {
336
- width: 100%;
337
- }
338
-
339
- .preview {
340
- height: 310px;
341
- }
342
-
343
- .preview.is-active {
344
- height: min(620px, calc(100vh - 28px));
345
- min-height: 520px;
346
- }
347
-
348
- .preview-body::before {
349
- inset: 18px;
350
- }
351
-}
cmd/portal-tunnel/README.md
+1
-116
@@ -72,92 +72,6 @@ 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-facilitator-url https://portal.example.com:4017/api/x402 \
82
- --x402-network eip155:8453 \
83
- --x402-price "$0.001" \
84
- --x402-resource /
85
-```
86
-
87
-When `--x402-*` is used with a positional target, the CLI runs routed HTTP mode
88
-internally as `--http-route /=<target>`. `--x402-pay-to` defaults to the tunnel
89
-identity address; set it explicitly when payments should be received by another
90
-wallet. The x402 paywall uses tunnel metadata: `--name` for the app name,
91
-`--description` for the resource description, and `--thumbnail` for the app
92
-logo. Empty `--x402-resource` uses the requested URL in the x402 payment
93
-requirement. Set it only when a stable resource URL should be advertised. The tunnel does
94
-not infer a relay facilitator URL; set `--x402-facilitator-url` explicitly, or
95
-use relay/frontend tooling that writes the desired facilitator URL into the
96
-tunnel config. x402 is not available in raw TCP or UDP modes.
97
-
98
-For route-specific static prices, use agent config and attach `x402` to the
99
-routes that should be paid:
100
-
101
-```toml
102
-[[tunnels]]
103
-id = "paid-site"
104
-name = "paid-site"
105
-relays = ["https://portal.example.com"]
106
-discovery = false
107
-
108
-[[tunnels.http_routes]]
109
-prefix = "/"
110
-upstream = "http://127.0.0.1:5173"
111
-
112
-[[tunnels.http_routes]]
113
-prefix = "/api/report"
114
-upstream = "http://127.0.0.1:3001"
115
-
116
-[tunnels.http_routes.x402]
117
-network = "eip155:8453"
118
-price = "$0.010"
119
-pay_to = "identity"
120
-facilitator_url = "https://portal.example.com:4017/api/x402"
121
-resource = "/api/report"
122
-mime_type = "application/json"
123
-
124
-[[tunnels.http_routes]]
125
-prefix = "/api/dataset"
126
-upstream = "http://127.0.0.1:3001"
127
-
128
-[tunnels.http_routes.x402]
129
-network = "eip155:8453"
130
-price = "$0.050"
131
-pay_to = "identity"
132
-facilitator_url = "https://portal.example.com:4017/api/x402"
133
-resource = "/api/dataset"
134
-mime_type = "application/json"
135
-```
136
-
137
-For product/catalog pricing that depends on each request, put x402 in the Go
138
-app itself and wrap the protected handler with `portal/x402`:
139
-
140
-```go
141
-protected, err := portalx402.NewHTTPRouteHandler(portalx402.HTTPRouteHandlerConfig{
142
- Prefix: "/api/premium",
143
- Next: premiumHandler,
144
- X402: x402Config,
145
- TunnelIdentity: appIdentity,
146
- Metadata: metadata,
147
- PriceResolver: func(ctx context.Context, req portalx402.HTTPRequestContext) (string, error) {
148
- return catalog.PriceForPath(req.Path)
149
- },
150
-})
151
-```
152
-
153
-`cmd/payment-app` includes this native x402 pattern. Run it with:
154
-
155
-```text
156
-payment-app --x402-facilitator-url https://portal.example.com:4017/api/x402 \
157
- --x402-network eip155:8453 \
158
- --x402-price "$0.01"
159
-```
160
-
75
Use dedicated raw TCP mode for non-HTTP services that need a public TCP port:
76
77
```text
@@ -258,16 +172,6 @@ Common flags:
172
--owner Service owner metadata
173
--hide Hide service from relay listing screens
174
--http-route HTTP route mapping in PATH=UPSTREAM form; repeatable
261
---x402-network x402 payment network, such as eip155:8453
262
---x402-price x402 route price, such as $0.001
263
---x402-pay-to x402 recipient address; defaults to the tunnel identity address
264
---x402-facilitator-url
265
- x402 facilitator URL
266
---x402-resource x402 protected resource URL; empty uses the requested URL
267
---x402-mime-type x402 protected resource MIME type
268
---x402-max-timeout x402 max payment timeout seconds advertised to clients
269
---x402-payment-timeout
270
- x402 middleware verify/settle timeout seconds
175
--tcp Request a dedicated raw TCP port on the relay
176
--udp Enable public UDP relay in addition to the default stream path
177
--udp-addr Local UDP target; defaults to the primary target when --udp is enabled
@@ -326,8 +230,7 @@ Useful commands:
230
- `portal agent run --config config.toml --foreground` runs the agent in the
231
current terminal and opens the dashboard when the terminal is interactive.
232
- `portal agent dashboard` attaches to a running agent and opens the local TUI
329
- for tunnels, relays, multi-hop routes, editable tunnel settings, and x402
330
- facilitator URLs.
233
+ for tunnels, relays, multi-hop routes, and editable tunnel settings.
234
- `portal agent stop` asks the local agent to shut down, then disables or stops
235
the OS service so intentional shutdown is not immediately restarted.
236
- `portal agent restart` stops the running agent if present, installs or updates
@@ -372,24 +275,6 @@ http_routes = [
275
{ prefix = "/api", upstream = "http://127.0.0.1:3001" },
276
{ prefix = "/", upstream = "http://127.0.0.1:5173" },
277
]
375
-
376
-[[tunnels]]
377
-id = "paid-api"
378
-name = "paid-api"
379
-description = "Paid API"
380
-relays = ["https://portal.example.com"]
381
-discovery = false
382
-
383
-[[tunnels.http_routes]]
384
-prefix = "/"
385
-upstream = "http://127.0.0.1:3000"
386
-
387
-[tunnels.http_routes.x402]
388
-network = "eip155:8453"
389
-price = "$0.001"
390
-pay_to = "identity"
391
-facilitator_url = "https://portal.example.com:4017/api/x402"
392
-resource = "/"
278
```
279
280
## Install Behavior
cmd/portal-tunnel/agent/config.go
+2
-24
@@ -13,7 +13,6 @@ 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"
16
"github.com/gosuda/portal-tunnel/v2/utils"
17
)
18
@@ -63,9 +62,8 @@ type TunnelConfig struct {
62
}
63
64
type HTTPRouteConfig struct {
66
- Prefix string `koanf:"prefix"`
67
- Upstream string `koanf:"upstream"`
68
- X402 *types.X402Config `koanf:"x402"`
65
+ Prefix string `koanf:"prefix"`
66
+ Upstream string `koanf:"upstream"`
67
}
68
69
func LoadExistingConfig(path string) (Config, error) {
@@ -179,9 +177,6 @@ func tunnelConfigDocumentMap(cfg TunnelConfig) map[string]any {
177
routeMap := make(map[string]any)
178
addStringDocumentField(routeMap, "prefix", route.Prefix)
179
addStringDocumentField(routeMap, "upstream", route.Upstream)
182
- if route.X402 != nil && !route.X402.Empty() {
183
- routeMap["x402"] = x402ConfigDocumentMap(*route.X402)
184
- }
180
routes = append(routes, routeMap)
181
}
182
out["http_routes"] = routes
@@ -219,23 +214,6 @@ func tunnelConfigDocumentMap(cfg TunnelConfig) map[string]any {
214
return out
215
}
216
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.MaxTimeoutSeconds != 0 {
231
- out["max_timeout_seconds"] = cfg.MaxTimeoutSeconds
232
- }
233
- if cfg.PaymentTimeoutSecs != 0 {
234
- out["payment_timeout_seconds"] = cfg.PaymentTimeoutSecs
235
- }
236
- return out
237
-}
238
-
217
func addStringDocumentField(out map[string]any, key, value string) {
218
if strings.TrimSpace(value) != "" {
219
out[key] = value
cmd/portal-tunnel/agent/dashboard.go
+1
-34
@@ -62,7 +62,6 @@ const (
62
agentDashboardSettingsFieldOwner
63
agentDashboardSettingsFieldThumbnail
64
agentDashboardSettingsFieldHide
65
- agentDashboardSettingsFieldX402Facilitator
65
agentDashboardSettingsFieldCount
66
)
67
@@ -99,7 +98,6 @@ type agentDashboardModel struct {
98
metadataOwner textinput.Model
99
metadataThumbnail textinput.Model
100
metadataHide textinput.Model
102
- x402FacilitatorURL textinput.Model
101
}
102
103
type agentDashboardStatusMsg struct {
@@ -167,7 +165,6 @@ func RunDashboard(configPath, stateDir string) error {
165
metadataOwner: newAgentDashboardInlineInput("owner"),
166
metadataThumbnail: newAgentDashboardInlineInput("https://..."),
167
metadataHide: newAgentDashboardInlineInput("true or false"),
170
- x402FacilitatorURL: newAgentDashboardInlineInput("https://relay.example.com/api/x402"),
168
}
169
model.resizeInputs(0)
170
@@ -705,9 +702,6 @@ func (m agentDashboardModel) updateSettingsKeys(msg tea.KeyMsg) (tea.Model, tea.
702
703
func (m *agentDashboardModel) focusSettingsField(field int) {
704
fieldCount := agentDashboardSettingsFieldCount
708
- if tunnel, ok := m.selectedTunnelStatus(); ok && !tunnel.X402Enabled {
709
- fieldCount = agentDashboardSettingsFieldX402Facilitator
710
- }
705
if field < 0 {
706
field = fieldCount - 1
707
}
@@ -723,7 +717,6 @@ func (m *agentDashboardModel) focusSettingsField(field int) {
717
&m.metadataOwner,
718
&m.metadataThumbnail,
719
&m.metadataHide,
726
- &m.x402FacilitatorURL,
720
} {
721
input.Blur()
722
}
@@ -746,8 +739,6 @@ func (m *agentDashboardModel) focusedSettingsInput() *textinput.Model {
739
return &m.metadataThumbnail
740
case agentDashboardSettingsFieldHide:
741
return &m.metadataHide
749
- case agentDashboardSettingsFieldX402Facilitator:
750
- return &m.x402FacilitatorURL
742
default:
743
return nil
744
}
@@ -761,7 +752,6 @@ func (m *agentDashboardModel) blurSettingsInputs() {
752
&m.metadataOwner,
753
&m.metadataThumbnail,
754
&m.metadataHide,
764
- &m.x402FacilitatorURL,
755
} {
756
input.Blur()
757
}
@@ -776,7 +766,6 @@ func (m *agentDashboardModel) clearSettingsDraft() {
766
&m.metadataOwner,
767
&m.metadataThumbnail,
768
&m.metadataHide,
779
- &m.x402FacilitatorURL,
769
} {
770
input.Reset()
771
}
@@ -802,7 +791,6 @@ func (m *agentDashboardModel) resizeInputs(width int) {
791
&m.metadataOwner,
792
&m.metadataThumbnail,
793
&m.metadataHide,
805
- &m.x402FacilitatorURL,
794
} {
795
input.Width = settingsWidth
796
}
@@ -826,23 +814,18 @@ func (m *agentDashboardModel) ensureSettingsDraft(tunnel types.AgentTunnelStatus
814
func (m *agentDashboardModel) loadSettingsDraft(tunnel types.AgentTunnelStatus) {
815
metadata := tunnel.Metadata
816
m.settingsEditTunnelID = tunnel.ID
829
- if !tunnel.X402Enabled && m.settingsFocus == agentDashboardSettingsFieldX402Facilitator {
830
- m.settingsFocus = agentDashboardSettingsFieldMaxActiveRelays
831
- }
817
m.settingsMaxRelays.SetValue(strconv.Itoa(tunnel.MaxActiveRelays))
818
m.metadataDescription.SetValue(strings.TrimSpace(metadata.Description))
819
m.metadataTags.SetValue(strings.Join(metadata.Tags, ","))
820
m.metadataOwner.SetValue(strings.TrimSpace(metadata.Owner))
821
m.metadataThumbnail.SetValue(strings.TrimSpace(metadata.Thumbnail))
822
m.metadataHide.SetValue(strconv.FormatBool(metadata.Hide))
838
- m.x402FacilitatorURL.SetValue(strings.TrimSpace(tunnel.X402FacilitatorURL))
823
m.settingsMaxRelays.CursorEnd()
824
m.metadataDescription.CursorEnd()
825
m.metadataTags.CursorEnd()
826
m.metadataOwner.CursorEnd()
827
m.metadataThumbnail.CursorEnd()
828
m.metadataHide.CursorEnd()
845
- m.x402FacilitatorURL.CursorEnd()
829
}
830
831
func (m agentDashboardModel) addTunnelFromInput() (tea.Model, tea.Cmd) {
@@ -936,12 +919,6 @@ func (m agentDashboardModel) applySettingsEdit() (tea.Model, tea.Cmd) {
919
MaxActiveRelays: &maxActiveRelays,
920
Metadata: &metadata,
921
}
939
- if tunnel.X402Enabled {
940
- facilitatorURL := strings.TrimSpace(m.x402FacilitatorURL.Value())
941
- if facilitatorURL != strings.TrimSpace(tunnel.X402FacilitatorURL) {
942
- req.X402FacilitatorURL = &facilitatorURL
943
- }
944
- }
922
m.err = nil
923
return m, agentDashboardRun(func(ctx context.Context) error {
924
return UpdateTunnel(ctx, m.stateDir, tunnel.ID, req)
@@ -1324,13 +1301,6 @@ func (m agentDashboardModel) renderSettingsInputRows(pane *agentDashboardView, w
1301
{label: "Thumbnail", input: m.metadataThumbnail, field: agentDashboardSettingsFieldThumbnail},
1302
{label: "Hidden", input: m.metadataHide, field: agentDashboardSettingsFieldHide},
1303
}
1327
- if tunnel.X402Enabled {
1328
- rows = append(rows, struct {
1329
- label string
1330
- input textinput.Model
1331
- field int
1332
- }{label: "Facilitator", input: m.x402FacilitatorURL, field: agentDashboardSettingsFieldX402Facilitator})
1333
- }
1304
for _, row := range rows {
1305
if len(pane.lines)-startLine >= height {
1306
return
@@ -1687,15 +1657,12 @@ func (m agentDashboardModel) settingsChanged(tunnel types.AgentTunnelStatus) boo
1657
return true
1658
}
1659
metadata := tunnel.Metadata
1690
- x402Changed := tunnel.X402Enabled &&
1691
- strings.TrimSpace(m.x402FacilitatorURL.Value()) != strings.TrimSpace(tunnel.X402FacilitatorURL)
1660
return maxRelays != tunnel.MaxActiveRelays ||
1661
strings.TrimSpace(m.metadataDescription.Value()) != strings.TrimSpace(metadata.Description) ||
1662
!slices.Equal(utils.SplitCSV(m.metadataTags.Value()), metadata.Tags) ||
1663
strings.TrimSpace(m.metadataOwner.Value()) != strings.TrimSpace(metadata.Owner) ||
1664
strings.TrimSpace(m.metadataThumbnail.Value()) != strings.TrimSpace(metadata.Thumbnail) ||
1697
- hide != metadata.Hide ||
1698
- x402Changed
1665
+ hide != metadata.Hide
1666
}
1667
1668
func relayDashboardFeatures(relay types.AgentRelayStatus) string {
cmd/portal-tunnel/agent/manager.go
+8
-54
@@ -161,7 +161,6 @@ func (m *manager) UpdateTunnel(id string, req types.AgentTunnelUpdateRequest) er
161
}
162
updateMetadata := req.Metadata != nil && !req.Metadata.Empty()
163
updateMaxActiveRelays := req.MaxActiveRelays != nil
164
- restartTunnel := false
164
if err := m.updateTunnelConfig(id, func(tunnel *TunnelConfig) error {
165
if req.MaxActiveRelays != nil {
166
if *req.MaxActiveRelays <= 0 {
@@ -186,25 +185,6 @@ func (m *manager) UpdateTunnel(id string, req types.AgentTunnelUpdateRequest) er
185
tunnel.Hide = *req.Metadata.Hide
186
}
187
}
189
- if req.X402FacilitatorURL != nil {
190
- facilitatorURL := strings.TrimSpace(*req.X402FacilitatorURL)
191
- x402Route := false
192
- for i := range tunnel.HTTPRoutes {
193
- if tunnel.HTTPRoutes[i].X402 == nil || tunnel.HTTPRoutes[i].X402.Empty() {
194
- continue
195
- }
196
- x402Route = true
197
- x402Config := *tunnel.HTTPRoutes[i].X402
198
- if strings.TrimSpace(x402Config.FacilitatorURL) != facilitatorURL {
199
- restartTunnel = true
200
- }
201
- x402Config.FacilitatorURL = facilitatorURL
202
- tunnel.HTTPRoutes[i].X402 = &x402Config
203
- }
204
- if !x402Route {
205
- return errors.New("tunnel has no x402 http routes")
206
- }
207
- }
188
return nil
189
}); err != nil {
190
return err
@@ -213,21 +193,10 @@ func (m *manager) UpdateTunnel(id string, req types.AgentTunnelUpdateRequest) er
193
id = strings.TrimSpace(id)
194
m.mu.RLock()
195
tunnel := m.tunnels[id]
216
- rootCtx := m.rootCtx
196
m.mu.RUnlock()
197
if tunnel == nil {
198
return fmt.Errorf("tunnel %q not found", id)
199
}
221
- if restartTunnel {
222
- if err := tunnel.Stop(context.Background()); err != nil {
223
- return err
224
- }
225
- if rootCtx == nil {
226
- rootCtx = context.Background()
227
- }
228
- tunnel.Start(rootCtx)
229
- return nil
230
- }
200
return tunnel.UpdateSettings(updateMetadata, updateMaxActiveRelays)
201
}
202
@@ -597,29 +566,15 @@ func (t *managedTunnel) Snapshot() types.AgentTunnelStatus {
566
state = "starting"
567
}
568
600
- x402Enabled := false
601
- x402FacilitatorURL := ""
602
- for _, route := range cfg.HTTPRoutes {
603
- if route.X402 == nil || route.X402.Empty() {
604
- continue
605
- }
606
- x402Enabled = true
607
- if x402FacilitatorURL == "" {
608
- x402FacilitatorURL = strings.TrimSpace(route.X402.FacilitatorURL)
609
- }
610
- }
611
-
569
status := types.AgentTunnelStatus{
613
- ID: cfg.ID,
614
- Name: cfg.Name,
615
- State: state,
616
- TargetAddr: cfg.TargetAddr,
617
- LastError: lastError,
618
- MaxActiveRelays: cfg.MaxActiveRelays,
619
- Metadata: metadataFromTunnelConfig(cfg),
620
- X402Enabled: x402Enabled,
621
- X402FacilitatorURL: x402FacilitatorURL,
622
- MultiHop: append([]string(nil), cfg.MultiHop...),
570
+ ID: cfg.ID,
571
+ Name: cfg.Name,
572
+ State: state,
573
+ TargetAddr: cfg.TargetAddr,
574
+ LastError: lastError,
575
+ MaxActiveRelays: cfg.MaxActiveRelays,
576
+ Metadata: metadataFromTunnelConfig(cfg),
577
+ MultiHop: append([]string(nil), cfg.MultiHop...),
578
}
579
if exposure == nil {
580
if strings.TrimSpace(runtime.Address) != "" {
@@ -731,7 +686,6 @@ func (t *managedTunnel) runOnce(ctx context.Context) error {
686
routes = append(routes, sdk.HTTPRoute{
687
Prefix: route.Prefix,
688
Upstream: route.Upstream,
734
- X402: route.X402,
689
})
690
}
691
err = exposure.RunHTTPRoutes(ctx, routes, "")
cmd/portal-tunnel/main.go
-65
@@ -61,7 +61,6 @@ type exposeFlags struct {
61
hide bool
62
targetAddr string
63
httpRoutes []string
64
- x402 exposeX402Flags
64
udp bool
65
udpAddr string
66
tcp bool
@@ -70,17 +69,6 @@ type exposeFlags struct {
69
metricsAddr string
70
}
71
73
-type exposeX402Flags struct {
74
- network string
75
- price string
76
- payTo string
77
- facilitator string
78
- resource string
79
- mimeType string
80
- maxTimeout int
81
- paymentTimeout int
82
-}
83
-
72
func runExposeCommand(args []string) error {
73
installer.StartUpdateCheck(types.ReleaseVersion)
74
@@ -100,7 +88,6 @@ func runExposeCommand(args []string) error {
88
utils.StringFlag(fs, &flags.thumbnail, "thumbnail", "", "Service thumbnail URL metadata")
89
utils.BoolFlag(fs, &flags.hide, "hide", false, "Hide service from relay listing screens")
90
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")
103
- flags.x402.bind(fs)
91
utils.BoolFlagEnv(fs, &flags.udp, "udp", false, "Enable public UDP relay in addition to the default TCP relay", "UDP_ENABLED")
92
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")
93
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")
@@ -121,15 +108,7 @@ func runExposeCommand(args []string) error {
108
printExposeUsage(os.Stderr)
109
return err
110
}
124
- x402Config, err := flags.x402.config()
125
- if err != nil {
126
- printExposeUsage(os.Stderr)
127
- return err
128
- }
111
httpRouteInputs := append([]string(nil), flags.httpRoutes...)
130
- if x402Config != nil && flags.targetAddr != "" && len(httpRouteInputs) == 0 {
131
- httpRouteInputs = []string{"/=" + flags.targetAddr}
132
- }
112
switch {
113
case flags.targetAddr == "" && len(httpRouteInputs) == 0:
114
printExposeUsage(os.Stderr)
@@ -140,9 +119,6 @@ func runExposeCommand(args []string) error {
119
case len(httpRouteInputs) > 0 && flags.udp:
120
printExposeUsage(os.Stderr)
121
return errors.New("--udp cannot be combined with --http-route")
143
- case x402Config != nil && flags.tcp:
144
- printExposeUsage(os.Stderr)
145
- return errors.New("--x402 cannot be combined with --tcp")
122
}
123
124
ctx, stop := utils.SignalContext()
@@ -199,7 +175,6 @@ func runExposeCommand(args []string) error {
175
httpRoutes = append(httpRoutes, sdk.HTTPRoute{
176
Prefix: strings.TrimSpace(prefix),
177
Upstream: strings.TrimSpace(upstream),
202
- X402: x402Config,
178
})
179
}
180
@@ -209,46 +184,6 @@ func runExposeCommand(args []string) error {
184
return sdk.ProxyExposure(ctx, exposure)
185
}
186
212
-func (f *exposeX402Flags) bind(fs *flag.FlagSet) {
213
- utils.StringFlag(fs, &f.network, "x402-network", "", "x402 payment network, such as eip155:8453")
214
- utils.StringFlag(fs, &f.price, "x402-price", "", "x402 route price, such as $0.001")
215
- utils.StringFlag(fs, &f.payTo, "x402-pay-to", "", "x402 recipient address; empty uses the tunnel identity address")
216
- utils.StringFlag(fs, &f.facilitator, "x402-facilitator-url", "", "x402 facilitator URL")
217
- utils.StringFlag(fs, &f.resource, "x402-resource", "", "x402 protected resource URL; empty uses the requested URL")
218
- utils.StringFlag(fs, &f.mimeType, "x402-mime-type", "", "x402 protected resource MIME type")
219
- fs.IntVar(&f.maxTimeout, "x402-max-timeout", 0, "x402 max payment timeout seconds advertised to clients")
220
- fs.IntVar(&f.paymentTimeout, "x402-payment-timeout", 0, "x402 middleware verify/settle timeout seconds")
221
-}
222
-
223
-func (f exposeX402Flags) config() (*types.X402Config, error) {
224
- cfg := &types.X402Config{
225
- Network: f.network,
226
- Price: f.price,
227
- PayTo: f.payTo,
228
- FacilitatorURL: f.facilitator,
229
- Resource: f.resource,
230
- MimeType: f.mimeType,
231
- MaxTimeoutSeconds: f.maxTimeout,
232
- PaymentTimeoutSecs: f.paymentTimeout,
233
- }
234
- if cfg.Empty() {
235
- return nil, nil
236
- }
237
- switch {
238
- case strings.TrimSpace(cfg.FacilitatorURL) == "":
239
- return nil, errors.New("--x402-facilitator-url is required when x402 is enabled")
240
- case strings.TrimSpace(cfg.Network) == "":
241
- return nil, errors.New("--x402-network is required when x402 is enabled")
242
- case strings.TrimSpace(cfg.Price) == "":
243
- return nil, errors.New("--x402-price is required when x402 is enabled")
244
- case cfg.MaxTimeoutSeconds < 0:
245
- return nil, errors.New("--x402-max-timeout cannot be negative")
246
- case cfg.PaymentTimeoutSecs < 0:
247
- return nil, errors.New("--x402-payment-timeout cannot be negative")
248
- }
249
- return cfg, nil
250
-}
251
-
187
func runUpdateCommand(args []string) error {
188
var version string
189
fs := utils.NewFlagSet("update", printUpdateUsage)
cmd/relay-server/api.go
+23
-81
@@ -2,19 +2,17 @@ package main
2
3
import (
4
"crypto/sha256"
5
+ "crypto/subtle"
6
"embed"
7
"encoding/hex"
8
"errors"
9
"fmt"
10
"net"
11
"net/http"
11
- "net/url"
12
"strings"
13
- "time"
13
14
"github.com/gosuda/portal-tunnel/v2/cmd/portal-tunnel/installer"
15
"github.com/gosuda/portal-tunnel/v2/portal"
17
- "github.com/gosuda/portal-tunnel/v2/portal/auth"
16
"github.com/gosuda/portal-tunnel/v2/portal/identity"
17
"github.com/gosuda/portal-tunnel/v2/portal/policy"
18
"github.com/gosuda/portal-tunnel/v2/types"
@@ -31,11 +29,11 @@ const (
29
30
type RelayAPI struct {
31
server *portal.Server
34
- auth *auth.WalletAuthenticator
32
+ adminToken string
33
policyStatePath string
34
}
35
38
-func NewRelayAPI(server *portal.Server, identityPath string, adminWallets []string) (*RelayAPI, error) {
36
+func NewRelayAPI(server *portal.Server, identityPath, adminToken string) (*RelayAPI, error) {
37
if server == nil {
38
return nil, errors.New("relay api requires portal server")
39
}
@@ -50,19 +48,10 @@ func NewRelayAPI(server *portal.Server, identityPath string, adminWallets []stri
48
if err := loadPolicyState(policyStatePath, server); err != nil {
49
return nil, err
50
}
53
- relayIdentity := server.RelayIdentity()
54
- allowedWallets := append([]string{relayIdentity.Address}, adminWallets...)
55
- authenticator, err := auth.NewWalletAuthenticator(auth.WalletAuthConfig{
56
- AllowedAddresses: allowedWallets,
57
- Statement: "Sign in to Portal relay admin",
58
- })
59
- if err != nil {
60
- return nil, err
61
- }
51
52
api := &RelayAPI{
53
server: server,
65
- auth: authenticator,
54
+ adminToken: strings.TrimSpace(adminToken),
55
policyStatePath: strings.TrimSpace(policyStatePath),
56
}
57
return api, nil
@@ -134,38 +123,29 @@ func (api *RelayAPI) serveAdmin(w http.ResponseWriter, r *http.Request) {
123
case types.PathAdmin:
124
http.NotFound(w, r)
125
return
137
- case types.PathAdminAuthChallenge:
138
- if !utils.RequireMethod(w, r, http.MethodPost) {
139
- return
140
- }
141
- api.handleWalletChallenge(w, r)
142
- return
126
case types.PathAdminAuthLogin:
127
if !utils.RequireMethod(w, r, http.MethodPost) {
128
return
129
}
147
- api.handleWalletLogin(w, r)
130
+ api.handleAdminLogin(w, r)
131
return
132
case types.PathAdminLogout:
133
if !utils.RequireMethod(w, r, http.MethodPost) {
134
return
135
}
153
- api.auth.DeleteSession(adminAccessToken(r))
136
utils.WriteAPIData(w, http.StatusOK, map[string]any{})
137
return
138
case types.PathAdminAuthStatus:
139
if !utils.RequireMethod(w, r, http.MethodGet) {
140
return
141
}
160
- walletAddress, authenticated := api.authenticatedWallet(r)
161
- utils.WriteAPIData(w, http.StatusOK, types.WalletAuthStatusResponse{
162
- Authenticated: authenticated,
163
- WalletAddress: walletAddress,
142
+ utils.WriteAPIData(w, http.StatusOK, types.AdminAuthStatusResponse{
143
+ Authenticated: api.authenticatedAdmin(r),
144
})
145
return
146
}
147
168
- if _, ok := api.authenticatedWallet(r); !ok {
148
+ if !api.authenticatedAdmin(r) {
149
utils.WriteAPIError(w, http.StatusUnauthorized, types.APIErrorCodeUnauthorized, "unauthorized")
150
return
151
}
@@ -185,7 +165,7 @@ func (api *RelayAPI) servePolicy(w http.ResponseWriter, r *http.Request) {
165
path = types.PathRoot
166
}
167
188
- if _, ok := api.authenticatedWallet(r); !ok {
168
+ if !api.authenticatedAdmin(r) {
169
utils.WriteAPIError(w, http.StatusUnauthorized, types.APIErrorCodeUnauthorized, "unauthorized")
170
return
171
}
@@ -353,58 +333,22 @@ func applyLeasePolicyUpdate(w http.ResponseWriter, runtime *policy.Runtime, iden
333
return true
334
}
335
356
-func (api *RelayAPI) handleWalletChallenge(w http.ResponseWriter, r *http.Request) {
357
- req, ok := utils.DecodeJSONRequestAs[types.WalletAuthChallengeRequest](w, r, controlBodyLimit, utils.InvalidRequestError(errors.New("invalid request body")))
358
- if !ok {
359
- return
360
- }
361
- resp, err := api.auth.IssueChallenge(req, adminAuthDomain(r, api.server.RelayIdentity().Name), adminAuthURI(r, types.PathAdminAuthLogin), time.Now().UTC())
362
- if err != nil {
363
- writeWalletAuthError(w, err)
364
- return
365
- }
366
- utils.WriteAPIData(w, http.StatusCreated, resp)
367
-}
368
-
369
-func (api *RelayAPI) handleWalletLogin(w http.ResponseWriter, r *http.Request) {
370
- req, ok := utils.DecodeJSONRequestAs[types.WalletAuthLoginRequest](w, r, controlBodyLimit, utils.InvalidRequestError(errors.New("invalid request body")))
336
+func (api *RelayAPI) handleAdminLogin(w http.ResponseWriter, r *http.Request) {
337
+ req, ok := utils.DecodeJSONRequestAs[types.AdminAuthLoginRequest](w, r, controlBodyLimit, utils.InvalidRequestError(errors.New("invalid request body")))
338
if !ok {
339
return
340
}
374
- token, walletAddress, err := api.auth.Login(req, time.Now().UTC())
375
- if err != nil {
376
- writeWalletAuthError(w, err)
341
+ if !api.tokenAllowed(req.Token) {
342
+ utils.WriteAPIError(w, http.StatusUnauthorized, types.APIErrorCodeUnauthorized, "invalid admin token")
343
return
344
}
379
-
380
- utils.WriteAPIData(w, http.StatusOK, types.WalletAuthLoginResponse{
381
- AccessToken: token,
382
- WalletAddress: walletAddress,
345
+ utils.WriteAPIData(w, http.StatusOK, types.AdminAuthLoginResponse{
346
+ AccessToken: api.adminToken,
347
})
348
}
349
386
-func (api *RelayAPI) authenticatedWallet(r *http.Request) (string, bool) {
387
- return api.auth.ValidateSession(adminAccessToken(r))
388
-}
389
-
390
-func adminAuthDomain(r *http.Request, fallback string) string {
391
- domain := strings.TrimSpace(r.Host)
392
- if domain != "" {
393
- return domain
394
- }
395
- return strings.TrimSpace(fallback)
396
-}
397
-
398
-func adminAuthURI(r *http.Request, endpointPath string) string {
399
- scheme := "https"
400
- if r.TLS == nil {
401
- scheme = "http"
402
- }
403
- return (&url.URL{
404
- Scheme: scheme,
405
- Host: adminAuthDomain(r, "localhost"),
406
- Path: endpointPath,
407
- }).String()
350
+func (api *RelayAPI) authenticatedAdmin(r *http.Request) bool {
351
+ return api.tokenAllowed(adminAccessToken(r))
352
}
353
354
func adminAccessToken(r *http.Request) string {
@@ -415,15 +359,13 @@ func adminAccessToken(r *http.Request) string {
359
return strings.TrimSpace(parts[1])
360
}
361
418
-func writeWalletAuthError(w http.ResponseWriter, err error) {
419
- switch {
420
- case errors.Is(err, auth.ErrWalletAuthUnauthorized):
421
- utils.WriteAPIError(w, http.StatusForbidden, types.APIErrorCodeUnauthorized, err.Error())
422
- case errors.Is(err, auth.ErrWalletAuthChallengeNotFound), errors.Is(err, auth.ErrWalletAuthChallengeExpired), errors.Is(err, auth.ErrWalletAuthInvalidSignature):
423
- utils.WriteAPIError(w, http.StatusUnauthorized, types.APIErrorCodeUnauthorized, err.Error())
424
- default:
425
- utils.WriteAPIError(w, http.StatusBadRequest, types.APIErrorCodeInvalidRequest, err.Error())
362
+func (api *RelayAPI) tokenAllowed(raw string) bool {
363
+ token := strings.TrimSpace(raw)
364
+ expected := strings.TrimSpace(api.adminToken)
365
+ if token == "" || expected == "" || len(token) != len(expected) {
366
+ return false
367
}
368
+ return subtle.ConstantTimeCompare([]byte(token), []byte(expected)) == 1
369
}
370
371
func savePolicyState(path string, runtime *policy.Runtime) {
cmd/relay-server/main.go
+4
-29
@@ -16,7 +16,6 @@ import (
16
"github.com/gosuda/portal-tunnel/v2/portal/acme"
17
"github.com/gosuda/portal-tunnel/v2/portal/identity"
18
"github.com/gosuda/portal-tunnel/v2/portal/overlay"
19
- portalx402 "github.com/gosuda/portal-tunnel/v2/portal/x402"
19
"github.com/gosuda/portal-tunnel/v2/types"
20
"github.com/gosuda/portal-tunnel/v2/utils"
21
)
@@ -47,12 +46,9 @@ type relayServerConfig struct {
46
TCPEnabled bool
47
MinPort int
48
MaxPort int
50
- AdminWallets string
49
+ AdminToken string
50
PProfEnabled bool
51
PProfAddr string
53
- X402Enabled bool
54
- X402Network string
55
- X402RPCURL string
52
53
ACMEDNSProvider string
54
ENSGaslessEnabled bool
@@ -90,12 +86,9 @@ func runServeCommand(args []string) error {
86
utils.IntFlagEnv(fs, &cfg.MinPort, "min-port", 0, utils.ParseOptionalPortNumber, "inclusive minimum lease port shared by UDP and raw TCP transports (0=disabled)", "MIN_PORT")
87
utils.IntFlagEnv(fs, &cfg.MaxPort, "max-port", 0, utils.ParseOptionalPortNumber, "inclusive maximum lease port shared by UDP and raw TCP transports (0=disabled)", "MAX_PORT")
88
93
- utils.StringFlagEnv(fs, &cfg.AdminWallets, "admin-wallets", "", "admin wallet address allowlist, comma-separated; relay identity address is always allowed", "ADMIN_WALLETS")
89
+ utils.StringFlagEnv(fs, &cfg.AdminToken, "admin-token", "", "admin bearer token for relay admin and policy APIs", "ADMIN_TOKEN")
90
utils.BoolFlagEnv(fs, &cfg.PProfEnabled, "pprof-enabled", false, "enable pprof diagnostics HTTP server", "PPROF_ENABLED")
91
utils.StringFlagEnv(fs, &cfg.PProfAddr, "pprof-addr", portal.DefaultPProfListenAddr, "pprof diagnostics listen address when enabled", "PPROF_ADDR")
96
- utils.BoolFlagEnv(fs, &cfg.X402Enabled, "x402-facilitator-enabled", false, "enable relay-local x402 facilitator endpoints under /api/x402", "X402_FACILITATOR_ENABLED")
97
- utils.StringFlagEnv(fs, &cfg.X402Network, "x402-network", "", "x402 facilitator CAIP-2 network, such as eip155:8453", "X402_NETWORK")
98
- utils.StringFlagEnv(fs, &cfg.X402RPCURL, "x402-rpc-url", "", "x402 facilitator RPC URL; empty uses the PublicNode default for supported networks", "X402_RPC_URL")
92
93
utils.StringFlagEnv(fs, &cfg.ACMEDNSProvider, "acme-dns-provider", "", "DNS provider for managed DNS-01/A-record sync, ECH HTTPS records, and ENS gasless DNSSEC/TXT automation (cloudflare|gcloud|hetzner|njalla|route53|vultr); leave empty to use manual fullchain.pem/privatekey.pem from IDENTITY_PATH", "ACME_DNS_PROVIDER")
94
utils.BoolFlagEnv(fs, &cfg.ENSGaslessEnabled, "ens-gasless-enabled", false, "enable ENS gasless DNS import automation for the managed DNS zone and lease hostnames", "ENS_GASLESS_ENABLED")
@@ -139,11 +132,9 @@ func runServeCommand(args []string) error {
132
Bool("tcp_enabled", cfg.TCPEnabled).
133
Int("min_port", cfg.MinPort).
134
Int("max_port", cfg.MaxPort).
142
- Bool("admin_wallets_configured", len(utils.SplitCSV(cfg.AdminWallets)) > 0).
135
+ Bool("admin_token_configured", strings.TrimSpace(cfg.AdminToken) != "").
136
Bool("pprof_enabled", cfg.PProfEnabled).
137
Str("pprof_addr", cfg.PProfAddr).
145
- Bool("x402_facilitator_enabled", cfg.X402Enabled).
146
- Str("x402_network", strings.TrimSpace(cfg.X402Network)).
138
Str("acme_dns_provider", cfg.ACMEDNSProvider).
139
Bool("ens_gasless_enabled", cfg.ENSGaslessEnabled).
140
Msg("configured relay server")
@@ -171,8 +162,6 @@ func runServer(ctx context.Context, cfg relayServerConfig) error {
162
MaxPort: cfg.MaxPort,
163
PProfEnabled: cfg.PProfEnabled,
164
PProfListenAddr: cfg.PProfAddr,
174
- X402Enabled: cfg.X402Enabled,
175
- X402Network: cfg.X402Network,
165
ACME: acme.Config{
166
KeyDir: cfg.IdentityPath,
167
DNSProvider: cfg.ACMEDNSProvider,
@@ -195,26 +184,12 @@ func runServer(ctx context.Context, cfg relayServerConfig) error {
184
return fmt.Errorf("create relay server: %w", err)
185
}
186
198
- relayAPI, err := NewRelayAPI(server, cfg.IdentityPath, utils.SplitCSV(cfg.AdminWallets))
187
+ relayAPI, err := NewRelayAPI(server, cfg.IdentityPath, cfg.AdminToken)
188
if err != nil {
189
return fmt.Errorf("create relay api: %w", err)
190
}
191
192
apiMux := relayAPI.Handler()
204
- if cfg.X402Enabled {
205
- relayIdentity := server.RelayIdentity()
206
- if err := portalx402.MountFacilitator(apiMux, portalx402.FacilitatorConfig{
207
- Network: cfg.X402Network,
208
- RPCURL: cfg.X402RPCURL,
209
- Identity: relayIdentity.Identity,
210
- }); err != nil {
211
- return err
212
- }
213
- log.Info().
214
- Str("path", types.PathX402Facilitator).
215
- Str("network", strings.TrimSpace(cfg.X402Network)).
216
- Msg("x402 facilitator enabled")
217
- }
193
194
if err := server.Start(ctx, apiMux); err != nil {
195
return fmt.Errorf("start relay server: %w", err)
docker-compose.yml
+2
-7
@@ -106,16 +106,11 @@ services:
106
UDP_ENABLED: ${UDP_ENABLED:-false}
107
TCP_ENABLED: ${TCP_ENABLED:-false}
108
109
- # Admin/auth configuration
110
- ADMIN_WALLETS: ${ADMIN_WALLETS:-}
109
+ # Admin/auth configuration.
110
+ ADMIN_TOKEN: ${ADMIN_TOKEN:-}
111
TRUST_PROXY_HEADERS: ${TRUST_PROXY_HEADERS:-true}
112
TRUSTED_PROXY_CIDRS: ${TRUSTED_PROXY_CIDRS:-}
113
114
- # Optional: relay-local x402 facilitator exposed under /api/x402
115
- X402_FACILITATOR_ENABLED: ${X402_FACILITATOR_ENABLED:-false}
116
- X402_NETWORK: ${X402_NETWORK:-eip155:84532}
117
- X402_RPC_URL: ${X402_RPC_URL:-https://base-sepolia-rpc.publicnode.com}
118
-
114
# Optional diagnostics; keep loopback unless the pprof port is protected.
115
PPROF_ENABLED: ${PPROF_ENABLED:-false}
116
PPROF_ADDR: ${PPROF_ADDR:-127.0.0.1:6060}
docs/src/routes/api-reference/+page.md
+6
-8
@@ -41,7 +41,6 @@ The envelope does not apply to streaming or delegated endpoints:
41
| `/sdk/connect` | HTTP/1.1 connection hijack |
42
| `/v1/sign` | keyless TLS signer protocol |
43
| `/api/install.sh`, `/api/install.ps1`, `/api/install/bin/*` | script or binary bytes |
44
-| `/api/x402/*` | x402 facilitator API |
44
45
Unknown routes may be handled by the frontend/proxy layer or return a normal
46
HTTP 404 outside the envelope.
@@ -57,8 +56,9 @@ HTTP 404 outside the envelope.
56
| Signed descriptor | relay discovery announce | signed `RelayDescriptor` body |
57
| Signed hop route | relay overlay route | signed `HopRoute` body |
58
60
-Admin and SDK login both use SIWE, but they issue different tokens and are not
61
-interchangeable.
59
+Admin auth and SDK lease auth issue different tokens and are not
60
+interchangeable. SDK lease registration uses SIWE; relay admin access uses the
61
+configured admin token.
62
63
## Endpoint Groups
64
@@ -105,9 +105,8 @@ SDK clients.
105
106
| Method | Path | Auth | Body | Response |
107
|--------|------|------|------|----------|
108
-| `POST` | `/api/admin/auth/challenge` | None | `WalletAuthChallengeRequest` | `WalletAuthChallengeResponse` |
109
-| `POST` | `/api/admin/auth/login` | SIWE signature body | `WalletAuthLoginRequest` | `WalletAuthLoginResponse` |
110
-| `GET` | `/api/admin/auth/status` | Optional admin bearer | none | `WalletAuthStatusResponse` |
108
+| `POST` | `/api/admin/auth/login` | None | `AdminAuthLoginRequest` | `AdminAuthLoginResponse` |
109
+| `GET` | `/api/admin/auth/status` | Optional admin bearer | none | `AdminAuthStatusResponse` |
110
| `POST` | `/api/admin/auth/logout` | Admin bearer | none | `{}` |
111
112
`/admin` itself is a frontend route, not a relay API endpoint.
@@ -122,14 +121,13 @@ SDK clients.
121
| `POST` | `/api/policy/leases` | Admin bearer | `LeasePolicyUpdate` | `{}` |
122
| `POST` | `/api/policy/ips` | Admin bearer | `IPPolicyUpdate` | `{}` |
123
125
-### Relay And Payment
124
+### Relay
125
126
| Method | Path | Auth | Response |
127
|--------|------|------|----------|
128
| `GET` | `/discovery` | None | `DiscoveryResponse` |
129
| `POST` | `/discovery/announce` | Signed descriptor | `DiscoveryAnnounceResponse` |
130
| `POST` | `/v1/sign` | Lease token header | keyless signer response |
132
-| `ANY` | `/api/x402/*` | x402-specific | delegated facilitator response |
131
132
## Shared Types
133
docs/src/routes/api-reference/admin/+page.md
+11
-31
@@ -9,16 +9,15 @@ Operator endpoints are the control surface for a relay. They all return
9
the standard JSON envelope described in [API Reference](/api-reference), except
10
for internal operational endpoints that are not part of the stable API.
11
12
-`/admin` is reserved for the frontend route. Relay wallet auth endpoints live
12
+`/admin` is reserved for the frontend route. Relay admin auth endpoints live
13
under `/api/admin`, and enforcement settings live under `/api/policy`.
14
15
## Auth Flow
16
17
-1. `POST /api/admin/auth/challenge` with the wallet address.
18
-2. Sign the returned `siwe_message`.
19
-3. `POST /api/admin/auth/login` with the challenge id, message, and signature.
20
-4. Send the returned `access_token` as `Authorization: Bearer <token>`.
21
-5. `POST /api/admin/auth/logout` to invalidate the current token.
17
+1. Set `ADMIN_TOKEN` on the relay.
18
+2. `POST /api/admin/auth/login` with `{ "token": "<admin-token>" }`.
19
+3. Send the returned `access_token` as `Authorization: Bearer <token>`.
20
+4. `POST /api/admin/auth/logout` to clear the browser-stored token.
21
22
Admin bearer tokens are separate from SDK lease tokens.
23
@@ -26,9 +25,8 @@ Admin bearer tokens are separate from SDK lease tokens.
25
26
| Method | Path | Auth | Body | Data |
27
|--------|------|------|------|------|
29
-| `POST` | `/api/admin/auth/challenge` | None | `WalletAuthChallengeRequest` | `WalletAuthChallengeResponse` |
30
-| `POST` | `/api/admin/auth/login` | SIWE signature body | `WalletAuthLoginRequest` | `WalletAuthLoginResponse` |
31
-| `GET` | `/api/admin/auth/status` | Optional bearer | none | `WalletAuthStatusResponse` |
28
+| `POST` | `/api/admin/auth/login` | None | `AdminAuthLoginRequest` | `AdminAuthLoginResponse` |
29
+| `GET` | `/api/admin/auth/status` | Optional bearer | none | `AdminAuthStatusResponse` |
30
| `POST` | `/api/admin/auth/logout` | Bearer | none | `{}` |
31
| `GET` | `/api/policy` | Bearer | none | `PolicySettings` |
32
| `POST` | `/api/policy` | Bearer | `PolicySettings` | `PolicySettings` |
@@ -38,41 +36,23 @@ Admin bearer tokens are separate from SDK lease tokens.
36
37
## Auth Payloads
38
41
-`WalletAuthChallengeRequest`:
39
+`AdminAuthLoginRequest`:
40
41
| Field | Type | Required |
42
|-------|------|----------|
45
-| `address` | `string` | yes |
43
+| `token` | `string` | yes |
44
47
-`WalletAuthChallengeResponse`:
48
-
49
-| Field | Type |
50
-|-------|------|
51
-| `challenge_id` | `string` |
52
-| `expires_at` | `string` |
53
-| `siwe_message` | `string` |
54
-
55
-`WalletAuthLoginRequest`:
56
-
57
-| Field | Type | Required |
58
-|-------|------|----------|
59
-| `challenge_id` | `string` | yes |
60
-| `siwe_message` | `string` | yes |
61
-| `siwe_signature` | `string` | yes |
62
-
63
-`WalletAuthLoginResponse`:
45
+`AdminAuthLoginResponse`:
46
47
| Field | Type |
48
|-------|------|
49
| `access_token` | `string` |
68
-| `wallet_address` | `string` |
50
70
-`WalletAuthStatusResponse`:
51
+`AdminAuthStatusResponse`:
52
53
| Field | Type | Notes |
54
|-------|------|-------|
55
| `authenticated` | `boolean` | true only when a valid bearer token was sent |
75
-| `wallet_address` | `string` | omitted when unauthenticated |
56
57
## State
58
docs/src/routes/api-reference/sdk/+page.md
+1
-9
@@ -12,7 +12,7 @@ that switches to a raw stream after a successful HTTP/1.1 response.
12
13
## Flow
14
15
-1. `GET /sdk/domain` checks relay compatibility and optional ENS/x402 support.
15
+1. `GET /sdk/domain` checks relay compatibility and optional ENS support.
16
2. `POST /sdk/register/challenge` creates a SIWE challenge for the requested identity.
17
3. The SDK signs the returned `siwe_message`.
18
4. `POST /sdk/register` exchanges the signature for a lease `access_token`.
@@ -39,7 +39,6 @@ that switches to a raw stream after a successful HTTP/1.1 response.
39
| `protocol_version` | `string` | SDK tunnel protocol version |
40
| `release_version` | `string` | relay software release |
41
| `ens` | `ENSStatus` | gasless ENS status |
42
-| `x402` | `X402FacilitatorInfo` | relay-local payment facilitator info |
42
43
`ENSStatus`:
44
@@ -48,13 +47,6 @@ that switches to a raw stream after a successful HTTP/1.1 response.
47
| `enabled`, `verified` | `boolean` |
48
| `provider`, `address`, `dnssec_state`, `ds_record`, `message`, `last_error` | `string` |
49
51
-`X402FacilitatorInfo`:
52
-
53
-| Field | Type |
54
-|-------|------|
55
-| `enabled` | `boolean` |
56
-| `url`, `network`, `network_name`, `supported_url` | `string` |
57
-
50
## Register Challenge
51
52
`RegisterChallengeRequest`:
docs/src/routes/cli-reference/+page.md
+2
-84
@@ -76,7 +76,6 @@ not supported.
76
|------|---------|-------|
77
| Default HTTPS stream | `portal expose 3000` | Relay routes by SNI; tunnel process terminates tenant TLS |
78
| Routed HTTP | `portal expose --http-route /api=3001 --http-route /=5173` | Tunnel process runs the HTTP reverse proxy |
79
-| Routed HTTP with x402 | `portal expose 3000 --x402-facilitator-url https://portal.example.com/api/x402 --x402-network eip155:8453 --x402-price "$0.001"` | Tunnel process enforces payment before proxying to the upstream |
79
| Dedicated raw TCP | `portal expose localhost:25565 --tcp` | Relay allocates a public TCP port |
80
| UDP relay | `portal expose 8080 --udp --udp-addr 19132` | Relay allocates a public UDP port |
81
@@ -99,14 +98,6 @@ not supported.
98
| `--owner` | string | | Service owner metadata |
99
| `--hide` | bool | `false` | Hide service from relay listing screens |
100
| `--http-route` | string | | HTTP route mapping in `PATH=UPSTREAM` form; repeatable |
102
-| `--x402-network` | string | | x402 payment network, such as `eip155:8453` |
103
-| `--x402-price` | string | | x402 route price, such as `$0.001` |
104
-| `--x402-pay-to` | string | identity | x402 recipient address; empty uses the tunnel identity address |
105
-| `--x402-facilitator-url` | string | | x402 facilitator URL |
106
-| `--x402-resource` | string | requested URL | x402 protected resource URL |
107
-| `--x402-mime-type` | string | | x402 protected resource MIME type |
108
-| `--x402-max-timeout` | int | `0` | x402 max payment timeout seconds advertised to clients |
109
-| `--x402-payment-timeout` | int | `0` | x402 middleware verify/settle timeout seconds |
101
| `--tcp` | bool | `false` | Request a dedicated raw TCP port on the relay |
102
| `--udp` | bool | `false` | Enable public UDP relay in addition to the default stream path |
103
| `--udp-addr` | string | | Local UDP target; defaults to the primary target when `--udp` is enabled |
@@ -150,79 +141,6 @@ portal expose --name myapp \
141
Route matching is longest-prefix-first. `/api` matches `/api/*` and strips the
142
`/api` prefix before proxying to the upstream.
143
153
-Require x402 payment before a local upstream receives traffic:
154
-
155
-```bash
156
-portal expose 3000 --name paid-api \
157
- --relays https://portal.example.com \
158
- --discovery=false \
159
- --x402-facilitator-url https://portal.example.com/api/x402 \
160
- --x402-network eip155:8453 \
161
- --x402-price "$0.001"
162
-```
163
-
164
-With `portal expose`, the `--x402-*` flags apply one shared price to the routed
165
-HTTP handler created by that command. For route-specific prices, use agent
166
-config and attach x402 to each paid route:
167
-
168
-```toml
169
-[[tunnels]]
170
-id = "paid-site"
171
-name = "paid-site"
172
-relays = ["https://portal.example.com"]
173
-discovery = false
174
-
175
-[[tunnels.http_routes]]
176
-prefix = "/"
177
-upstream = "http://127.0.0.1:5173"
178
-
179
-[[tunnels.http_routes]]
180
-prefix = "/api/report"
181
-upstream = "http://127.0.0.1:3001"
182
-
183
-[tunnels.http_routes.x402]
184
-network = "eip155:8453"
185
-price = "$0.010"
186
-pay_to = "identity"
187
-facilitator_url = "https://portal.example.com/api/x402"
188
-resource = "/api/report"
189
-mime_type = "application/json"
190
-
191
-[[tunnels.http_routes]]
192
-prefix = "/api/dataset"
193
-upstream = "http://127.0.0.1:3001"
194
-
195
-[tunnels.http_routes.x402]
196
-network = "eip155:8453"
197
-price = "$0.050"
198
-pay_to = "identity"
199
-facilitator_url = "https://portal.example.com/api/x402"
200
-resource = "/api/dataset"
201
-mime_type = "application/json"
202
-```
203
-
204
-Native Go apps can keep pricing inside the application instead. Wrap the paid
205
-handler with `portal/x402` and provide a resolver:
206
-
207
-```go
208
-protected, err := portalx402.NewHTTPRouteHandler(portalx402.HTTPRouteHandlerConfig{
209
- Prefix: "/api/premium",
210
- Next: premiumHandler,
211
- X402: x402Config,
212
- TunnelIdentity: appIdentity,
213
- Metadata: metadata,
214
- PriceResolver: func(ctx context.Context, req portalx402.HTTPRequestContext) (string, error) {
215
- return catalog.PriceForPath(req.Path)
216
- },
217
-})
218
-```
219
-
220
-The payment app exposes the same pattern:
221
-
222
-```bash
223
-go run ./cmd/payment-app --x402-facilitator-url https://portal.example.com/api/x402 --x402-network eip155:8453 --x402-price "$0.01"
224
-```
225
-
144
Expose a Minecraft server:
145
146
```bash
@@ -284,7 +202,7 @@ portal agent restart
202
|---------|-------------|
203
| `portal agent run` | Install or update and start the managed agent service |
204
| `portal agent run --config config.toml --foreground` | Run the agent in the current terminal |
287
-| `portal agent dashboard` | Open the local TUI for tunnels, relays, multi-hop routes, settings, and x402 facilitator URLs |
205
+| `portal agent dashboard` | Open the local TUI for tunnels, relays, multi-hop routes, and settings |
206
| `portal agent stop` | Gracefully stop the agent and disable or stop the OS service |
207
| `portal agent restart` | Stop the current agent if present, install or update the service, and start it again |
208
@@ -353,7 +271,7 @@ Prints the installed version string and exits.
271
272
- [Getting Started](/getting-started): run your first tunnel
273
- [Portal Agent](/portal-agent): run durable multi-tunnel services
356
-- [Wallet and ENS](/wallet-and-ens): understand wallet auth and ENS gasless DNS
274
+- [Wallet and ENS](/wallet-and-ens): understand admin tokens, wallet auth, and ENS gasless DNS
275
- [Concepts](/concepts): understand the relay and transport model
276
- [TCP and UDP Tunneling](/tcp-udp-tunneling): raw TCP and UDP setup
277
- [Deployment](/deployment): run your own relay server
docs/src/routes/concepts/+page.md
+2
-2
@@ -160,8 +160,8 @@ datagram authentication.
160
161
Reusing the same identity path keeps the same tunnel identity across runs.
162
163
-Browser wallet login is separate from tunnel registration. It is used for relay
164
-admin access and optional local agent status access. See
163
+Relay admin token login and optional browser wallet login for local agent status
164
+are both separate from tunnel registration. See
165
[Wallet and ENS](/wallet-and-ens) for the distinction.
166
167
## Domain Boundary
docs/src/routes/configuration/+page.md
+2
-44
@@ -58,17 +58,11 @@ The relay server (`relay-server`) reads configuration from environment variables
58
| `PPROF_ENABLED` | `false` | bool | Enable the relay pprof diagnostics HTTP server |
59
| `PPROF_ADDR` | `127.0.0.1:6060` | string | pprof listen address when enabled; keep it on loopback unless the port is protected |
60
61
-### Payments
61
+### Admin
62
63
| Variable | Default | Type | Description |
64
|----------|---------|------|-------------|
65
-| `X402_FACILITATOR_ENABLED` | `false` | bool | Enable the relay-local x402 facilitator under `/api/x402` |
66
-| `X402_NETWORK` | | string | CAIP-2 network served by the facilitator, such as `eip155:8453` |
67
-| `X402_RPC_URL` | | string | RPC URL used by the facilitator; empty uses the facilitator default for supported networks |
68
-
69
-The relay-local facilitator uses the relay identity private key from
70
-`IDENTITY_PATH/identity.json`. `/sdk/domain` exposes only the public facilitator
71
-URL and network; `X402_RPC_URL` is not returned to clients.
65
+| `ADMIN_TOKEN` | | string | Bearer token source for relay admin and policy APIs; set a long random value for production relays |
66
67
### Frontend API Service
68
@@ -255,42 +249,6 @@ Tunnel fields mirror `portal expose` flags:
249
| `identity_json` | string | Identity JSON payload; overrides `identity_path` contents and is persisted there when both are set |
250
| `udp`, `udp_addr`, `tcp` | bool/string | UDP and raw TCP relay options |
251
| `description`, `tags`, `owner`, `thumbnail`, `hide` | mixed | Lease metadata shown by relays |
258
-| `http_routes.x402` | table | x402 payment settings for one HTTP route; set `facilitator_url` explicitly or let frontend/configuration tooling write it |
259
-
260
-`http_routes.x402` is evaluated by the tunnel process before proxying to the
261
-upstream. Use it when a specific HTTP path should require payment:
262
-
263
-```toml
264
-[[tunnels]]
265
-id = "paid-api"
266
-name = "paid-api"
267
-relays = ["https://portal.example.com"]
268
-discovery = false
269
-
270
-[[tunnels.http_routes]]
271
-prefix = "/"
272
-upstream = "http://127.0.0.1:5173"
273
-
274
-[[tunnels.http_routes]]
275
-prefix = "/api/report"
276
-upstream = "http://127.0.0.1:3001"
277
-
278
-[tunnels.http_routes.x402]
279
-network = "eip155:8453"
280
-price = "$0.010"
281
-pay_to = "identity"
282
-facilitator_url = "https://portal.example.com:4017/api/x402"
283
-resource = "/api/report"
284
-mime_type = "application/json"
285
-max_timeout_seconds = 0
286
-payment_timeout_seconds = 0
287
-```
288
-
289
-Repeat `[[tunnels.http_routes]]` with a different `x402.price` for each static
290
-priced path. If prices depend on product state, user input, or a database row,
291
-wrap the app's Go handler with `portal/x402` and use a `PriceResolver`; tunnel
292
-config is intentionally static.
293
-
252
For a task-oriented walkthrough, see [Portal Agent](/portal-agent).
253
254
### `identity.json`
docs/src/routes/deployment/+page.md
+2
-2
@@ -176,7 +176,7 @@ Portal's own nginx, it should TCP-passthrough `portal.example.com` and
176
177
If the relay joins public discovery, set `BOOTSTRAPS` to at least one reachable relay URL and keep `WIREGUARD_PORT/udp` open.
178
179
-The relay identity wallet can always sign in through admin auth. Set `ADMIN_WALLETS` only when you need additional admin wallets.
179
+Set `ADMIN_TOKEN` to a long random value before exposing the admin UI or policy API.
180
181
Leave `TRUSTED_PROXY_CIDRS` empty for the default private and loopback proxy ranges. Set it only when you need a stricter proxy source allowlist.
182
@@ -359,7 +359,7 @@ It owns:
359
- `/ui/thumbnail/<hostname>`, when optional screenshot generation is enabled.
360
- The landing-page flag persisted at `PORTAL_FRONTEND_STATE_PATH`; a Compose deployment can store it under `./.portal-certs/frontend-state/state.json`.
361
362
-The Go relay remains the owner of authentication, policy enforcement, lease state, tunnel ingress, install scripts, discovery, and x402 facilitator paths.
362
+The Go relay remains the owner of authentication, policy enforcement, lease state, tunnel ingress, install scripts, and discovery paths.
363
364
### Custom Frontend
365
docs/src/routes/portal-agent/+page.md
+3
-3
@@ -126,7 +126,7 @@ Dashboard panes:
126
| Pane | Purpose |
127
|------|---------|
128
| Tunnels | Add, select, and delete simple target tunnels |
129
-| Settings | Edit max active relays, public metadata, and x402 facilitator URLs for x402 routes |
129
+| Settings | Edit max active relays and public metadata |
130
| Relays | Connect or disconnect relays for the selected tunnel |
131
| Multi-hop | Build and apply an ordered multi-hop route |
132
@@ -215,7 +215,7 @@ Control endpoints:
215
| `GET` | `/agent/status` | Bearer token or wallet session | Read agent and tunnel status |
216
| `POST` | `/agent/shutdown` | Bearer token | Ask the agent to stop |
217
| `POST` | `/agent/tunnels` | Bearer token | Add a simple target tunnel |
218
-| `PATCH` | `/agent/tunnels/{id}` | Bearer token | Update metadata, max active relays, or x402 facilitator URL |
218
+| `PATCH` | `/agent/tunnels/{id}` | Bearer token | Update metadata or max active relays |
219
| `DELETE` | `/agent/tunnels/{id}` | Bearer token | Delete a tunnel |
220
| `POST` | `/agent/tunnels/{id}/relays` | Bearer token | Connect a relay |
221
| `DELETE` | `/agent/tunnels/{id}/relays` | Bearer token | Disconnect a relay |
@@ -260,5 +260,5 @@ transport disabled on the relay, or an invalid multi-hop route.
260
## Next Steps
261
262
- [Configuration Reference](/configuration#configtoml): every agent config field
263
-- [Wallet and ENS](/wallet-and-ens): wallet auth and ENS gasless behavior
263
+- [Wallet and ENS](/wallet-and-ens): admin tokens, wallet auth, and ENS gasless behavior
264
- [CLI Reference](/cli-reference): command flags and examples
docs/src/routes/prerequisites/+page.md
+3
-2
@@ -37,11 +37,12 @@ If you plan to run your own relay server:
37
38
## Optional
39
40
-- Ethereum wallet for relay admin login or optional local agent status access
40
+- Long random admin token for relay admin access
41
+- Ethereum wallet for optional local agent status access
42
- DNS provider account for relay-managed ACME, ECH DNS records, and optional ENS
43
gasless DNS import
44
45
## Next Steps
46
47
- [Getting Started](/getting-started): install Portal and create your first tunnel
47
-- [Wallet and ENS](/wallet-and-ens): understand wallet auth and ENS gasless DNS
48
+- [Wallet and ENS](/wallet-and-ens): understand admin tokens, wallet auth, and ENS gasless DNS
docs/src/routes/security-model/+page.md
+4
-3
@@ -64,11 +64,12 @@ Raw TCP and UDP port transports do not add tenant TLS. Use application-level enc
64
65
Registration uses a SIWE challenge signed by the SDK's secp256k1 identity key. The key is loaded from `identity.json` either as a raw secp256k1 `private_key` or derived from a BIP-39 `mnemonic` and `derivation_path`. The relay then issues a lease-scoped ES256K access token used by renew, unregister, reverse connect, and QUIC datagram authentication.
66
67
-Browser wallet login is a separate admin/status mechanism. It does not replace
68
-the local tunnel identity used for lease registration.
67
+Relay admin token login and optional local agent wallet login are separate from
68
+lease registration. They do not replace the local tunnel identity used for
69
+registration.
70
71
## Next Steps
72
73
- [Architecture](/architecture) - deep dive into Portal's internal design
73
-- [Wallet and ENS](/wallet-and-ens) - wallet auth and ENS gasless DNS import
74
+- [Wallet and ENS](/wallet-and-ens) - admin tokens, wallet auth, and ENS gasless DNS import
75
- [Self-Hosting](/self-hosting) - run your own relay server
docs/src/routes/self-hosting/+page.md
+5
-42
@@ -38,12 +38,13 @@ docker run -d \
38
-p 4017:4017 \
39
-e PORTAL_URL=https://relay.example.com:4017 \
40
-e IDENTITY_PATH=/portal-certs \
41
+ -e ADMIN_TOKEN="$(openssl rand -hex 32)" \
42
-v $(pwd)/relay-data:/portal-certs \
43
ghcr.io/gosuda/portal:2
44
```
45
45
-Replace `relay.example.com` with your domain. The relay identity address is
46
-allowed to use relay admin auth by default.
46
+Replace `relay.example.com` with your domain. Keep the generated
47
+`ADMIN_TOKEN`; it is required for relay admin and policy access.
48
49
## Docker Compose Setup
50
@@ -63,6 +64,7 @@ services:
64
API_PORT: "4017"
65
SNI_PORT: "443"
66
IDENTITY_PATH: /portal-certs
67
+ ADMIN_TOKEN: ${ADMIN_TOKEN}
68
volumes:
69
- ./relay-data:/portal-certs
70
```
@@ -81,6 +83,7 @@ docker compose up -d
83
| `API_PORT` | `4017` | Admin/API server port. |
84
| `SNI_PORT` | `443` | TCP SNI router port for tunnel traffic. |
85
| `IDENTITY_PATH` | `./.portal-certs` | Relay state directory containing `identity.json`, `policy.json`, and TLS materials. |
86
+| `ADMIN_TOKEN` | | Bearer token source for relay admin and policy APIs. |
87
88
## Connecting Your Tunnel
89
@@ -139,46 +142,6 @@ ports:
142
143
See [TCP/UDP Tunneling](/tcp-udp-tunneling) for usage details.
144
142
-## Optional: Enable x402 Facilitator
143
-
144
-The relay can expose a relay-local x402 facilitator at `/api/x402`. Frontends and
145
-configuration tools can read `/sdk/domain` for the current relay's facilitator
146
-URL and network, then call `/api/x402/supported` for mechanism details when needed.
147
-
148
-```yaml
149
-environment:
150
- X402_FACILITATOR_ENABLED: "true"
151
- X402_NETWORK: eip155:8453
152
- X402_RPC_URL: https://base-mainnet.example
153
-```
154
-
155
-The relay-local facilitator uses the relay identity private key from
156
-`IDENTITY_PATH/identity.json`. Fund that identity only as required for
157
-settlement gas.
158
-
159
-For CLI-created x402 routes, pass the selected facilitator explicitly:
160
-
161
-```bash
162
-portal expose 3000 \
163
- --relays https://relay.example.com:4017 \
164
- --discovery=false \
165
- --x402-facilitator-url https://relay.example.com:4017/api/x402 \
166
- --x402-network eip155:8453 \
167
- --x402-price "$0.001"
168
-```
169
-
170
-Native Go apps can use the same relay facilitator without putting payment
171
-policy in the tunnel config. The payment app includes a native paid image route:
172
-
173
-```bash
174
-go run ./cmd/payment-app \
175
- --relays https://relay.example.com:4017 \
176
- --discovery=false \
177
- --x402-facilitator-url https://relay.example.com:4017/api/x402 \
178
- --x402-network eip155:8453 \
179
- --x402-price "$0.01"
180
-```
181
-
145
## Troubleshooting
146
147
**Port already in use**
docs/src/routes/siwe-authentication/+page.md
+4
-12
@@ -1,6 +1,6 @@
1
---
2
title: SIWE Authentication
3
-description: How Portal uses SIWE for tunnel registration and wallet login.
3
+description: How Portal uses SIWE for tunnel registration and local agent wallet status access.
4
---
5
6
# SIWE Authentication
@@ -8,7 +8,7 @@ description: How Portal uses SIWE for tunnel registration and wallet login.
8
Portal uses Sign-In with Ethereum (SIWE) in two places:
9
10
- tunnel registration, signed automatically by the local tunnel identity
11
-- browser wallet login for relay admin and optional local agent status access
11
+- optional browser wallet login for local agent status access
12
13
For the full operational guide, see [Wallet and ENS](/wallet-and-ens).
14
@@ -32,17 +32,9 @@ portal expose 3000 --name myapp
32
There is no `--auth siwe` flag. SIWE is part of the normal registration
33
protocol.
34
35
-## Wallet Login
35
+## Agent Wallet Status Access
36
37
-The relay admin UI uses browser wallet login:
38
-
39
-1. request `/api/admin/auth/challenge`
40
-2. sign the returned SIWE message with the connected wallet
41
-3. submit `/api/admin/auth/login`
42
-4. use the returned `access_token` as `Authorization: Bearer <access_token>`
43
-
44
-The relay identity address is allowed by default. Add more admin wallets with
45
-`ADMIN_WALLETS`.
37
+Relay admin access uses `ADMIN_TOKEN`, not SIWE.
38
39
The local agent also exposes `/agent/auth/*` wallet endpoints. Agent wallet
40
sessions can read `/agent/status`; tunnel mutations still require the local
docs/src/routes/wallet-and-ens/+page.md
+14
-28
@@ -1,6 +1,6 @@
1
---
2
title: Wallet and ENS
3
-description: How Portal uses local identities, wallet login, SIWE, and ENS gasless DNS import.
3
+description: How Portal uses local identities, admin tokens, SIWE, and ENS gasless DNS import.
4
---
5
6
# Wallet and ENS
@@ -13,8 +13,8 @@ related, but they do not all mean "connect a browser wallet".
13
| Surface | Key material | Purpose |
14
|---------|--------------|---------|
15
| Tunnel identity | Local `identity.json` secp256k1 private key, or BIP-39 mnemonic plus derivation path | Signs SIWE lease registration challenges |
16
-| Relay identity | Relay `IDENTITY_PATH/identity.json` secp256k1 private key, or BIP-39 mnemonic plus derivation path | Signs relay descriptors, admin default wallet, lease access tokens, and ENS base-domain address |
17
-| Relay admin wallet | Browser wallet address allowlist | Signs in to `/admin` and receives an admin bearer token |
16
+| Relay identity | Relay `IDENTITY_PATH/identity.json` secp256k1 private key, or BIP-39 mnemonic plus derivation path | Signs relay descriptors, lease access tokens, and ENS base-domain address |
17
+| Relay admin token | `ADMIN_TOKEN` | Signs in to `/admin` and authorizes relay policy changes |
18
| Agent wallet | Optional browser wallet allowlist | Reads loopback agent status through `/agent/status` |
19
| ENS gasless DNS | DNSSEC plus `ENS1 ...` TXT records | Lets ENS-aware clients resolve the relay domain and lease hostnames to Portal identities |
20
@@ -54,39 +54,25 @@ portal expose 3000 \
54
The public lease name is a single DNS label such as `myapp`. It is not an ENS
55
name such as `alice.eth`.
56
57
-## Relay Admin Wallet Login
57
+## Relay Admin Token Login
58
59
-The relay admin API uses browser wallet login. The relay creates a SIWE
60
-challenge for the connected wallet and returns an admin bearer token after the
61
-signature verifies.
62
-
63
-Allowed admin wallets:
64
-
65
-- the relay identity address is always allowed
66
-- additional wallets come from `ADMIN_WALLETS`
59
+The relay admin API uses a configured token. Set `ADMIN_TOKEN` to a long random
60
+value before exposing the admin UI or policy API.
61
62
Example:
63
64
```bash
71
-ADMIN_WALLETS=0x1234567890abcdef1234567890abcdef12345678,0xabcdefabcdefabcdefabcdefabcdefabcdefabcd
72
-```
73
-
74
-To find the relay identity address:
75
-
76
-```bash
77
-jq -r .address .portal-certs/identity.json
65
+ADMIN_TOKEN=$(openssl rand -hex 32)
66
```
67
80
-Admin wallet flow:
68
+Admin token flow:
69
82
-1. `POST /api/admin/auth/challenge` with `{ "address": "0x..." }`.
83
-2. Sign the returned `siwe_message` in the browser wallet.
84
-3. `POST /api/admin/auth/login` with the challenge id, exact SIWE message, and
85
- signature.
86
-4. The relay returns an `access_token`.
87
-5. Admin endpoints require `Authorization: Bearer <access_token>`.
70
+1. `POST /api/admin/auth/login` with `{ "token": "<admin-token>" }`.
71
+2. The relay returns an `access_token`.
72
+3. Admin endpoints require `Authorization: Bearer <access_token>`.
73
89
-Challenges expire after two minutes. Admin bearer tokens expire after 24 hours.
74
+The token returned by login is the configured admin token; browser logout clears
75
+the local stored token.
76
77
## Agent Wallet Login
78
@@ -120,7 +106,7 @@ See [Portal Agent](/portal-agent) for the control API details.
106
## ENS Gasless DNS Import
107
108
ENS gasless DNS import is optional relay-side DNS automation. It is separate
123
-from tunnel registration and admin wallet login.
109
+from tunnel registration and admin token login.
110
111
When enabled, Portal uses the configured DNS provider to:
112
frontend/README.md
+1
-1
@@ -114,7 +114,7 @@ Relay server exposes:
114
- `/` - relay API identity response
115
- `/api/state` - public leases
116
- `/api/install.sh` and `/api/install.ps1` - CLI installers
117
-- `/api/admin/auth/*` - admin wallet auth endpoints
117
+- `/api/admin/auth/*` - admin token auth endpoints
118
- `/api/policy/*` - relay policy endpoints
119
- `/sdk/*` - SDK/control endpoints
120
- `/discovery` - relay discovery when enabled
frontend/package-lock.json
+1
-226
@@ -24,9 +24,7 @@
24
"react": "^19.2.1",
25
"react-dom": "^19.2.1",
26
"react-router-dom": "^7.10.1",
27
- "tailwind-merge": "^3.4.0",
28
- "viem": "^2.48.11",
29
- "wagmi": "^3.6.14"
27
+ "tailwind-merge": "^3.4.0"
28
},
29
"devDependencies": {
30
"@tailwindcss/vite": "^4.1.17",
@@ -3445,109 +3443,6 @@
3443
"url": "https://opencollective.com/vitest"
3444
}
3445
},
3448
- "node_modules/@wagmi/connectors": {
3449
- "version": "8.0.13",
3450
- "resolved": "https://registry.npmjs.org/@wagmi/connectors/-/connectors-8.0.13.tgz",
3451
- "integrity": "sha512-uRWutZygoX5K1Vk4svKeMxmyeTEjdZn8f/+EDKQEQaFMvy6yMVlLpy5DKFK0NRjtTIinRrqVccDqJQYCcC62hw==",
3452
- "license": "MIT",
3453
- "funding": {
3454
- "url": "https://github.com/sponsors/wevm"
3455
- },
3456
- "peerDependencies": {
3457
- "@base-org/account": "^2.5.1",
3458
- "@coinbase/wallet-sdk": "^4.3.6",
3459
- "@metamask/connect-evm": "^1.0.0",
3460
- "@safe-global/safe-apps-provider": "~0.18.6",
3461
- "@safe-global/safe-apps-sdk": "^9.1.0",
3462
- "@wagmi/core": "3.4.11",
3463
- "@walletconnect/ethereum-provider": "^2.21.1",
3464
- "accounts": "~0.10",
3465
- "porto": "~0.2.35",
3466
- "typescript": ">=5.7.3",
3467
- "viem": "2.x"
3468
- },
3469
- "peerDependenciesMeta": {
3470
- "@base-org/account": {
3471
- "optional": true
3472
- },
3473
- "@coinbase/wallet-sdk": {
3474
- "optional": true
3475
- },
3476
- "@metamask/connect-evm": {
3477
- "optional": true
3478
- },
3479
- "@safe-global/safe-apps-provider": {
3480
- "optional": true
3481
- },
3482
- "@safe-global/safe-apps-sdk": {
3483
- "optional": true
3484
- },
3485
- "@walletconnect/ethereum-provider": {
3486
- "optional": true
3487
- },
3488
- "accounts": {
3489
- "optional": true
3490
- },
3491
- "porto": {
3492
- "optional": true
3493
- },
3494
- "typescript": {
3495
- "optional": true
3496
- }
3497
- }
3498
- },
3499
- "node_modules/@wagmi/core": {
3500
- "version": "3.4.11",
3501
- "resolved": "https://registry.npmjs.org/@wagmi/core/-/core-3.4.11.tgz",
3502
- "integrity": "sha512-dmrZr0fKxCmG3pyWO9//oYtE5cgWRrgaQjC0+jX6IiuufE4MSW7gSL2MKdK8bvT3jg4Ra8cdRIJpveHJSHKdNw==",
3503
- "license": "MIT",
3504
- "dependencies": {
3505
- "eventemitter3": "5.0.1",
3506
- "mipd": "0.0.7",
3507
- "zustand": "5.0.0"
3508
- },
3509
- "funding": {
3510
- "url": "https://github.com/sponsors/wevm"
3511
- },
3512
- "peerDependencies": {
3513
- "@tanstack/query-core": ">=5.0.0",
3514
- "accounts": "~0.8.1",
3515
- "typescript": ">=5.7.3",
3516
- "viem": "2.x"
3517
- },
3518
- "peerDependenciesMeta": {
3519
- "@tanstack/query-core": {
3520
- "optional": true
3521
- },
3522
- "accounts": {
3523
- "optional": true
3524
- },
3525
- "typescript": {
3526
- "optional": true
3527
- }
3528
- }
3529
- },
3530
- "node_modules/abitype": {
3531
- "version": "1.2.3",
3532
- "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz",
3533
- "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==",
3534
- "license": "MIT",
3535
- "funding": {
3536
- "url": "https://github.com/sponsors/wevm"
3537
- },
3538
- "peerDependencies": {
3539
- "typescript": ">=5.0.4",
3540
- "zod": "^3.22.0 || ^4.0.0"
3541
- },
3542
- "peerDependenciesMeta": {
3543
- "typescript": {
3544
- "optional": true
3545
- },
3546
- "zod": {
3547
- "optional": true
3548
- }
3549
- }
3550
- },
3446
"node_modules/acorn": {
3447
"version": "8.16.0",
3448
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
@@ -4722,21 +4617,6 @@
4617
"dev": true,
4618
"license": "ISC"
4619
},
4725
- "node_modules/isows": {
4726
- "version": "1.0.7",
4727
- "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz",
4728
- "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==",
4729
- "funding": [
4730
- {
4731
- "type": "github",
4732
- "url": "https://github.com/sponsors/wevm"
4733
- }
4734
- ],
4735
- "license": "MIT",
4736
- "peerDependencies": {
4737
- "ws": "*"
4738
- }
4739
- },
4620
"node_modules/jiti": {
4621
"version": "2.6.1",
4622
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
@@ -5226,26 +5106,6 @@
5106
"url": "https://github.com/sponsors/isaacs"
5107
}
5108
},
5229
- "node_modules/mipd": {
5230
- "version": "0.0.7",
5231
- "resolved": "https://registry.npmjs.org/mipd/-/mipd-0.0.7.tgz",
5232
- "integrity": "sha512-aAPZPNDQ3uMTdKbuO2YmAw2TxLHO0moa4YKAyETM/DTj5FloZo+a+8tU+iv4GmW+sOxKLSRwcSFuczk+Cpt6fg==",
5233
- "funding": [
5234
- {
5235
- "type": "github",
5236
- "url": "https://github.com/sponsors/wagmi-dev"
5237
- }
5238
- ],
5239
- "license": "MIT",
5240
- "peerDependencies": {
5241
- "typescript": ">=5.0.4"
5242
- },
5243
- "peerDependenciesMeta": {
5244
- "typescript": {
5245
- "optional": true
5246
- }
5247
- }
5248
- },
5109
"node_modules/ms": {
5110
"version": "2.1.3",
5111
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -5315,36 +5175,6 @@
5175
"node": ">= 0.8.0"
5176
}
5177
},
5318
- "node_modules/ox": {
5319
- "version": "0.14.20",
5320
- "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.20.tgz",
5321
- "integrity": "sha512-rby38C3nDn8eQkf29Zgw4hkCZJ64Qqi0zRPWL8ENUQ7JVuoITqrVtwWQgM/He19SCMUEc7hS/Sjw0jIOSLJhOw==",
5322
- "funding": [
5323
- {
5324
- "type": "github",
5325
- "url": "https://github.com/sponsors/wevm"
5326
- }
5327
- ],
5328
- "license": "MIT",
5329
- "dependencies": {
5330
- "@adraffy/ens-normalize": "^1.11.0",
5331
- "@noble/ciphers": "^1.3.0",
5332
- "@noble/curves": "1.9.1",
5333
- "@noble/hashes": "^1.8.0",
5334
- "@scure/bip32": "^1.7.0",
5335
- "@scure/bip39": "^1.6.0",
5336
- "abitype": "^1.2.3",
5337
- "eventemitter3": "5.0.1"
5338
- },
5339
- "peerDependencies": {
5340
- "typescript": ">=5.4.0"
5341
- },
5342
- "peerDependenciesMeta": {
5343
- "typescript": {
5344
- "optional": true
5345
- }
5346
- }
5347
- },
5178
"node_modules/p-limit": {
5179
"version": "3.1.0",
5180
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -6142,36 +5972,6 @@
5972
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
5973
}
5974
},
6145
- "node_modules/viem": {
6146
- "version": "2.48.11",
6147
- "resolved": "https://registry.npmjs.org/viem/-/viem-2.48.11.tgz",
6148
- "integrity": "sha512-+WZ5E0dBS6GtKb+1wEk5DeYRRRW42+pFnXCo67Ydodf42sBwO+hu3wnQy66lc4MKmHz+llPVdbyehYr9oTE2iw==",
6149
- "funding": [
6150
- {
6151
- "type": "github",
6152
- "url": "https://github.com/sponsors/wevm"
6153
- }
6154
- ],
6155
- "license": "MIT",
6156
- "dependencies": {
6157
- "@noble/curves": "1.9.1",
6158
- "@noble/hashes": "1.8.0",
6159
- "@scure/bip32": "1.7.0",
6160
- "@scure/bip39": "1.6.0",
6161
- "abitype": "1.2.3",
6162
- "isows": "1.0.7",
6163
- "ox": "0.14.20",
6164
- "ws": "8.18.3"
6165
- },
6166
- "peerDependencies": {
6167
- "typescript": ">=5.0.4"
6168
- },
6169
- "peerDependenciesMeta": {
6170
- "typescript": {
6171
- "optional": true
6172
- }
6173
- }
6174
- },
5975
"node_modules/vite": {
5976
"version": "7.3.1",
5977
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
@@ -6338,31 +6138,6 @@
6138
"node": ">=18"
6139
}
6140
},
6341
- "node_modules/wagmi": {
6342
- "version": "3.6.14",
6343
- "resolved": "https://registry.npmjs.org/wagmi/-/wagmi-3.6.14.tgz",
6344
- "integrity": "sha512-kHRaaD9DeFaUN6WcVMEKApTwVwImt0fGINh04Z4WYXLJhkb0rBxG4js+3t8EzXsV/veec5dV90j7jfGwjGSsqQ==",
6345
- "license": "MIT",
6346
- "dependencies": {
6347
- "@wagmi/connectors": "8.0.13",
6348
- "@wagmi/core": "3.4.11",
6349
- "use-sync-external-store": "1.4.0"
6350
- },
6351
- "funding": {
6352
- "url": "https://github.com/sponsors/wevm"
6353
- },
6354
- "peerDependencies": {
6355
- "@tanstack/react-query": ">=5.0.0",
6356
- "react": ">=18",
6357
- "typescript": ">=5.7.3",
6358
- "viem": "2.x"
6359
- },
6360
- "peerDependenciesMeta": {
6361
- "typescript": {
6362
- "optional": true
6363
- }
6364
- }
6365
- },
6141
"node_modules/webidl-conversions": {
6142
"version": "8.0.1",
6143
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
frontend/package.json
+1
-3
@@ -32,9 +32,7 @@
32
"react": "^19.2.1",
33
"react-dom": "^19.2.1",
34
"react-router-dom": "^7.10.1",
35
- "tailwind-merge": "^3.4.0",
36
- "viem": "^2.48.11",
37
- "wagmi": "^3.6.14"
35
+ "tailwind-merge": "^3.4.0"
36
},
37
"devDependencies": {
38
"@tailwindcss/vite": "^4.1.17",
frontend/src/components/Header.tsx
+62
-80
@@ -1,11 +1,11 @@
1
import { useEffect, useState } from "react";
2
-import { Loader2, LogOut, Wallet } from "lucide-react";
2
+import { KeyRound, Loader2, LogOut } from "lucide-react";
3
import { Button } from "@/components/ui/button";
4
import { ThemeToggleButton } from "@/components/ThemeToggleButton";
5
import { useAuth } from "@/hooks/useAuth";
6
import { apiClient } from "@/lib/apiClient";
7
import { BROWSER_API_PATHS } from "@/lib/apiPaths";
8
-import type { DomainResponse, X402FacilitatorInfo } from "@/types/api";
8
+import type { DomainResponse } from "@/types/api";
9
import {
10
Tooltip,
11
TooltipContent,
@@ -22,18 +22,6 @@ interface HeaderProps {
22
23
const repoURL = "https://github.com/gosuda/portal-tunnel";
24
25
-function formatWalletAddress(address: string): string {
26
- const trimmed = address.trim();
27
- if (trimmed.length <= 12) {
28
- return trimmed;
29
- }
30
- return `${trimmed.slice(0, 6)}...${trimmed.slice(-4)}`;
31
-}
32
-
33
-function facilitatorNetworkLabel(x402: X402FacilitatorInfo): string {
34
- return x402.network_name?.trim() || x402.network?.trim() || "enabled";
35
-}
36
-
25
export function Header({
26
title = "PORTAL",
27
isAdmin,
@@ -42,23 +30,23 @@ export function Header({
30
}: HeaderProps) {
31
const [releaseVersion, setReleaseVersion] = useState("");
32
const [ensVerified, setENSVerified] = useState(false);
45
- const [x402, setX402] = useState<X402FacilitatorInfo | null>(null);
33
const {
34
isAuthenticated,
35
isLoading,
49
- walletAddress,
36
login,
37
logout,
38
} = useAuth();
39
+ const [adminToken, setAdminToken] = useState("");
40
const [authError, setAuthError] = useState("");
41
55
- const handleWalletLogin = async () => {
42
+ const handleAdminLogin = async () => {
43
setAuthError("");
57
- const result = await login();
44
+ const result = await login(adminToken);
45
if (!result.success) {
59
- setAuthError(result.error || "Wallet login failed.");
46
+ setAuthError(result.error || "Admin login failed.");
47
return;
48
}
49
+ setAdminToken("");
50
await onAuthChange?.();
51
};
52
@@ -68,13 +56,6 @@ export function Header({
56
await onAuthChange?.();
57
};
58
71
- const walletLabel = isAuthenticated && walletAddress
72
- ? formatWalletAddress(walletAddress)
73
- : "Wallet";
74
- const walletTooltip = authError || (
75
- isAuthenticated && walletAddress ? walletAddress : "Connect wallet"
76
- );
77
-
59
useEffect(() => {
60
let cancelled = false;
61
@@ -90,13 +71,11 @@ export function Header({
71
: ""
72
);
73
setENSVerified(status?.ens?.verified === true);
93
- setX402(status?.x402?.enabled === true ? status.x402 : null);
74
}
75
} catch {
76
if (!cancelled) {
77
setReleaseVersion("");
78
setENSVerified(false);
99
- setX402(null);
79
}
80
}
81
})();
@@ -140,14 +119,6 @@ export function Header({
119
ENS verified
120
</span>
121
)}
143
- {x402 && (
144
- <span
145
- className="inline-flex h-6 max-w-40 cursor-default items-center overflow-hidden text-ellipsis whitespace-nowrap rounded-full bg-emerald-500/10 px-2.5 text-xs font-semibold text-emerald-700 ring-1 ring-emerald-500/20 dark:text-emerald-300"
146
- title="x402 facilitator enabled"
147
- >
148
- x402 {facilitatorNetworkLabel(x402)}
149
- </span>
150
- )}
122
</div>
123
</div>
124
</div>
@@ -200,55 +171,66 @@ export function Header({
171
172
<ThemeToggleButton className="inline-flex shrink-0" />
173
203
- <TooltipProvider>
204
- <Tooltip>
205
- <TooltipTrigger asChild>
206
- <Button
207
- variant={isAuthenticated ? "secondary" : "outline"}
208
- onClick={isAuthenticated ? undefined : handleWalletLogin}
209
- disabled={isLoading}
210
- className={`h-12 rounded-full border-border/70 bg-background/90 px-3 text-foreground shadow-sm transition-all hover:bg-background disabled:cursor-not-allowed sm:px-4 ${
211
- isAuthenticated
212
- ? "cursor-default"
213
- : "cursor-pointer hover:-translate-y-0.5 hover:border-primary/40 hover:text-primary"
214
- }`}
215
- aria-label={isAuthenticated ? "Wallet connected" : "Connect wallet"}
216
- >
217
- {isLoading ? (
218
- <Loader2 className="h-5 w-5 animate-spin" />
219
- ) : (
220
- <Wallet className="h-5 w-5" />
221
- )}
222
- <span className="max-w-28 truncate font-mono text-xs sm:max-w-36">
223
- {walletLabel}
224
- </span>
225
- </Button>
226
- </TooltipTrigger>
227
- <TooltipContent>
228
- <p>{walletTooltip}</p>
229
- </TooltipContent>
230
- </Tooltip>
231
- </TooltipProvider>
174
+ {isAdmin && (
175
+ <div className="flex max-w-full flex-wrap items-center justify-end gap-2">
176
+ {authError && (
177
+ <span className="max-w-64 text-right text-xs font-medium text-destructive">
178
+ {authError}
179
+ </span>
180
+ )}
181
233
- {isAuthenticated && (
234
- <TooltipProvider>
235
- <Tooltip>
236
- <TooltipTrigger asChild>
182
+ {!isAuthenticated ? (
183
+ <form
184
+ className="flex max-w-full flex-wrap items-center justify-end gap-2"
185
+ onSubmit={(event) => {
186
+ event.preventDefault();
187
+ void handleAdminLogin();
188
+ }}
189
+ >
190
+ <input
191
+ type="password"
192
+ value={adminToken}
193
+ onChange={(event) => setAdminToken(event.target.value)}
194
+ placeholder="Admin token"
195
+ autoComplete="current-password"
196
+ className="h-12 w-44 rounded-full border border-border/70 bg-background/90 px-4 text-sm text-foreground shadow-sm outline-none transition focus:border-primary/50 sm:w-56"
197
+ disabled={isLoading}
198
+ />
199
<Button
200
+ type="submit"
201
variant="outline"
239
- size="icon"
240
- onClick={handleLogout}
241
- className="h-12 w-12 cursor-pointer rounded-full border-border/70 bg-background/90 text-foreground shadow-sm transition-all hover:-translate-y-0.5 hover:border-destructive/40 hover:bg-background hover:text-destructive"
242
- aria-label="Logout"
202
+ disabled={isLoading}
203
+ className="h-12 rounded-full border-border/70 bg-background/90 px-4 text-foreground shadow-sm transition-all hover:-translate-y-0.5 hover:border-primary/40 hover:text-primary disabled:cursor-not-allowed"
204
>
244
- <LogOut className="h-5 w-5" />
205
+ {isLoading ? (
206
+ <Loader2 className="h-5 w-5 animate-spin" />
207
+ ) : (
208
+ <KeyRound className="h-5 w-5" />
209
+ )}
210
+ <span className="text-sm font-semibold">Login</span>
211
</Button>
246
- </TooltipTrigger>
247
- <TooltipContent>
248
- <p>Logout</p>
249
- </TooltipContent>
250
- </Tooltip>
251
- </TooltipProvider>
212
+ </form>
213
+ ) : (
214
+ <TooltipProvider>
215
+ <Tooltip>
216
+ <TooltipTrigger asChild>
217
+ <Button
218
+ variant="outline"
219
+ size="icon"
220
+ onClick={handleLogout}
221
+ className="h-12 w-12 cursor-pointer rounded-full border-border/70 bg-background/90 text-foreground shadow-sm transition-all hover:-translate-y-0.5 hover:border-destructive/40 hover:bg-background hover:text-destructive"
222
+ aria-label="Logout"
223
+ >
224
+ <LogOut className="h-5 w-5" />
225
+ </Button>
226
+ </TooltipTrigger>
227
+ <TooltipContent>
228
+ <p>Logout</p>
229
+ </TooltipContent>
230
+ </Tooltip>
231
+ </TooltipProvider>
232
+ )}
233
+ </div>
234
)}
235
</div>
236
</header>
frontend/src/hooks/useAuth.ts
+35
-70
@@ -1,24 +1,16 @@
1
import { useEffect, useState } from "react";
2
-import {
3
- useAccount,
4
- useConnect,
5
- useConnectors,
6
- useDisconnect,
7
- useSignMessage,
8
-} from "wagmi";
2
import { BROWSER_API_PATHS } from "@/lib/apiPaths";
3
import { APIClientError, apiClient } from "@/lib/apiClient";
11
-import { writeAdminAuthToken } from "@/lib/adminAuthToken";
4
+import { readAdminAuthToken, writeAdminAuthToken } from "@/lib/adminAuthToken";
5
import type {
13
- WalletAuthChallengeResponse,
14
- WalletAuthLoginResponse,
15
- WalletAuthStatusResponse,
6
+ AdminAuthLoginRequest,
7
+ AdminAuthLoginResponse,
8
+ AdminAuthStatusResponse,
9
} from "@/types/api";
10
11
interface AuthState {
12
isAuthenticated: boolean;
13
isLoading: boolean;
21
- walletAddress: string;
14
}
15
16
interface LoginResult {
@@ -30,35 +22,42 @@ function emptyAuthState(): AuthState {
22
return {
23
isAuthenticated: false,
24
isLoading: false,
33
- walletAddress: "",
25
};
26
}
27
28
+function authErrorMessage(err: unknown): string {
29
+ if (err instanceof APIClientError) {
30
+ return err.message || "Admin login failed.";
31
+ }
32
+ return err instanceof Error ? err.message : "Admin login failed.";
33
+}
34
+
35
async function fetchAuthState(): Promise<AuthState> {
36
+ if (!readAdminAuthToken()) {
37
+ return emptyAuthState();
38
+ }
39
try {
39
- const data = await apiClient.get<WalletAuthStatusResponse>(
40
+ const data = await apiClient.get<AdminAuthStatusResponse>(
41
BROWSER_API_PATHS.admin.authStatus
42
);
43
+ if (!data.authenticated) {
44
+ writeAdminAuthToken("");
45
+ return emptyAuthState();
46
+ }
47
return {
48
isAuthenticated: data.authenticated,
49
isLoading: false,
45
- walletAddress: data.wallet_address || "",
50
};
51
} catch {
52
+ writeAdminAuthToken("");
53
return emptyAuthState();
54
}
55
}
56
57
export function useAuth() {
53
- const { address: connectedAddress, isConnected } = useAccount();
54
- const connectors = useConnectors();
55
- const { connectAsync } = useConnect();
56
- const { disconnectAsync } = useDisconnect();
57
- const { signMessageAsync } = useSignMessage();
58
const [authState, setAuthState] = useState<AuthState>({
59
isAuthenticated: false,
60
isLoading: true,
61
- walletAddress: "",
61
});
62
63
const checkAuth = async () => {
@@ -71,35 +70,16 @@ export function useAuth() {
70
})();
71
}, []);
72
74
- const login = async (): Promise<LoginResult> => {
73
+ const login = async (token: string): Promise<LoginResult> => {
74
+ const trimmed = token.trim();
75
+ if (!trimmed) {
76
+ return { success: false, error: "Admin token is required." };
77
+ }
78
try {
76
- let address = connectedAddress;
77
- if (!isConnected || !address) {
78
- const connector = connectors[0];
79
- if (!connector) {
80
- return { success: false, error: "Wallet connector is unavailable." };
81
- }
82
- const connected = await connectAsync({ connector });
83
- address = connected.accounts[0];
84
- }
85
- if (!address) {
86
- return { success: false, error: "Wallet provider is unavailable." };
87
- }
88
- const challenge = await apiClient.post<WalletAuthChallengeResponse>(
89
- BROWSER_API_PATHS.admin.authChallenge,
90
- { address }
91
- );
92
- const signature = await signMessageAsync({
93
- account: address,
94
- message: challenge.siwe_message,
95
- });
96
- const data = await apiClient.post<WalletAuthLoginResponse>(
79
+ const body: AdminAuthLoginRequest = { token: trimmed };
80
+ const data = await apiClient.post<AdminAuthLoginResponse>(
81
BROWSER_API_PATHS.admin.authLogin,
98
- {
99
- challenge_id: challenge.challenge_id,
100
- siwe_message: challenge.siwe_message,
101
- siwe_signature: signature,
102
- }
82
+ body
83
);
84
const accessToken = data.access_token?.trim() || "";
85
if (!accessToken) {
@@ -107,24 +87,15 @@ export function useAuth() {
87
return { success: false, error: "Admin login did not return an access token." };
88
}
89
writeAdminAuthToken(accessToken);
110
- setAuthState((prev) => ({
111
- ...prev,
90
+ setAuthState({
91
isAuthenticated: true,
113
- walletAddress: data.wallet_address || address,
114
- }));
92
+ isLoading: false,
93
+ });
94
return { success: true };
95
} catch (err: unknown) {
117
- if (err instanceof APIClientError) {
118
- return {
119
- success: false,
120
- error: err.message || "Wallet login failed.",
121
- };
122
- }
123
-
124
- return {
125
- success: false,
126
- error: err instanceof Error ? err.message : "Wallet login failed.",
127
- };
96
+ writeAdminAuthToken("");
97
+ setAuthState(emptyAuthState());
98
+ return { success: false, error: authErrorMessage(err) };
99
}
100
};
101
@@ -136,18 +107,12 @@ export function useAuth() {
107
} finally {
108
writeAdminAuthToken("");
109
}
139
- setAuthState((prev) => ({ ...prev, isAuthenticated: false, walletAddress: "" }));
140
- try {
141
- await disconnectAsync();
142
- } catch {
143
- // Some wallet connectors cannot be disconnected programmatically.
144
- }
110
+ setAuthState(emptyAuthState());
111
};
112
113
return {
114
isAuthenticated: authState.isAuthenticated,
115
isLoading: authState.isLoading,
150
- walletAddress: authState.walletAddress,
116
login,
117
logout,
118
checkAuth,
frontend/src/lib/apiClient.ts
-1
@@ -120,7 +120,6 @@ async function request<T>(path: string, init: RequestInit): Promise<T> {
120
isPathOrChild(pathname, RELAY_API_PATHS.admin.root);
121
if (
122
requiresAdminAuth &&
123
- pathname !== BROWSER_API_PATHS.admin.authChallenge &&
123
pathname !== BROWSER_API_PATHS.admin.authLogin
124
) {
125
const token = readAdminAuthToken();
frontend/src/lib/apiPaths.ts
-1
@@ -4,7 +4,6 @@ export const RELAY_API_PATHS = {
4
},
5
admin: {
6
root: "/api/admin",
7
- authChallenge: "/api/admin/auth/challenge",
7
authLogin: "/api/admin/auth/login",
8
logout: "/api/admin/auth/logout",
9
authStatus: "/api/admin/auth/status",
frontend/src/lib/wagmi.ts
deleted
-11
@@ -1,11 +0,0 @@
1
-import { createConfig, http } from "wagmi";
2
-import { mainnet } from "wagmi/chains";
3
-import { injected } from "wagmi/connectors";
4
-
5
-export const wagmiConfig = createConfig({
6
- chains: [mainnet],
7
- connectors: [injected()],
8
- transports: {
9
- [mainnet.id]: http(),
10
- },
11
-});
frontend/src/main.tsx
+20
-24
@@ -3,9 +3,7 @@ import { hero } from "@ssgoi/react/view-transitions";
3
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4
import ReactDOM from "react-dom/client";
5
import { BrowserRouter } from "react-router-dom";
6
-import { WagmiProvider } from "wagmi";
6
import { ThemeProvider } from "@/components/ThemeProvider";
8
-import { wagmiConfig } from "@/lib/wagmi";
7
import App from "./App.tsx";
8
import "./index.css";
9
@@ -13,27 +11,25 @@ const queryClient = new QueryClient();
11
12
ReactDOM.createRoot(document.getElementById("root")!).render(
13
<ThemeProvider>
16
- <WagmiProvider config={wagmiConfig}>
17
- <QueryClientProvider client={queryClient}>
18
- <Ssgoi
19
- config={{
20
- transitions: [
21
- {
22
- from: "/",
23
- to: "/server/*",
24
- transition: hero(),
25
- symmetric: true,
26
- },
27
- ],
28
- }}
29
- >
30
- <div style={{ position: "relative", minHeight: "100vh" }}>
31
- <BrowserRouter>
32
- <App />
33
- </BrowserRouter>
34
- </div>
35
- </Ssgoi>
36
- </QueryClientProvider>
37
- </WagmiProvider>
14
+ <QueryClientProvider client={queryClient}>
15
+ <Ssgoi
16
+ config={{
17
+ transitions: [
18
+ {
19
+ from: "/",
20
+ to: "/server/*",
21
+ transition: hero(),
22
+ symmetric: true,
23
+ },
24
+ ],
25
+ }}
26
+ >
27
+ <div style={{ position: "relative", minHeight: "100vh" }}>
28
+ <BrowserRouter>
29
+ <App />
30
+ </BrowserRouter>
31
+ </div>
32
+ </Ssgoi>
33
+ </QueryClientProvider>
34
</ThemeProvider>
35
);
frontend/src/pages/Admin.tsx
+1
-1
@@ -70,7 +70,7 @@ export function Admin() {
70
</div>
71
<main className="mx-auto flex w-full max-w-6xl flex-1 items-center justify-center px-6 py-16">
72
<div className="rounded-lg border border-border bg-card px-6 py-5 text-center text-sm text-muted-foreground shadow-sm">
73
- Connect a wallet from the header to view admin controls.
73
+ Enter the admin token from the header to view admin controls.
74
</div>
75
</main>
76
</div>
frontend/src/types/api.ts
+4
-17
@@ -72,20 +72,16 @@ export interface PolicySettings {
72
tcp_port: PolicyPortSettings;
73
}
74
75
-export interface WalletAuthStatusResponse {
75
+export interface AdminAuthStatusResponse {
76
authenticated: boolean;
77
- wallet_address?: string;
77
}
78
80
-export interface WalletAuthChallengeResponse {
81
- challenge_id: string;
82
- expires_at: string;
83
- siwe_message: string;
79
+export interface AdminAuthLoginRequest {
80
+ token: string;
81
}
82
86
-export interface WalletAuthLoginResponse {
83
+export interface AdminAuthLoginResponse {
84
access_token?: string;
88
- wallet_address?: string;
85
}
86
87
export interface ENSStatus {
@@ -99,19 +95,10 @@ export interface ENSStatus {
95
last_error?: string;
96
}
97
102
-export interface X402FacilitatorInfo {
103
- enabled: boolean;
104
- url?: string;
105
- network?: string;
106
- network_name?: string;
107
- supported_url?: string;
108
-}
109
-
98
export interface DomainResponse {
99
protocol_version: string;
100
release_version: string;
101
ens: ENSStatus;
114
- x402: X402FacilitatorInfo;
102
}
103
104
export interface RelayDescriptor {
go.mod
-39
@@ -16,7 +16,6 @@ require (
16
github.com/go-acme/lego/v4 v4.34.0
17
github.com/go-jose/go-jose/v4 v4.1.4
18
github.com/gosuda/keyless_tls v0.0.2-0.20260507061030-5128be6b5008
19
- github.com/gosuda/x402-facilitator v0.0.0-20260413025142-cb6c4794b9a5
19
github.com/hashicorp/yamux v0.1.2
20
github.com/hetznercloud/hcloud-go/v2 v2.40.0
21
github.com/knadh/koanf/parsers/toml/v2 v2.2.0
@@ -31,7 +30,6 @@ require (
30
github.com/spruceid/siwe-go v0.2.1
31
github.com/tyler-smith/go-bip39 v1.1.0
32
github.com/vultr/govultr/v3 v3.30.0
34
- github.com/x402-foundation/x402/go v0.0.0-20260526081544-8cf020c5335e
33
golang.org/x/crypto v0.51.0
34
golang.org/x/mod v0.35.0
35
golang.org/x/net v0.55.0
@@ -45,12 +43,7 @@ require (
43
require (
44
cloud.google.com/go/auth v0.20.0 // indirect
45
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
48
- filippo.io/edwards25519 v1.0.0-rc.1 // indirect
49
- github.com/KyleBanks/depth v1.2.1 // indirect
50
- github.com/Microsoft/go-winio v0.6.2 // indirect
46
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect
52
- github.com/PuerkitoBio/purell v1.1.1 // indirect
53
- github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
47
github.com/atotto/clipboard v0.1.4 // indirect
48
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect
49
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
@@ -65,8 +58,6 @@ require (
58
github.com/aws/smithy-go v1.24.2 // indirect
59
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
60
github.com/beorn7/perks v1.0.1 // indirect
68
- github.com/bits-and-blooms/bitset v1.24.4 // indirect
69
- github.com/blocto/solana-go-sdk v1.30.0 // indirect
61
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
62
github.com/cespare/xxhash/v2 v2.3.0 // indirect
63
github.com/charmbracelet/colorprofile v0.4.1 // indirect
@@ -76,23 +67,13 @@ require (
67
github.com/clipperhouse/displaywidth v0.9.0 // indirect
68
github.com/clipperhouse/stringish v0.1.1 // indirect
69
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
79
- github.com/consensys/gnark-crypto v0.18.1 // indirect
80
- github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect
70
github.com/dchest/uniuri v1.2.0 // indirect
82
- github.com/deckarep/golang-set/v2 v2.8.0 // indirect
71
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
84
- github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect
72
github.com/ethereum/go-ethereum v1.17.1 // indirect
73
github.com/felixge/httpsnoop v1.0.4 // indirect
74
github.com/fsnotify/fsnotify v1.9.0 // indirect
88
- github.com/ghodss/yaml v1.0.0 // indirect
75
github.com/go-logr/logr v1.4.3 // indirect
76
github.com/go-logr/stdr v1.2.2 // indirect
91
- github.com/go-ole/go-ole v1.3.0 // indirect
92
- github.com/go-openapi/jsonpointer v0.19.5 // indirect
93
- github.com/go-openapi/jsonreference v0.19.6 // indirect
94
- github.com/go-openapi/spec v0.20.4 // indirect
95
- github.com/go-openapi/swag v0.19.15 // indirect
77
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
78
github.com/google/btree v1.1.2 // indirect
79
github.com/google/go-querystring v1.2.0 // indirect
@@ -100,16 +81,11 @@ require (
81
github.com/google/uuid v1.6.0 // indirect
82
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
83
github.com/googleapis/gax-go/v2 v2.21.0 // indirect
103
- github.com/gorilla/websocket v1.4.2 // indirect
84
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
85
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
86
github.com/holiman/uint256 v1.3.2 // indirect
107
- github.com/josharian/intern v1.0.0 // indirect
87
github.com/knadh/koanf/maps v0.1.2 // indirect
109
- github.com/labstack/echo/v4 v4.15.1 // indirect
110
- github.com/labstack/gommon v0.4.2 // indirect
88
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
112
- github.com/mailru/easyjson v0.7.7 // indirect
89
github.com/mattn/go-colorable v0.1.14 // indirect
90
github.com/mattn/go-isatty v0.0.21 // indirect
91
github.com/mattn/go-localereader v0.0.1 // indirect
@@ -117,7 +93,6 @@ require (
93
github.com/miekg/dns v1.1.72 // indirect
94
github.com/mitchellh/copystructure v1.2.0 // indirect
95
github.com/mitchellh/reflectwalk v1.0.2 // indirect
120
- github.com/mr-tron/base58 v1.2.0 // indirect
96
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
97
github.com/muesli/cancelreader v0.2.2 // indirect
98
github.com/muesli/termenv v0.16.0 // indirect
@@ -126,20 +101,7 @@ require (
101
github.com/prometheus/procfs v0.16.1 // indirect
102
github.com/relvacode/iso8601 v1.1.1-0.20210511065120-b30b151cc433 // indirect
103
github.com/rivo/uniseg v0.4.7 // indirect
129
- github.com/shirou/gopsutil v3.21.11+incompatible // indirect
130
- github.com/supranational/blst v0.3.16 // indirect
131
- github.com/swaggo/echo-swagger v1.4.1 // indirect
132
- github.com/swaggo/files/v2 v2.0.0 // indirect
133
- github.com/swaggo/swag v1.16.4 // indirect
134
- github.com/tklauser/go-sysconf v0.3.12 // indirect
135
- github.com/tklauser/numcpus v0.6.1 // indirect
136
- github.com/valyala/bytebufferpool v1.0.0 // indirect
137
- github.com/valyala/fasttemplate v1.2.2 // indirect
138
- github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
139
- github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
140
- github.com/xeipuuv/gojsonschema v1.2.0 // indirect
104
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
142
- github.com/yusufpapurcu/wmi v1.2.4 // indirect
105
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
106
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
107
go.opentelemetry.io/otel v1.43.0 // indirect
@@ -153,7 +115,6 @@ require (
115
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
116
google.golang.org/grpc v1.80.0 // indirect
117
google.golang.org/protobuf v1.36.11 // indirect
156
- gopkg.in/yaml.v2 v2.4.0 // indirect
118
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
119
)
120
go.sum
-214
@@ -4,22 +4,8 @@ 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
-filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
8
-filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
9
-github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
10
-github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
11
-github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
12
-github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
13
-github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
14
-github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
7
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU=
8
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
17
-github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
18
-github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
19
-github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
20
-github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
21
-github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
22
-github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
9
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
10
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
11
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
@@ -58,14 +44,8 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE
44
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
45
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
46
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
61
-github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
62
-github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
63
-github.com/blocto/solana-go-sdk v1.30.0 h1:GEh4GDjYk1lMhV/hqJDCyuDeCuc5dianbN33yxL88NU=
64
-github.com/blocto/solana-go-sdk v1.30.0/go.mod h1:Xoyhhb3hrGpEQ5rJps5a3OgMwDpmEhrd9bgzFKkkwMs=
47
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
48
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
67
-github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk=
68
-github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s=
49
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
50
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
51
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
@@ -88,66 +68,25 @@ github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfa
68
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
69
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
70
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
91
-github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
92
-github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8=
93
-github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4=
94
-github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M=
95
-github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
96
-github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
97
-github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw=
98
-github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo=
99
-github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
100
-github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
101
-github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
102
-github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
103
-github.com/consensys/gnark-crypto v0.18.1 h1:RyLV6UhPRoYYzaFnPQA4qK3DyuDgkTgskDdoGqFt3fI=
104
-github.com/consensys/gnark-crypto v0.18.1/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c=
71
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
106
-github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
107
-github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
108
-github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg=
109
-github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI=
110
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
111
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
112
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
72
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
73
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
115
-github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=
116
-github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc=
74
github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g=
75
github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY=
119
-github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ=
120
-github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
76
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
77
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
78
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
79
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
125
-github.com/deepmap/oapi-codegen v1.6.0 h1:w/d1ntwh91XI0b/8ja7+u5SvA4IFfM0UNNLmiDR1gg0=
126
-github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M=
127
-github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A=
128
-github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
80
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
81
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
131
-github.com/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn27fRjSls=
132
-github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw=
133
-github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk=
134
-github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8=
82
github.com/ethereum/go-ethereum v1.17.1 h1:IjlQDjgxg2uL+GzPRkygGULPMLzcYWncEI7wbaizvho=
83
github.com/ethereum/go-ethereum v1.17.1/go.mod h1:7UWOVHL7K3b8RfVRea022btnzLCaanwHtBuH1jUCH/I=
84
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
85
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
86
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
87
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
141
-github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY=
142
-github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg=
88
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
89
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
145
-github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI=
146
-github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww=
147
-github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
148
-github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
149
-github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
150
-github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
90
github.com/go-acme/lego/v4 v4.34.0 h1:oRsIuPJ4ORX7ufviXvelUpBSez2XxeKGwo5pNG9BVeY=
91
github.com/go-acme/lego/v4 v4.34.0/go.mod h1:gsmdlx/ZS6OUeXbOj0U+VnCLLfEFj4WCYRkcGpZw+pc=
92
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
@@ -157,32 +96,11 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
96
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
97
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
98
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
160
-github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
161
-github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
162
-github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
163
-github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
164
-github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
165
-github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
166
-github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
167
-github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
168
-github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
169
-github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
170
-github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
171
-github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
172
-github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
99
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
100
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
101
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
176
-github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
177
-github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
178
-github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
179
-github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
180
-github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
181
-github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
102
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
103
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
184
-github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
185
-github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
104
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
105
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
106
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@@ -190,8 +108,6 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
108
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
109
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
110
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
193
-github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
194
-github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
111
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
112
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
113
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -200,20 +116,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA
116
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
117
github.com/googleapis/gax-go/v2 v2.21.0 h1:h45NjjzEO3faG9Lg/cFrBh2PgegVVgzqKzuZl/wMbiI=
118
github.com/googleapis/gax-go/v2 v2.21.0/go.mod h1:But/NJU6TnZsrLai/xBAQLLz+Hc7fHZJt/hsCz3Fih4=
203
-github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
204
-github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
119
github.com/gosuda/keyless_tls v0.0.2-0.20260507061030-5128be6b5008 h1:KuP/5VlPJwqZNyAV5U60C/j8Pc5O8ENkWPTgP7mEvj0=
120
github.com/gosuda/keyless_tls v0.0.2-0.20260507061030-5128be6b5008/go.mod h1:BOhUZgiAAQzxKO3QcC4fCXgd/+lqxgIu1OyIYTqtta8=
207
-github.com/gosuda/x402-facilitator v0.0.0-20260413025142-cb6c4794b9a5 h1:lFZ+IYBiGRrFgBJl0NX2u/KAAX3Epj2y3vkJnbYWdLg=
208
-github.com/gosuda/x402-facilitator v0.0.0-20260413025142-cb6c4794b9a5/go.mod h1:rcqknkHCWwkH9EIDlGAAIvAZkYv3U6q4wPvYKwt7ynU=
209
-github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac=
210
-github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc=
211
-github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
212
-github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
213
-github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0=
214
-github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc=
215
-github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE=
216
-github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0=
121
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
122
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
123
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
@@ -224,28 +128,10 @@ github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8
128
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
129
github.com/hetznercloud/hcloud-go/v2 v2.40.0 h1:fuP7khfiDQAIXdKyQq7f3LnnOjyZg0PXTafXjUKkqIA=
130
github.com/hetznercloud/hcloud-go/v2 v2.40.0/go.mod h1:ANz38eerXjPv00dm9dckKhttOGtYeeGmjjvwL5e6c5E=
227
-github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330=
228
-github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg=
229
-github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao=
230
-github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA=
131
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
132
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
233
-github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
234
-github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
235
-github.com/influxdata/influxdb-client-go/v2 v2.4.0 h1:HGBfZYStlx3Kqvsv1h2pJixbCl/jhnFtxpKFAv9Tu5k=
236
-github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8=
237
-github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSHzRbhzK8RdXOsAdfDgO49TtqC1oZ+acxPrkfTxcCs=
238
-github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
239
-github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU=
240
-github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
241
-github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
242
-github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
243
-github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
244
-github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
133
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
134
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
247
-github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
248
-github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
135
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
136
github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
137
github.com/knadh/koanf/parsers/toml/v2 v2.2.0 h1:2nV7tHYJ5OZy2BynQ4mOJ6k5bDqbbCzRERLUKBytz3A=
@@ -254,28 +140,14 @@ github.com/knadh/koanf/providers/file v1.2.1 h1:bEWbtQwYrA+W2DtdBrQWyXqJaJSG3KrP
140
github.com/knadh/koanf/providers/file v1.2.1/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA=
141
github.com/knadh/koanf/v2 v2.3.4 h1:fnynNSDlujWE+v83hAp8wKr/cdoxHLO0629SN+U8Urc=
142
github.com/knadh/koanf/v2 v2.3.4/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
257
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
143
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
144
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
260
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
261
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
145
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
146
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
147
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
148
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
266
-github.com/labstack/echo/v4 v4.15.1 h1:S9keusg26gZpjMmPqB5hOEvNKnmd1lNmcHrbbH2lnFs=
267
-github.com/labstack/echo/v4 v4.15.1/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c=
268
-github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
269
-github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
270
-github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4=
271
-github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
149
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
150
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
274
-github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
275
-github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
276
-github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
277
-github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
278
-github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
151
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
152
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
153
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
@@ -289,20 +161,12 @@ github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byF
161
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
162
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
163
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
292
-github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
293
-github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
164
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
165
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
296
-github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
297
-github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
298
-github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A=
299
-github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
166
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
167
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
168
github.com/montanaflynn/stats v0.9.0 h1:tsBJ0RXwph9BmAuFoCmqGv6e8xa0MENQ8m0ptKq29mQ=
169
github.com/montanaflynn/stats v0.9.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
304
-github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
305
-github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
170
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
171
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
172
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
@@ -311,26 +175,9 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc
175
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
176
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
177
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
314
-github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
315
-github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
316
-github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
178
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
179
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
319
-github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM=
320
-github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
321
-github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
322
-github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
323
-github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
324
-github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
325
-github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0=
326
-github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ=
327
-github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c=
328
-github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
329
-github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
330
-github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
331
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
180
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
333
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
181
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
182
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
183
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
@@ -349,62 +196,21 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
196
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
197
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
198
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
352
-github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
353
-github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
199
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
200
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
201
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
357
-github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
358
-github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
359
-github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
360
-github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
202
github.com/spruceid/siwe-go v0.2.1 h1:BroySys6CyUzeyNppTseEOT/w56xTdOfcmECTI7rnuc=
203
github.com/spruceid/siwe-go v0.2.1/go.mod h1:MHpHbptGsM3lHth2L8quhZ9ipiwST8zsJH1CjWpeO1k=
363
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
364
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
365
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
204
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
205
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
368
-github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE=
369
-github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
370
-github.com/swaggo/echo-swagger v1.4.1 h1:Yf0uPaJWp1uRtDloZALyLnvdBeoEL5Kc7DtnjzO/TUk=
371
-github.com/swaggo/echo-swagger v1.4.1/go.mod h1:C8bSi+9yH2FLZsnhqMZLIZddpUxZdBYuNHbtaS1Hljc=
372
-github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
373
-github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
374
-github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
375
-github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
376
-github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
377
-github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
378
-github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
379
-github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
380
-github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
381
-github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
206
github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
207
github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U=
384
-github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
385
-github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
386
-github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
387
-github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
388
-github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
389
-github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
208
github.com/vultr/govultr/v3 v3.30.0 h1:kTeDJ+5or6g4CQJmD6Kmz4R63B18poNZ8RP87r9LZdg=
209
github.com/vultr/govultr/v3 v3.30.0/go.mod h1:2zyUw9yADQaGwKnwDesmIOlBNLrm7edsCfWHFJpWKf8=
392
-github.com/x402-foundation/x402/go v0.0.0-20260526081544-8cf020c5335e h1:0N+e1CjhzAqm6CKn9zhimQqtpjrTjwbx5+IMvcgyXHM=
393
-github.com/x402-foundation/x402/go v0.0.0-20260526081544-8cf020c5335e/go.mod h1:58Cdk20g83eAI3QvxAiQJze7qWUgkjCj9uZlPb4M4HM=
394
-github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
395
-github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
396
-github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
397
-github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
398
-github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
399
-github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
210
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
211
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
402
-github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
403
-github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
212
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
213
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
406
-github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
407
-github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
214
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
215
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
216
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
@@ -436,7 +242,6 @@ golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aI
242
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
243
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
244
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
439
-golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
245
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
246
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
247
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
@@ -445,27 +250,17 @@ golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
250
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
251
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
252
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
448
-golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
449
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
450
-golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
253
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
254
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
453
-golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
255
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
455
-golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
456
-golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
256
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
257
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
258
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
460
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
259
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
462
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
463
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
260
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
261
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
262
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
263
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
468
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
264
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
265
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
266
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
@@ -487,17 +282,8 @@ google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07
282
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
283
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
284
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
490
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
491
-gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
285
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
286
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
494
-gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
495
-gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
496
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
497
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
498
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
499
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
500
-gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
287
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
288
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
289
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI=
portal/api_server.go
-13
@@ -17,7 +17,6 @@ import (
17
"github.com/gosuda/portal-tunnel/v2/portal/auth"
18
"github.com/gosuda/portal-tunnel/v2/portal/identity"
19
"github.com/gosuda/portal-tunnel/v2/portal/keyless"
20
- portalx402 "github.com/gosuda/portal-tunnel/v2/portal/x402"
20
"github.com/gosuda/portal-tunnel/v2/types"
21
"github.com/gosuda/portal-tunnel/v2/utils"
22
)
@@ -250,22 +249,10 @@ func (s *Server) handleDomain(w http.ResponseWriter, r *http.Request) {
249
return
250
}
251
253
- cfg := s.config()
254
- x402 := types.X402FacilitatorInfo{
255
- Enabled: cfg.X402Enabled,
256
- }
257
- if x402.Enabled {
258
- x402.Network = strings.TrimSpace(cfg.X402Network)
259
- x402.NetworkName = portalx402.NetworkDisplayName(x402.Network)
260
- x402.URL = cfg.PortalURL + types.PathX402Facilitator
261
- x402.SupportedURL = cfg.PortalURL + types.X402SupportedPath
262
- }
263
-
252
utils.WriteAPIData(w, http.StatusOK, types.DomainResponse{
253
ProtocolVersion: types.SDKVersion,
254
ReleaseVersion: types.ReleaseVersion,
255
ENS: s.acmeManager.ENSStatus(),
268
- X402: x402,
256
})
257
}
258
portal/server.go
-3
@@ -56,8 +56,6 @@ type ServerConfig struct {
56
MaxPort int
57
PProfEnabled bool
58
PProfListenAddr string
59
- X402Enabled bool
60
- X402Network string
59
ACME acme.Config
60
}
61
@@ -92,7 +90,6 @@ func normalizeServerConfig(cfg ServerConfig) (ServerConfig, error) {
90
if cfg.PProfEnabled {
91
cfg.PProfListenAddr = utils.StringOrDefault(strings.TrimSpace(cfg.PProfListenAddr), DefaultPProfListenAddr)
92
}
95
- cfg.X402Network = strings.TrimSpace(cfg.X402Network)
93
94
hasPortRange := cfg.MinPort > 0 && cfg.MaxPort > 0
95
if cfg.UDPEnabled || cfg.TCPEnabled {
portal/server_test.go
-17
@@ -296,8 +296,6 @@ func TestServerStartDomainReportsCompatibilityInfo(t *testing.T) {
296
SNIPort: 4443,
297
APIListenAddr: "127.0.0.1:0",
298
SNIListenAddr: "127.0.0.1:0",
299
- X402Enabled: true,
300
- X402Network: " eip155:84532 ",
299
})
300
if err != nil {
301
t.Fatalf("NewServer() error = %v", err)
@@ -340,21 +338,6 @@ func TestServerStartDomainReportsCompatibilityInfo(t *testing.T) {
338
if envelope.Data.ReleaseVersion != types.ReleaseVersion {
339
t.Fatalf("DomainResponse.ReleaseVersion = %q, want %q", envelope.Data.ReleaseVersion, types.ReleaseVersion)
340
}
343
- if !envelope.Data.X402.Enabled {
344
- t.Fatal("DomainResponse.X402.Enabled = false, want true")
345
- }
346
- if envelope.Data.X402.Network != "eip155:84532" {
347
- t.Fatalf("DomainResponse.X402.Network = %q, want eip155:84532", envelope.Data.X402.Network)
348
- }
349
- if envelope.Data.X402.NetworkName != "Base Sepolia" {
350
- t.Fatalf("DomainResponse.X402.NetworkName = %q, want Base Sepolia", envelope.Data.X402.NetworkName)
351
- }
352
- if envelope.Data.X402.URL != "https://localhost:4017/api/x402" {
353
- t.Fatalf("DomainResponse.X402.URL = %q, want https://localhost:4017/api/x402", envelope.Data.X402.URL)
354
- }
355
- if envelope.Data.X402.SupportedURL != "https://localhost:4017/api/x402/supported" {
356
- t.Fatalf("DomainResponse.X402.SupportedURL = %q, want https://localhost:4017/api/x402/supported", envelope.Data.X402.SupportedURL)
357
- }
341
}
342
343
func TestRegisterLeaseIncludesSNIPortForPublicIngress(t *testing.T) {
portal/x402/x402.go
deleted
-201
@@ -1,201 +0,0 @@
1
-package x402
2
-
3
-import (
4
- "context"
5
- "errors"
6
- "fmt"
7
- "net/http"
8
- "strings"
9
- "time"
10
-
11
- facilitatorapi "github.com/gosuda/x402-facilitator/api"
12
- facilitatorcore "github.com/gosuda/x402-facilitator/facilitator"
13
- facilitatortypes "github.com/gosuda/x402-facilitator/types"
14
- foundationx402 "github.com/x402-foundation/x402/go"
15
- x402http "github.com/x402-foundation/x402/go/http"
16
- x402nethttp "github.com/x402-foundation/x402/go/http/nethttp"
17
- evmserver "github.com/x402-foundation/x402/go/mechanisms/evm/exact/server"
18
-
19
- "github.com/gosuda/portal-tunnel/v2/portal/identity"
20
- "github.com/gosuda/portal-tunnel/v2/types"
21
-)
22
-
23
-const (
24
- defaultPaymentTimeout = 30 * time.Second
25
- defaultRouteDescription = "Portal protected route"
26
-)
27
-
28
-var networkDisplayNames = map[string]string{
29
- "eip155:1": "Ethereum Mainnet",
30
- "eip155:8453": "Base Mainnet",
31
- "eip155:84532": "Base Sepolia",
32
- "eip155:42161": "Arbitrum One",
33
- "eip155:421614": "Arbitrum Sepolia",
34
-}
35
-
36
-var networkPublicNodeRPCURLs = map[string]string{
37
- "eip155:1": "https://ethereum-rpc.publicnode.com",
38
- "eip155:8453": "https://base-rpc.publicnode.com",
39
- "eip155:84532": "https://base-sepolia-rpc.publicnode.com",
40
- "eip155:42161": "https://arbitrum-one-rpc.publicnode.com",
41
- "eip155:421614": "https://arbitrum-sepolia-rpc.publicnode.com",
42
-}
43
-
44
-func NetworkDisplayName(network string) string {
45
- return networkDisplayNames[strings.TrimSpace(strings.ToLower(network))]
46
-}
47
-
48
-func PublicNodeRPCURL(network string) string {
49
- return networkPublicNodeRPCURLs[strings.TrimSpace(strings.ToLower(network))]
50
-}
51
-
52
-func isTestnetNetwork(network string) bool {
53
- switch strings.TrimSpace(strings.ToLower(network)) {
54
- case "eip155:84532", "eip155:421614":
55
- return true
56
- default:
57
- return false
58
- }
59
-}
60
-
61
-type FacilitatorConfig struct {
62
- Network string
63
- RPCURL string
64
- Identity types.Identity
65
-}
66
-
67
-func MountFacilitator(mux *http.ServeMux, cfg FacilitatorConfig) error {
68
- if mux == nil {
69
- return errors.New("x402 facilitator requires an api mux")
70
- }
71
- network := strings.TrimSpace(cfg.Network)
72
- if network == "" {
73
- return errors.New("--x402-network is required when --x402-facilitator-enabled is set")
74
- }
75
- privateKey := strings.TrimSpace(cfg.Identity.PrivateKey)
76
- if privateKey == "" {
77
- return errors.New("relay identity private key is required when --x402-facilitator-enabled is set")
78
- }
79
- rpcURL := strings.TrimSpace(cfg.RPCURL)
80
- if rpcURL == "" {
81
- rpcURL = PublicNodeRPCURL(network)
82
- }
83
- facilitator, err := facilitatorcore.NewFacilitator(facilitatortypes.Exact, network, rpcURL, privateKey)
84
- if err != nil {
85
- return fmt.Errorf("create x402 facilitator: %w", err)
86
- }
87
- mux.Handle(types.PathX402Facilitator+"/", http.StripPrefix(types.PathX402Facilitator, facilitatorapi.NewServer(facilitator)))
88
- return nil
89
-}
90
-
91
-type HTTPRequestContext = x402http.HTTPRequestContext
92
-
93
-type PriceResolver func(context.Context, HTTPRequestContext) (string, error)
94
-
95
-type HTTPRouteHandlerConfig struct {
96
- Prefix string
97
- Next http.Handler
98
- X402 types.X402Config
99
- TunnelIdentity types.Identity
100
- Metadata types.LeaseMetadata
101
- PriceResolver PriceResolver
102
-}
103
-
104
-func NewHTTPRouteHandler(cfg HTTPRouteHandlerConfig) (http.Handler, error) {
105
- next := cfg.Next
106
- if next == nil {
107
- next = http.NotFoundHandler()
108
- }
109
- prefix := strings.TrimSpace(cfg.Prefix)
110
- if prefix == "" {
111
- prefix = "/"
112
- }
113
- network := strings.TrimSpace(cfg.X402.Network)
114
- if network == "" {
115
- return nil, fmt.Errorf("http route %q x402 network is required", prefix)
116
- }
117
- facilitatorURL := strings.TrimSpace(cfg.X402.FacilitatorURL)
118
- if facilitatorURL == "" {
119
- return nil, fmt.Errorf("http route %q x402 facilitator_url is required", prefix)
120
- }
121
- priceValue := strings.TrimSpace(cfg.X402.Price)
122
- if priceValue == "" && cfg.PriceResolver == nil {
123
- return nil, fmt.Errorf("http route %q x402 price is required", prefix)
124
- }
125
- var price any = foundationx402.Price(priceValue)
126
- if cfg.PriceResolver != nil {
127
- price = x402http.DynamicPriceFunc(func(ctx context.Context, req x402http.HTTPRequestContext) (foundationx402.Price, error) {
128
- resolvedPrice, err := cfg.PriceResolver(ctx, req)
129
- if err != nil {
130
- return "", err
131
- }
132
- resolvedPrice = strings.TrimSpace(resolvedPrice)
133
- if resolvedPrice == "" {
134
- return "", fmt.Errorf("http route %q x402 dynamic price is empty", prefix)
135
- }
136
- return foundationx402.Price(resolvedPrice), nil
137
- })
138
- }
139
- payTo := strings.TrimSpace(cfg.X402.PayTo)
140
- if payTo == "" || strings.EqualFold(payTo, types.X402PayToIdentity) {
141
- payTo = strings.TrimSpace(cfg.TunnelIdentity.Address)
142
- }
143
- if payTo == "" {
144
- return nil, fmt.Errorf("http route %q x402 pay_to is required", prefix)
145
- }
146
- payTo, err := identity.NormalizeEVMAddress(payTo)
147
- if err != nil {
148
- return nil, fmt.Errorf("http route %q x402 pay_to: %w", prefix, err)
149
- }
150
- if cfg.X402.PaymentTimeoutSecs < 0 {
151
- return nil, errors.New("x402 payment_timeout_seconds cannot be negative")
152
- }
153
- if cfg.X402.MaxTimeoutSeconds < 0 {
154
- return nil, errors.New("x402 max_timeout_seconds cannot be negative")
155
- }
156
-
157
- timeout := defaultPaymentTimeout
158
- if cfg.X402.PaymentTimeoutSecs > 0 {
159
- timeout = time.Duration(cfg.X402.PaymentTimeoutSecs) * time.Second
160
- }
161
- resource := strings.TrimSpace(cfg.X402.Resource)
162
- description := strings.TrimSpace(cfg.Metadata.Description)
163
- if description == "" {
164
- description = defaultRouteDescription
165
- }
166
- middleware := x402nethttp.X402Payment(x402nethttp.Config{
167
- Routes: x402http.RoutesConfig{
168
- "*": x402http.RouteConfig{
169
- Accepts: []x402http.PaymentOption{
170
- {
171
- Scheme: types.X402SchemeExact,
172
- PayTo: payTo,
173
- Price: price,
174
- Network: foundationx402.Network(network),
175
- MaxTimeoutSeconds: cfg.X402.MaxTimeoutSeconds,
176
- },
177
- },
178
- Resource: resource,
179
- Description: description,
180
- MimeType: strings.TrimSpace(cfg.X402.MimeType),
181
- },
182
- },
183
- Facilitator: x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{
184
- URL: facilitatorURL,
185
- }),
186
- Schemes: []x402nethttp.SchemeConfig{
187
- {
188
- Network: foundationx402.Network(network),
189
- Server: evmserver.NewExactEvmScheme(),
190
- },
191
- },
192
- PaywallConfig: &x402http.PaywallConfig{
193
- AppName: strings.TrimSpace(cfg.TunnelIdentity.Name),
194
- AppLogo: strings.TrimSpace(cfg.Metadata.Thumbnail),
195
- Testnet: isTestnetNetwork(network),
196
- },
197
- SyncFacilitatorOnStart: true,
198
- Timeout: timeout,
199
- })
200
- return middleware(next), nil
201
-}
sdk/http.go
+1
-17
@@ -19,7 +19,6 @@ import (
19
"github.com/andybalholm/brotli"
20
"github.com/rs/zerolog/log"
21
22
- portalx402 "github.com/gosuda/portal-tunnel/v2/portal/x402"
22
"github.com/gosuda/portal-tunnel/v2/types"
23
"github.com/gosuda/portal-tunnel/v2/utils"
24
)
@@ -133,8 +132,6 @@ type HTTPRoute struct {
132
Prefix string
133
// Upstream is the target HTTP URL, or a loopback host:port shorthand.
134
Upstream string
136
- // X402 enables payment enforcement for this public route.
137
- X402 *types.X402Config
135
}
136
137
type httpRoute struct {
@@ -163,20 +160,7 @@ func newHTTPRouteHandler(routeConfigs []HTTPRoute, tunnelIdentity types.Identity
160
return nil, fmt.Errorf("duplicate http route prefix %q", route.prefix)
161
}
162
seen[route.prefix] = struct{}{}
166
- var handler http.Handler = route.newReverseProxy()
167
- if routeConfig.X402 != nil && !routeConfig.X402.Empty() {
168
- handler, err = portalx402.NewHTTPRouteHandler(portalx402.HTTPRouteHandlerConfig{
169
- Prefix: route.prefix,
170
- Next: handler,
171
- X402: *routeConfig.X402,
172
- TunnelIdentity: tunnelIdentity,
173
- Metadata: metadata,
174
- })
175
- if err != nil {
176
- return nil, err
177
- }
178
- }
179
- route.handler = handler
163
+ route.handler = route.newReverseProxy()
164
routes = append(routes, route)
165
}
166
types/agent.go
+13
-17
@@ -8,18 +8,16 @@ type AgentStatusResponse struct {
8
}
9
10
type AgentTunnelStatus struct {
11
- ID string `json:"id"`
12
- Name string `json:"name,omitempty"`
13
- Address string `json:"address,omitempty"`
14
- State string `json:"state"`
15
- TargetAddr string `json:"target_addr,omitempty"`
16
- LastError string `json:"last_error,omitempty"`
17
- MaxActiveRelays int `json:"max_active_relays,omitempty"`
18
- Metadata LeaseMetadata `json:"metadata,omitempty"`
19
- X402Enabled bool `json:"x402_enabled,omitempty"`
20
- X402FacilitatorURL string `json:"x402_facilitator_url,omitempty"`
21
- MultiHop []string `json:"multi_hop,omitempty"`
22
- Relays []AgentRelayStatus `json:"relays,omitempty"`
11
+ ID string `json:"id"`
12
+ Name string `json:"name,omitempty"`
13
+ Address string `json:"address,omitempty"`
14
+ State string `json:"state"`
15
+ TargetAddr string `json:"target_addr,omitempty"`
16
+ LastError string `json:"last_error,omitempty"`
17
+ MaxActiveRelays int `json:"max_active_relays,omitempty"`
18
+ Metadata LeaseMetadata `json:"metadata,omitempty"`
19
+ MultiHop []string `json:"multi_hop,omitempty"`
20
+ Relays []AgentRelayStatus `json:"relays,omitempty"`
21
}
22
23
type AgentRelayStatus struct {
@@ -51,15 +49,13 @@ type AgentMultiHopRequest struct {
49
}
50
51
type AgentTunnelUpdateRequest struct {
54
- MaxActiveRelays *int `json:"max_active_relays,omitempty"`
55
- Metadata *AgentMetadataRequest `json:"metadata,omitempty"`
56
- X402FacilitatorURL *string `json:"x402_facilitator_url,omitempty"`
52
+ MaxActiveRelays *int `json:"max_active_relays,omitempty"`
53
+ Metadata *AgentMetadataRequest `json:"metadata,omitempty"`
54
}
55
56
func (r AgentTunnelUpdateRequest) Empty() bool {
57
return r.MaxActiveRelays == nil &&
61
- (r.Metadata == nil || r.Metadata.Empty()) &&
62
- r.X402FacilitatorURL == nil
58
+ (r.Metadata == nil || r.Metadata.Empty())
59
}
60
61
type AgentMetadataRequest struct {
types/api.go
+15
-4
@@ -184,10 +184,9 @@ func HopRouteBytes(method string, route HopRoute) ([]byte, error) {
184
}
185
186
type DomainResponse struct {
187
- ProtocolVersion string `json:"protocol_version"`
188
- ReleaseVersion string `json:"release_version"`
189
- ENS ENSStatus `json:"ens"`
190
- X402 X402FacilitatorInfo `json:"x402"`
187
+ ProtocolVersion string `json:"protocol_version"`
188
+ ReleaseVersion string `json:"release_version"`
189
+ ENS ENSStatus `json:"ens"`
190
}
191
192
type ENSStatus struct {
@@ -205,6 +204,18 @@ type PublicStateResponse struct {
204
Leases []Lease `json:"leases,omitempty"`
205
}
206
207
+type AdminAuthLoginRequest struct {
208
+ Token string `json:"token"`
209
+}
210
+
211
+type AdminAuthLoginResponse struct {
212
+ AccessToken string `json:"access_token,omitempty"`
213
+}
214
+
215
+type AdminAuthStatusResponse struct {
216
+ Authenticated bool `json:"authenticated"`
217
+}
218
+
219
type WalletAuthChallengeRequest struct {
220
Address string `json:"address"`
221
}
types/paths.go
+5
-11
@@ -6,12 +6,11 @@ const (
6
PathHealthz = PathAPIPrefix + "/healthz"
7
PathState = PathAPIPrefix + "/state"
8
9
- PathAdmin = PathAPIPrefix + "/admin"
10
- PathAdminPrefix = PathAdmin + "/"
11
- PathAdminAuthChallenge = PathAdmin + "/auth/challenge"
12
- PathAdminAuthLogin = PathAdmin + "/auth/login"
13
- PathAdminLogout = PathAdmin + "/auth/logout"
14
- PathAdminAuthStatus = PathAdmin + "/auth/status"
9
+ PathAdmin = PathAPIPrefix + "/admin"
10
+ PathAdminPrefix = PathAdmin + "/"
11
+ PathAdminAuthLogin = PathAdmin + "/auth/login"
12
+ PathAdminLogout = PathAdmin + "/auth/logout"
13
+ PathAdminAuthStatus = PathAdmin + "/auth/status"
14
15
PathPolicy = PathAPIPrefix + "/policy"
16
PathPolicyPrefix = PathPolicy + "/"
@@ -23,11 +22,6 @@ const (
22
PathInstallPowerShell = PathAPIPrefix + "/install.ps1"
23
PathInstallBinPrefix = PathAPIPrefix + "/install/bin/"
24
26
- PathX402Facilitator = PathAPIPrefix + "/x402"
27
- X402SupportedPath = PathX402Facilitator + "/supported"
28
- X402VerifyPath = PathX402Facilitator + "/verify"
29
- X402SettlePath = PathX402Facilitator + "/settle"
30
-
25
PathV1Sign = "/v1/sign"
26
27
PathSDKPrefix = "/sdk"
types/x402.go
deleted
-49
@@ -1,49 +0,0 @@
1
-package types
2
-
3
-const (
4
- X402SchemeExact = "exact"
5
- X402PayToIdentity = "identity"
6
-)
7
-
8
-type X402Config struct {
9
- Network string `json:"network,omitempty" koanf:"network"`
10
- Price string `json:"price,omitempty" koanf:"price"`
11
- PayTo string `json:"pay_to,omitempty" koanf:"pay_to"`
12
- FacilitatorURL string `json:"facilitator_url,omitempty" koanf:"facilitator_url"`
13
- Resource string `json:"resource,omitempty" koanf:"resource"`
14
- MimeType string `json:"mime_type,omitempty" koanf:"mime_type"`
15
- MaxTimeoutSeconds int `json:"max_timeout_seconds,omitempty" koanf:"max_timeout_seconds"`
16
- PaymentTimeoutSecs int `json:"payment_timeout_seconds,omitempty" koanf:"payment_timeout_seconds"`
17
-}
18
-
19
-func (c X402Config) Empty() bool {
20
- return c.Network == "" &&
21
- c.Price == "" &&
22
- c.PayTo == "" &&
23
- c.FacilitatorURL == "" &&
24
- c.Resource == "" &&
25
- c.MimeType == "" &&
26
- c.MaxTimeoutSeconds == 0 &&
27
- c.PaymentTimeoutSecs == 0
28
-}
29
-
30
-type X402FacilitatorInfo struct {
31
- Enabled bool `json:"enabled"`
32
- URL string `json:"url,omitempty"`
33
- Network string `json:"network,omitempty"`
34
- NetworkName string `json:"network_name,omitempty"`
35
- SupportedURL string `json:"supported_url,omitempty"`
36
-}
37
-
38
-type X402SupportedKind struct {
39
- X402Version int `json:"x402Version"`
40
- Scheme string `json:"scheme"`
41
- Network string `json:"network"`
42
- Extra map[string]any `json:"extra,omitempty"`
43
-}
44
-
45
-type X402SupportedResponse struct {
46
- Kinds []X402SupportedKind `json:"kinds"`
47
- Extensions []string `json:"extensions"`
48
- Signers map[string][]string `json:"signers"`
49
-}