[READ-ONLY] a fast, modern browser for the npm registry

fix(cli): handle authentication failures

+67 -4
+15 -2
cli/src/cli.ts
··· 3 3 import { listen } from 'listhen' 4 4 import { toNodeListener } from 'h3' 5 5 import { createConnectorApp, generateToken, CONNECTOR_VERSION } from './server' 6 - import { initLogger, showToken, logInfo } from './logger' 6 + import { getNpmUser } from './npm-client' 7 + import { initLogger, showToken, logInfo, showAuthRequired } from './logger' 7 8 8 9 const DEFAULT_PORT = 31415 9 10 ··· 22 23 }, 23 24 async run({ args }) { 24 25 const port = Number.parseInt(args.port as string, 10) || DEFAULT_PORT 25 - const token = generateToken() 26 26 27 27 initLogger() 28 + 29 + // Check npm authentication before starting 30 + logInfo('Checking npm authentication...') 31 + const npmUser = await getNpmUser() 32 + 33 + if (!npmUser) { 34 + showAuthRequired() 35 + process.exit(1) 36 + } 37 + 38 + logInfo(`Authenticated as: ${npmUser}`) 39 + 40 + const token = generateToken() 28 41 showToken(token, port) 29 42 30 43 const app = createConnectorApp(token)
+18
cli/src/logger.ts
··· 76 76 } 77 77 78 78 /** 79 + * Show authentication required error in a box 80 + */ 81 + export function showAuthRequired(): void { 82 + p.note( 83 + [ 84 + pc.red('Not logged in to npm'), 85 + '', 86 + 'Please run the following command to log in:', 87 + '', 88 + ` ${pc.cyan('npm login')}`, 89 + '', 90 + 'Then restart the connector.', 91 + ].join('\n'), 92 + 'Authentication required', 93 + ) 94 + } 95 + 96 + /** 79 97 * Create a spinner for async operations 80 98 */ 81 99 export function createSpinner() {
+28 -2
cli/src/npm-client.ts
··· 10 10 exitCode: number 11 11 /** True if the operation failed due to missing/invalid OTP */ 12 12 requiresOtp?: boolean 13 + /** True if the operation failed due to authentication failure (not logged in or token expired) */ 14 + authFailure?: boolean 13 15 } 14 16 15 17 function detectOtpRequired(stderr: string): boolean { ··· 18 20 'one-time password', 19 21 'This operation requires a one-time password', 20 22 '--otp=<code>', 21 - 'OTP', 22 23 ] 23 24 const lowerStderr = stderr.toLowerCase() 24 25 return otpPatterns.some(pattern => lowerStderr.includes(pattern.toLowerCase())) 25 26 } 26 27 28 + function detectAuthFailure(stderr: string): boolean { 29 + const authPatterns = [ 30 + 'ENEEDAUTH', 31 + 'You must be logged in', 32 + 'authentication error', 33 + 'Unable to authenticate', 34 + 'code E401', 35 + 'code E403', 36 + '401 Unauthorized', 37 + '403 Forbidden', 38 + 'not logged in', 39 + 'npm login', 40 + 'npm adduser', 41 + ] 42 + const lowerStderr = stderr.toLowerCase() 43 + return authPatterns.some(pattern => lowerStderr.includes(pattern.toLowerCase())) 44 + } 45 + 27 46 function filterNpmWarnings(stderr: string): string { 28 47 return stderr 29 48 .split('\n') ··· 70 89 const err = error as { stdout?: string, stderr?: string, code?: number } 71 90 const stderr = err.stderr?.trim() ?? String(error) 72 91 const requiresOtp = detectOtpRequired(stderr) 92 + const authFailure = detectAuthFailure(stderr) 73 93 74 94 if (!options.silent) { 75 95 if (requiresOtp) { 76 96 logError('OTP required') 97 + } 98 + else if (authFailure) { 99 + logError('Authentication required - please run "npm login" and restart the connector') 77 100 } 78 101 else { 79 102 logError(filterNpmWarnings(stderr).split('\n')[0] || 'Command failed') ··· 84 107 stdout: err.stdout?.trim() ?? '', 85 108 stderr: requiresOtp 86 109 ? 'This operation requires a one-time password (OTP).' 87 - : filterNpmWarnings(stderr), 110 + : authFailure 111 + ? 'Authentication failed. Please run "npm login" and restart the connector.' 112 + : filterNpmWarnings(stderr), 88 113 exitCode: err.code ?? 1, 89 114 requiresOtp, 115 + authFailure, 90 116 } 91 117 } 92 118 }
+4
cli/src/server.ts
··· 353 353 await Promise.all(runningOps) 354 354 } 355 355 356 + // Check if any operation had an auth failure 357 + const authFailure = results.some(r => r.result.authFailure) 358 + 356 359 return { 357 360 success: true, 358 361 data: { 359 362 results, 360 363 otpRequired, 364 + authFailure, 361 365 }, 362 366 } as ApiResponse 363 367 }),
+2
cli/src/types.ts
··· 36 36 exitCode: number 37 37 /** True if the operation failed due to missing/invalid OTP */ 38 38 requiresOtp?: boolean 39 + /** True if the operation failed due to authentication failure (not logged in or token expired) */ 40 + authFailure?: boolean 39 41 } 40 42 41 43 export interface PendingOperation {