fix(ai): probe model CORS with a dummy POST (#411)
This commit is contained in:
1 parent
dcfd11468d
commit
8528f5a912
4 files changed
+120
-23
No files matched your search
@@ -54,7 +54,11 @@ describe("direct tag suggestions", () => {
|
||||
modelId: "meta/llama",
|
||||
}), { headers: { "content-type": "application/json" } });
|
||||
}
|
||||
if (String(url).includes("integrate.api.nvidia.com")) throw new TypeError("Failed to fetch");
|
||||
if (String(url).includes("integrate.api.nvidia.com")) {
|
||||
expect(init?.method).toBe("POST");
|
||||
expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer edgeever-cors-probe");
|
||||
throw new TypeError("Failed to fetch");
|
||||
}
|
||||
return new Response(JSON.stringify({ suggestions: [{ name: "proxied", existing: false }] }), {
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { createEdgeEverClient } from "./index.ts";
|
||||
import { probeAiProviderCors } from "./ai-direct-stream.ts";
|
||||
|
||||
describe("direct AI generation client", () => {
|
||||
test("prepares on the instance then streams from the model provider", async () => {
|
||||
@@ -56,7 +57,11 @@ describe("direct AI generation client", () => {
|
||||
baseUrl: "https://notes.example",
|
||||
tryDirectAiGeneration: true,
|
||||
fetch: async (url, init) => {
|
||||
urls.push({ url: String(url), method: init?.method ?? "GET" });
|
||||
urls.push({
|
||||
url: String(url),
|
||||
method: init?.method ?? "GET",
|
||||
auth: new Headers(init?.headers).get("Authorization"),
|
||||
});
|
||||
if (String(url).endsWith("/direct-target")) {
|
||||
return new Response(JSON.stringify({
|
||||
provider: "openai-compatible",
|
||||
@@ -65,6 +70,8 @@ describe("direct AI generation client", () => {
|
||||
}), { headers: { "content-type": "application/json" } });
|
||||
}
|
||||
if (String(url).includes("integrate.api.nvidia.com")) {
|
||||
expect(init?.method).toBe("POST");
|
||||
expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer edgeever-cors-probe");
|
||||
throw new TypeError("Failed to fetch");
|
||||
}
|
||||
return new Response("data: {\"type\":\"text-delta\",\"text\":\"proxied\"}\n\n", {
|
||||
@@ -78,22 +85,27 @@ describe("direct AI generation client", () => {
|
||||
{ onEvent: (event) => events.push(event) },
|
||||
);
|
||||
expect(urls).toEqual([
|
||||
{ url: "https://notes.example/api/v1/ai/direct-target", method: "GET" },
|
||||
{ url: "https://integrate.api.nvidia.com/v1/chat/completions", method: "OPTIONS" },
|
||||
{ url: "https://notes.example/api/v1/ai/generate", method: "POST" },
|
||||
{ url: "https://notes.example/api/v1/ai/direct-target", method: "GET", auth: null },
|
||||
{ url: "https://integrate.api.nvidia.com/v1/chat/completions", method: "POST", auth: "Bearer edgeever-cors-probe" },
|
||||
{ url: "https://notes.example/api/v1/ai/generate", method: "POST", auth: null },
|
||||
]);
|
||||
expect(urls.some((call) => call.url.endsWith("/prepare"))).toBe(false);
|
||||
expect(events).toEqual([{ type: "text-delta", text: "proxied" }]);
|
||||
});
|
||||
|
||||
test("direct-connects from the browser after a successful CORS probe", async () => {
|
||||
const urls = [];
|
||||
const calls = [];
|
||||
const client = createEdgeEverClient({
|
||||
baseUrl: "https://notes.example",
|
||||
token: "user-token",
|
||||
tryDirectAiGeneration: true,
|
||||
fetch: async (url, init) => {
|
||||
urls.push(String(url));
|
||||
const headers = new Headers(init?.headers);
|
||||
calls.push({
|
||||
url: String(url),
|
||||
method: init?.method ?? "GET",
|
||||
auth: headers.get("Authorization"),
|
||||
});
|
||||
if (String(url).endsWith("/direct-target")) {
|
||||
return new Response(JSON.stringify({
|
||||
provider: "openai-compatible",
|
||||
@@ -101,8 +113,17 @@ describe("direct AI generation client", () => {
|
||||
modelId: "openai/gpt-4o-mini",
|
||||
}), { headers: { "content-type": "application/json" } });
|
||||
}
|
||||
if (String(url).includes("openrouter.ai") && (init?.method ?? "GET") === "OPTIONS") {
|
||||
return new Response(null, { status: 204 });
|
||||
if (String(url).includes("openrouter.ai")) {
|
||||
if (headers.get("Authorization") === "Bearer edgeever-cors-probe") {
|
||||
return new Response("{}", { status: 401 });
|
||||
}
|
||||
expect(headers.get("Authorization")).toBe("Bearer or-key");
|
||||
return new Response([
|
||||
`data: {"choices":[{"delta":{"content":"<edgeever-result-x>\\nHello"}}]}`,
|
||||
`data: {"choices":[{"delta":{"content":"</edgeever-result-x>"},"finish_reason":"stop"}]}`,
|
||||
"data: [DONE]",
|
||||
"",
|
||||
].join("\n\n"), { headers: { "content-type": "text/event-stream" } });
|
||||
}
|
||||
if (String(url).endsWith("/prepare")) {
|
||||
return new Response(JSON.stringify({
|
||||
@@ -116,14 +137,6 @@ describe("direct AI generation client", () => {
|
||||
resultBoundary: { start: "<edgeever-result-x>", end: "</edgeever-result-x>" },
|
||||
}), { headers: { "content-type": "application/json" } });
|
||||
}
|
||||
if (String(url).includes("openrouter.ai")) {
|
||||
return new Response([
|
||||
`data: {"choices":[{"delta":{"content":"<edgeever-result-x>\\nHello"}}]}`,
|
||||
`data: {"choices":[{"delta":{"content":"</edgeever-result-x>"},"finish_reason":"stop"}]}`,
|
||||
"data: [DONE]",
|
||||
"",
|
||||
].join("\n\n"), { headers: { "content-type": "text/event-stream" } });
|
||||
}
|
||||
throw new Error(`unexpected ${url}`);
|
||||
},
|
||||
});
|
||||
@@ -132,11 +145,11 @@ describe("direct AI generation client", () => {
|
||||
{ action: "summarize", title: "Note", contentMarkdown: "Body" },
|
||||
{ onEvent: (event) => events.push(event) },
|
||||
);
|
||||
expect(urls).toEqual([
|
||||
"https://notes.example/api/v1/ai/direct-target",
|
||||
"https://openrouter.ai/api/v1/chat/completions",
|
||||
"https://notes.example/api/v1/ai/generate/prepare",
|
||||
"https://openrouter.ai/api/v1/chat/completions",
|
||||
expect(calls).toEqual([
|
||||
{ url: "https://notes.example/api/v1/ai/direct-target", method: "GET", auth: "Bearer user-token" },
|
||||
{ url: "https://openrouter.ai/api/v1/chat/completions", method: "POST", auth: "Bearer edgeever-cors-probe" },
|
||||
{ url: "https://notes.example/api/v1/ai/generate/prepare", method: "POST", auth: "Bearer user-token" },
|
||||
{ url: "https://openrouter.ai/api/v1/chat/completions", method: "POST", auth: "Bearer or-key" },
|
||||
]);
|
||||
expect(events.filter((event) => event.type === "text-delta").map((event) => event.text).join("")).toBe("Hello");
|
||||
});
|
||||
@@ -166,3 +179,73 @@ describe("direct AI generation client", () => {
|
||||
expect(events).toEqual([{ type: "text-delta", text: "proxied" }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("AI provider CORS probe", () => {
|
||||
const openaiTarget = {
|
||||
provider: "openai-compatible",
|
||||
baseUrl: "https://api.example/v1",
|
||||
modelId: "demo",
|
||||
};
|
||||
|
||||
test("POSTs a dummy key and treats a readable 401 as CORS success", async () => {
|
||||
let cancelled = false;
|
||||
const body = new ReadableStream({
|
||||
start() {},
|
||||
cancel() {
|
||||
cancelled = true;
|
||||
},
|
||||
});
|
||||
const allowed = await probeAiProviderCors(openaiTarget, async (url, init) => {
|
||||
expect(String(url)).toBe("https://api.example/v1/chat/completions");
|
||||
expect(init?.method).toBe("POST");
|
||||
expect(init?.mode).toBe("cors");
|
||||
const headers = new Headers(init?.headers);
|
||||
expect(headers.get("Authorization")).toBe("Bearer edgeever-cors-probe");
|
||||
expect(headers.get("Content-Type")).toBe("application/json");
|
||||
expect(headers.get("Accept")).toBe("text/event-stream");
|
||||
expect(JSON.parse(String(init?.body)).stream).toBe(true);
|
||||
return new Response(body, { status: 401 });
|
||||
});
|
||||
expect(allowed).toBe(true);
|
||||
expect(cancelled).toBe(true);
|
||||
});
|
||||
|
||||
test("sends Anthropic and Google dummy credentials on the real provider paths", async () => {
|
||||
const anthropicAllowed = await probeAiProviderCors({
|
||||
provider: "anthropic",
|
||||
baseUrl: "https://api.anthropic.com/v1",
|
||||
modelId: "claude",
|
||||
}, async (url, init) => {
|
||||
expect(String(url)).toBe("https://api.anthropic.com/v1/messages");
|
||||
expect(init?.method).toBe("POST");
|
||||
expect(new Headers(init?.headers).get("x-api-key")).toBe("edgeever-cors-probe");
|
||||
return new Response("{}", { status: 401 });
|
||||
});
|
||||
const googleAllowed = await probeAiProviderCors({
|
||||
provider: "google",
|
||||
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
||||
modelId: "models/gemini-2.5-flash",
|
||||
}, async (url, init) => {
|
||||
expect(String(url)).toBe("https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:streamGenerateContent?alt=sse");
|
||||
expect(init?.method).toBe("POST");
|
||||
expect(new Headers(init?.headers).get("x-goog-api-key")).toBe("edgeever-cors-probe");
|
||||
return new Response("{}", { status: 403 });
|
||||
});
|
||||
expect(anthropicAllowed).toBe(true);
|
||||
expect(googleAllowed).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false when the browser blocks the probe", async () => {
|
||||
const allowed = await probeAiProviderCors(openaiTarget, async () => {
|
||||
throw new TypeError("Failed to fetch");
|
||||
});
|
||||
expect(allowed).toBe(false);
|
||||
});
|
||||
|
||||
test("rethrows when the caller aborts the probe", async () => {
|
||||
const signal = AbortSignal.abort();
|
||||
await expect(probeAiProviderCors(openaiTarget, async () => {
|
||||
throw new DOMException("The operation was aborted.", "AbortError");
|
||||
}, signal)).rejects.toMatchObject({ name: "AbortError" });
|
||||
});
|
||||
});
|
||||
@@ -126,7 +126,15 @@ export const probeAiProviderCors = async (
|
||||
resultBoundary: { start: "<edgeever-result-probe>", end: "</edgeever-result-probe>" },
|
||||
});
|
||||
try {
|
||||
await fetchImpl(request.url, { method: "OPTIONS", mode: "cors", signal });
|
||||
// POST with the dummy key so the browser preflight matches the real direct request.
|
||||
const response = await fetchImpl(request.url, {
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
signal,
|
||||
});
|
||||
await response.body?.cancel().catch(() => {});
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (signal?.aborted) throw error;
|
||||
|
||||
@@ -95,6 +95,8 @@ describe("direct companion client", () => {
|
||||
}), { headers: { "content-type": "application/json" } });
|
||||
}
|
||||
if (String(url).includes("integrate.api.nvidia.com")) {
|
||||
expect(init?.method).toBe("POST");
|
||||
expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer edgeever-cors-probe");
|
||||
throw new TypeError("Failed to fetch");
|
||||
}
|
||||
return new Response("data: {\"type\":\"text-delta\",\"text\":\"proxied\"}\n\n", {
|
||||
|
||||
Reference in new issue
Block a user