204 lines
4.8 KiB
TypeScript
204 lines
4.8 KiB
TypeScript
const sensitiveKey = /token|secret|password|api.?key|authorization/iu
|
|
|
|
function redactValue(
|
|
value: unknown,
|
|
seen: WeakSet<object>,
|
|
depth = 0
|
|
): unknown {
|
|
if (depth > 8) {
|
|
return '[TRUNCATED]'
|
|
}
|
|
if (typeof value === 'string') {
|
|
return redactSensitiveText(value)
|
|
}
|
|
if (!value || typeof value !== 'object') {
|
|
return value
|
|
}
|
|
if (seen.has(value)) {
|
|
return '[CIRCULAR]'
|
|
}
|
|
seen.add(value)
|
|
if (Array.isArray(value)) {
|
|
return value
|
|
.slice(0, 100)
|
|
.map((item) => redactValue(item, seen, depth + 1))
|
|
}
|
|
return Object.fromEntries(
|
|
Object.entries(value)
|
|
.slice(0, 100)
|
|
.map(([key, item]) => [
|
|
key,
|
|
sensitiveKey.test(key)
|
|
? '[REDACTED]'
|
|
: redactValue(item, seen, depth + 1)
|
|
])
|
|
)
|
|
}
|
|
|
|
export function redactSensitiveText(value: string): string {
|
|
return value
|
|
.replace(
|
|
/\bAuthorization\b(\s*[:=]\s*)(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\r\n,;}]+)/giu,
|
|
'Authorization$1[REDACTED]'
|
|
)
|
|
.replace(/\bBearer\s+\S+/giu, 'Bearer [REDACTED]')
|
|
.replace(
|
|
/(["']?)(api[-_ ]?key|token|secret|password|authorization)\1(\s*[:=]\s*)"[^"\r\n]*"/giu,
|
|
'$1$2$1$3"[REDACTED]"'
|
|
)
|
|
.replace(
|
|
/(["']?)(api[-_ ]?key|token|secret|password|authorization)\1(\s*[:=]\s*)'[^'\r\n]*'/giu,
|
|
"$1$2$1$3'[REDACTED]'"
|
|
)
|
|
.replace(
|
|
/\b(api[-_ ]?key|token|secret|password|authorization)\b(\s*[:=]\s*|\s+)(["']?)[^\s"',}]+/giu,
|
|
'$1$2[REDACTED]'
|
|
)
|
|
}
|
|
|
|
export function safeToolErrorDetail(
|
|
value: unknown,
|
|
maximum = 2_000
|
|
): string | undefined {
|
|
if (!Number.isSafeInteger(maximum) || maximum < 1) {
|
|
return undefined
|
|
}
|
|
const parts: string[] = []
|
|
let remaining = maximum
|
|
const seen = new WeakSet<object>()
|
|
|
|
const append = (value: string): void => {
|
|
const text = [...value]
|
|
.filter((character) => {
|
|
const code = character.charCodeAt(0)
|
|
return (
|
|
code === 9 ||
|
|
code === 10 ||
|
|
code === 13 ||
|
|
(code > 31 && code !== 127)
|
|
)
|
|
})
|
|
.join('')
|
|
.trim()
|
|
if (!text || remaining <= 0) {
|
|
return
|
|
}
|
|
const separator = parts.length > 0 ? '\n' : ''
|
|
const available = Math.max(0, remaining - separator.length)
|
|
if (available === 0) {
|
|
return
|
|
}
|
|
const bounded = text.slice(0, available)
|
|
parts.push(`${separator}${bounded}`)
|
|
remaining -= separator.length + bounded.length
|
|
}
|
|
|
|
const collect = (candidate: unknown, depth = 0): void => {
|
|
if (remaining <= 0 || depth > 4 || candidate === undefined) {
|
|
return
|
|
}
|
|
if (typeof candidate === 'string') {
|
|
const boundedCandidate = candidate.slice(
|
|
0,
|
|
Math.min(candidate.length, remaining * 4)
|
|
)
|
|
append(boundedCandidate)
|
|
return
|
|
}
|
|
if (!candidate || typeof candidate !== 'object') {
|
|
return
|
|
}
|
|
if (seen.has(candidate)) {
|
|
return
|
|
}
|
|
seen.add(candidate)
|
|
if (Array.isArray(candidate)) {
|
|
for (const item of candidate.slice(0, 20)) {
|
|
collect(item, depth + 1)
|
|
}
|
|
return
|
|
}
|
|
const record = candidate as Record<string, unknown>
|
|
collect(record.message, depth + 1)
|
|
for (const key of [
|
|
'code',
|
|
'errno',
|
|
'syscall',
|
|
'hostname',
|
|
'address',
|
|
'port',
|
|
'status',
|
|
'statusCode'
|
|
]) {
|
|
const metadata = record[key]
|
|
if (
|
|
typeof metadata === 'string' ||
|
|
typeof metadata === 'number'
|
|
) {
|
|
append(`${key}: ${metadata}`)
|
|
}
|
|
}
|
|
if (record.cause !== undefined) {
|
|
append('cause:')
|
|
collect(record.cause, depth + 1)
|
|
}
|
|
for (const key of [
|
|
'content',
|
|
'error',
|
|
'errors',
|
|
'stderr',
|
|
'detail',
|
|
'data'
|
|
]) {
|
|
collect(record[key], depth + 1)
|
|
}
|
|
}
|
|
|
|
collect(value)
|
|
return parts.join('').trim() || undefined
|
|
}
|
|
|
|
export function safeToolArgumentSummary(
|
|
toolArguments: Record<string, unknown>,
|
|
preview?: unknown[],
|
|
maximum = 1_000
|
|
): string {
|
|
const previewText = preview
|
|
?.slice(0, 100)
|
|
.flatMap((item) => {
|
|
if (!item || typeof item !== 'object') {
|
|
return []
|
|
}
|
|
const content = (item as Record<string, unknown>).content
|
|
return typeof content === 'string' ? [content] : []
|
|
})
|
|
.join(' ')
|
|
.trim()
|
|
if (previewText) {
|
|
return redactSensitiveText(previewText).slice(0, maximum)
|
|
}
|
|
return JSON.stringify(
|
|
redactValue(toolArguments, new WeakSet())
|
|
).slice(0, maximum)
|
|
}
|
|
|
|
export function boundedToolDetail(
|
|
value: unknown,
|
|
maximum: number
|
|
): string | undefined {
|
|
if (!Number.isSafeInteger(maximum) || maximum < 1 || value === undefined) {
|
|
return undefined
|
|
}
|
|
let text: string | undefined
|
|
if (typeof value === 'string') {
|
|
text = value
|
|
} else {
|
|
try {
|
|
text = JSON.stringify(value, null, 2)
|
|
} catch {
|
|
return undefined
|
|
}
|
|
}
|
|
return text ? text.slice(0, maximum) : undefined
|
|
}
|