feat: update environment variables for portal configuration and improve HTTP request handling

rabbitprincess committed Jun 5, 2026 at 00:21 UTC 2cb88a34759435ad9f199d6e2a02be958c7de891
2 files changed +126 -127
portal/x402/handler.go
+122 -122
@@ -1,122 +1,122 @@
1 -package x402
2 -
3 -import (
4 - "context"
5 - "encoding/json"
6 - "errors"
7 - "fmt"
8 - "net/http"
9 - "strings"
10 -
11 - "github.com/gosuda/portal-tunnel/v2/types"
12 - "github.com/gosuda/portal-tunnel/v2/utils"
13 -)
14 -
15 -// USDCPaymentHandler serves both the wallet prepare endpoint and one protected resource.
16 -type USDCPaymentHandler struct {
17 - payment *Payment
18 - protectedPath string
19 - method string
20 - handler types.X402PaymentHandlerFunc
21 -}
22 -
23 -// NewUSDCPaymentHandler returns a complete HTTP handler for one Sui USDC x402 payment flow.
24 -func NewUSDCPaymentHandler(payment types.X402Payment, protectedPath, protectedMethod string, handler types.X402PaymentHandlerFunc) (*USDCPaymentHandler, error) {
25 - if handler == nil {
26 - return nil, errors.New("USDC payment handler is required")
27 - }
28 - paid, err := NewUSDCPayment(payment)
29 - if err != nil {
30 - return nil, err
31 - }
32 - payment = paid.payment
33 -
34 - protectedPath = strings.TrimSpace(protectedPath)
35 - if protectedPath == "" {
36 - protectedPath = payment.ResourcePath
37 - }
38 - if protectedPath == "" {
39 - return nil, errors.New("USDC payment protected path is required")
40 - }
41 - if !strings.HasPrefix(protectedPath, "/") {
42 - return nil, fmt.Errorf("USDC payment protected path %q must start with /", protectedPath)
43 - }
44 - protectedPath = utils.NormalizeURLPath(protectedPath)
45 - if protectedPath == types.X402PreparePath {
46 - return nil, fmt.Errorf("USDC payment protected path cannot be %s", types.X402PreparePath)
47 - }
48 -
49 - paid.payment.ResourcePath = protectedPath
50 - return &USDCPaymentHandler{
51 - payment: paid,
52 - protectedPath: protectedPath,
53 - method: strings.TrimSpace(protectedMethod),
54 - handler: handler,
55 - }, nil
56 -}
57 -
58 -// Payment returns the normalized payment contract used by this handler.
59 -func (h *USDCPaymentHandler) Payment() types.X402Payment {
60 - if h == nil || h.payment == nil {
61 - return types.X402Payment{}
62 - }
63 - return h.payment.payment
64 -}
65 -
66 -func (h *USDCPaymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
67 - if h == nil || h.payment == nil {
68 - http.Error(w, "payment is not configured", http.StatusInternalServerError)
69 - return
70 - }
71 -
72 - path := "/"
73 - if r.URL != nil {
74 - path = r.URL.Path
75 - }
76 - switch path {
77 - case types.X402PreparePath:
78 - if !utils.RequireMethod(w, r, http.MethodPost) {
79 - return
80 - }
81 - var req types.X402PreparePaymentRequest
82 - if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
83 - http.Error(w, "invalid payment prepare request", http.StatusBadRequest)
84 - return
85 - }
86 - h.payment.WritePrepare(w, r, req.Sender, h.protectedPath)
87 - case h.protectedPath:
88 - if h.method != "" && !utils.RequireMethod(w, r, h.method) {
89 - return
90 - }
91 - if h.handler == nil {
92 - http.NotFound(w, r)
93 - return
94 - }
95 -
96 - ctx := r.Context()
97 - cancel := func() {}
98 - payment := h.payment.payment
99 - if payment.RequestTimeout > 0 {
100 - ctx, cancel = context.WithTimeout(ctx, payment.RequestTimeout)
101 - }
102 - defer cancel()
103 -
104 - paymentPayload, ok := h.payment.Verify(ctx, w, r)
105 - if !ok {
106 - return
107 - }
108 - settled, ok := h.payment.Settle(ctx, w, r, paymentPayload)
109 - if !ok {
110 - return
111 - }
112 - utils.SetPaymentResponseHeaders(w.Header(), settled)
113 -
114 - h.handler(w, r, types.X402PaymentResult{
115 - TransactionID: strings.TrimSpace(settled.Transaction),
116 - Network: string(settled.Network),
117 - Payer: strings.TrimSpace(settled.Payer),
118 - })
119 - default:
120 - http.NotFound(w, r)
121 - }
122 -}
1 +package x402
2 +
3 +import (
4 + "context"
5 + "encoding/json"
6 + "errors"
7 + "fmt"
8 + "net/http"
9 + "strings"
10 +
11 + "github.com/gosuda/portal-tunnel/v2/types"
12 + "github.com/gosuda/portal-tunnel/v2/utils"
13 +)
14 +
15 +// USDCPaymentHandler serves both the wallet prepare endpoint and one protected resource.
16 +type USDCPaymentHandler struct {
17 + payment *Payment
18 + protectedPath string
19 + method string
20 + handler types.X402PaymentHandlerFunc
21 +}
22 +
23 +// NewUSDCPaymentHandler returns a complete HTTP handler for one Sui USDC x402 payment flow.
24 +func NewUSDCPaymentHandler(payment types.X402Payment, protectedPath, protectedMethod string, handler types.X402PaymentHandlerFunc) (*USDCPaymentHandler, error) {
25 + if handler == nil {
26 + return nil, errors.New("USDC payment handler is required")
27 + }
28 + paid, err := NewUSDCPayment(payment)
29 + if err != nil {
30 + return nil, err
31 + }
32 + payment = paid.payment
33 +
34 + protectedPath = strings.TrimSpace(protectedPath)
35 + if protectedPath == "" {
36 + protectedPath = payment.ResourcePath
37 + }
38 + if protectedPath == "" {
39 + return nil, errors.New("USDC payment protected path is required")
40 + }
41 + if !strings.HasPrefix(protectedPath, "/") {
42 + return nil, fmt.Errorf("USDC payment protected path %q must start with /", protectedPath)
43 + }
44 + protectedPath = utils.NormalizeURLPath(protectedPath)
45 + if protectedPath == types.X402PreparePath {
46 + return nil, fmt.Errorf("USDC payment protected path cannot be %s", types.X402PreparePath)
47 + }
48 +
49 + paid.payment.ResourcePath = protectedPath
50 + return &USDCPaymentHandler{
51 + payment: paid,
52 + protectedPath: protectedPath,
53 + method: strings.TrimSpace(protectedMethod),
54 + handler: handler,
55 + }, nil
56 +}
57 +
58 +// Payment returns the normalized payment contract used by this handler.
59 +func (h *USDCPaymentHandler) Payment() types.X402Payment {
60 + if h == nil || h.payment == nil {
61 + return types.X402Payment{}
62 + }
63 + return h.payment.payment
64 +}
65 +
66 +func (h *USDCPaymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
67 + if h == nil || h.payment == nil {
68 + http.Error(w, "payment is not configured", http.StatusInternalServerError)
69 + return
70 + }
71 +
72 + path := "/"
73 + if r.URL != nil {
74 + path = r.URL.Path
75 + }
76 + switch path {
77 + case types.X402PreparePath:
78 + if !utils.RequireMethod(w, r, http.MethodPost) {
79 + return
80 + }
81 + var req types.X402PreparePaymentRequest
82 + if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
83 + http.Error(w, "invalid payment prepare request", http.StatusBadRequest)
84 + return
85 + }
86 + h.payment.WritePrepare(w, r, req.Sender, h.protectedPath)
87 + case h.protectedPath:
88 + if h.method != "" && !utils.RequireMethod(w, r, h.method) {
89 + return
90 + }
91 + if h.handler == nil {
92 + http.NotFound(w, r)
93 + return
94 + }
95 +
96 + ctx := r.Context()
97 + cancel := func() {}
98 + payment := h.payment.payment
99 + if payment.RequestTimeout > 0 {
100 + ctx, cancel = context.WithTimeout(ctx, payment.RequestTimeout)
101 + }
102 + defer cancel()
103 +
104 + paymentPayload, ok := h.payment.Verify(ctx, w, r)
105 + if !ok {
106 + return
107 + }
108 + settled, ok := h.payment.Settle(ctx, w, r, paymentPayload)
109 + if !ok {
110 + return
111 + }
112 + utils.SetPaymentResponseHeaders(w.Header(), settled)
113 +
114 + h.handler(w, r, types.X402PaymentResult{
115 + TransactionID: strings.TrimSpace(settled.Transaction),
116 + Network: string(settled.Network),
117 + Payer: strings.TrimSpace(settled.Payer),
118 + })
119 + default:
120 + http.NotFound(w, r)
121 + }
122 +}
sdk/http.go
+4 -5
@@ -346,8 +346,8 @@ func (r *httpRoute) rewriteProxyRequest(pr *httputil.ProxyRequest) {
346 path := utils.NormalizeURLPath(pr.In.URL.Path)
347 rawPath := pr.In.URL.RawPath
348 if r.prefix != "/" {
349 - switch {
350 - case path == r.prefix:
349 + switch path {
350 + case r.prefix:
351 path = "/"
352 default:
353 path = strings.TrimPrefix(path, r.prefix)
@@ -357,10 +357,9 @@ func (r *httpRoute) rewriteProxyRequest(pr *httputil.ProxyRequest) {
357 }
358
359 if rawPath != "" {
360 - switch {
361 - case rawPath == r.prefix:
360 + if rawPath == r.prefix {
361 rawPath = "/"
363 - case strings.HasPrefix(rawPath, r.prefix+"/"):
362 + } else if strings.HasPrefix(rawPath, r.prefix+"/") {
363 rawPath = strings.TrimPrefix(rawPath, r.prefix)
364 }
365 }