feat(relay-server): add CORS support to static file endpoints
- Removed BOOTSTRAPS build arg from Dockerfile and simplified build-wasm command - Updated BOOTSTRAP_URIS env var to use only local relay for development - Added setCORSHeaders function and applied it to all static file handlers (/static/, /frontend/, root, WASM, admin, manifest, service worker) to enable cross-origin requests for better portal UI integration
lemon-mint committed
Nov 7, 2025 at 01:50 UTC
ad3f9377af2fdee8d2ccbefc04665d0d35e2a99f
3 files changed
+45
-3
Dockerfile
+2
-3
@@ -14,8 +14,7 @@ RUN go mod download
14
COPY . .
15
16
# Build WASM and server
17
-ARG BOOTSTRAPS=""
18
-RUN make build-wasm BOOTSTRAPS="$BOOTSTRAPS"
17
+RUN make build-wasm
18
19
# Build server
20
RUN make build-server
@@ -32,7 +31,7 @@ COPY --from=builder /src/dist /app/dist
31
ENV STATIC_DIR=/app/dist
32
ENV PORTAL_UI_URL=http://localhost:4017
33
ENV POSTAL_FRONTEND_URL=http://*.localhost:4017
35
-ENV BOOTSTRAP_URIS=ws://localhost:4017/relay,wss://some.app:21762/relay
34
+ENV BOOTSTRAP_URIS=ws://localhost:4017/relay
35
36
# Expose ports
37
# 4017: relay server and portal frontend
cmd/relay-server/frontend.go
+33
@@ -141,6 +141,13 @@ func cacheWasmFile(name, fullPath string) error {
141
return nil
142
}
143
144
+// setCORSHeaders sets CORS headers for static file serving
145
+func setCORSHeaders(w http.ResponseWriter) {
146
+ w.Header().Set("Access-Control-Allow-Origin", "*")
147
+ w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
148
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept, Accept-Encoding")
149
+}
150
+
151
// createPortalMux creates a new HTTP mux for portal frontend
152
func createPortalMux() *http.ServeMux {
153
// Initialize WASM cache on startup
@@ -152,12 +159,22 @@ func createPortalMux() *http.ServeMux {
159
160
// Static file handler for /static/
161
mux.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) {
162
+ setCORSHeaders(w)
163
+ if r.Method == http.MethodOptions {
164
+ w.WriteHeader(http.StatusOK)
165
+ return
166
+ }
167
path := strings.TrimPrefix(r.URL.Path, "/static/")
168
servePortalStaticFile(w, r, path)
169
})
170
171
// Static file handler for /frontend/ (for unified caching)
172
mux.HandleFunc("/frontend/", func(w http.ResponseWriter, r *http.Request) {
173
+ setCORSHeaders(w)
174
+ if r.Method == http.MethodOptions {
175
+ w.WriteHeader(http.StatusOK)
176
+ return
177
+ }
178
path := strings.TrimPrefix(r.URL.Path, "/frontend/")
179
180
// Special handling for manifest.json - generate dynamically
@@ -171,6 +188,11 @@ func createPortalMux() *http.ServeMux {
188
189
// Root handler for portal frontend
190
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
191
+ setCORSHeaders(w)
192
+ if r.Method == http.MethodOptions {
193
+ w.WriteHeader(http.StatusOK)
194
+ return
195
+ }
196
if r.URL.Path == "/" {
197
serveStaticFile(w, r, "portal.html", "text/html; charset=utf-8")
198
return
@@ -212,6 +234,7 @@ func serveCompressedWasm(w http.ResponseWriter, r *http.Request, path string) {
234
}
235
236
// Set immutable cache headers for content-addressed files
237
+ setCORSHeaders(w)
238
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
239
w.Header().Set("Content-Type", "application/wasm")
240
@@ -266,6 +289,8 @@ func serveAdminStatic(w http.ResponseWriter, r *http.Request, path string) {
289
}
290
291
// Try to read from embedded FS
292
+ setCORSHeaders(w)
293
+
294
fullPath := filepath.Join("static", path)
295
data, err := assetsFS.ReadFile(fullPath)
296
if err != nil {
@@ -334,6 +359,8 @@ func servePortalStatic(w http.ResponseWriter, r *http.Request) {
359
360
// serveStaticFile reads and serves a file from the static directory
361
func serveStaticFile(w http.ResponseWriter, r *http.Request, path string, contentType string) {
362
+ setCORSHeaders(w)
363
+
364
fullPath := filepath.Join(staticDir, path)
365
366
file, err := os.Open(fullPath)
@@ -374,6 +401,8 @@ func serveStaticFile(w http.ResponseWriter, r *http.Request, path string, conten
401
// serveStaticFileWithFallback reads and serves a file from the static directory
402
// If the file is not found, it falls back to portal.html for SPA routing
403
func serveStaticFileWithFallback(w http.ResponseWriter, r *http.Request, path string, contentType string) {
404
+ setCORSHeaders(w)
405
+
406
fullPath := filepath.Join(staticDir, path)
407
408
file, err := os.Open(fullPath)
@@ -477,6 +506,8 @@ func isHexString(s string) bool {
506
507
// serveDynamicManifest generates and serves manifest.json dynamically
508
func serveDynamicManifest(w http.ResponseWriter, r *http.Request) {
509
+ setCORSHeaders(w)
510
+
511
// Find the content-addressed WASM file
512
wasmCacheMu.RLock()
513
var wasmHash string
@@ -543,6 +574,8 @@ func serveDynamicManifest(w http.ResponseWriter, r *http.Request) {
574
575
// serveDynamicServiceWorker serves service-worker.js with injected manifest and config
576
func serveDynamicServiceWorker(w http.ResponseWriter, r *http.Request) {
577
+ setCORSHeaders(w)
578
+
579
// Read the service-worker.js template
580
fullPath := filepath.Join(staticDir, "service-worker.js")
581
content, err := os.ReadFile(fullPath)
cmd/relay-server/view.go
+10
@@ -53,12 +53,22 @@ func serveHTTP(_ context.Context, addr string, serv *portal.RelayServer, nodeID
53
54
// Static assets for admin UI (embedded files)
55
adminMux.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) {
56
+ setCORSHeaders(w)
57
+ if r.Method == http.MethodOptions {
58
+ w.WriteHeader(http.StatusOK)
59
+ return
60
+ }
61
path := strings.TrimPrefix(r.URL.Path, "/static/")
62
serveAdminStatic(w, r, path)
63
})
64
65
// Portal frontend files (for unified caching)
66
adminMux.HandleFunc("/frontend/", func(w http.ResponseWriter, r *http.Request) {
67
+ setCORSHeaders(w)
68
+ if r.Method == http.MethodOptions {
69
+ w.WriteHeader(http.StatusOK)
70
+ return
71
+ }
72
path := strings.TrimPrefix(r.URL.Path, "/frontend/")
73
74
// Special handling for manifest.json - generate dynamically