Skip to content

Commit 669c5cd

Browse files
committed
Allow dashboard fallback CORS preflight
1 parent 22c5d70 commit 669c5cd

2 files changed

Lines changed: 64 additions & 4 deletions

File tree

src/server.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ function isAdminRequest(request: FastifyRequest): boolean {
135135
}
136136

137137
function shouldRequireAuth(request: FastifyRequest): boolean {
138+
if (request.method === "OPTIONS") return false;
138139
if (request.method === "GET" && request.url.startsWith("/admin/config")) return false;
139140
if (request.method === "GET" && request.url.startsWith("/admin/commandcode/credentials")) {
140141
return false;
@@ -144,12 +145,28 @@ function shouldRequireAuth(request: FastifyRequest): boolean {
144145

145146
function isPublicAdminRequest(request: FastifyRequest): boolean {
146147
return (
147-
request.method === "GET" &&
148-
(request.url.startsWith("/admin/config") ||
149-
request.url.startsWith("/admin/commandcode/credentials"))
148+
request.method === "OPTIONS" ||
149+
(request.method === "GET" &&
150+
(request.url.startsWith("/admin/config") ||
151+
request.url.startsWith("/admin/commandcode/credentials")))
150152
);
151153
}
152154

155+
function sameHostnameOrigin(request: FastifyRequest): string | undefined {
156+
const origin = request.headers.origin;
157+
if (!origin) return undefined;
158+
const host = request.headers.host;
159+
if (!host) return undefined;
160+
try {
161+
const originUrl = new URL(origin);
162+
const hostName = host.split(":")[0];
163+
if (originUrl.protocol === "http:" && originUrl.hostname === hostName) return origin;
164+
} catch {
165+
return undefined;
166+
}
167+
return undefined;
168+
}
169+
153170
function asOpenAIRequest(
154171
value: z.infer<typeof chatCompletionRequestSchema>,
155172
): OpenAIChatCompletionRequest {
@@ -320,8 +337,22 @@ export async function createApp(options: CreateAppOptions = {}): Promise<Fastify
320337
},
321338
},
322339
});
323-
await app.register(rateLimit, { max: config.rateLimitMax, timeWindow: config.rateLimitWindow });
324340
if (config.corsOrigin) await app.register(cors, { origin: config.corsOrigin });
341+
app.addHook("onRequest", async (request, reply) => {
342+
if (config.corsOrigin) return;
343+
const origin = sameHostnameOrigin(request);
344+
if (!origin) return;
345+
reply.header("access-control-allow-origin", origin);
346+
reply.header("vary", "origin");
347+
reply.header("access-control-allow-methods", "GET,PUT,POST,OPTIONS");
348+
reply.header("access-control-allow-headers", "authorization,content-type,x-api-key");
349+
});
350+
app.options("*", async (request, reply) => {
351+
const origin = config.corsOrigin ? request.headers.origin : sameHostnameOrigin(request);
352+
if (!origin && !config.corsOrigin) return reply.code(404).send();
353+
return reply.code(204).send();
354+
});
355+
await app.register(rateLimit, { max: config.rateLimitMax, timeWindow: config.rateLimitWindow });
325356

326357
let balanceAlertTimer: NodeJS.Timeout | undefined;
327358
if (balanceAlertManager) {

tests/admin-config.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,4 +248,33 @@ describe("JSON dashboard configuration", () => {
248248
);
249249
await app.close();
250250
});
251+
252+
it("allows same-host dashboard fallback writes from portless mobile origins", async () => {
253+
const file = tempConfigFile({
254+
routing: { policy: "daily_burn_priority", maxInFlightPerCredential: 4 },
255+
credentials: [{ id: "alpha", apiKey: "alpha-secret", weight: 1 }],
256+
});
257+
const app = await createApp({
258+
upstream: new FakeCommandCodeClient(),
259+
configEnv: { COMMANDCODE_CREDENTIALS_FILE: file },
260+
configAuthPaths: [],
261+
configOverrides: { bridgeApiKey: "bridge-secret", logLevel: "silent" },
262+
});
263+
264+
const preflight = await app.inject({
265+
method: "OPTIONS",
266+
url: "/admin/config",
267+
headers: {
268+
origin: "http://100.88.251.70",
269+
"access-control-request-method": "PUT",
270+
"access-control-request-headers": "authorization,content-type",
271+
host: "100.88.251.70:9992",
272+
},
273+
});
274+
expect(preflight.statusCode).toBe(204);
275+
expect(preflight.headers["access-control-allow-origin"]).toBe("http://100.88.251.70");
276+
expect(preflight.headers["access-control-allow-headers"]).toContain("authorization");
277+
278+
await app.close();
279+
});
251280
});

0 commit comments

Comments
 (0)