diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f8da9e5..450940a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,6 +11,17 @@ updates: groups: development-toolchain: dependency-type: development + ignore: + # @types/node must track the runtime floor in package.json (engines.node + # ">=24"), not the newest release. A major bump types the project against + # a Node it does not claim to support, so tsc starts accepting APIs that + # do not exist for users on the supported version — a type error that only + # surfaces at their runtime. test/migration_guard.ts asserts that pairing, + # so a major bump lands as red CI on main; #52 did exactly that and main + # stayed broken until it was reverted. Minor and patch updates still flow. + # Raise this deliberately, together with engines.node, when the floor moves. + - dependency-name: '@types/node' + update-types: ['version-update:semver-major'] - package-ecosystem: github-actions directory: / diff --git a/src/core/device.ts b/src/core/device.ts index d779172..e6e8f70 100644 --- a/src/core/device.ts +++ b/src/core/device.ts @@ -99,7 +99,22 @@ export async function pollForToken( } const action = classifyPoll(resp); if (action === "ready") return resp.access_token as string; - if (action === "denied") throw new Error("authorization denied in the browser"); + // Denial is RECOVERABLE, and "authorization denied in the browser" said + // neither that nor what to do next — it read like a verdict on the account. + // It also matters that a denial is not necessarily the user's own: any + // signed-in account that learns a user_code can refuse it, so a denial the + // user did not perform means their code reached someone else. + // + // Kept to ONE line on purpose: this is rendered by formatErrorLine, which + // puts its glyph on the first line only — a multi-line message loses the + // glyph on every line after the first. + if (action === "denied") { + throw new Error( + "authorization denied — nothing was authorized and no key was created. " + + "Run `aether auth login` again to retry; if you didn't deny it, " + + "someone else has your login code — don't share it.", + ); + } if (action === "expired") throw new Error("login timed out — run `aether auth login` again"); if (action === "slow_down") interval += 5; }