Skip to content

Commit 7c1ebf7

Browse files
committed
feat: add x402 facilitator token support and update documentation
- Introduced `x402FacilitatorToken` in various configurations and structures. - Updated README and CLI reference to include new token requirements. - Enhanced error handling for missing authorization tokens in Casper payments. - Bumped version to v2.3.3 for the release.
1 parent ee48c9d commit 7c1ebf7

13 files changed

Lines changed: 177 additions & 115 deletions

File tree

cmd/portal-tunnel/README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ for the Sui wallet flow. Casper clients instead consume the protected route's
7070
402 requirements, sign with an external Casper x402 SDK, and retry with
7171
`PAYMENT-SIGNATURE` or `X-PAYMENT`; Portal settles it through the configured
7272
Casper facilitator before proxying the request.
73+
The hosted CSPR.cloud facilitator requires `CSPR_CLOUD_API_KEY`, which Portal
74+
sends as its server-side authorization token. A custom unauthenticated
75+
facilitator does not require the token.
7376

7477
Raw TCP and UDP:
7578

@@ -116,6 +119,7 @@ Common `portal expose` flags:
116119
--x402-network Optional Sui or Casper CAIP-2 network
117120
--x402-asset wCSPR CEP-18 contract hash required by Casper
118121
--x402-endpoint Optional Sui RPC or Casper facilitator endpoint; repeatable
122+
--x402-facilitator-token Casper facilitator authorization token; defaults to CSPR_CLOUD_API_KEY
119123
--tcp Request a dedicated raw TCP port on the relay
120124
--udp Enable public UDP relay
121125
--udp-addr Local UDP target
@@ -138,7 +142,7 @@ Tunnel opens a small form for name, target or HTTP routes, x402 payment settings
138142
relays, discovery, and max active relays. After creation, routed HTTP paths,
139143
route-level x402 amounts, payment network, and discovery mode are read-only in
140144
the Settings pane. Edit `http_routes`, `x402_pay_to`, `x402_testnet`,
141-
`x402_network`, `x402_asset`, `x402_endpoints`, or `discovery` in TOML, then
145+
`x402_network`, `x402_asset`, `x402_endpoints`, `x402_facilitator_token`, or `discovery` in TOML, then
142146
restart the agent or tunnel to change them.
143147

144148
## Constraints
@@ -152,6 +156,7 @@ restart the agent or tunnel to change them.
152156
- Route payment amounts such as `0.01` are part of `--http-route` and require
153157
`--x402-pay-to`. Sui is the default; Casper additionally requires
154158
`--x402-network casper:...` and the wCSPR contract in `--x402-asset`.
159+
The default CSPR.cloud facilitator also requires `CSPR_CLOUD_API_KEY`.
155160
- `--multi-hop` cannot be combined with `--multi-hop-depth`.
156161
- Multi-hop currently supports only the default SNI TLS stream transport.
157162
- `--tcp` and `--udp` require matching relay transport support.

cmd/portal-tunnel/agent/config.go

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -39,31 +39,32 @@ type AgentConfig struct {
3939
}
4040

4141
type TunnelConfig struct {
42-
ID string `koanf:"id"`
43-
Name string `koanf:"name"`
44-
TargetAddr string `koanf:"target"`
45-
HTTPRoutes []HTTPRouteConfig `koanf:"http_routes"`
46-
RelayURLs []string `koanf:"relays"`
47-
Discovery *bool `koanf:"discovery"`
48-
IdentityPath string `koanf:"identity_path"`
49-
IdentityJSON string `koanf:"identity_json"`
50-
UDPEnabled bool `koanf:"udp"`
51-
UDPAddr string `koanf:"udp_addr"`
52-
TCPEnabled bool `koanf:"tcp"`
53-
MultiHop []string `koanf:"multi_hop"`
54-
MultiHopDepth int `koanf:"multi_hop_depth"`
55-
BanMITM *bool `koanf:"ban_mitm"`
56-
MaxActiveRelays int `koanf:"max_active_relays"`
57-
Description string `koanf:"description"`
58-
Tags []string `koanf:"tags"`
59-
Owner string `koanf:"owner"`
60-
Thumbnail string `koanf:"thumbnail"`
61-
Hide bool `koanf:"hide"`
62-
X402PayTo string `koanf:"x402_pay_to"`
63-
X402Testnet bool `koanf:"x402_testnet"`
64-
X402Network string `koanf:"x402_network"`
65-
X402Asset string `koanf:"x402_asset"`
66-
X402Endpoints []string `koanf:"x402_endpoints"`
42+
ID string `koanf:"id"`
43+
Name string `koanf:"name"`
44+
TargetAddr string `koanf:"target"`
45+
HTTPRoutes []HTTPRouteConfig `koanf:"http_routes"`
46+
RelayURLs []string `koanf:"relays"`
47+
Discovery *bool `koanf:"discovery"`
48+
IdentityPath string `koanf:"identity_path"`
49+
IdentityJSON string `koanf:"identity_json"`
50+
UDPEnabled bool `koanf:"udp"`
51+
UDPAddr string `koanf:"udp_addr"`
52+
TCPEnabled bool `koanf:"tcp"`
53+
MultiHop []string `koanf:"multi_hop"`
54+
MultiHopDepth int `koanf:"multi_hop_depth"`
55+
BanMITM *bool `koanf:"ban_mitm"`
56+
MaxActiveRelays int `koanf:"max_active_relays"`
57+
Description string `koanf:"description"`
58+
Tags []string `koanf:"tags"`
59+
Owner string `koanf:"owner"`
60+
Thumbnail string `koanf:"thumbnail"`
61+
Hide bool `koanf:"hide"`
62+
X402PayTo string `koanf:"x402_pay_to"`
63+
X402Testnet bool `koanf:"x402_testnet"`
64+
X402Network string `koanf:"x402_network"`
65+
X402Asset string `koanf:"x402_asset"`
66+
X402Endpoints []string `koanf:"x402_endpoints"`
67+
X402FacilitatorToken string `koanf:"x402_facilitator_token"`
6768
}
6869

6970
type HTTPRouteConfig struct {
@@ -227,6 +228,7 @@ func tunnelConfigDocumentMap(cfg TunnelConfig) map[string]any {
227228
addStringDocumentField(out, "x402_network", cfg.X402Network)
228229
addStringDocumentField(out, "x402_asset", cfg.X402Asset)
229230
addStringSliceDocumentField(out, "x402_endpoints", cfg.X402Endpoints)
231+
addStringDocumentField(out, "x402_facilitator_token", cfg.X402FacilitatorToken)
230232
return out
231233
}
232234

@@ -277,6 +279,7 @@ func (cfg *Config) ApplyDefaults(configPath string) error {
277279
t.X402Network = strings.ToLower(strings.TrimSpace(t.X402Network))
278280
t.X402Asset = strings.TrimSpace(t.X402Asset)
279281
t.X402Endpoints = compactStrings(t.X402Endpoints)
282+
t.X402FacilitatorToken = strings.TrimSpace(t.X402FacilitatorToken)
280283
if t.ID == "" {
281284
t.ID = t.Name
282285
}

cmd/portal-tunnel/agent/dashboard.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1048,7 +1048,7 @@ func (m agentDashboardModel) addTunnelRequest() (types.AgentTunnelRequest, error
10481048
return types.AgentTunnelRequest{}, fmt.Errorf("X402 Network requires paid routes")
10491049
}
10501050
if strings.HasPrefix(x402Network, "casper:") && x402Asset == "" {
1051-
return types.AgentTunnelRequest{}, fmt.Errorf("Casper payments require X402 Asset")
1051+
return types.AgentTunnelRequest{}, fmt.Errorf("casper payments require X402 Asset")
10521052
}
10531053

10541054
discoveryRaw := strings.TrimSpace(m.addDiscovery.Value())

cmd/portal-tunnel/agent/manager.go

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -703,26 +703,31 @@ func (t *managedTunnel) runOnce(ctx context.Context) error {
703703
if cfg.BanMITM != nil {
704704
banMITM = *cfg.BanMITM
705705
}
706+
x402FacilitatorToken := strings.TrimSpace(cfg.X402FacilitatorToken)
707+
if x402FacilitatorToken == "" {
708+
x402FacilitatorToken = strings.TrimSpace(os.Getenv("CSPR_CLOUD_API_KEY"))
709+
}
706710
exposure, err := sdk.Expose(ctx, sdk.ExposeConfig{
707-
RelayURLs: append([]string(nil), cfg.RelayURLs...),
708-
Discovery: discovery,
709-
Identity: types.Identity{Name: cfg.Name},
710-
IdentityPath: cfg.IdentityPath,
711-
IdentityJSON: cfg.IdentityJSON,
712-
TargetAddr: cfg.TargetAddr,
713-
UDPAddr: cfg.UDPAddr,
714-
UDPEnabled: cfg.UDPEnabled,
715-
TCPEnabled: cfg.TCPEnabled,
716-
MultiHop: append([]string(nil), cfg.MultiHop...),
717-
MultiHopDepth: cfg.MultiHopDepth,
718-
BanMITM: banMITM,
719-
MaxActiveRelays: cfg.MaxActiveRelays,
720-
Metadata: metadataFromTunnelConfig(cfg),
721-
X402PayTo: cfg.X402PayTo,
722-
X402Testnet: cfg.X402Testnet,
723-
X402Network: cfg.X402Network,
724-
X402Asset: cfg.X402Asset,
725-
X402Endpoints: append([]string(nil), cfg.X402Endpoints...),
711+
RelayURLs: append([]string(nil), cfg.RelayURLs...),
712+
Discovery: discovery,
713+
Identity: types.Identity{Name: cfg.Name},
714+
IdentityPath: cfg.IdentityPath,
715+
IdentityJSON: cfg.IdentityJSON,
716+
TargetAddr: cfg.TargetAddr,
717+
UDPAddr: cfg.UDPAddr,
718+
UDPEnabled: cfg.UDPEnabled,
719+
TCPEnabled: cfg.TCPEnabled,
720+
MultiHop: append([]string(nil), cfg.MultiHop...),
721+
MultiHopDepth: cfg.MultiHopDepth,
722+
BanMITM: banMITM,
723+
MaxActiveRelays: cfg.MaxActiveRelays,
724+
Metadata: metadataFromTunnelConfig(cfg),
725+
X402PayTo: cfg.X402PayTo,
726+
X402Testnet: cfg.X402Testnet,
727+
X402Network: cfg.X402Network,
728+
X402Asset: cfg.X402Asset,
729+
X402Endpoints: append([]string(nil), cfg.X402Endpoints...),
730+
X402FacilitatorToken: x402FacilitatorToken,
726731
})
727732
if err != nil {
728733
return err

cmd/portal-tunnel/main.go

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -47,32 +47,33 @@ func main() {
4747
}
4848

4949
type exposeFlags struct {
50-
relayCSV string
51-
multiHopCSV string
52-
discovery bool
53-
banMITM bool
54-
identityPath string
55-
identityJSON string
56-
name string
57-
desc string
58-
tags string
59-
owner string
60-
thumbnail string
61-
hide bool
62-
x402PayTo string
63-
x402Testnet bool
64-
x402Network string
65-
x402Asset string
66-
x402Endpoints []string
67-
targetAddr string
68-
httpRoutes []string
69-
serve string
70-
udp bool
71-
udpAddr string
72-
tcp bool
73-
maxActiveRelays int
74-
multiHopDepth int
75-
metricsAddr string
50+
relayCSV string
51+
multiHopCSV string
52+
discovery bool
53+
banMITM bool
54+
identityPath string
55+
identityJSON string
56+
name string
57+
desc string
58+
tags string
59+
owner string
60+
thumbnail string
61+
hide bool
62+
x402PayTo string
63+
x402Testnet bool
64+
x402Network string
65+
x402Asset string
66+
x402Endpoints []string
67+
x402FacilitatorToken string
68+
targetAddr string
69+
httpRoutes []string
70+
serve string
71+
udp bool
72+
udpAddr string
73+
tcp bool
74+
maxActiveRelays int
75+
multiHopDepth int
76+
metricsAddr string
7677
}
7778

7879
func runExposeCommand(args []string) error {
@@ -98,6 +99,7 @@ func runExposeCommand(args []string) error {
9899
utils.StringFlag(fs, &flags.x402Network, "x402-network", "", "x402 CAIP-2 network; supported values are sui:mainnet, sui:testnet, casper:casper, and casper:casper-test")
99100
utils.StringFlag(fs, &flags.x402Asset, "x402-asset", "", "Payment asset contract; required for Casper wCSPR")
100101
utils.RepeatedStringFlag(fs, &flags.x402Endpoints, "x402-endpoint", "x402 chain RPC or hosted facilitator endpoint; repeat for Sui RPC fallback, while Casper uses the first facilitator endpoint")
102+
utils.StringFlagEnv(fs, &flags.x402FacilitatorToken, "x402-facilitator-token", "", "Casper facilitator authorization token", "CSPR_CLOUD_API_KEY")
101103
utils.RepeatedStringFlag(fs, &flags.httpRoutes, "http-route", "HTTP route mapping in PATH=UPSTREAM [METHOD[,METHOD...]:PAYMENT_AMOUNT] form; repeat to aggregate multiple local HTTP services behind one public URL")
102104
utils.StringFlag(fs, &flags.serve, "serve", "", "Serve a local static site: pass a directory (served with index.html) or an HTML file (its folder is served with that file as the SPA/CSR entry). Unknown paths fall back to the entry file")
103105
utils.BoolFlagEnv(fs, &flags.udp, "udp", false, "Enable public UDP relay in addition to the default TCP relay", "UDP_ENABLED")
@@ -240,11 +242,12 @@ func runExposeCommand(args []string) error {
240242
Thumbnail: flags.thumbnail,
241243
Hide: flags.hide,
242244
},
243-
X402PayTo: flags.x402PayTo,
244-
X402Testnet: flags.x402Testnet,
245-
X402Network: flags.x402Network,
246-
X402Asset: flags.x402Asset,
247-
X402Endpoints: flags.x402Endpoints,
245+
X402PayTo: flags.x402PayTo,
246+
X402Testnet: flags.x402Testnet,
247+
X402Network: flags.x402Network,
248+
X402Asset: flags.x402Asset,
249+
X402Endpoints: flags.x402Endpoints,
250+
X402FacilitatorToken: flags.x402FacilitatorToken,
248251
})
249252
if err != nil {
250253
return fmt.Errorf("failed to start relays: %w", err)

config.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Bump protocol versions only when wire-level behavior changes.
33

44
[release]
5-
version = "v2.3.2"
5+
version = "v2.3.3"
66
base_url = "https://github.com/gosuda/portal-tunnel/releases"
77

88
[protocol]

docs/src/routes/cli-reference/+page.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ not supported.
104104
| `--x402-network` | string | | Optional Sui or Casper CAIP-2 network |
105105
| `--x402-asset` | string | | wCSPR CEP-18 contract hash required by Casper |
106106
| `--x402-endpoint` | string | | Optional Sui RPC or Casper facilitator endpoint; repeatable |
107+
| `--x402-facilitator-token` | string | `CSPR_CLOUD_API_KEY` | Casper facilitator authorization token; prefer the environment variable so the secret is not exposed in the process arguments |
107108
| `--http-route` | string | | HTTP route mapping in `PATH=UPSTREAM [METHOD[,METHOD...]:PAYMENT_AMOUNT]` form; repeatable; route amounts require `--x402-pay-to` |
108109
| `--tcp` | bool | `false` | Request a dedicated raw TCP port on the relay |
109110
| `--udp` | bool | `false` | Enable public UDP relay in addition to the default stream path |
@@ -121,7 +122,8 @@ not supported.
121122
`--x402-pay-to`.
122123
- Tunnel paid routes use Sui mainnet by default. Casper requires an explicit
123124
`--x402-network casper:...` and `--x402-asset`; it uses the hosted facilitator
124-
by default or the first `--x402-endpoint` override.
125+
by default or the first `--x402-endpoint` override. The default CSPR.cloud
126+
facilitator also requires `CSPR_CLOUD_API_KEY`.
125127

126128
### Examples
127129

docs/src/routes/configuration/+page.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ The `portal expose` subcommand accepts the following flags. Flags that read from
167167
| `--x402-network` | | string | | Optional Sui or Casper CAIP-2 network |
168168
| `--x402-asset` | | string | | wCSPR CEP-18 contract hash required by Casper |
169169
| `--x402-endpoint` | | string | | Optional Sui RPC or Casper facilitator endpoint; repeatable |
170+
| `--x402-facilitator-token` | `CSPR_CLOUD_API_KEY` | string | | Casper facilitator authorization token |
170171

171172
### Routing
172173

@@ -271,6 +272,7 @@ Tunnel fields mirror `portal expose` flags:
271272
| `x402_network` | string | Optional CAIP-2 network: `sui:mainnet`, `sui:testnet`, `casper:casper`, or `casper:casper-test` |
272273
| `x402_asset` | string | wCSPR CEP-18 contract hash; required for Casper payments |
273274
| `x402_endpoints` | string array | Optional Sui RPC endpoints or Casper facilitator URL; Casper uses the first endpoint |
275+
| `x402_facilitator_token` | string | Casper facilitator authorization token; when omitted, the agent uses `CSPR_CLOUD_API_KEY` |
274276
| `http_routes[].amount` | string | Optional human payment amount, such as `0.01`, for one HTTP route prefix; requires `x402_pay_to` |
275277
| `http_routes[].methods` | string array | Optional HTTP methods that require payment on that route; empty means every method |
276278

@@ -293,6 +295,9 @@ request reaches the upstream.
293295
Casper has no Go chain SDK, so `casper:*` payments delegate `/verify` and
294296
`/settle` to a remote x402 facilitator over HTTP. The wCSPR CEP-18 contract
295297
hash differs per network deployment, so it must be set as the payment asset.
298+
The default CSPR.cloud facilitator requires an access token in
299+
`CSPR_CLOUD_API_KEY`; use `x402_facilitator_token` only when the agent service
300+
cannot receive that environment variable.
296301

297302
```toml
298303
x402_network = "casper:casper-test"

docs/src/routes/portal-agent/+page.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ x402_pay_to = "account-hash-..."
9393
x402_endpoints = ["https://x402-facilitator.cspr.cloud"]
9494
```
9595

96+
Set `CSPR_CLOUD_API_KEY` in the agent service environment for the hosted
97+
CSPR.cloud facilitator. If the service cannot receive that environment
98+
variable, set `x402_facilitator_token` in this tunnel's TOML instead. Custom
99+
facilitators that do not require authentication can omit both.
100+
96101
If a route has `amount`, the tunnel serves `/x402/client.js` and
97102
`/x402/prepare` on the public tunnel origin for the Sui wallet flow. Casper
98103
clients consume the protected route's 402 requirements, sign with an external

portal/x402/casper.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ type casperFacilitator struct {
106106
client *facilitatorclient.Client
107107
}
108108

109-
func newCasperFacilitator(network string, endpoints ...string) (facilitatorcore.Facilitator, error) {
109+
func newCasperFacilitator(network, token string, endpoints ...string) (facilitatorcore.Facilitator, error) {
110110
network = strings.ToLower(strings.TrimSpace(network))
111111
if network == "" {
112112
network = CasperMainnetNetwork
@@ -121,10 +121,22 @@ func newCasperFacilitator(network string, endpoints ...string) (facilitatorcore.
121121
break
122122
}
123123
}
124+
token = strings.TrimSpace(token)
125+
if strings.EqualFold(strings.TrimRight(url, "/"), DefaultCasperFacilitatorURL) && token == "" {
126+
return nil, errors.New("CSPR.cloud x402 facilitator requires an authorization token")
127+
}
124128
client, err := facilitatorclient.NewClient(url)
125129
if err != nil {
126130
return nil, fmt.Errorf("create casper x402 facilitator: %w", err)
127131
}
132+
if token != "" {
133+
client.CreateAuthHeader = func() (map[string]map[string]string, error) {
134+
return map[string]map[string]string{
135+
"verify": {"Authorization": token},
136+
"settle": {"Authorization": token},
137+
}, nil
138+
}
139+
}
128140
return &casperFacilitator{network: network, client: client}, nil
129141
}
130142

@@ -196,7 +208,7 @@ func NewCasperPayment(payment types.X402Payment) (*Payment, error) {
196208
},
197209
}
198210
endpoints := append([]string(nil), payment.Endpoints...)
199-
facilitator, err := newCasperFacilitator(requirements.Network, endpoints...)
211+
facilitator, err := newCasperFacilitator(requirements.Network, payment.FacilitatorToken, endpoints...)
200212
if err != nil {
201213
return nil, err
202214
}
@@ -209,6 +221,7 @@ func NewCasperPayment(payment types.X402Payment) (*Payment, error) {
209221
payment.Amount = requirements.Amount
210222
payment.MaxTimeoutSeconds = requirements.MaxTimeoutSeconds
211223
payment.Endpoints = endpoints
224+
payment.FacilitatorToken = ""
212225
payment.ResourcePath = strings.TrimSpace(payment.ResourcePath)
213226
payment.ResourceDescription = strings.TrimSpace(payment.ResourceDescription)
214227
payment.ResourceMimeType = strings.TrimSpace(payment.ResourceMimeType)

0 commit comments

Comments
 (0)