mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 16:10:30 +02:00
fix: Show friendly message in computer use cli when connection token is invalid (no-changelog) (#30288)
This commit is contained in:
parent
ab8475b4cf
commit
dc7dcaf1b1
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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/);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user