diff --git a/go/cmd/mir-signal/main.go b/go/cmd/mir-signal/main.go index e699b9d..6843670 100644 --- a/go/cmd/mir-signal/main.go +++ b/go/cmd/mir-signal/main.go @@ -144,7 +144,12 @@ func newHTTPServer(addr string, handler http.Handler) *http.Server { func withStatic(sig http.Handler, dir string) http.Handler { fs := http.FileServer(http.Dir(dir)) indexPath := filepath.Join(dir, "index.html") - signalPaths := map[string]bool{"/agent/signal": true, "/attach": true, "/pair": true, "/turn-credentials": true, "/healthz": true} + // Every path the signal server owns must be forwarded here, or --webroot mode + // 404s it into the static file server. This list MUST stay in sync with the + // routes registered in signal.Server.Handler(); main_test.go's + // TestWithStaticForwardsSignalingPaths guards against drift (it caught /registry + // going missing in production). + signalPaths := map[string]bool{"/agent/signal": true, "/attach": true, "/pair": true, "/turn-credentials": true, "/healthz": true, "/registry": true} return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if signalPaths[r.URL.Path] { sig.ServeHTTP(w, r) diff --git a/go/cmd/mir-signal/main_test.go b/go/cmd/mir-signal/main_test.go index d90aa05..e26abeb 100644 --- a/go/cmd/mir-signal/main_test.go +++ b/go/cmd/mir-signal/main_test.go @@ -1,6 +1,7 @@ package main import ( + "io" "net/http" "net/http/httptest" "os" @@ -8,8 +9,47 @@ import ( "strings" "testing" "time" + + "github.com/srcful/terminal-relay/go/internal/signal" ) +// TestWithStaticForwardsSignalingPaths guards the production --webroot wiring: every +// path the signal server owns must be forwarded to it, not 404'd into the static +// file server. /registry shipped registered in signal.Server.Handler() but MISSING +// from withStatic's signalPaths map, so it 404'd in production (tests that used +// Server.Handler() directly never saw the wrapper). This pins the wrapper itself. +func TestWithStaticForwardsSignalingPaths(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte("spa"), 0o644); err != nil { + t.Fatal(err) + } + ts := httptest.NewServer(withStatic(signal.New().Handler(), dir)) + defer ts.Close() + + // /registry MUST be forwarded to the signal server (handleRegistry returns a JSON + // array for a wallet query) — not 404'd into the static FS. + resp, err := http.Get(ts.URL + "/registry?wallet=test") + if err != nil { + t.Fatal(err) + } + body, _ := io.ReadAll(resp.Body) + resp.Body.Close() + if resp.StatusCode != http.StatusOK || !strings.HasPrefix(strings.TrimSpace(string(body)), "[") { + t.Fatalf("/registry via withStatic = %d %q; want 200 JSON array (forwarded to signal, not static 404)", resp.StatusCode, body) + } + + // /healthz is forwarded too. + if r, err := http.Get(ts.URL + "/healthz"); err != nil || r.StatusCode != http.StatusOK { + t.Fatalf("/healthz via withStatic not forwarded (err=%v)", err) + } + + // A missing static asset still 404s via the FS — proves we aren't trivially + // forwarding everything to the signal server. + if r, err := http.Get(ts.URL + "/vendor/missing-asset.js"); err != nil || r.StatusCode != http.StatusNotFound { + t.Fatalf("a missing static asset should 404 via the FS (got err=%v)", err) + } +} + func TestNewHTTPServerSetsTimeouts(t *testing.T) { handler := http.NewServeMux() srv := newHTTPServer(":0", handler) diff --git a/web/sw.js b/web/sw.js index 40c4845..bc77cac 100644 --- a/web/sw.js +++ b/web/sw.js @@ -66,7 +66,7 @@ const SHELL = [ // Live conversations with the relay — never cached, never intercepted. // (WebSocket upgrades bypass the fetch handler anyway; this covers the plain // HTTP ones like /turn-credentials and keeps the list in one place.) -const SIGNALING = ['/agent/signal', '/attach', '/pair', '/turn-credentials', '/healthz']; +const SIGNALING = ['/agent/signal', '/attach', '/pair', '/turn-credentials', '/healthz', '/registry']; self.addEventListener('install', (event) => { event.waitUntil(