configure hosted api endpoint and refresh local mock wiring #23

Merged
dirtydishes merged 4 commits from lavender/configure-hosted-api-endpoint into main 2026-06-14 19:39:48 +00:00
5 changed files with 213 additions and 6 deletions
Showing only changes of commit 7e095b51f6 - Show all commits

View file

@ -60,6 +60,7 @@ COMPUTE_DELIVER_POLICY=new
COMPUTE_CONSUMER_RESET=false
API_DELIVER_POLICY=new
API_CONSUMER_RESET=false
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
NBBO_MAX_AGE_MS=1000
NEXT_PUBLIC_NBBO_MAX_AGE_MS=1000
NEXT_PUBLIC_LIVE_HOT_WINDOW=600

View file

@ -400,6 +400,7 @@ Default `smart-money` policy rejects lower-information prints and keeps higher-c
| `REST_DEFAULT_LIMIT` | `200` | Default REST record count. |
| `API_DELIVER_POLICY` | `new` | JetStream consumer start policy used by API live subscribers. |
| `API_CONSUMER_RESET` | `false` | Resets/recreates API live durable consumers on startup when true. |
| `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` | Comma-separated browser origins allowed to call the API directly; local web and desktop-local dev rely on these headers. |
| `LIVE_LIMIT_DEFAULT` | `1000` | Optional generic live cache depth default. |
| `LIVE_LIMIT_FLOW` | `500` | Live cache depth for flow packet events unless overridden. |
| `LIVE_LIMIT_SMART_MONEY` | `300` | Live cache depth for smart-money events unless overridden. |

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,8 +1371,13 @@ const run = async () => {
hostname: env.API_HOST,
port: env.API_PORT,
fetch: async (req: Request, serverRef: any) => {
const handleApiRequest = async (): Promise<Response> => {
const url = new URL(req.url);
if (req.method === "OPTIONS") {
return createCorsPreflightResponse(req, corsAllowedOrigins);
}
if (req.method === "GET" && url.pathname === "/health") {
return jsonResponse({ status: "ok" });
}
@ -1952,6 +1965,10 @@ const run = async () => {
}
return jsonResponse({ error: "not found" }, 404);
};
const response = await handleApiRequest();
return withCorsHeaders(req, response, corsAllowedOrigins);
},
websocket: {
open: (socket: any) => {

View file

@ -0,0 +1,81 @@
import { describe, expect, it } from "bun:test";
import {
createCorsPreflightResponse,
parseCorsAllowedOrigins,
resolveCorsOrigin,
withCorsHeaders
} from "../src/cors";
describe("api cors helpers", () => {
const allowedOrigins = parseCorsAllowedOrigins(
"https://flow.deltaisland.io, http://127.0.0.1:3000/, http://localhost:3100"
);
it("normalizes configured origins", () => {
expect(allowedOrigins.has("https://flow.deltaisland.io")).toBe(true);
expect(allowedOrigins.has("http://127.0.0.1:3000")).toBe(true);
expect(allowedOrigins.has("http://localhost:3100")).toBe(true);
expect(allowedOrigins.has("http://127.0.0.1:3000/")).toBe(false);
});
it("reflects allowed browser origins", () => {
const req = new Request("https://api.flow.deltaisland.io/prints/options", {
headers: {
origin: "http://127.0.0.1:3000"
}
});
expect(resolveCorsOrigin(req, allowedOrigins)).toBe("http://127.0.0.1:3000");
});
it("does not reflect unknown origins", () => {
const req = new Request("https://api.flow.deltaisland.io/prints/options", {
headers: {
origin: "http://evil.example"
}
});
expect(resolveCorsOrigin(req, allowedOrigins)).toBeNull();
});
it("adds cors headers to normal responses for allowed origins", async () => {
const req = new Request("https://api.flow.deltaisland.io/health", {
headers: {
origin: "https://flow.deltaisland.io"
}
});
const response = withCorsHeaders(
req,
new Response(JSON.stringify({ status: "ok" }), {
headers: {
"content-type": "application/json"
}
}),
allowedOrigins
);
expect(response.headers.get("access-control-allow-origin")).toBe("https://flow.deltaisland.io");
expect(response.headers.get("vary")).toBe("Origin");
expect(response.headers.get("content-type")).toBe("application/json");
expect(await response.json()).toEqual({ status: "ok" });
});
it("answers preflight requests for allowed origins", () => {
const req = new Request("https://api.flow.deltaisland.io/lookup/options-support", {
method: "OPTIONS",
headers: {
origin: "http://localhost:3100",
"access-control-request-method": "POST",
"access-control-request-headers": "content-type,authorization"
}
});
const response = createCorsPreflightResponse(req, allowedOrigins);
expect(response.status).toBe(204);
expect(response.headers.get("access-control-allow-origin")).toBe("http://localhost:3100");
expect(response.headers.get("access-control-allow-methods")).toContain("POST");
expect(response.headers.get("access-control-allow-headers")).toBe(
"content-type,authorization"
);
});
});