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
8 changes: 3 additions & 5 deletions packages/ai/src/auth/oauth/aimlapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,9 @@ async function loginAimlapi(interaction: ProviderAuthInteraction): Promise<OAuth
);
}

if (account.provider) {
throw new Error(
`This email signs in via ${account.provider} on AI/ML API — sign in at https://aimlapi.com/app and create an API key manually instead.`,
);
}
// A correct emailed code proves ownership regardless of how the account was
// originally created — sign-in/code/verify has no dependency on a linked
// OAuth provider (e.g. Google), so this path works even for those accounts.
await sendSignInCode(email, interaction.signal);
interaction.notify({ type: "info", message: `A 6-digit code was sent to ${email}.` });
const rawCode = await interaction.prompt({ type: "text", message: "Enter the 6-digit code" });
Expand Down
26 changes: 17 additions & 9 deletions packages/ai/test/aimlapi-oauth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,19 +94,27 @@ describe.sequential("AI/ML API OAuth", () => {
expect(calls).toEqual([ACCOUNT_URL, PASSWORDLESS_URL]);
});

it("rejects an account linked to a third-party sign-in provider", async () => {
it("signs in an account linked to a third-party provider through the same emailed code — sign-in/code/verify has no provider dependency", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () => jsonResponse({ action: "sign-in", provider: "google" })),
vi.fn(async (input: string | URL | Request) => {
const url = input instanceof Request ? input.url : String(input);
if (url === ACCOUNT_URL) return jsonResponse({ action: "sign-in", provider: "google" });
if (url === SEND_CODE_URL) return new Response(null, { status: 204 });
if (url === VERIFY_CODE_URL) return jsonResponse({ token: "session-token", exp: 9999999999 });
if (url === KEYS_URL) return jsonResponse({ key: "aiml-google-key", id: "key-3" });
throw new Error(`Unexpected request: ${url}`);
}),
);

await expect(
aimlapiOAuth.login({
signal: neverAbortedSignal,
prompt: async () => "user@example.com",
notify: () => {},
}),
).rejects.toThrow(/signs in via google/);
const prompts = ["user@example.com", "123456"];
const credential = await aimlapiOAuth.login({
signal: neverAbortedSignal,
prompt: async () => prompts.shift() ?? "",
notify: () => {},
});

expect(credential).toMatchObject({ access: "aiml-google-key" });
});

it("reports an invalid verification code", async () => {
Expand Down
Loading