From 10ea1aecd374e975ad307821c8e4a2fcfbea2559 Mon Sep 17 00:00:00 2001 From: Prathamesh Hukkeri Date: Mon, 27 Jul 2026 17:46:50 +0530 Subject: [PATCH] fix(auth): preserve config on transient server/network errors The validateToken function was deleting the cached config for any exception thrown by api.validate(), including temporary outages, DNS failures, or non-auth server errors. This forced users to re-login even when their credentials were valid. Changes: - Distinguish auth errors (401, 430, JWT-related) from transient errors - Preserve cached config for network failures and server errors - Show warning message for transient errors instead of error - Extract invalidateConfig helper function - Add isJwtError helper to detect JWT-specific errors Fixes #232 --- src/cli/validateToken.ts | 56 ++++++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/src/cli/validateToken.ts b/src/cli/validateToken.ts index 122ffca..ec3d69f 100644 --- a/src/cli/validateToken.ts +++ b/src/cli/validateToken.ts @@ -1,9 +1,13 @@ -import { API as APIInterface } from '@metacall/protocol/protocol'; +import { + API as APIInterface, + isProtocolError, + ProtocolError +} from '@metacall/protocol/protocol'; import { unlink } from 'fs/promises'; import { configFilePath, save } from '../config'; import { exists } from '../utils'; import args from './args'; -import { error, info } from './messages'; +import { error, info, warn } from './messages'; const handleValidateToken = async (api: APIInterface): Promise => { const validToken = await api.validate(); @@ -14,6 +18,11 @@ const handleValidateToken = async (api: APIInterface): Promise => { } }; +const invalidateConfig = async (): Promise => { + const configFile = configFilePath(); + (await exists(configFile)) && (await unlink(configFile)); +}; + const validateToken = async (api: APIInterface): Promise => { try { await handleValidateToken(api); @@ -26,18 +35,49 @@ const validateToken = async (api: APIInterface): Promise => { return error('FaaS is not serving locally.'); } - // Removing cache such that user will have to login again. + // Check if this is a transient network/server error (non-auth) + if (!isProtocolError(err)) { + // Network error, DNS failure, etc. — preserve config + warn('Unable to reach the server. Using cached credentials.'); + return; + } - const configFile = configFilePath(); + const protocolErr = err; + const status = protocolErr.status; - (await exists(configFile)) && (await unlink(configFile)); + // Only delete config for auth-specific errors (401, 403) + // or JWT-specific errors (invalid signature, expired) + const isAuthError = + status === 401 || + status === 403 || + (status === undefined && isJwtError(protocolErr)); - info('Try to login again!'); + if (isAuthError) { + await invalidateConfig(); + info('Try to login again!'); + return error( + `Token validation failed, potential causes include:\n\t1) The JWT may be mistranslated (Invalid Signature).\n\t2) JWT might have expired.` + ); + } - return error( - `Token validation failed, potential causes include:\n\t1) The JWT may be mistranslated (Invalid Signature).\n\t2) JWT might have expired.` + // Server error (5xx) or other non-auth failure — preserve config + warn( + `Server error (${status ?? 'unknown'}). Using cached credentials.` ); } }; +const isJwtError = (err: ProtocolError): boolean => { + const message = (err.message ?? '').toLowerCase(); + const data = String(err.data ?? '').toLowerCase(); + return ( + message.includes('jwt') || + message.includes('token') || + data.includes('jwt') || + data.includes('token') || + data.includes('malformed') || + data.includes('expired') + ); +}; + export default validateToken;