import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, } from '../services/analytics/index.js' import { logForDebugging } from '../utils/debug.js' import { errorMessage } from '../utils/errors.js' import { jsonStringify } from '../utils/slowOperations.js' const DEBUG_MSG_LIMIT = 2000 const SECRET_FIELD_NAMES = [ 'session_ingress_token', 'environment_secret', 'access_token', 'secret', 'token', ] const SECRET_PATTERN = new RegExp( `"(${SECRET_FIELD_NAMES.join('|')})"\\s*:\\s*"([^"]*)"`, 'g', ) const REDACT_MIN_LENGTH = 16 export function redactSecrets(s: string): string { return s.replace(SECRET_PATTERN, (_match, field: string, value: string) => { if (value.length < REDACT_MIN_LENGTH) { return `"${field}":"[REDACTED]"` } const redacted = `${value.slice(0, 8)}...${value.slice(-4)}` return `"${field}":"${redacted}"` }) } /** Truncate a string for debug logging, collapsing newlines. */ export function debugTruncate(s: string): string { const flat = s.replace(/\n/g, '\\n') if (flat.length <= DEBUG_MSG_LIMIT) { return flat } return flat.slice(0, DEBUG_MSG_LIMIT) + `... (${flat.length} chars)` } /** Truncate a JSON-serializable value for debug logging. */ export function debugBody(data: unknown): string { const raw = typeof data === 'string' ? data : jsonStringify(data) const s = redactSecrets(raw) if (s.length <= DEBUG_MSG_LIMIT) { return s } return s.slice(0, DEBUG_MSG_LIMIT) + `... (${s.length} chars)` } /** * Extract a descriptive error message from an axios error (or any error). * For HTTP errors, appends the server's response body message if available, * since axios's default message only includes the status code. */ export function describeAxiosError(err: unknown): string { const msg = errorMessage(err) if (err && typeof err === 'object' && 'response' in err) { const response = (err as { response?: { data?: unknown } }).response if (response?.data && typeof response.data === 'object') { const data = response.data as Record const detail = typeof data.message === 'string' ? data.message : typeof data.error === 'object' && data.error && 'message' in data.error && typeof (data.error as Record).message === 'string' ? (data.error as Record).message : undefined if (detail) { return `${msg}: ${detail}` } } } return msg } /** * Extract the HTTP status code from an axios error, if present. * Returns undefined for non-HTTP errors (e.g. network failures). */ export function extractHttpStatus(err: unknown): number | undefined { if ( err && typeof err === 'object' && 'response' in err && (err as { response?: { status?: unknown } }).response && typeof (err as { response: { status?: unknown } }).response.status === 'number' ) { return (err as { response: { status: number } }).response.status } return undefined } /** * Pull a human-readable message out of an API error response body. * Checks `data.message` first, then `data.error.message`. */ export function extractErrorDetail(data: unknown): string | undefined { if (!data || typeof data !== 'object') return undefined if ('message' in data && typeof data.message === 'string') { return data.message } if ( 'error' in data && data.error !== null && typeof data.error === 'object' && 'message' in data.error && typeof data.error.message === 'string' ) { return data.error.message } return undefined } /** * Log a bridge init skip — debug message + `tengu_bridge_repl_skipped` * analytics event. Centralizes the event name and the AnalyticsMetadata * cast so call sites don't each repeat the 5-line boilerplate. */ export function logBridgeSkip( reason: string, debugMsg?: string, v2?: boolean, ): void { if (debugMsg) { logForDebugging(debugMsg) } logEvent('tengu_bridge_repl_skipped', { reason: reason as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, ...(v2 !== undefined && { v2 }), }) }