Files
agency/server/spa.go
zeekay f8a1fcfcc1 feat(checkout): server-side BFF to close P0 anon-mint regression — 0.1.1
The onboarding checkout POSTed the mint straight from the browser to commerce
/v1/checkout/sessions. commerce v1.46.4 (P0) closed that anonymous path, so the
browser POST now 401s and checkout is broken.

Fix: a small stdlib-only Go server (server/) serves the embedded Vite SPA AND a
single server-side checkout entry POST /v1/checkout. The browser holds no token
and can choose no org/price/redirect — it only names a plan. The BFF calls
commerce with a per-org Published storefront token from KMS; org is derived from
the token, prices from the server-side plans map, success/cancel URLs from
PUBLIC_BASE_URL (https://hanzo.agency). Fails closed (503) without the token —
never an anon mint.

- server/main.go, checkout.go, spa.go, embed.go — BFF + SPA server
- server/checkout_test.go — authed mint, no client override, fail-closed, wire
- src/lib/commerce.ts — browser now calls same-origin /v1/checkout
- Dockerfile — node build (dist) -> go build (embed) -> alpine
- pairs with commerce v1.46.7 (adds hanzo.agency to the redirect allowlist)
2026-07-02 07:23:18 -07:00

41 lines
1.1 KiB
Go

package main
import (
"io/fs"
"net/http"
"strings"
)
// spaHandler serves the embedded Vite SPA. Hashed asset requests (a file that
// exists in the bundle) are served with an immutable long cache; every other
// path falls through to index.html so the client-side router (react-router)
// renders it, with no-cache so a deploy rolls out without a stale shell.
func spaHandler() http.Handler {
root := spaFS()
files := http.FileServer(http.FS(root))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p := strings.TrimPrefix(r.URL.Path, "/")
if p == "" {
p = "index.html"
}
if i := strings.LastIndexByte(p, '.'); i >= 0 && !strings.Contains(p[i:], "/") {
if st, err := fs.Stat(root, p); err == nil && !st.IsDir() {
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
files.ServeHTTP(w, r)
return
}
}
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
idx, err := fs.ReadFile(root, "index.html")
if err != nil {
http.Error(w, "agency SPA not built", http.StatusServiceUnavailable)
return
}
_, _ = w.Write(idx)
})
}