UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

284 lines (283 loc) 13.6 kB
import { a as asOptionalRecord } from "./record-coerce-DItp3I4t.js"; import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js"; import { t as isBlockedObjectKey } from "./prototype-keys-CuYw53fZ.js"; import { p as redactSensitiveText } from "./redact-BtvPPfTi.js"; import { i as isSensitiveUrlQueryParamName } from "./redact-sensitive-url-BN1NZvXG.js"; import "./redact-snapshot-DcfZQ78H.js"; import { t as isSecretRefShape } from "./redact-snapshot.secret-ref-NJzUnCLU.js"; import path from "node:path"; //#region src/logging/diagnostic-support-redaction.ts const SECRET_SUPPORT_FIELD_RE = /(?:authorization|cookie|credential|key|password|passwd|secret|token)/iu; const PAYLOAD_SUPPORT_FIELD_RE = /(?:body|chat|content|detail|error|header|instruction|message|payload|prompt|result|text|tool|transcript)/iu; const IDENTIFIER_SUPPORT_FIELD_RE = /(?:account[-_]?id|chat[-_]?id|conversation[-_]?id|email|message[-_]?id|phone|thread[-_]?id|user[-_]?id|username)/iu; const PRIVATE_MAP_SUPPORT_FIELD_RE = /^(?:accounts|chats|conversations|messages|threads|users)$/iu; const CONFIG_PRIVATE_FIELD_RE = /(?:allow[-_]?from|allow[-_]?to|deny[-_]?from|deny[-_]?to|blocked[-_]?from|blocked[-_]?users|owner[-_]?id|sender[-_]?id|recipient[-_]?id)/iu; const SENSITIVE_COMMAND_ARG_RE = /^--(?:aws[-_]?secret[-_]?access[-_]?key|awsSecretAccessKey|SecretAccessKey|api[-_]?key|hook[-_]?token|password|password-file|passwd|secret|token)(?:=.*)?$/iu; const BASIC_AUTH_RE = /\bBasic\s+[A-Za-z0-9+/]+={0,2}/giu; const COOKIE_HEADER_RE = /\b(Cookie|Set-Cookie)\s*:\s*[^\r\n]+/giu; const AWS_ACCESS_KEY_ID_RE = /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/gu; const AWS_SECRET_ACCESS_KEY_RE = /(?<![A-Za-z0-9/+=_,-])(?<!;base64,[A-Za-z0-9+/=]*)(?=[A-Za-z0-9/+=]{40}(?![A-Za-z0-9/+=]))(?=[A-Za-z0-9/+=]{0,39}[A-Z])(?=[A-Za-z0-9/+=]{0,39}[a-z])(?=[A-Za-z0-9/+=]{0,39}[0-9/+=])(?=[A-Za-z0-9/+=]{0,39}[^A-Fa-f0-9])[A-Za-z0-9/+=]{40}(?![A-Za-z0-9/+=_,-])/gu; const JWT_RE = /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/gu; const URL_USERINFO_RE = /\b([a-z][a-z0-9+.-]*:\/\/)([^/@\s:?#]+)(?::([^/@\s?#]+))?@/giu; const URL_PARAM_RE = /([?&])([^=&\s]+)=([^&#\s]+)/giu; const EMAIL_RE = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/giu; const MATRIX_USER_ID_RE = /@[A-Za-z0-9._=-]+:[A-Za-z0-9.-]+/gu; const MATRIX_ROOM_ID_RE = /![A-Za-z0-9._=-]+:[A-Za-z0-9.-]+/gu; const MATRIX_EVENT_ID_RE = /\$[A-Za-z0-9_-]{16,}/gu; const HANDLE_RE = /(^|[^\w:/])@[A-Za-z0-9_]{5,}\b(?!\.)/gu; const LONG_DECIMAL_ID_RE = /\b\d{9,}\b/gu; const MAX_SUPPORT_STRING_LENGTH = 2e3; const MAX_SUPPORT_SNAPSHOT_DEPTH = 10; const MAX_SUPPORT_ARRAY_ITEMS = 1e3; const MAX_SUPPORT_OBJECT_ENTRIES = 1e3; const DEFAULT_TRUNCATION_SUFFIX = "...<truncated>"; const TRUNCATED_SUPPORT_FIELD = "<truncated>"; function isPrivateSupportField(key) { return SECRET_SUPPORT_FIELD_RE.test(key) || PAYLOAD_SUPPORT_FIELD_RE.test(key) || IDENTIFIER_SUPPORT_FIELD_RE.test(key); } function isPrivateConfigField(key) { return isPrivateSupportField(key) || CONFIG_PRIVATE_FIELD_RE.test(key); } function sanitizeSecretRefForSupport(value) { const sanitized = createSupportRecord(); if (typeof value.source === "string") sanitized.source = value.source; if (typeof value.provider === "string") sanitized.provider = value.provider; sanitized.id = "<redacted>"; return sanitized; } function privateMapEntryLabel(key) { const normalized = key.toLowerCase(); return normalized.endsWith("s") ? normalized.slice(0, -1) : normalized; } function createSupportRecord() { return Object.create(null); } function countOwnObjectEntries(record) { let count = 0; for (const key in record) if (Object.hasOwn(record, key)) count += 1; return count; } function limitedSupportObjectEntries(record) { let count = 0; const entries = []; for (const key in record) { if (!Object.hasOwn(record, key)) continue; count += 1; if (isBlockedObjectKey(key) || entries.length >= MAX_SUPPORT_OBJECT_ENTRIES) continue; entries.push({ key, value: record[key] }); } entries.sort((a, b) => a.key.localeCompare(b.key)); return { count, entries }; } function limitedSupportArray(value) { return { count: value.length, items: value.slice(0, MAX_SUPPORT_ARRAY_ITEMS) }; } function addTruncationMetadata(sanitized, count) { if (count > MAX_SUPPORT_OBJECT_ENTRIES) sanitized[TRUNCATED_SUPPORT_FIELD] = { truncated: true, count, limit: MAX_SUPPORT_OBJECT_ENTRIES }; } function supportArrayResult(items, count) { if (count <= MAX_SUPPORT_ARRAY_ITEMS) return items; return { items, truncated: true, count, limit: MAX_SUPPORT_ARRAY_ITEMS }; } function isWindowsAbsolutePath(value) { return /^(?:[A-Za-z]:[\\/]|\\\\)/u.test(value); } function normalizePathPrefix(value) { return isWindowsAbsolutePath(value) ? path.win32.resolve(value) : path.resolve(value); } function addPathPrefix(prefixes, prefix, label, caseInsensitive) { if (!prefixes.has(prefix)) prefixes.set(prefix, { prefix, label, caseInsensitive }); } function addPathPrefixVariants(prefixes, value, label) { if (!value) return; const normalized = normalizePathPrefix(value); const caseInsensitive = isWindowsAbsolutePath(normalized); addPathPrefix(prefixes, normalized, label, caseInsensitive); if (isWindowsAbsolutePath(normalized)) addPathPrefix(prefixes, normalized.replaceAll("\\", "/"), label, caseInsensitive); } function pathRedactionPrefixes(options) { const prefixes = /* @__PURE__ */ new Map(); addPathPrefixVariants(prefixes, options.stateDir, "$OPENCLAW_STATE_DIR"); addPathPrefixVariants(prefixes, options.env.HOME, "~"); addPathPrefixVariants(prefixes, options.env.USERPROFILE, "~"); return [...prefixes.values()].toSorted((a, b) => b.prefix.length - a.prefix.length); } function pathCandidates(file) { if (!isWindowsAbsolutePath(file)) return [path.resolve(file)]; const resolved = path.win32.resolve(file); return [resolved, resolved.replaceAll("\\", "/")]; } function hasPathPrefix(value, prefix) { return prefix.caseInsensitive ? value.toLowerCase().startsWith(prefix.prefix.toLowerCase()) : value.startsWith(prefix.prefix); } function matchPathPrefix(file, prefix) { if (file.length === prefix.prefix.length && hasPathPrefix(file, prefix)) return ""; if (!hasPathPrefix(file, prefix)) return; const next = file[prefix.prefix.length]; return next === "/" || next === "\\" ? file.slice(prefix.prefix.length) : void 0; } function isSupportAbsolutePath(value) { return path.isAbsolute(value) || isWindowsAbsolutePath(value); } function redactPathForSupport(file, options) { if (file == null || typeof file !== "string") return ""; if (file.startsWith("$")) return file; const candidates = pathCandidates(file); for (const next of candidates) for (const prefix of pathRedactionPrefixes(options)) { const suffix = matchPathPrefix(next, prefix); if (suffix !== void 0) return `${prefix.label}${suffix}`; } return redactSensitiveTextForSupport(candidates[0] ?? file); } function replaceKnownPathPrefix(value, prefix) { const search = prefix.caseInsensitive ? prefix.prefix.toLowerCase() : prefix.prefix; const haystack = prefix.caseInsensitive ? value.toLowerCase() : value; let offset = 0; let next = ""; while (offset < value.length) { const index = haystack.indexOf(search, offset); if (index === -1) { next += value.slice(offset); break; } next += value.slice(offset, index); next += prefix.label; offset = index + prefix.prefix.length; } return next; } function redactKnownPathPrefixesForSupport(value, redaction) { let next = value; for (const prefix of pathRedactionPrefixes(redaction)) next = replaceKnownPathPrefix(next, prefix); return next; } function redactTextForSupport(value) { let redacted = redactCommonCredentialTextForSupport(value); redacted = redactSensitiveTextForSupport(redacted); redacted = redactUrlSecretsForSupport(redacted); redacted = redactServiceIdentifiersForSupport(redacted); redacted = redactContactIdentifiersForSupport(redacted); return redactLongIdentifiersForSupport(redacted); } function redactSensitiveTextForSupport(value) { return redactSensitiveText(value, { mode: "tools" }); } function redactCommonCredentialTextForSupport(value) { return value.replace(BASIC_AUTH_RE, "Basic <redacted>").replace(COOKIE_HEADER_RE, "$1: <redacted>").replace(AWS_ACCESS_KEY_ID_RE, "<redacted-aws-key>").replace(JWT_RE, "<redacted-jwt>").replace(AWS_SECRET_ACCESS_KEY_RE, "<redacted-aws-secret-key>"); } function redactUrlSecretsForSupport(value) { return value.replace(URL_USERINFO_RE, (_match, scheme, _username, password) => password ? `${scheme}<redacted>:<redacted>@` : `${scheme}<redacted>@`).replace(URL_PARAM_RE, (match, prefix, key) => isSensitiveUrlQueryParamName(key) ? `${prefix}${key}=<redacted>` : match); } function redactContactIdentifiersForSupport(value) { return value.replace(EMAIL_RE, "<redacted-email>").replace(HANDLE_RE, "$1<redacted-handle>"); } function redactServiceIdentifiersForSupport(value) { return value.replace(MATRIX_USER_ID_RE, "<redacted-matrix-user>").replace(MATRIX_ROOM_ID_RE, "<redacted-matrix-room>").replace(MATRIX_EVENT_ID_RE, (eventId) => eventId === "$OPENCLAW_STATE_DIR" ? eventId : "<redacted-matrix-event>"); } function redactLongIdentifiersForSupport(value) { return value.replace(LONG_DECIMAL_ID_RE, "<redacted-id>"); } function redactSupportString(value, redaction, options = {}) { const maxLength = options.maxLength ?? MAX_SUPPORT_STRING_LENGTH; const truncationSuffix = options.truncationSuffix ?? DEFAULT_TRUNCATION_SUFFIX; const redacted = redactTextForSupport(value); const pathRedacted = isSupportAbsolutePath(redacted) ? redactPathForSupport(redacted, redaction) : redactKnownPathPrefixesForSupport(redacted, redaction); if (pathRedacted.length <= maxLength) return pathRedacted; return `${truncateUtf16Safe(pathRedacted, maxLength)}${truncationSuffix}`; } function sanitizeCommandArguments(args, redaction) { let redactNext = false; return args.map((arg) => { if (typeof arg !== "string") return sanitizeSupportSnapshotValue(arg, redaction); if (redactNext) { redactNext = false; return "<redacted>"; } if (SENSITIVE_COMMAND_ARG_RE.test(arg)) { const hasInlineValue = arg.includes("="); if (!hasInlineValue) redactNext = true; return hasInlineValue ? arg.replace(/[=].*/u, "=<redacted>") : arg; } return redactSupportString(arg, redaction); }); } /** Sanitizes general diagnostic snapshots while keeping bounded object/array structure. */ function sanitizeSupportSnapshotValue(value, redaction, key = "", depth = 0) { if (value == null || typeof value === "boolean") return value; if (typeof value === "number") return isPrivateSupportField(key) ? "<redacted>" : value; if (typeof value === "string") return isPrivateSupportField(key) ? "<redacted>" : redactSupportString(value, redaction); if (depth >= MAX_SUPPORT_SNAPSHOT_DEPTH) return "<truncated>"; if (Array.isArray(value)) { const { count, items } = limitedSupportArray(value); if (key === "programArguments") return supportArrayResult(sanitizeCommandArguments(items, redaction), count); return supportArrayResult(items.map((entry) => sanitizeSupportSnapshotValue(entry, redaction, key, depth + 1)), count); } const record = asOptionalRecord(value); if (!record) return "<unsupported>"; if (PRIVATE_MAP_SUPPORT_FIELD_RE.test(key)) return { count: countOwnObjectEntries(record) }; const sanitized = createSupportRecord(); const { count, entries } = limitedSupportObjectEntries(record); for (const { key: entryKey, value: entryValue } of entries) sanitized[entryKey] = isPrivateSupportField(entryKey) ? "<redacted>" : sanitizeSupportSnapshotValue(entryValue, redaction, entryKey, depth + 1); addTruncationMetadata(sanitized, count); return sanitized; } /** Sanitizes config-shaped values with stricter private field handling. */ function sanitizeSupportConfigValue(value, redaction, key = "", depth = 0) { if (value == null || typeof value === "boolean") return value; if (typeof value === "number") return isPrivateConfigField(key) ? "<redacted>" : value; if (typeof value === "string") { if (value === "__OPENCLAW_REDACTED__") return "<redacted>"; return isPrivateConfigField(key) ? "<redacted>" : redactSupportString(value, redaction); } if (depth >= MAX_SUPPORT_SNAPSHOT_DEPTH) return "<truncated>"; if (Array.isArray(value)) { if (isPrivateConfigField(key)) return { redacted: true, count: value.length }; const { count, items } = limitedSupportArray(value); return supportArrayResult(items.map((entry) => sanitizeSupportConfigValue(entry, redaction, key, depth + 1)), count); } const record = asOptionalRecord(value); if (!record) return "<unsupported>"; if (isPrivateConfigField(key)) return isSecretRefShape(record) ? sanitizeSecretRefForSupport(record) : "<redacted>"; const sanitized = createSupportRecord(); let privateEntryIndex = 0; const redactEntryKeys = PRIVATE_MAP_SUPPORT_FIELD_RE.test(key); const privateEntryLabel = redactEntryKeys ? privateMapEntryLabel(key) : ""; const { count, entries } = limitedSupportObjectEntries(record); for (const { key: entryKey, value: entryValue } of entries) { let outputKey = entryKey; if (redactEntryKeys) { privateEntryIndex += 1; outputKey = `<redacted-${privateEntryLabel}-${privateEntryIndex}>`; } sanitized[outputKey] = sanitizeSupportConfigValue(entryValue, redaction, entryKey, depth + 1); } addTruncationMetadata(sanitized, count); return sanitized; } //#endregion export { sanitizeSupportSnapshotValue as a, sanitizeSupportConfigValue as i, redactSupportString as n, redactTextForSupport as r, redactPathForSupport as t };