{"record":{"id":"65b514f0175bf1af","repo":"JuliusBrussee/caveman","slug":"device-login-timed-out-before-approval-65b514","errorCode":null,"errorMessage":"device login timed out before approval","messagePattern":"device login timed out before approval","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"packages/device-auth/src/index.ts","lineNumber":172,"sourceCode":"            client: options.client,\n            code,\n            credentials,\n            fetcher,\n            ...(options.signal === undefined ? {} : { signal: options.signal }),\n            sleep: wait,\n          });\n          acknowledged = true;\n        },\n      };\n    }\n    const errorCode = typeof payload.error === \"string\" ? payload.error : \"\";\n    if (errorCode === \"slow_down\") intervalMs = nextDevicePollIntervalMs(intervalMs, errorCode);\n    else if (errorCode !== \"\" && errorCode !== \"authorization_pending\") {\n      throw new Error(`device login failed: ${errorCode}`);\n    }\n    await wait(Math.max(intervalMs, 200));\n  }\n  throw new Error(\"device login timed out before approval\");\n}\n","sourceCodeStart":154,"sourceCodeEnd":174,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/df2ccd85c94ec3c8289cb62ac020d241ccfb0c60/packages/device-auth/src/index.ts#L154-L174","documentation":"runCavemanDeviceFlow starts a device-authorization login: it fetches a device code, prints it via onCode, then polls the token endpoint until the user approves in a browser or the code expires (deadline = Date.now() + expires_in*1000). This error is thrown when the while loop exits because the deadline passed without the token endpoint returning an access_token — i.e. the user never completed the approval within the code's lifetime. It is an expected, deliberate terminal state, not an internal failure.","triggerScenarios":"The poll loop at packages/device-auth/src/index.ts:113 runs until `Date.now() < deadline`; every poll returns either authorization_pending or slow_down (loop keeps waiting) or an access_token (function returns). The throw fires only when the loop condition becomes false: expires_in seconds elapsed with no access_token. Transient poll fetch errors are retried, not fatal, unless the deadline also passed (then a different 'device login polling failed' error is thrown).","commonSituations":"Running `caveman login` (or any flow calling runCavemanDeviceFlow) and walking away without opening verification_uri and entering user_code; opening the verification URL after the code expired (typically 5–15 minutes); the approval page being open but the org/admin approval never clicked; clock skew or a very short expires_in from the server making the window effectively zero; the user approving in a different account/tenant than the device_code belongs to.","solutions":["Re-run the login command to get a fresh device code, then open the verification_uri and enter the user_code promptly.","Make sure onCode surfaced the code visibly (open the verification_uri in a browser and enter user_code before expires_in elapses).","Check server-side approval requirements (admin consent, correct org) that might silently block the token from ever being issued.","For CI, skip the interactive device flow entirely and set CAVE_TOKEN instead of polling for approval.","If timeouts happen even when approving quickly, verify the machine clock is correct and check whether the server's expires_in is suspiciously small."],"exampleFix":"// before: start the flow, get distracted, code expires\nconst result = await runCavemanDeviceFlow(options);\n\n// after: surface the code early and finish approval promptly, or fall back to a token\ntry {\n  const result = await runCavemanDeviceFlow({\n    ...options,\n    onCode: (code) => {\n      console.log(`Open ${code.verification_uri} and enter ${code.user_code} (expires in ${code.expires_in}s)`);\n    },\n  });\n} catch (error) {\n  if (error instanceof Error && error.message === \"device login timed out before approval\") {\n    console.error(\"Approval not completed in time; run `caveman login` again or set CAVE_TOKEN.\");\n    process.exitCode = 1;\n  } else throw error;\n}","handlingStrategy":"try-catch","validationCode":"// Before starting the device flow, confirm a human can act now.\nfunction canRunDeviceFlow(): boolean {\n  const interactive = Boolean(process.stdout.isTTY) && !process.env.CI;\n  const hasStaticToken = Boolean(process.env.CAVE_TOKEN);\n  return interactive || hasStaticToken;\n}\nif (!canRunDeviceFlow()) {\n  throw new Error(\"Device flow needs a TTY for approval or CAVE_TOKEN for CI.\");\n}","typeGuard":"function isDeviceTimeout(error: unknown): error is Error {\n  return error instanceof Error &&\n    error.message === \"device login timed out before approval\";\n}","tryCatchPattern":"try {\n  const { code, credentials } = await runCavemanDeviceFlow({\n    onCode: (c) => console.log(`Open ${c.verification_uri} and enter ${c.user_code} (expires in ${c.expires_in}s)`),\n  });\n  await credentials.acknowledge();\n} catch (error) {\n  if (isDeviceTimeout(error)) {\n    console.error(\"Approval window expired — rerun `caveman login` and approve promptly, or set CAVE_TOKEN.\");\n    process.exitCode = 1;\n  } else {\n    throw error;\n  }\n}","preventionTips":["Always pass onCode and open/print verification_uri immediately; treat expires_in as a hard deadline.","Use CAVE_TOKEN in CI and headless environments instead of the interactive device flow.","Keep machine clocks synced (NTP) so expires_in bookkeeping is accurate.","Never walk away mid-login; if approval may be delayed, restart the flow to get a fresh code rather than waiting on an expired one."],"tags":["authentication","device-flow","timeout","oauth"],"backgroundTag":"device-code-authorization-timeout","analyzedSha":"df2ccd85c94ec3c8289cb62ac020d241ccfb0c60","analyzedAt":"2026-08-31T22:10:17.934Z","contentChangedAt":"2026-08-31T22:10:17.934Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}