diff --git a/bin/cli/commands/oauth.mjs b/bin/cli/commands/oauth.mjs index 8bf547b2c0..c9f8386d2b 100644 --- a/bin/cli/commands/oauth.mjs +++ b/bin/cli/commands/oauth.mjs @@ -228,20 +228,38 @@ async function runSocialFlow(def, opts) { async function runDeviceFlow(def, opts) { const providerKey = resolveBackendKey(def.id); - const startRes = await apiFetch(`/api/providers/${providerKey}/auth/start`, { - ...targetApiOptions(opts), - method: "POST", - }); + let startRes = await apiFetch(`/api/oauth/${providerKey}/device-code`, targetApiOptions(opts)); + if (!startRes.ok) { + startRes = await apiFetch(`/api/providers/${providerKey}/auth/start`, { + ...targetApiOptions(opts), + method: "POST", + }); + } if (!startRes.ok) { process.stderr.write(`Failed to start device flow: ${startRes.status}\n`); process.exit(1); } const start = await startRes.json(); - process.stdout.write( - `\nDevice code: ${start.userCode ?? start.user_code ?? ""}\nVisit: ${start.verificationUri ?? start.verification_uri}\n\n` - ); - if (opts.browser !== false) - await openBrowser(start.verificationUri ?? start.verification_uri ?? ""); + const userCode = start.userCode ?? start.user_code ?? ""; + const verificationUri = + start.verificationUriComplete ?? + start.verification_uri_complete ?? + start.verificationUri ?? + start.verification_uri ?? + start.authUrl ?? + start.url ?? + ""; + + if (userCode) { + process.stdout.write(`\nDevice code: ${userCode}\nVisit: ${verificationUri}\n\n`); + } else if (verificationUri) { + process.stdout.write(`\nVisit: ${verificationUri}\n\n`); + } else { + process.stdout.write(`\nAuthorization URL not available\n\n`); + } + + if (opts.browser !== false && verificationUri) + await openBrowser(verificationUri); process.stderr.write("Waiting for device authorization...\n"); const deadline = Date.now() + (opts.timeout ?? 300000); const intervalMs = (start.intervalMs ?? start.interval ?? 5) * 1000; diff --git a/tests/unit/oauth-device-flow-11164.test.ts b/tests/unit/oauth-device-flow-11164.test.ts new file mode 100644 index 0000000000..48cdb23b64 --- /dev/null +++ b/tests/unit/oauth-device-flow-11164.test.ts @@ -0,0 +1,38 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +test("device code normalization handles camelCase, snake_case, and authUrl without returning undefined", () => { + const cases = [ + { + input: { userCode: "ABCD-1234", verificationUri: "https://auth.example.com" }, + expectedCode: "ABCD-1234", + expectedUri: "https://auth.example.com", + }, + { + input: { user_code: "EFGH-5678", verification_uri: "https://auth.example.com/device" }, + expectedCode: "EFGH-5678", + expectedUri: "https://auth.example.com/device", + }, + { + input: { authUrl: "https://studio.example.com/auth" }, + expectedCode: "", + expectedUri: "https://studio.example.com/auth", + }, + ]; + + for (const c of cases) { + const userCode = c.input.userCode ?? c.input.user_code ?? ""; + const verificationUri = + c.input.verificationUriComplete ?? + c.input.verification_uri_complete ?? + c.input.verificationUri ?? + c.input.verification_uri ?? + c.input.authUrl ?? + c.input.url ?? + ""; + + assert.equal(userCode, c.expectedCode); + assert.equal(verificationUri, c.expectedUri); + assert.notEqual(verificationUri, "undefined"); + } +});