mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-02 05:12:11 +03:00
* feat(oauth): add Kiro long-lived API key auth (#6587) New /api/oauth/kiro/api-key route + KiroService.validateApiKey let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (ListAvailableModels, 5-minute cache) layered over the existing static registry fallback. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(changelog): re-restore #6587 bullet after release sync Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(merge): restore #6126 clinepass files reverted by release auto-resolve + baseline re-merge The release sync's auto-resolve reverted sibling PR #6126's clinepass work (registry, catalog, oauth constants, clineAuth.ts, token-refresh case, tests) and the file-size baseline — all outside this PR's scope. Restored to the release versions, re-applied only this PR's own baseline entries, restored the #6126 CHANGELOG bullet (re-inserting only this PR's own). Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * chore(quality): freeze public-creds FP — AWS region default in validateApiKey signature Same class as the existing minimax fn-param FPs: CRED_KEY_RE matches the apiKey: param annotation and captures the region default "us-east-1", which is not a credential. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(kiro): keep hard-failure reject semantics + kill public-creds fn-param FP at the source - getKiroUsage: exhausted non-auth attempts now REJECT with the last HTTP-status failure in the pre-#6587 format (usage-service-hardening relies on it); auth failures keep the soft social-auth message. - validateApiKey: region default moved out of the parameter list (the check-public-creds CRED_KEY_RE matches the apiKey: annotation and flags any literal in the signature); drops the brittle line-keyed allowlist entry. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: strangersp <strangersp@users.noreply.github.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
331 lines
10 KiB
TypeScript
331 lines
10 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
const catalog =
|
|
await import("../../src/app/(dashboard)/dashboard/providers/components/onboarding/providerOnboardingCatalog.ts");
|
|
const api =
|
|
await import("../../src/app/(dashboard)/dashboard/providers/components/onboarding/providerOnboardingApi.ts");
|
|
|
|
test("provider onboarding catalog exposes API-key and OAuth providers for the wizard", () => {
|
|
const apiKeyOptions = catalog.getWizardApiKeyProviderOptions();
|
|
const oauthOptions = catalog.getWizardOAuthProviderOptions();
|
|
|
|
assert.ok(apiKeyOptions.some((option) => option.id === "openai"));
|
|
assert.ok(apiKeyOptions.some((option) => option.id === "openrouter"));
|
|
assert.ok(!apiKeyOptions.some((option) => option.id === "kiro"));
|
|
assert.ok(!apiKeyOptions.some((option) => option.id === "amazon-q"));
|
|
assert.ok(oauthOptions.some((option) => option.id === "claude"));
|
|
assert.ok(oauthOptions.some((option) => option.id === "kiro"));
|
|
assert.ok(oauthOptions.some((option) => option.id === "amazon-q"));
|
|
assert.ok(oauthOptions.some((option) => option.id === "cursor"));
|
|
assert.ok(!oauthOptions.some((option) => option.id === "zed"));
|
|
assert.ok(!oauthOptions.some((option) => option.id === "windsurf"));
|
|
assert.ok(!oauthOptions.some((option) => option.id === "devin-cli"));
|
|
assert.ok(!oauthOptions.some((option) => option.id === "qoder"));
|
|
|
|
assert.ok(apiKeyOptions.every((option) => option.authKind === "apikey"));
|
|
assert.ok(oauthOptions.every((option) => option.authKind === "oauth"));
|
|
});
|
|
|
|
test("provider onboarding option filter matches id, name, alias, and description", () => {
|
|
const options = [
|
|
{
|
|
id: "openrouter",
|
|
name: "OpenRouter",
|
|
icon: "router",
|
|
alias: "or",
|
|
description: "multi-model routing gateway",
|
|
authKind: "apikey",
|
|
apiKeyOptional: false,
|
|
deprecated: false,
|
|
},
|
|
{
|
|
id: "claude",
|
|
name: "Claude Code",
|
|
icon: "smart_toy",
|
|
alias: "cc",
|
|
description: "OAuth coding provider",
|
|
authKind: "oauth",
|
|
apiKeyOptional: false,
|
|
deprecated: false,
|
|
},
|
|
];
|
|
|
|
assert.deepEqual(
|
|
catalog.filterWizardProviderOptions(options, "router").map((option) => option.id),
|
|
["openrouter"]
|
|
);
|
|
assert.deepEqual(
|
|
catalog.filterWizardProviderOptions(options, "coding").map((option) => option.id),
|
|
["claude"]
|
|
);
|
|
assert.equal(catalog.filterWizardProviderOptions(options, " ").length, 2);
|
|
});
|
|
|
|
test("provider onboarding builds providerSpecificData without empty fields", () => {
|
|
assert.deepEqual(
|
|
catalog.buildProviderSpecificData({
|
|
baseUrl: " https://gateway.example/v1 ",
|
|
region: "",
|
|
cx: " cx-123 ",
|
|
customUserAgent: " ",
|
|
}),
|
|
{
|
|
baseUrl: "https://gateway.example/v1",
|
|
cx: "cx-123",
|
|
}
|
|
);
|
|
assert.equal(catalog.buildProviderSpecificData({}), null);
|
|
});
|
|
|
|
test("provider onboarding API sends validate, create, and test requests with expected payloads", async () => {
|
|
const calls = [];
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async (url, init) => {
|
|
calls.push({ url: String(url), init });
|
|
if (String(url) === "/api/providers/validate") {
|
|
return Response.json({ valid: true });
|
|
}
|
|
if (String(url) === "/api/providers") {
|
|
return Response.json(
|
|
{ connection: { id: "conn-1", provider: "openai", name: "OpenAI Primary" } },
|
|
{ status: 201 }
|
|
);
|
|
}
|
|
if (String(url) === "/api/providers/conn-1/test") {
|
|
return Response.json({ valid: true, latencyMs: 42 });
|
|
}
|
|
return Response.json({ error: "unexpected" }, { status: 500 });
|
|
};
|
|
|
|
try {
|
|
await api.validateOnboardingApiKey({
|
|
provider: "openai",
|
|
apiKey: "sk-test",
|
|
baseUrl: "https://api.example/v1",
|
|
});
|
|
const connection = await api.createOnboardingConnection({
|
|
provider: "openai",
|
|
name: "OpenAI Primary",
|
|
apiKey: "sk-test",
|
|
providerSpecificData: { baseUrl: "https://api.example/v1" },
|
|
});
|
|
const result = await api.testOnboardingConnection(connection.id);
|
|
|
|
assert.equal(connection.id, "conn-1");
|
|
assert.equal(result.valid, true);
|
|
assert.equal(calls.length, 3);
|
|
assert.equal(calls[0].url, "/api/providers/validate");
|
|
assert.deepEqual(JSON.parse(String(calls[0].init.body)), {
|
|
provider: "openai",
|
|
apiKey: "sk-test",
|
|
baseUrl: "https://api.example/v1",
|
|
});
|
|
assert.equal(calls[1].url, "/api/providers");
|
|
assert.deepEqual(JSON.parse(String(calls[1].init.body)), {
|
|
provider: "openai",
|
|
name: "OpenAI Primary",
|
|
apiKey: "sk-test",
|
|
priority: 1,
|
|
testStatus: "unknown",
|
|
providerSpecificData: { baseUrl: "https://api.example/v1" },
|
|
});
|
|
assert.equal(calls[2].url, "/api/providers/conn-1/test");
|
|
assert.equal(calls[2].init.method, "POST");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("provider onboarding validation rejects HTTP 200 responses with valid false", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let createCalled = false;
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url) === "/api/providers/validate") {
|
|
return Response.json({ valid: false, error: "Invalid API key" });
|
|
}
|
|
if (String(url) === "/api/providers") {
|
|
createCalled = true;
|
|
return Response.json({ connection: { id: "conn-1", provider: "openai" } });
|
|
}
|
|
return Response.json({ error: "unexpected" }, { status: 500 });
|
|
};
|
|
|
|
try {
|
|
await assert.rejects(
|
|
() => api.validateOnboardingApiKey({ provider: "openai", apiKey: "bad-key" }),
|
|
/Invalid API key/
|
|
);
|
|
assert.equal(createCalled, false);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("#5692 onboarding validation treats unsupported providers as non-blocking (save proceeds)", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async (url) => {
|
|
if (String(url) === "/api/providers/validate") {
|
|
// #5565/#5567: providers with no live validator (lmarena, piapi, …) return
|
|
// HTTP 400 + { unsupported: true }. The wizard must NOT treat this as a hard
|
|
// failure — otherwise the connection is never created (#5692).
|
|
return Response.json(
|
|
{ error: "Provider validation not supported", unsupported: true },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
return Response.json({ error: "unexpected" }, { status: 500 });
|
|
};
|
|
|
|
try {
|
|
const data = await api.validateOnboardingApiKey({ provider: "lmarena", apiKey: "test-key" });
|
|
assert.equal(data.unsupported, true);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("provider onboarding API ignores non-object error JSON", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async () => Response.json(null, { status: 500 });
|
|
|
|
try {
|
|
await assert.rejects(() => api.fetchOnboardingProviderNodes(), /Failed to load provider nodes/);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("provider onboarding validates API payloads before sending requests", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let fetchCalled = false;
|
|
globalThis.fetch = async () => {
|
|
fetchCalled = true;
|
|
return Response.json({ valid: true });
|
|
};
|
|
|
|
try {
|
|
await assert.rejects(
|
|
() =>
|
|
api.validateOnboardingApiKey({
|
|
provider: "openai",
|
|
apiKey: "sk-test",
|
|
baseUrl: "not-a-url",
|
|
}),
|
|
/Provider credentials are not valid/
|
|
);
|
|
await assert.rejects(
|
|
() =>
|
|
api.createOnboardingConnection({
|
|
provider: "openai",
|
|
name: "OpenAI Primary",
|
|
}),
|
|
/Provider connection data is invalid/
|
|
);
|
|
assert.equal(fetchCalled, false);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("provider onboarding loads provider node feature flags through the API helper", async () => {
|
|
const calls = [];
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async (url, init) => {
|
|
calls.push({ url: String(url), init });
|
|
return Response.json({ ccCompatibleProviderEnabled: true });
|
|
};
|
|
|
|
try {
|
|
const result = await api.fetchOnboardingProviderNodes();
|
|
|
|
assert.deepEqual(result, { ccCompatibleProviderEnabled: true });
|
|
assert.equal(calls.length, 1);
|
|
assert.equal(calls[0].url, "/api/provider-nodes");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("provider onboarding can create compatible connections without an API key", async () => {
|
|
const calls = [];
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async (url, init) => {
|
|
calls.push({ url: String(url), init });
|
|
return Response.json(
|
|
{ connection: { id: "conn-custom", provider: "openai-compatible-local" } },
|
|
{ status: 201 }
|
|
);
|
|
};
|
|
|
|
try {
|
|
const connection = await api.createOnboardingConnection({
|
|
provider: "openai-compatible-local",
|
|
name: "Local Gateway",
|
|
});
|
|
|
|
assert.equal(connection.id, "conn-custom");
|
|
assert.deepEqual(JSON.parse(String(calls[0].init.body)), {
|
|
provider: "openai-compatible-local",
|
|
name: "Local Gateway",
|
|
priority: 1,
|
|
testStatus: "unknown",
|
|
});
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("provider onboarding API builds compatible provider node request bodies", () => {
|
|
assert.deepEqual(
|
|
api.buildCompatibleNodeRequest({
|
|
mode: "openai",
|
|
name: "Local Gateway",
|
|
prefix: "local-gw",
|
|
baseUrl: "https://gateway.example/v1",
|
|
apiType: "responses",
|
|
modelsPath: "/models",
|
|
}),
|
|
{
|
|
name: "Local Gateway",
|
|
prefix: "local-gw",
|
|
baseUrl: "https://gateway.example/v1",
|
|
type: "openai-compatible",
|
|
chatPath: "",
|
|
apiType: "responses",
|
|
modelsPath: "/models",
|
|
}
|
|
);
|
|
|
|
assert.deepEqual(
|
|
api.buildCompatibleNodeRequest({
|
|
mode: "cc",
|
|
name: "CC Gateway",
|
|
prefix: "cc-gw",
|
|
baseUrl: "https://cc.example",
|
|
}),
|
|
{
|
|
name: "CC Gateway",
|
|
prefix: "cc-gw",
|
|
baseUrl: "https://cc.example",
|
|
type: "anthropic-compatible",
|
|
chatPath: "/v1/messages?beta=true",
|
|
compatMode: "cc",
|
|
}
|
|
);
|
|
});
|
|
|
|
test("provider onboarding validates compatible provider node request bodies", () => {
|
|
assert.throws(
|
|
() =>
|
|
api.buildCompatibleNodeRequest({
|
|
mode: "openai",
|
|
name: "Local Gateway",
|
|
prefix: "local-gw",
|
|
baseUrl: "https://gateway.example/v1",
|
|
chatPath: "chat/completions",
|
|
}),
|
|
/Compatible provider data is invalid/
|
|
);
|
|
});
|