diff --git a/dist/index.js b/dist/index.js index c24d4d0..d1b9d7c 100644 --- a/dist/index.js +++ b/dist/index.js @@ -32488,6 +32488,74 @@ const fs = __importStar(__nccwpck_require__(1943)); const path = __importStar(__nccwpck_require__(6928)); const sdk_1 = __nccwpck_require__(6381); const dead_code_1 = __nccwpck_require__(1655); +/** Fields that should be redacted from logs */ +const SENSITIVE_KEYS = new Set([ + 'authorization', + 'cookie', + 'set-cookie', + 'access_token', + 'refresh_token', + 'api_key', + 'apikey', + 'password', + 'secret', + 'token', + 'x-api-key', +]); +const MAX_VALUE_LENGTH = 1000; +/** + * Safely serialize a value for logging, handling circular refs, BigInt, and large values. + * Redacts sensitive fields. + */ +function safeSerialize(value, maxLength = MAX_VALUE_LENGTH) { + try { + const seen = new WeakSet(); + const serialized = JSON.stringify(value, (key, val) => { + // Redact sensitive keys + if (key && SENSITIVE_KEYS.has(key.toLowerCase())) { + return '[REDACTED]'; + } + // Handle BigInt + if (typeof val === 'bigint') { + return val.toString(); + } + // Handle circular references + if (typeof val === 'object' && val !== null) { + if (seen.has(val)) { + return '[Circular]'; + } + seen.add(val); + } + return val; + }, 2); + // Truncate if too long + if (serialized && serialized.length > maxLength) { + return serialized.slice(0, maxLength) + '... [truncated]'; + } + return serialized ?? '[undefined]'; + } + catch { + return '[unserializable]'; + } +} +/** + * Redact sensitive fields from an object (shallow copy). + */ +function redactSensitive(obj) { + if (!obj || typeof obj !== 'object') { + return null; + } + const result = {}; + for (const [key, value] of Object.entries(obj)) { + if (SENSITIVE_KEYS.has(key.toLowerCase())) { + result[key] = '[REDACTED]'; + } + else { + result[key] = value; + } + } + return result; +} async function createZipArchive(workspacePath) { const zipPath = path.join(workspacePath, '.dead-code-hunter-repo.zip'); core.info('Creating zip archive...'); @@ -32575,21 +32643,96 @@ async function run() { } } catch (error) { - if (error.response) { - const status = error.response.status; - if (status === 401) { - core.error('Invalid API key. Get your key at https://dashboard.supermodeltools.com'); + // Log error details for debugging (using debug level for potentially sensitive data) + core.info('--- Error Debug Info ---'); + core.info(`Error type: ${error?.constructor?.name ?? 'unknown'}`); + core.info(`Error message: ${error?.message ?? 'no message'}`); + core.info(`Error name: ${error?.name ?? 'no name'}`); + // Check various error structures used by different HTTP clients + // Use core.debug for detailed/sensitive info, core.info for safe summaries + try { + if (error?.response) { + core.info(`Response status: ${error.response.status ?? 'unknown'}`); + core.info(`Response statusText: ${error.response.statusText ?? 'unknown'}`); + core.info(`Response data: ${safeSerialize(error.response.data)}`); + // Headers may contain sensitive values - use debug level + core.debug(`Response headers: ${safeSerialize(redactSensitive(error.response.headers))}`); } - else { - core.error(`API error (${status})`); + if (error?.body) { + core.info(`Error body: ${safeSerialize(error.body)}`); + } + if (error?.status) { + core.info(`Error status: ${error.status}`); + } + if (error?.statusCode) { + core.info(`Error statusCode: ${error.statusCode}`); + } + if (error?.cause) { + core.debug(`Error cause: ${safeSerialize(error.cause)}`); + } + } + catch { + core.debug('Failed to serialize some error properties'); + } + core.info('--- End Debug Info ---'); + let errorMessage = 'An unknown error occurred'; + let helpText = ''; + // Try multiple error structures + const status = error?.response?.status || error?.status || error?.statusCode; + let apiMessage = ''; + // Try to extract message from various locations + try { + apiMessage = + error?.response?.data?.message || + error?.response?.data?.error || + error?.response?.data || + error?.body?.message || + error?.body?.error || + error?.message || + ''; + if (typeof apiMessage !== 'string') { + apiMessage = safeSerialize(apiMessage, 500); } } - if (error instanceof Error) { - core.setFailed(error.message); + catch { + apiMessage = ''; + } + if (status === 401) { + errorMessage = 'Invalid API key'; + helpText = 'Get your key at https://dashboard.supermodeltools.com'; + } + else if (status === 500) { + errorMessage = apiMessage || 'Internal server error'; + // Check for common issues and provide guidance + if (apiMessage.includes('Nested archives')) { + helpText = 'Your repository contains nested archive files (.zip, .tar, etc.). ' + + 'Add them to .gitattributes with "export-ignore" to exclude from analysis. ' + + 'Example: tests/fixtures/*.zip export-ignore'; + } + else if (apiMessage.includes('exceeds maximum')) { + helpText = 'Your repository or a file within it exceeds size limits. ' + + 'Consider excluding large files using .gitattributes with "export-ignore".'; + } + } + else if (status === 413) { + errorMessage = 'Repository archive too large'; + helpText = 'Reduce archive size by excluding large files in .gitattributes'; + } + else if (status === 429) { + errorMessage = 'Rate limit exceeded'; + helpText = 'Please wait before retrying'; + } + else if (status) { + errorMessage = apiMessage || `API error (${status})`; } else { - core.setFailed('An unknown error occurred'); + errorMessage = apiMessage || error?.message || 'An unknown error occurred'; + } + core.error(`Error: ${errorMessage}`); + if (helpText) { + core.error(`Help: ${helpText}`); } + core.setFailed(errorMessage); } } run(); diff --git a/src/index.ts b/src/index.ts index d9ce640..d11d522 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,83 @@ import * as path from 'path'; import { Configuration, DefaultApi } from '@supermodeltools/sdk'; import { findDeadCode, formatPrComment } from './dead-code'; +/** Fields that should be redacted from logs */ +const SENSITIVE_KEYS = new Set([ + 'authorization', + 'cookie', + 'set-cookie', + 'access_token', + 'refresh_token', + 'api_key', + 'apikey', + 'password', + 'secret', + 'token', + 'x-api-key', +]); + +const MAX_VALUE_LENGTH = 1000; + +/** + * Safely serialize a value for logging, handling circular refs, BigInt, and large values. + * Redacts sensitive fields. + */ +function safeSerialize(value: unknown, maxLength = MAX_VALUE_LENGTH): string { + try { + const seen = new WeakSet(); + + const serialized = JSON.stringify(value, (key, val) => { + // Redact sensitive keys + if (key && SENSITIVE_KEYS.has(key.toLowerCase())) { + return '[REDACTED]'; + } + + // Handle BigInt + if (typeof val === 'bigint') { + return val.toString(); + } + + // Handle circular references + if (typeof val === 'object' && val !== null) { + if (seen.has(val)) { + return '[Circular]'; + } + seen.add(val); + } + + return val; + }, 2); + + // Truncate if too long + if (serialized && serialized.length > maxLength) { + return serialized.slice(0, maxLength) + '... [truncated]'; + } + + return serialized ?? '[undefined]'; + } catch { + return '[unserializable]'; + } +} + +/** + * Redact sensitive fields from an object (shallow copy). + */ +function redactSensitive(obj: Record | null | undefined): Record | null { + if (!obj || typeof obj !== 'object') { + return null; + } + + const result: Record = {}; + for (const [key, value] of Object.entries(obj)) { + if (SENSITIVE_KEYS.has(key.toLowerCase())) { + result[key] = '[REDACTED]'; + } else { + result[key] = value; + } + } + return result; +} + async function createZipArchive(workspacePath: string): Promise { const zipPath = path.join(workspacePath, '.dead-code-hunter-repo.zip'); @@ -120,20 +197,96 @@ async function run(): Promise { } } catch (error: any) { - if (error.response) { - const status = error.response.status; - if (status === 401) { - core.error('Invalid API key. Get your key at https://dashboard.supermodeltools.com'); - } else { - core.error(`API error (${status})`); + // Log error details for debugging (using debug level for potentially sensitive data) + core.info('--- Error Debug Info ---'); + core.info(`Error type: ${error?.constructor?.name ?? 'unknown'}`); + core.info(`Error message: ${error?.message ?? 'no message'}`); + core.info(`Error name: ${error?.name ?? 'no name'}`); + + // Check various error structures used by different HTTP clients + // Use core.debug for detailed/sensitive info, core.info for safe summaries + try { + if (error?.response) { + core.info(`Response status: ${error.response.status ?? 'unknown'}`); + core.info(`Response statusText: ${error.response.statusText ?? 'unknown'}`); + core.info(`Response data: ${safeSerialize(error.response.data)}`); + // Headers may contain sensitive values - use debug level + core.debug(`Response headers: ${safeSerialize(redactSensitive(error.response.headers))}`); + } + if (error?.body) { + core.info(`Error body: ${safeSerialize(error.body)}`); + } + if (error?.status) { + core.info(`Error status: ${error.status}`); } + if (error?.statusCode) { + core.info(`Error statusCode: ${error.statusCode}`); + } + if (error?.cause) { + core.debug(`Error cause: ${safeSerialize(error.cause)}`); + } + } catch { + core.debug('Failed to serialize some error properties'); + } + core.info('--- End Debug Info ---'); + + let errorMessage = 'An unknown error occurred'; + let helpText = ''; + + // Try multiple error structures + const status = error?.response?.status || error?.status || error?.statusCode; + let apiMessage = ''; + + // Try to extract message from various locations + try { + apiMessage = + error?.response?.data?.message || + error?.response?.data?.error || + error?.response?.data || + error?.body?.message || + error?.body?.error || + error?.message || + ''; + if (typeof apiMessage !== 'string') { + apiMessage = safeSerialize(apiMessage, 500); + } + } catch { + apiMessage = ''; } - if (error instanceof Error) { - core.setFailed(error.message); + if (status === 401) { + errorMessage = 'Invalid API key'; + helpText = 'Get your key at https://dashboard.supermodeltools.com'; + } else if (status === 500) { + errorMessage = apiMessage || 'Internal server error'; + + // Check for common issues and provide guidance + if (apiMessage.includes('Nested archives')) { + helpText = 'Your repository contains nested archive files (.zip, .tar, etc.). ' + + 'Add them to .gitattributes with "export-ignore" to exclude from analysis. ' + + 'Example: tests/fixtures/*.zip export-ignore'; + } else if (apiMessage.includes('exceeds maximum')) { + helpText = 'Your repository or a file within it exceeds size limits. ' + + 'Consider excluding large files using .gitattributes with "export-ignore".'; + } + } else if (status === 413) { + errorMessage = 'Repository archive too large'; + helpText = 'Reduce archive size by excluding large files in .gitattributes'; + } else if (status === 429) { + errorMessage = 'Rate limit exceeded'; + helpText = 'Please wait before retrying'; + } else if (status) { + errorMessage = apiMessage || `API error (${status})`; } else { - core.setFailed('An unknown error occurred'); + errorMessage = apiMessage || error?.message || 'An unknown error occurred'; + } + + core.error(`Error: ${errorMessage}`); + if (helpText) { + core.error(`Help: ${helpText}`); } + + core.setFailed(errorMessage); } }