d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
204 lines
5.1 KiB
TypeScript
204 lines
5.1 KiB
TypeScript
/**
|
|
* Centralized redaction utilities for sensitive data
|
|
*/
|
|
|
|
import { filterUserFileForDisplay, isUserFile } from '@/lib/core/utils/user-file'
|
|
|
|
export const REDACTED_MARKER = '[REDACTED]'
|
|
export const TRUNCATED_MARKER = '[TRUNCATED]'
|
|
|
|
const BYPASS_REDACTION_KEYS = new Set(['nextPageToken'])
|
|
|
|
/** Keys that contain large binary/encoded data that should be truncated in logs */
|
|
const LARGE_DATA_KEYS = new Set(['base64'])
|
|
|
|
const SENSITIVE_KEY_PATTERNS: RegExp[] = [
|
|
/^api[_-]?key$/i,
|
|
/^access[_-]?token$/i,
|
|
/^refresh[_-]?token$/i,
|
|
/^client[_-]?secret$/i,
|
|
/^private[_-]?key$/i,
|
|
/^auth[_-]?token$/i,
|
|
/^.*secret$/i,
|
|
/^.*password$/i,
|
|
/^.*token$/i,
|
|
/^.*credential$/i,
|
|
/^authorization$/i,
|
|
/^bearer$/i,
|
|
/^private$/i,
|
|
/^auth$/i,
|
|
]
|
|
|
|
/**
|
|
* Patterns for sensitive values in strings (for redacting values, not keys)
|
|
* Each pattern has a replacement function
|
|
*/
|
|
const SENSITIVE_VALUE_PATTERNS: Array<{
|
|
pattern: RegExp
|
|
replacement: string
|
|
}> = [
|
|
// Bearer tokens
|
|
{
|
|
pattern: /Bearer\s+[A-Za-z0-9\-._~+/]+=*/gi,
|
|
replacement: `Bearer ${REDACTED_MARKER}`,
|
|
},
|
|
// Basic auth
|
|
{
|
|
pattern: /Basic\s+[A-Za-z0-9+/]+=*/gi,
|
|
replacement: `Basic ${REDACTED_MARKER}`,
|
|
},
|
|
// API keys that look like sk-..., pk-..., etc.
|
|
{
|
|
pattern: /\b(sk|pk|api|key)[_-][A-Za-z0-9\-._]{20,}\b/gi,
|
|
replacement: REDACTED_MARKER,
|
|
},
|
|
// JSON-style password fields: password: "value" or password: 'value'
|
|
{
|
|
pattern: /password['":\s]*['"][^'"]+['"]/gi,
|
|
replacement: `password: "${REDACTED_MARKER}"`,
|
|
},
|
|
// JSON-style token fields: token: "value" or token: 'value'
|
|
{
|
|
pattern: /token['":\s]*['"][^'"]+['"]/gi,
|
|
replacement: `token: "${REDACTED_MARKER}"`,
|
|
},
|
|
// JSON-style api_key fields: api_key: "value" or api-key: "value"
|
|
{
|
|
pattern: /api[_-]?key['":\s]*['"][^'"]+['"]/gi,
|
|
replacement: `api_key: "${REDACTED_MARKER}"`,
|
|
},
|
|
]
|
|
|
|
export function isSensitiveKey(key: string): boolean {
|
|
if (BYPASS_REDACTION_KEYS.has(key)) {
|
|
return false
|
|
}
|
|
const lowerKey = key.toLowerCase()
|
|
return SENSITIVE_KEY_PATTERNS.some((pattern) => pattern.test(lowerKey))
|
|
}
|
|
|
|
/**
|
|
* Redacts sensitive patterns from a string value
|
|
* @param value - The string to redact
|
|
* @returns The string with sensitive patterns redacted
|
|
*/
|
|
export function redactSensitiveValues(value: string): string {
|
|
if (!value || typeof value !== 'string') {
|
|
return value
|
|
}
|
|
|
|
let result = value
|
|
for (const { pattern, replacement } of SENSITIVE_VALUE_PATTERNS) {
|
|
result = result.replace(pattern, replacement)
|
|
}
|
|
return result
|
|
}
|
|
|
|
export function isLargeDataKey(key: string): boolean {
|
|
return LARGE_DATA_KEYS.has(key)
|
|
}
|
|
|
|
export function redactApiKeys(obj: any): any {
|
|
if (obj === null || obj === undefined) {
|
|
return obj
|
|
}
|
|
|
|
if (typeof obj !== 'object') {
|
|
return obj
|
|
}
|
|
|
|
if (Array.isArray(obj)) {
|
|
return obj.map((item) => redactApiKeys(item))
|
|
}
|
|
|
|
if (isUserFile(obj)) {
|
|
const filtered = filterUserFileForDisplay(obj)
|
|
const result: Record<string, any> = {}
|
|
for (const [key, value] of Object.entries(filtered)) {
|
|
if (isLargeDataKey(key) && typeof value === 'string') {
|
|
result[key] = TRUNCATED_MARKER
|
|
} else {
|
|
result[key] = value
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
const result: Record<string, any> = {}
|
|
|
|
for (const [key, value] of Object.entries(obj)) {
|
|
if (isSensitiveKey(key)) {
|
|
result[key] = REDACTED_MARKER
|
|
} else if (isLargeDataKey(key) && typeof value === 'string') {
|
|
result[key] = TRUNCATED_MARKER
|
|
} else if (typeof value === 'object' && value !== null) {
|
|
result[key] = redactApiKeys(value)
|
|
} else {
|
|
result[key] = value
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Sanitizes a string for safe logging by truncating and redacting sensitive patterns
|
|
*
|
|
* @param value - The string to sanitize
|
|
* @param maxLength - Maximum length of the output (default: 100)
|
|
* @returns The sanitized string
|
|
*/
|
|
export function sanitizeForLogging(value: string, maxLength = 100): string {
|
|
if (!value) return ''
|
|
|
|
let sanitized = value.substring(0, maxLength)
|
|
|
|
sanitized = redactSensitiveValues(sanitized)
|
|
|
|
return sanitized
|
|
}
|
|
|
|
/**
|
|
* Sanitizes event data for error reporting/analytics
|
|
*
|
|
* @param event - The event data to sanitize
|
|
* @returns Sanitized event data safe for external reporting
|
|
*/
|
|
export function sanitizeEventData(event: any): any {
|
|
if (event === null || event === undefined) {
|
|
return event
|
|
}
|
|
|
|
if (typeof event === 'string') {
|
|
return redactSensitiveValues(event)
|
|
}
|
|
|
|
if (typeof event !== 'object') {
|
|
return event
|
|
}
|
|
|
|
if (Array.isArray(event)) {
|
|
return event.map((item) => sanitizeEventData(item))
|
|
}
|
|
|
|
const sanitized: Record<string, unknown> = {}
|
|
|
|
for (const [key, value] of Object.entries(event)) {
|
|
if (isSensitiveKey(key)) {
|
|
continue
|
|
}
|
|
|
|
if (typeof value === 'string') {
|
|
sanitized[key] = redactSensitiveValues(value)
|
|
} else if (Array.isArray(value)) {
|
|
sanitized[key] = value.map((v) => sanitizeEventData(v))
|
|
} else if (value && typeof value === 'object') {
|
|
sanitized[key] = sanitizeEventData(value)
|
|
} else {
|
|
sanitized[key] = value
|
|
}
|
|
}
|
|
|
|
return sanitized
|
|
}
|