mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 14:25:51 +00:00
- webhookSanitizer: redact before truncate to avoid split secrets at boundary - webhookSanitizer: return safe placeholder on error instead of raw content - peerSessions: use discriminated union return type for type safety Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
58 lines
2.5 KiB
TypeScript
58 lines
2.5 KiB
TypeScript
/**
|
|
* Sanitize inbound GitHub webhook payload content before it enters the session.
|
|
*
|
|
* Called from useReplBridge.tsx when feature('KAIROS_GITHUB_WEBHOOKS') is enabled.
|
|
* Strips known secret patterns (tokens, API keys, credentials) while preserving
|
|
* the meaningful content (PR titles, descriptions, commit messages, etc.).
|
|
*
|
|
* Must be synchronous and never throw — on error, returns a safe placeholder.
|
|
*/
|
|
|
|
/** Patterns that match known secret/token formats. */
|
|
const SECRET_PATTERNS: Array<{ pattern: RegExp; replacement: string }> = [
|
|
// GitHub tokens (PAT, OAuth, App, Server-to-server)
|
|
{ pattern: /\b(ghp|gho|ghs|ghu|github_pat)_[A-Za-z0-9_]{10,}\b/g, replacement: '[REDACTED_GITHUB_TOKEN]' },
|
|
// Anthropic API keys
|
|
{ pattern: /\bsk-ant-[A-Za-z0-9_-]{10,}\b/g, replacement: '[REDACTED_ANTHROPIC_KEY]' },
|
|
// Generic Bearer tokens in headers
|
|
{ pattern: /(Bearer\s+)[A-Za-z0-9._\-/+=]{20,}/gi, replacement: '$1[REDACTED_TOKEN]' },
|
|
// AWS access keys
|
|
{ pattern: /\b(AKIA|ASIA)[A-Z0-9]{16}\b/g, replacement: '[REDACTED_AWS_KEY]' },
|
|
// AWS secret keys (40-char base64-like strings after common labels)
|
|
{ pattern: /(aws_secret_access_key|secret_key|SecretAccessKey)['":\s=]+[A-Za-z0-9/+=]{30,}/gi, replacement: '$1=[REDACTED_AWS_SECRET]' },
|
|
// Generic API key patterns (key=value or "key": "value")
|
|
{ pattern: /(api[_-]?key|apikey|secret|password|token|credential)['":\s=]+["']?[A-Za-z0-9._\-/+=]{16,}["']?/gi, replacement: '$1=[REDACTED]' },
|
|
// npm tokens
|
|
{ pattern: /\bnpm_[A-Za-z0-9]{36}\b/g, replacement: '[REDACTED_NPM_TOKEN]' },
|
|
// Slack tokens
|
|
{ pattern: /\bxox[bporas]-[A-Za-z0-9-]{10,}\b/g, replacement: '[REDACTED_SLACK_TOKEN]' },
|
|
]
|
|
|
|
/** Maximum content length before truncation (100KB). */
|
|
const MAX_CONTENT_LENGTH = 100_000
|
|
|
|
export function sanitizeInboundWebhookContent(content: string): string {
|
|
try {
|
|
if (!content) return content
|
|
|
|
let sanitized = content
|
|
|
|
// Redact known secret patterns first (before truncation to avoid
|
|
// splitting a secret across the truncation boundary)
|
|
for (const { pattern, replacement } of SECRET_PATTERNS) {
|
|
pattern.lastIndex = 0
|
|
sanitized = sanitized.replace(pattern, replacement)
|
|
}
|
|
|
|
// Truncate excessively large payloads after redaction
|
|
if (sanitized.length > MAX_CONTENT_LENGTH) {
|
|
sanitized = sanitized.slice(0, MAX_CONTENT_LENGTH) + '\n... [truncated]'
|
|
}
|
|
|
|
return sanitized
|
|
} catch {
|
|
// Never throw, never return raw content — return a safe placeholder
|
|
return '[webhook content redacted due to sanitization error]'
|
|
}
|
|
}
|