Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 6x 6x 6x 6x 6x 6x 2x 2x 2x 101x 101x 101x 101x 101x 101x 101x 101x 101x 101x 101x 2x 2x 35x 35x 35x 2x 2x 2x 65x 6x 6x 65x 1x 1x 65x 65x 1x 1x 65x 29x 29x 65x 3x 3x 65x 2x 2x 65x 1x 1x 22x 22x 65x 2x 2x 2x 65x 20x 60x 35x 35x 20x 59x 22x 22x 65x | /**
* Strips personally identifiable and credential-like data from text and values before they
* are sent off the device as diagnostic logs. Pattern-based and deliberately conservative:
* it removes what it recognizes, so it complements (not replaces) not logging sensitive
* data in the first place.
*
* @module scrubPii
* @category Helpers
* @source
*/
/** Marker substituted for removed text or values. */
const REDACTED = '[redacted]';
/** Longest array or object-key list kept when copying a value; the rest is summarized. */
const MAX_ITEMS = 20;
/** Deepest nesting copied before a value is replaced by a type marker. */
const MAX_DEPTH = 4;
/**
* Object keys whose values are never sent, matched case-insensitively against the whole key
* with `-`/`_` removed. Covers credentials, contact details, and location.
*/
const SENSITIVE_KEY =
/^(authorization|proxyauthorization|cookie|setcookie|auth|bearer|jwt|csrf|xsrf|xapikey|xauthtoken|email|mail|phone|telephone|mobile|fax|address|street|zip|zipcode|postcode|postalcode|city|country|location|latitude|longitude|lat|lng|ip|ipaddress|username|firstname|lastname|fullname|ssn|dob|birthday|.*(password|passwd|passphrase|secret|token|apikey|clientid|credential|session|cookie).*)$/i;
/**
* Email addresses. The lookbehind makes a scan start only at the beginning of a run of local-part
* characters; without it, a long run with no `@` is rescanned from every position (quadratic).
*/
const EMAIL = /(?<![a-z0-9._%+-])[a-z0-9._%+-]+@[a-z0-9-]+(?:\.[a-z0-9-]+)*\.[a-z]{2,}/gi;
/** Absolute `http(s)`/`ws(s)`/`ftp` URLs, up to whitespace or a closing delimiter. */
const URL_PATTERN = /\b(?:https?|wss?|ftp):\/\/[^\s"'<>)\]}]+/gi;
/** Dotted-quad IPv4 addresses. */
const IPV4 = /\b(?:\d{1,3}\.){3}\d{1,3}\b/g;
/** IPv6 addresses (four or more hex groups, so clock times like `12:30:45` are left alone). */
const IPV6 = /\b(?:[0-9a-f]{1,4}:){4,7}[0-9a-f]{1,4}\b/gi;
/** The user-name segment of macOS, Linux, and Windows home-directory paths. */
const HOME_PATH = /(\/Users\/|\/home\/|[a-z]:\\Users\\)[^/\\\s]+/gi;
/** `Bearer`/`Basic` authorization credentials. */
const AUTH_SCHEME = /\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi;
/** JSON Web Tokens. */
const JWT = /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]*/g;
/**
* `key=value` / `key: value` pairs whose key names a credential, including compound keys
* such as `client_secret`, `refresh_token`, `clientSecret`, and `x-api-key`.
*/
const SECRET_PAIR =
/(?<![\w.-])([\w.-]*(?:api[_-]?key|token|secret|password|passwd|passphrase|session|cookie|authorization|client[_-]?id|credential)[\w.-]*["']?\s*[=:]\s*)["']?(?:(?:Bearer|Basic)\s+)?[^\s"',;&]+["']?/gi;
/** Longest text scrubbed in full; the sender keeps only a few hundred characters anyway. */
const MAX_SCRUB_CHARS = 50_000;
/**
* Reduces a URL to its origin and path, dropping credentials, the query string and the
* fragment, any of which can carry user input or tokens.
*
* @param url - A URL found in text
* @returns The URL without `user:pass@`, `?query` or `#hash`
* @example
* ```typescript
* stripUrl('https://user:pw@shop.example.com/p/1?q=aspirin&token=abc#top');
* // 'https://shop.example.com/p/1'
* ```
* @source
*/
function stripUrl(url: string): string {
try {
const parsed = new URL(url);
return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
} catch {
// Not parseable (e.g. truncated); cut at the first query/fragment marker instead.
return url.replace(/^([a-z]+:\/\/)[^/@\s]*@/i, '$1').replace(/[?#].*$/, '');
}
}
/**
* Removes recognizable personal and credential data from a string: email addresses, IP
* addresses, URL credentials/queries/fragments, home-directory user names, authorization
* headers, JWTs, and `token=...`-style pairs.
*
* @category Helpers
* @group Formatters
* @param text - The text to scrub (only the first 50,000 characters are kept)
* @returns The text with matches replaced by markers such as `[email]`
* @example
* ```typescript
* scrubText('mail me@example.com via https://x.com/a?q=1 from /Users/jo/app.js');
* // 'mail [email] via https://x.com/a from /Users/[user]/app.js'
* ```
* @source
*/
export function scrubText(text: string): string {
return text
.slice(0, MAX_SCRUB_CHARS)
.replace(URL_PATTERN, stripUrl)
.replace(EMAIL, '[email]')
.replace(SECRET_PAIR, `$1${REDACTED}`)
.replace(AUTH_SCHEME, '$1 [token]')
.replace(JWT, '[token]')
.replace(IPV4, '[ip]')
.replace(IPV6, '[ip]')
.replace(HOME_PATH, '$1[user]');
}
/**
* Whether an object key names sensitive data whose value must not be sent.
*
* @param key - The property name
* @returns `true` for credential, contact, and location keys
* @example
* ```typescript
* isSensitiveKey('Authorization'); // true
* isSensitiveKey('access_token'); // true
* isSensitiveKey('title'); // false
* ```
* @source
*/
function isSensitiveKey(key: string): boolean {
return SENSITIVE_KEY.test(key.replace(/[-_\s]/g, ''));
}
/**
* Copies a value into a JSON-safe form with sensitive data removed: strings are scrubbed,
* values under sensitive keys are replaced, and size, depth, and cycles are bounded.
*
* @category Helpers
* @group Formatters
* @param value - Any value passed to a logger
* @param seen - Objects already visited on this path, used to cut cycles
* @param depth - Current nesting depth
* @returns A scrubbed, serializable copy
* @example
* ```typescript
* scrubValue({ title: 'Aspirin', headers: { Authorization: 'Bearer abcdefgh1234' }, email: 'a@b.co' });
* // { title: 'Aspirin', headers: { Authorization: '[redacted]' }, email: '[redacted]' }
* ```
* @source
*/
export function scrubValue(value: unknown, seen = new WeakSet<object>(), depth = 0): unknown {
if (typeof value === 'string') {
return scrubText(value);
}
if (typeof value === 'function') {
return '[function]';
}
if (typeof value === 'symbol') {
return scrubText(value.toString());
}
if (typeof value === 'bigint') {
return value.toString();
}
if (typeof value !== 'object' || value === null) {
return value;
}
if (value instanceof Error) {
return { name: value.name, message: scrubText(value.message) };
}
if (seen.has(value)) {
return '[circular]';
}
if (depth >= MAX_DEPTH) {
return Array.isArray(value) ? '[array]' : '[object]';
}
seen.add(value);
try {
if (Array.isArray(value)) {
const items = value.slice(0, MAX_ITEMS).map((item) => scrubValue(item, seen, depth + 1));
return value.length > MAX_ITEMS ? [...items, `[${value.length - MAX_ITEMS} more]`] : items;
}
if (value instanceof Map || value instanceof Set) {
return scrubValue([...value], seen, depth);
}
const result: Record<string, unknown> = {};
for (const [key, item] of Object.entries(value).slice(0, MAX_ITEMS)) {
result[key] = isSensitiveKey(key) ? REDACTED : scrubValue(item, seen, depth + 1);
}
return result;
} finally {
seen.delete(value);
}
}
|