fix: Show friendly message in computer use cli when connection token is invalid (no-changelog) (#30288)

This commit is contained in:
Bernhard Wittmann 2026-05-12 10:34:33 +02:00 committed by GitHub
parent ab8475b4cf
commit dc7dcaf1b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 95 additions and 3 deletions

View File

@ -5,13 +5,14 @@ import * as fs from 'node:fs/promises';
import { isOriginAllowed, parseConfig } from './config';
import { cliConfirmResourceAccess, sanitizeForTerminal } from './confirm-resource-cli';
import { GatewayClient } from './gateway-client';
import { GatewayAuthError, GatewayClient } from './gateway-client';
import { GatewaySession } from './gateway-session';
import {
configure,
logger,
printBanner,
printConnected,
printInvalidToken,
printModuleStatus,
printToolList,
} from './logger';
@ -223,7 +224,15 @@ async function main(
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
await client.start();
try {
await client.start();
} catch (error) {
if (error instanceof GatewayAuthError) {
printInvalidToken(origin);
process.exit(1);
}
throw error;
}
printConnected(url);
printToolList(client.tools);

View File

@ -41,7 +41,7 @@ jest.mock('./tools/browser', () => ({
}));
import type { GatewayConfig } from './config';
import { GatewayClient } from './gateway-client';
import { GatewayAuthError, GatewayClient } from './gateway-client';
import type { GatewaySession } from './gateway-session';
import type { AffectedResource, ConfirmResourceAccess, ToolDefinition } from './tools/types';
import { INSTANCE_RESOURCE_DECISION_KEYS } from './tools/types';
@ -257,3 +257,65 @@ describe('GatewayClient.checkPermissions', () => {
});
});
});
describe('GatewayClient.uploadCapabilities', () => {
const originalFetch = global.fetch;
beforeEach(() => {
global.fetch = jest.fn();
});
afterEach(() => {
global.fetch = originalFetch;
});
function makeMinimalClient(): GatewayClient {
const client = new GatewayClient({
url: 'http://localhost:5678',
apiKey: 'tok',
config: makeConfig(),
session: makeSession(),
confirmResourceAccess: jest.fn(),
});
// Bypass tool discovery — uploadCapabilities only needs definitions to exist.
// @ts-expect-error — accessing private field for testing
client.allDefinitions = [];
// @ts-expect-error — accessing private field for testing
client.activeToolCategories = [];
return client;
}
function mockFetchResponse(status: number, body = ''): void {
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: status >= 200 && status < 300,
status,
text: jest.fn().mockResolvedValue(body),
json: jest.fn().mockResolvedValue({ data: { ok: true } }),
});
}
it('throws GatewayAuthError on 401', async () => {
mockFetchResponse(401, 'invalid token');
const client = makeMinimalClient();
await expect(client['uploadCapabilities']()).rejects.toBeInstanceOf(GatewayAuthError);
});
it('throws GatewayAuthError on 403', async () => {
mockFetchResponse(403, 'forbidden');
const client = makeMinimalClient();
await expect(client['uploadCapabilities']()).rejects.toBeInstanceOf(GatewayAuthError);
});
it('throws plain Error on non-auth failure (500)', async () => {
mockFetchResponse(500, 'server exploded');
const client = makeMinimalClient();
const promise = client['uploadCapabilities']();
await expect(promise).rejects.not.toBeInstanceOf(GatewayAuthError);
await expect(promise).rejects.toThrow(/Failed to upload capabilities: 500/);
});
});

View File

@ -32,6 +32,17 @@ import { formatErrorResult } from './tools/utils';
const MAX_RECONNECT_DELAY_MS = 30_000;
const MAX_AUTH_RETRIES = 5;
/** Thrown when the gateway rejects our pairing token with 401/403. */
export class GatewayAuthError extends Error {
constructor(
readonly status: number,
readonly body: string,
) {
super(`Gateway rejected token: ${status} ${body}`);
this.name = 'GatewayAuthError';
}
}
/** Tag tool definitions with a category annotation (mutates in place for efficiency). */
function tagCategory(defs: ToolDefinition[], category: string): ToolDefinition[] {
for (const def of defs) {
@ -301,6 +312,9 @@ export class GatewayClient {
if (!response.ok) {
const text = await response.text();
if (response.status === 401 || response.status === 403) {
throw new GatewayAuthError(response.status, text);
}
throw new Error(`Failed to upload capabilities: ${response.status} ${text}`);
}

View File

@ -259,6 +259,13 @@ export function printAuthFailure(): void {
logger.error(` ${pc.red('✗')} Authentication failed — waiting for new pairing token`);
}
export function printInvalidToken(url: string): void {
logger.error(` ${pc.red('✗')} Connection token invalid`);
logger.error(
` ${pc.dim(`Go to ${url} and reconnect n8n Computer Use using a new connection token`)}`,
);
}
export function printReinitializing(): void {
logger.info(` ${pc.magenta('▸')} Re-initializing gateway connection`);
}