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