UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

207 lines 7.05 kB
/** * Default replacement used when a sensitive field is redacted. */ export const DEFAULT_REDACTED_VALUE = "[redacted]"; /** * Default replacement used when recursive redaction exceeds `maxDepth`. */ export const DEFAULT_TRUNCATED_VALUE = "[truncated]"; /** * Default replacement used when recursive redaction finds a circular object. */ export const DEFAULT_CIRCULAR_VALUE = "[circular]"; /** * Exact header/object keys redacted by default. */ export const DEFAULT_SENSITIVE_KEYS = [ "authorization", "proxy-authorization", "cookie", "set-cookie", "x-api-key", "api-key", "apikey", "access-token", "refresh-token", "credentials", "accesskey", "jwt", "session", ]; /** * Key substrings redacted by default. * * Matching is case-insensitive. */ export const DEFAULT_SENSITIVE_KEY_TERMS = [ "token", "password", "secret", "credential", "accesskey", "jwt", "session", "private-key", "privatekey", ]; function normalizeKey(key) { return key.toLowerCase(); } function redactSensitiveText(value, replacement) { return value .replace(/([a-z][a-z0-9+.-]*:\/\/)[^\s/:@]+:[^\s/@]+@/gi, (_match, prefix) => `${prefix}${replacement}:${replacement}@`) .replace(/\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, (match) => { const scheme = match.slice(0, match.indexOf(" ")); return `${scheme} ${replacement}`; }) .replace(/\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, () => replacement) .replace(/(\b(?:access[_-]?key|api[_-]?key|jwt|password|secret|session|token)\s*[=:]\s*)[^\s,;]+/gi, (_match, prefix) => `${prefix}${replacement}`) .replace(/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, () => replacement); } /** * Return whether a key should be redacted. * * Checks default exact keys, default key terms, user-provided exact keys, * user-provided key terms, and finally `shouldRedactKey`. * * @param key - Object or header key to evaluate. * @param options - Optional redaction behavior. * @param context - Optional path/value context for custom decisions. * @returns `true` when the key should be replaced. */ export function isSensitiveKey(key, options = {}, context) { const normalized = normalizeKey(key); const exactKeys = new Set([...DEFAULT_SENSITIVE_KEYS, ...(options.sensitiveKeys ?? [])].map(normalizeKey)); if (exactKeys.has(normalized)) return true; const terms = [ ...DEFAULT_SENSITIVE_KEY_TERMS, ...(options.sensitiveKeyTerms ?? []), ].map(normalizeKey); if (terms.some((term) => normalized.includes(term))) return true; return (options.shouldRedactKey?.({ key, path: context?.path ?? [], value: context?.value, }) ?? false); } function redactUnknown(value, options, path, seen) { const maxDepth = options.maxDepth ?? 6; if (path.length > maxDepth) { return options.truncatedValue ?? DEFAULT_TRUNCATED_VALUE; } if (value === null || value === undefined) return value; const valueType = typeof value; if (valueType === "number" || valueType === "boolean") { return value; } if (valueType === "string") { return redactSensitiveText(value, options.replacement ?? DEFAULT_REDACTED_VALUE); } if (valueType === "bigint") { return value.toString(); } if (value instanceof Date) { return value; } if (value instanceof Error) { return { name: value.name, message: redactUnknown(value.message, options, [...path, "message"], seen), stack: redactUnknown(value.stack, options, [...path, "stack"], seen), }; } if (valueType !== "object") { return String(value); } const objectValue = value; if (seen.has(objectValue)) { return options.circularValue ?? DEFAULT_CIRCULAR_VALUE; } seen.add(objectValue); if (Array.isArray(value)) { const output = value.map((item, index) => redactUnknown(item, options, [...path, String(index)], seen)); seen.delete(objectValue); return output; } const output = {}; for (const [key, nestedValue] of Object.entries(value)) { output[key] = isSensitiveKey(key, options, { path: [...path, key], value: nestedValue, }) ? (options.replacement ?? DEFAULT_REDACTED_VALUE) : redactUnknown(nestedValue, options, [...path, key], seen); } seen.delete(objectValue); return output; } /** * Recursively redact a value using Beignet's default sensitive-key rules plus * any custom rules in `options`. * * This returns a copy for objects and arrays. Numbers and booleans are returned * as is unless they are under a sensitive key. High-confidence credential * shapes inside strings are replaced, including authorization schemes, JWTs, * credential-bearing URLs, secret assignments, and private keys. Some runtime * shapes are normalized: * `bigint` becomes a string, `Error` becomes a plain object with `name`, * `message`, and `stack`, and class instances are copied from enumerable * entries. * * @param value - Value to redact. * @param options - Optional redaction behavior. * @returns A redacted value typed as the input type for caller convenience. */ export function redactValue(value, options = {}) { return redactUnknown(value, options, [], new WeakSet()); } function headerEntries(headers) { if (typeof Headers !== "undefined" && headers instanceof Headers) { const entries = []; headers.forEach((value, key) => { entries.push([key, value]); }); return entries; } if (typeof headers[Symbol.iterator] === "function") { return headers; } return Object.entries(headers); } /** * Redact headers into a plain object. * * Sensitive header names such as `authorization`, `cookie`, and token-like keys * are replaced. Non-sensitive values are passed through `redactValue(...)` so * nested object values are still sanitized. * * @param headers - Headers object, iterable entries, or plain object. * @param options - Optional redaction behavior. * @returns A plain object with redacted header values. */ export function redactHeaders(headers, options = {}) { const output = {}; for (const [key, value] of headerEntries(headers)) { output[key] = isSensitiveKey(key, options, { path: [key], value, }) ? (options.replacement ?? DEFAULT_REDACTED_VALUE) : redactValue(value, options); } return output; } /** * Create a reusable redactor function from options. * * @param options - Redaction behavior to apply on each call. * @returns A function that redacts values with the provided options. */ export function createRedactor(options = {}) { return (value) => redactValue(value, options); } //# sourceMappingURL=redaction.js.map