Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions apps/web/src/onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,13 +430,16 @@ async function postOnboarding(
apiKey: string,
baseURL?: string,
): Promise<{ readonly response: Response; readonly body: unknown }> {
// A key copied from a provider console often carries a trailing
// newline; sent verbatim the provider 401s a valid key (CL-6682).
const trimmedKey = apiKey.trim();
const response = await fetch(`/api/onboarding/${path}`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(
baseURL !== undefined
? { provider, apiKey, baseURL }
: { provider, apiKey },
? { provider, apiKey: trimmedKey, baseURL }
: { provider, apiKey: trimmedKey },
),
});
const body: unknown = await response.json().catch(() => null);
Expand Down
21 changes: 21 additions & 0 deletions apps/web/test/onboarding.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,27 @@ describe("submitCredential", () => {
});
});

// CL-6682: a key copied from a provider console often carries a
// trailing newline; sent verbatim the provider 401s a valid key.
test("strips leading/trailing whitespace from the pasted key", async () => {
let requestBody: unknown = null;
globalThis.fetch = (async (_url: string, init: RequestInit) => {
requestBody = JSON.parse((init as RequestInit).body as string);
return json({
kind: "ready",
tenantSlug: "ada-user1",
deployed: [],
pending: [],
});
}) as unknown as typeof fetch;

await submitCredential("anthropic", " sk-ant-good\n");
expect(requestBody).toEqual({
provider: "anthropic",
apiKey: "sk-ant-good",
});
});

test("a bench whose agents are all live reports nothing pending", async () => {
globalThis.fetch = (async () =>
json({
Expand Down
4 changes: 3 additions & 1 deletion packages/settings-ui/src/connections-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,13 @@ export function completeConnectorCredential(
status: "active";
modelGuidance?: string;
}> {
// A key copied from a provider console often carries a trailing
// newline; sent verbatim the provider 401s a valid key (CL-6682).
return request(
`/api/tenants/${tenantId}/connections/${connectorId}/complete`,
CompleteResult,
"saving that connection",
{ method: "POST", body: JSON.stringify({ apiKey }) },
{ method: "POST", body: JSON.stringify({ apiKey: apiKey.trim() }) },
);
}

Expand Down
11 changes: 11 additions & 0 deletions packages/settings-ui/test/connections-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,17 @@ describe("completeConnectorCredential", () => {
expect(result).toEqual({ credentialId: "cred_1", status: "active" });
});

// CL-6682: a key copied from a provider console often carries a
// trailing newline; sent verbatim it 401s a perfectly valid key.
test("strips leading/trailing whitespace from the pasted key", async () => {
const calls = stubFetch(() =>
json({ credentialId: "cred_1", status: "active" }, 200),
);
await completeConnectorCredential("tnt_1", "granola", " sk-good\n");
const body = JSON.parse(String(calls[0]?.init?.body));
expect(body).toEqual({ apiKey: "sk-good" });
});

// CL-6377: connecting is the one round-trip — a rejected key throws
// straight from this call, with no separate test step beforehand.
test("throws ConnectionsApiError with the probe's own message on a 422", async () => {
Expand Down
Loading