ruvector/studio/lib/sanitize.ts
rUv 814f595995 feat(studio): Add complete RuVector Studio application
Major additions:
- Complete Next.js studio application with 1600+ components
- Docker support (Dockerfile.combined, docker-compose.yml)
- GCP deployment documentation and benchmarks
- SQL benchmark scripts for performance testing
- Sentry integration for monitoring
- Comprehensive test suite and mocks

Studio features:
- Dashboard and admin interfaces
- Data visualization components
- Authentication and user management
- API integration with RuVector backend
- Static data and public assets

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-06 23:04:48 +00:00

218 lines
6.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

export function sanitizeUrlHashParams(url: string): string {
return url.split('#')[0]
}
/**
* Best-effort sanitizer for arrays of objects.
* - Redacts likely secrets by key name (password, token, apiKey, etc.)
* - Redacts likely secrets by value pattern (IPv4/IPv6, AWS keys, Bearer/JWT, generic long tokens)
* - Recurses into nested arrays/objects up to `maxDepth`; beyond that replaces with a notice
* - Handles circular references
*
* @param {any[]} inputArr - Array of items to sanitize (non-objects are copied as-is).
* @param {Object} [opts]
* @param {number} [opts.maxDepth=3] - Maximum depth to traverse (0 == only top level).
* @param {string} [opts.redaction="[REDACTED]"] - Replacement text for sensitive values.
* @param {string} [opts.truncationNotice="[REDACTED: max depth reached]"] - Used when depth limit is hit.
* @param {string[]} [opts.sensitiveKeys] - Extra key names to treat as sensitive (case-insensitive).
* @returns {any[]} a deeply-sanitized clone of the input array
*/
export function sanitizeArrayOfObjects(
inputArr: unknown[],
opts: {
maxDepth?: number
redaction?: string
truncationNotice?: string
sensitiveKeys?: string[]
} = {}
): unknown[] {
const {
maxDepth = 3,
redaction = '[REDACTED]',
truncationNotice = '[REDACTED: max depth reached]',
sensitiveKeys = [],
} = opts
// Common sensitive key names (case-insensitive). Extendable via opts.sensitiveKeys.
const sensitiveKeySet = new Set(
[
'password',
'passwd',
'pwd',
'pass',
'secret',
'token',
'id_token',
'access_token',
'refresh_token',
'apikey',
'api_key',
'api-key',
'apiKey',
'key',
'privatekey',
'private_key',
'client_secret',
'clientSecret',
'auth',
'authorization',
'ssh_key',
'sshKey',
'bearer',
'session',
'cookie',
'csrf',
'xsrf',
'ip',
'ip_address',
'ipAddress',
'aws_access_key_id',
'aws_secret_access_key',
'gcp_service_account_key',
...sensitiveKeys,
].map((k) => k.toLowerCase())
)
// Value patterns that often indicate secrets or PII
const patterns = [
// IPv4
{ re: /\b(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)\b/g, reason: 'ip' },
// IPv6 (simplified but effective)
{ re: /\b(?:[A-Fa-f0-9]{1,4}:){2,7}[A-Fa-f0-9]{1,4}\b/g, reason: 'ip6' },
// AWS Access Key ID (starts with AKIA/ASIA, 16 remaining upper alnum)
{ re: /\b(AKI|ASI)A[0-9A-Z]{16}\b/g, reason: 'aws_access_key_id' },
// AWS Secret Access Key (40 base64-ish chars)
{ re: /\b[0-9A-Za-z/+]{40}\b/g, reason: 'aws_secret_access_key_like' },
// Bearer tokens
{ re: /\bBearer\s+[A-Za-z0-9\-._~+/]+=*\b/g, reason: 'bearer' },
// JWT (three base64url segments separated by dots)
{ re: /\b[A-Za-z0-9-_]+?\.[A-Za-z0-9-_]+?\.[A-Za-z0-9-_]+?\b/g, reason: 'jwt_like' },
// Generic long API-ish token (conservative: 2464 safe chars)
{ re: /\b[A-Za-z0-9_\-]{24,64}\b/g, reason: 'long_token' },
]
const seen = new WeakMap()
function isPlainObject(v: unknown): v is Record<string, unknown> {
if (v === null || typeof v !== 'object') return false
const proto = Object.getPrototypeOf(v)
return proto === Object.prototype || proto === null
}
function redactString(str: string) {
let out = str
for (const { re } of patterns) out = out.replace(re, redaction)
return out
}
function shouldRedactByKey(key: string | symbol | number) {
return sensitiveKeySet.has(String(key).toLowerCase())
}
function sanitizeValue(value: unknown, depth: number): unknown {
if (
value == null ||
typeof value === 'number' ||
typeof value === 'boolean' ||
typeof value === 'bigint'
) {
return value
}
if (typeof value === 'string') {
return redactString(value)
}
if (typeof value === 'function') {
return '[Function]'
}
if (value instanceof Date) {
return value.toISOString()
}
if (value instanceof RegExp) {
return value.toString()
}
if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
return `[TypedArray byteLength=${value.byteLength}]`
}
if (value instanceof ArrayBuffer) {
return `[ArrayBuffer byteLength=${value.byteLength}]`
}
if (depth >= maxDepth) {
return truncationNotice
}
if (typeof value === 'object') {
if (seen.has(value)) {
return '[Circular]'
}
if (Array.isArray(value)) {
const outArr: unknown[] = []
seen.set(value, outArr)
for (let i = 0; i < value.length; i++) {
outArr[i] = sanitizeValue(value[i], depth + 1)
}
return outArr
}
if (isPlainObject(value)) {
const outObj: Record<string | symbol | number, unknown> = {}
seen.set(value, outObj)
for (const [k, v] of Object.entries(value)) {
if (shouldRedactByKey(k)) {
outObj[k] = redaction
} else {
outObj[k] = sanitizeValue(v, depth + 1)
}
}
return outObj
}
if (value instanceof Map) {
const out: unknown[] = []
seen.set(value, out)
for (const [k, v] of value.entries()) {
const redactedKey = shouldRedactByKey(k) ? redaction : sanitizeValue(k, depth + 1)
const redactedVal = shouldRedactByKey(k) ? redaction : sanitizeValue(v, depth + 1)
out.push([redactedKey, redactedVal])
}
return out
}
if (value instanceof Set) {
const out: unknown[] = []
seen.set(value, out)
for (const v of value.values()) {
out.push(sanitizeValue(v, depth + 1))
}
return out
}
if (value instanceof URL) return value.toString()
if (value instanceof Error) {
const o = {
name: value.name,
message: redactString(value.message),
stack: truncationNotice,
}
seen.set(value, o)
return o
}
try {
return redactString(String(value))
} catch {
return redactString(Object.prototype.toString.call(value))
}
}
return redactString(String(value))
}
return inputArr.map((item) => sanitizeValue(item, 0))
}