export interface NetworkPlatformError { error: { more_info: string; message: string; type?: string; errors?: { path: string; message: string; }[]; }; } export class CLIError extends Error { name = 'CLIError'; exitCode: ExitCodes; constructor(message: string, exitCode: ExitCodes) { super(message); this.exitCode = exitCode; } } export class PlatformError extends Error { name = 'PlatformError'; networkError: NetworkPlatformError; status: number; constructor(networkError: NetworkPlatformError, status: number) { super(PlatformError.errorMessage(networkError, status)); this.networkError = networkError; this.status = status; } protected static errorMessage(networkError: NetworkPlatformError, status: number) { let message = 'Error communicating with Root Platform'; message += `\n Status: ${status}`; message += `\n Type: ${networkError?.error?.type || 'unknown_type'}`; message += `\n Message: ${networkError?.error?.message}`; if (networkError?.error?.more_info) { message += `\n More info: ${networkError.error.more_info}`; } // Defensive: only iterate if the server actually sent an array. A keyed-map or any // non-array shape would crash `.forEach`, masking the original API error with a // `TypeError` from inside the error-reporting path itself. Also tolerate partially // malformed items (nulls, missing fields) so one bad entry doesn't swallow the rest. if (networkError?.error?.type === 'validation_error' && Array.isArray(networkError.error.errors)) { message += '\n Validation errors:'; for (const e of networkError.error.errors as ({ path?: string; message?: string } | null)[]) { if (!e || typeof e !== 'object') continue; message += `\n - ${e.path ?? '?'}: ${e.message ?? '?'}`; } } const suggestion = PlatformError.getSuggestion(networkError, status); if (suggestion) { message += `\n\n${suggestion}`; } return message; } protected static getSuggestion(networkError: NetworkPlatformError, status: number): string { const errorType = networkError?.error?.type; const errorMessage = networkError?.error?.message?.toLowerCase() || ''; if (errorMessage.includes('no definition in review') || errorMessage.includes('no draft definition')) { return 'Tip: There is no draft definition to publish.\n Run `rp push` to upload your local definition, then retry `rp publish`.'; } if (status === 401 || errorType === 'authorization_error' || errorType === 'authentication_error') { return 'Tip: Verify your API key is correct and has the required permissions.\n You can find your API keys at https://app.rootplatform.com/settings/api-keys'; } if (status === 403 || errorType === 'forbidden_error') { return 'Tip: Your API key does not have permission to perform this action.\n Check that your API key has the required scopes enabled.'; } if (status === 404 || errorType === 'not_found_error') { return 'Tip: The requested resource was not found.\n Verify the product module key or resource identifier is correct.'; } if (status === 429 || errorType === 'rate_limit_error') { return 'Tip: You have exceeded the API rate limit.\n Please wait a moment before retrying your request.'; } if (status >= 500 || errorType === 'internal_error' || errorType === 'server_error') { return 'Tip: The Root Platform is experiencing issues.\n Please try again later or contact support if the problem persists.'; } if (errorType === 'validation_error') { return 'Tip: Check the validation errors above and correct your input.'; } if (errorType === 'request_timeout_error') { return 'Tip: The request timed out.\n This may happen with large payloads. Try reducing the size of your request.'; } return ''; } } export class NetworkError extends Error { name = 'NetworkError'; cause: Error; endpoint: string; constructor(cause: Error, endpoint: string) { super(NetworkError.formatMessage(cause, endpoint)); this.cause = cause; this.endpoint = endpoint; } protected static formatMessage(cause: Error, _endpoint: string): string { let message = 'Failed to connect to Root Platform'; message += `\n Error: ${cause.message}`; const suggestion = NetworkError.getSuggestion(cause); if (suggestion) { message += `\n\n${suggestion}`; } return message; } protected static getSuggestion(cause: Error): string { const errorMessage = cause.message.toLowerCase(); if (errorMessage.includes('enotfound') || errorMessage.includes('getaddrinfo')) { return 'Tip: Unable to resolve the hostname.\n Check your internet connection and verify the API host is correct.'; } if (errorMessage.includes('econnrefused')) { return 'Tip: Connection was refused by the server.\n Verify the API host and port are correct.'; } if (errorMessage.includes('etimedout') || errorMessage.includes('timeout')) { return 'Tip: The connection timed out.\n Check your internet connection and try again.'; } if (errorMessage.includes('econnreset') || errorMessage.includes('socket hang up')) { return 'Tip: The connection was reset unexpectedly.\n This may be a temporary network issue. Please try again.'; } if (errorMessage.includes('certificate') || errorMessage.includes('ssl') || errorMessage.includes('tls')) { return 'Tip: There was an SSL/TLS certificate error.\n Ensure the API host uses a valid certificate.'; } return 'Tip: Check your internet connection and verify the API host is correct.'; } } export enum ExitCodes { SUCCESS = 0, GENERAL_ERROR = 1, VERSION_MISMATCH = 2, }