allow local dev origins on api

This commit is contained in:
dirtydishes 2026-06-13 11:07:53 -04:00
parent 4446b228d7
commit 7e095b51f6
5 changed files with 213 additions and 6 deletions

107
services/api/src/cors.ts Normal file
View file

@ -0,0 +1,107 @@
export const DEFAULT_API_CORS_ORIGINS = [
"https://flow.deltaisland.io",
"http://127.0.0.1:3000",
"http://localhost:3000",
"http://127.0.0.1:3100",
"http://localhost:3100"
].join(",");
const DEFAULT_ALLOWED_HEADERS = "authorization,content-type,x-synthetic-admin-token";
const DEFAULT_ALLOWED_METHODS = "GET,POST,PUT,OPTIONS";
const normalizeOrigin = (origin: string): string | null => {
const trimmed = origin.trim();
if (!trimmed) {
return null;
}
if (trimmed === "*") {
return trimmed;
}
try {
return new URL(trimmed).origin;
} catch {
return null;
}
};
export const parseCorsAllowedOrigins = (value: string): Set<string> => {
const origins = new Set<string>();
for (const entry of value.split(",")) {
const origin = normalizeOrigin(entry);
if (origin) {
origins.add(origin);
}
}
return origins;
};
export const resolveCorsOrigin = (req: Request, allowedOrigins: Set<string>): string | null => {
const origin = normalizeOrigin(req.headers.get("origin") ?? "");
if (!origin) {
return null;
}
if (allowedOrigins.has("*")) {
return "*";
}
return allowedOrigins.has(origin) ? origin : null;
};
const appendVaryOrigin = (headers: Headers): void => {
const vary = headers.get("vary");
if (!vary) {
headers.set("vary", "Origin");
return;
}
if (!vary.split(",").some((value) => value.trim().toLowerCase() === "origin")) {
headers.set("vary", `${vary}, Origin`);
}
};
export const withCorsHeaders = (
req: Request,
response: Response,
allowedOrigins: Set<string>
): Response => {
if (response.status === 101) {
return response;
}
const allowedOrigin = resolveCorsOrigin(req, allowedOrigins);
if (!allowedOrigin) {
return response;
}
const headers = new Headers(response.headers);
headers.set("access-control-allow-origin", allowedOrigin);
appendVaryOrigin(headers);
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers
});
};
export const createCorsPreflightResponse = (
req: Request,
allowedOrigins: Set<string>
): Response => {
const headers = new Headers();
const allowedOrigin = resolveCorsOrigin(req, allowedOrigins);
if (allowedOrigin) {
headers.set("access-control-allow-origin", allowedOrigin);
headers.set("access-control-allow-methods", DEFAULT_ALLOWED_METHODS);
headers.set(
"access-control-allow-headers",
req.headers.get("access-control-request-headers") ?? DEFAULT_ALLOWED_HEADERS
);
headers.set("access-control-max-age", "86400");
appendVaryOrigin(headers);
}
return new Response(null, {
status: 204,
headers
});
};

View file

@ -138,6 +138,12 @@ import {
recordSyntheticProfileHit,
resolveSyntheticBackendMode
} from "./synthetic-control";
import {
DEFAULT_API_CORS_ORIGINS,
createCorsPreflightResponse,
parseCorsAllowedOrigins,
withCorsHeaders
} from "./cors";
const service = "api";
const logger = createLogger({ service });
@ -172,10 +178,12 @@ const envSchema = z.object({
return value;
}, z.boolean())
.default(false),
SYNTHETIC_ADMIN_TOKEN: z.string().default("")
SYNTHETIC_ADMIN_TOKEN: z.string().default(""),
API_CORS_ORIGINS: z.string().default(DEFAULT_API_CORS_ORIGINS)
});
const env = readEnv(envSchema);
const corsAllowedOrigins = parseCorsAllowedOrigins(env.API_CORS_ORIGINS);
const state = {
shuttingDown: false,
@ -1363,11 +1371,16 @@ const run = async () => {
hostname: env.API_HOST,
port: env.API_PORT,
fetch: async (req: Request, serverRef: any) => {
const url = new URL(req.url);
const handleApiRequest = async (): Promise<Response> => {
const url = new URL(req.url);
if (req.method === "GET" && url.pathname === "/health") {
return jsonResponse({ status: "ok" });
}
if (req.method === "OPTIONS") {
return createCorsPreflightResponse(req, corsAllowedOrigins);
}
if (req.method === "GET" && url.pathname === "/health") {
return jsonResponse({ status: "ok" });
}
if (req.method === "GET" && url.pathname === "/admin/synthetic/status") {
const authError = authenticateSyntheticAdminRequest(req);
@ -1951,7 +1964,11 @@ const run = async () => {
return jsonResponse({ error: "websocket upgrade failed" }, 400);
}
return jsonResponse({ error: "not found" }, 404);
return jsonResponse({ error: "not found" }, 404);
};
const response = await handleApiRequest();
return withCorsHeaders(req, response, corsAllowedOrigins);
},
websocket: {
open: (socket: any) => {