171 lines · 5.6 KB · TypeScript
RawDownload
import {  ANTHROPIC_API_KEY_ENV_KEY,  AWS_ACCESS_KEY_ID_ENV_KEY,  AWS_BEARER_TOKEN_BEDROCK_ENV_KEY,  AWS_SECRET_ACCESS_KEY_ENV_KEY,  AWS_SESSION_TOKEN_ENV_KEY,  AWS_WEB_IDENTITY_TOKEN_FILE_ENV_KEY,  BASETEN_API_KEY_ENV_KEY,  BEDROCK_AWS_ACCESS_KEY_ID_ENV_KEY,  BEDROCK_AWS_SECRET_ACCESS_KEY_ENV_KEY,  BEDROCK_AWS_SESSION_TOKEN_ENV_KEY,  COPILOT_API_KEY_ENV_KEY,  FIREWORKS_API_KEY_ENV_KEY,  GEMINI_API_KEY_ENV_KEY,  NEBIUS_API_KEY_ENV_KEY,  NVIDIA_API_KEY_ENV_KEY,  OPENAI_API_KEY_ENV_KEY,  OPENAI_COMPATIBLE_API_KEY_ENV_KEY,  OPENROUTER_API_KEY_ENV_KEY,} from "./constants.js"; /** * Redacts secrets from text before it is shown to the user or written to a log. * * This is a security boundary: any error message, header value, or provider * response body that could contain a credential must pass through here first. * It removes (1) the exact values of secrets currently set in the environment * and (2) anything matching known key/token shapes (OpenAI/OpenRouter `sk-…`, * `Bearer …`, LangSmith `ls…`, and "Incorrect API key provided: …" phrasing). */export function sanitizeDiagnosticText(value: string): string {  let sanitized = value;   for (const key of [    BASETEN_API_KEY_ENV_KEY,    AWS_ACCESS_KEY_ID_ENV_KEY,    AWS_BEARER_TOKEN_BEDROCK_ENV_KEY,    AWS_SECRET_ACCESS_KEY_ENV_KEY,    AWS_SESSION_TOKEN_ENV_KEY,    AWS_WEB_IDENTITY_TOKEN_FILE_ENV_KEY,    BEDROCK_AWS_ACCESS_KEY_ID_ENV_KEY,    BEDROCK_AWS_SECRET_ACCESS_KEY_ENV_KEY,    BEDROCK_AWS_SESSION_TOKEN_ENV_KEY,    COPILOT_API_KEY_ENV_KEY,    FIREWORKS_API_KEY_ENV_KEY,    GEMINI_API_KEY_ENV_KEY,    NEBIUS_API_KEY_ENV_KEY,    NVIDIA_API_KEY_ENV_KEY,    OPENAI_API_KEY_ENV_KEY,    OPENAI_COMPATIBLE_API_KEY_ENV_KEY,    ANTHROPIC_API_KEY_ENV_KEY,    OPENROUTER_API_KEY_ENV_KEY,    "LANGSMITH_API_KEY",  ]) {    const secret = process.env[key];     if (secret && secret.length > 0) {      sanitized = sanitized.split(secret).join(`[REDACTED:${key}]`);    }  }   return sanitized    .replace(      /(Incorrect API key provided:\s*)([^\s.]+)/giu,      "$1[REDACTED:API_KEY]",    )    .replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gu, "Bearer [REDACTED]")    .replace(/\bsk-or-v1-[A-Za-z0-9_-]+/gu, "[REDACTED:OPENROUTER_API_KEY]")    .replace(/\bsk-[A-Za-z0-9_-]+/gu, "[REDACTED:API_KEY]")    .replace(/\bls[v_][A-Za-z0-9_-]+/gu, "[REDACTED:LANGSMITH_API_KEY]");} /** * True when an object key name looks like it holds a credential, so its value * should be redacted before display, logging, or persistence. * * This is a security boundary shared by every redaction path (diagnostics, * OpenRouter response bodies, and MCP tool args/results). The term list is the * union of every term the individual paths previously matched, so a key * redacted by one path is redacted by all of them. *//** * The union of every substring that marks a key/field name as secret-bearing. * Single source of truth for all redaction paths — extend this, not the * individual call sites. */export const SECRET_KEY_PATTERN_SOURCE =  "api[-_]?key|authorization|bearer|token|secret|password|user_id|cookie"; export function isSecretLikeKey(key: string): boolean {  return new RegExp(SECRET_KEY_PATTERN_SOURCE, "iu").test(key);} /** * Recognizes an OpenRouter/provider 500 response so a friendlier, actionable * message can be shown instead of a raw stack trace. */export function isOpenRouterServerError(  error: unknown,  message: string,): boolean {  if (isRecord(error)) {    const status = error.statusCode ?? error.status;    const name = error instanceof Error ? error.name : null;     if (      (status === 500 || status === "500") &&      (name === "OpenRouterError" || "metadata" in error)    ) {      return true;    }  }   return /OpenRouterError/iu.test(String(error)) ||    /Internal Server Error/iu.test(message)    ? /\b500\b|Internal Server Error/iu.test(message)    : false;} /** * Produces a user-facing error message: a friendly note for provider 500s, * otherwise the error's own message with any secrets redacted. */export function getErrorMessage(error: unknown): string {  const message =    error instanceof Error ? error.message : "OpenWiki agent run failed.";   if (isOpenRouterServerError(error, message)) {    return "OpenRouter/provider returned 500 Internal Server Error. Try retrying or switching models with /model. Run with OPENWIKI_DEBUG=1 to show provider metadata.";  }   return sanitizeDiagnosticText(message);} /** * Whether a run failure looks like a credential rejection, judged from the HTTP * status (401/403, number or string) and the already-redacted message. Drives * whether the CLI shows the auth "how to fix" panel. */export function isAuthError(error: unknown, message: string): boolean {  const status = isRecord(error)    ? (error.statusCode ?? error.status)    : undefined;  const name = isRecord(error) ? error.name : undefined;   if (    status === 401 ||    status === 403 ||    status === "401" ||    status === "403"  ) {    return true;  }   if (    name === "CredentialsProviderError" ||    name === "InvalidIdentityTokenException" ||    name === "ExpiredTokenException" ||    name === "IDPRejectedClaimException"  ) {    return true;  }   return /\b40[13]\b|incorrect api key|invalid api key|unauthorized|forbidden|authentication|permission denied|not authorized|could not load credentials from any providers|invalid identity token|expired token|web identity token.+(?:expired|invalid|not valid)|security token.+invalid/iu.test(    message,  );} function isRecord(value: unknown): value is Record<string, unknown> {  return typeof value === "object" && value !== null;}