@beignet/core
Version:
Core framework primitives for Beignet
207 lines • 7.05 kB
JavaScript
/**
* 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