From 7f011787118fbbf71d8c2157933b9bab8bb54888 Mon Sep 17 00:00:00 2001 From: dbarr5 Date: Tue, 28 Jul 2026 06:11:34 -0400 Subject: [PATCH] fix(login): a denied device request now says how to recover, and pin @types/node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two loose ends from the LOOP-17 device-auth work. The CLI reported a denial as "authorization denied in the browser" — a verdict with no next step, which reads like the account was rejected rather than one request being refused. Denial is recoverable: nothing was authorized, no key was created, and re-running the command starts a fresh request. The message now says that. It also names the case the user cannot otherwise know about. A device request is bound to no one until someone acts on it, so any signed-in account that learns a user_code can refuse it. A denial the user did not perform therefore means their code reached somebody else, which is worth telling them while they still have the terminal open. This is the CLI half of the accepted-risk note in the 2026-07-28 phishing-resistance spec. Deliberately one line: formatErrorLine puts its glyph on the first line only, so a multi-line message loses it on every line after the first. The existing login.test.ts assertions pass unchanged. Also pins @types/node against major bumps in dependabot. It must track engines.node (">=24"), not the newest release — typing the project against a Node it does not claim to support lets tsc accept APIs that do not exist for users on the supported version, a type error that only surfaces at their runtime. migration_guard asserts the 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. Tests: 823 pass, 0 fail. Co-Authored-By: Claude Opus 5 --- .github/dependabot.yml | 11 +++++++++++ src/core/device.ts | 17 ++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) 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; }