diff --git a/packages/client/src/ai-direct-generate.test.mjs b/packages/client/src/ai-direct-generate.test.mjs
index 40561604..a39014f9 100644
--- a/packages/client/src/ai-direct-generate.test.mjs
+++ b/packages/client/src/ai-direct-generate.test.mjs
@@ -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" },
});
diff --git a/packages/client/src/ai-direct-stream.test.mjs b/packages/client/src/ai-direct-stream.test.mjs
index 7be6802f..502e3a23 100644
--- a/packages/client/src/ai-direct-stream.test.mjs
+++ b/packages/client/src/ai-direct-stream.test.mjs
@@ -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":"\\nHello"}}]}`,
+ `data: {"choices":[{"delta":{"content":""},"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: "", end: "" },
}), { headers: { "content-type": "application/json" } });
}
- if (String(url).includes("openrouter.ai")) {
- return new Response([
- `data: {"choices":[{"delta":{"content":"\\nHello"}}]}`,
- `data: {"choices":[{"delta":{"content":""},"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" });
+ });
+});
diff --git a/packages/client/src/ai-direct-stream.ts b/packages/client/src/ai-direct-stream.ts
index b1d1b83f..4c200d71 100644
--- a/packages/client/src/ai-direct-stream.ts
+++ b/packages/client/src/ai-direct-stream.ts
@@ -126,7 +126,15 @@ export const probeAiProviderCors = async (
resultBoundary: { start: "", end: "" },
});
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;
diff --git a/packages/client/src/companion-direct-stream.test.mjs b/packages/client/src/companion-direct-stream.test.mjs
index de2eb1fa..b8d0b985 100644
--- a/packages/client/src/companion-direct-stream.test.mjs
+++ b/packages/client/src/companion-direct-stream.test.mjs
@@ -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", {