@absoluteui/helpers/sanitize
Record cleanup, file-name sanitizing, invisible-character removal, PII masking, and redaction.
import { ... } from '@absoluteui/helpers/sanitize';How to use
Section titled “How to use”Clean data on the way in and scrub it on the way out: normalise imported rows, contact details, and file names, strip invisible characters, and redact secrets and personal data before logging. It is not an HTML sanitizer: put untrusted text into the page with textContent, and check URLs with @absoluteui/helpers/url. The Helpers and services guide lists the security defaults shared by all helpers.
How do I clean rows from an import before saving them?
Section titled “How do I clean rows from an import before saving them?”cleanRecords runs one pipeline: normalise each row (trim, strip invisible characters, lowercase keys, empty strings to null), drop empty rows, remove duplicates, filter, and map. coerceRecordTypes then turns strings into numbers, booleans, and dates.
import { parseCsv } from '@absoluteui/helpers/csv';import { cleanRecords, coerceRecordTypes } from '@absoluteui/helpers/sanitize';
const { rows } = parseCsv('Email,Name,FTE,Active\n ASHA@example.com , Asha Rao ,1,yes\n,,,\nasha@example.com,Asha Rao,0.8,yes\nliam@example.com,Liam Chen,x,no\n', { header: true });const clean = cleanRecords(rows, { normalize: { lowercaseKeys: true, collapseWhitespace: true, emptyToNull: true, lowercase: ['email'] }, dedupeBy: (r) => r.email, keep: 'last', map: (r) => coerceRecordTypes(r, { fte: 'number', active: 'boolean' }),});clean; // => [{ email: 'asha@example.com', name: 'Asha Rao', fte: 0.8, active: true }, { email: 'liam@example.com', name: 'Liam Chen', fte: null, active: false }]Unparseable values become null by default; pass { onInvalid: 'keep' } or 'omit' to change that.
How do I log an object without leaking secrets or personal data?
Section titled “How do I log an object without leaking secrets or personal data?”redact replaces values under secret-looking keys (passwords, tokens, Authorization, API keys); add your own keys, and maskStrings: true masks e-mails, phone and card numbers inside the remaining text. truncateForLog keeps the line short.
import { DEFAULT_REDACT_KEYS, maskPii, redact, truncateForLog } from '@absoluteui/helpers/sanitize';
const event = { user: 'E-1001', headers: { Authorization: 'Bearer abc.def' }, body: { note: 'Call asha@example.com', bankAccount: 'DE89370400440532013000' } };const safe = redact(event, { keys: [...DEFAULT_REDACT_KEYS, /bankAccount/i], maskStrings: true });safe; // => { user: 'E-1001', headers: { Authorization: '[REDACTED]' }, body: { note: 'Call a***@example.com', bankAccount: '[REDACTED]' } }event.headers.Authorization; // => 'Bearer abc.def' // the input is not changedmaskPii('Card 4111 1111 1111 1111 declined'); // => 'Card **** **** **** 1111 declined'truncateForLog(safe, 30).length; // => 30These are heuristics for logs and previews; the safest data is the data you never log.
How do I build a file name from user data?
Section titled “How do I build a file name from user data?”safeFileName produces one valid name on Windows, macOS, and Linux: path separators and control characters go, reserved device names are escaped, and long names are shortened without losing the extension.
import { safeFileName } from '@absoluteui/helpers/sanitize';
safeFileName('Payslip: Rao/Asha 2026-09.pdf'); // => 'Payslip_ Rao_Asha 2026-09.pdf'safeFileName('NUL.csv'); // => '_NUL.csv'safeFileName(`${'Quarterly headcount report '.repeat(20)}.xlsx`, { maxLength: 40 }).endsWith('.xlsx'); // => truesafeFileName('...', { fallback: 'export.csv' }); // => 'export.csv'How do I normalise contact details before matching them?
Section titled “How do I normalise contact details before matching them?”Normalise before comparing or storing, so Asha@Example.com and asha@example.com are one person. stripInvisible removes zero-width and bidi control characters that make two names look equal when they are not.
import { normalizeEmail, normalizePhone, stripInvisible } from '@absoluteui/helpers/sanitize';
normalizeEmail(' Asha.Rao@Example.COM ', { lowercaseLocal: true }); // => 'asha.rao@example.com'normalizeEmail('not-an-email'); // => undefinednormalizePhone('080 5550 1234', { defaultCountryCode: '91' }); // => '+918055501234'const typed = 'asha' + String.fromCharCode(0x200b) + '.rao';typed === 'asha.rao'; // => falsestripInvisible(typed) === 'asha.rao'; // => trueWhat goes wrong?
Section titled “What goes wrong?”sanitizeSearchQuerycleans text for display and matching, but it is not an injection defence: always use parameterised queries on the server.maskPiimisses formats it does not recognise, and may mask a number that only looks like a card.normalizePhoneworks on digits only; it does not know numbering plans, so it cannot tell a valid number from a well-formed one.
import { normalizePhone, sanitizeSearchQuery } from '@absoluteui/helpers/sanitize';
sanitizeSearchQuery(" Rao'; DROP TABLE staff;-- "); // => "Rao'; DROP TABLE staff;--"normalizePhone('12'); // => undefined // too few digits for E.164API reference
Section titled “API reference”Functions
Section titled “Functions”maskPii
Masks e-mail addresses, phone numbers, card numbers, IBANs and SSN-shaped numbers in free text (for logs, support tickets, previews). Heuristic: never a substitute for not logging PII.
function maskPii(text: string, options?: MaskPiiOptions): string;Example:
maskPii('Mail ada@example.com, card 4111 1111 1111 1111') // => 'Mail a***@example.com, card **** **** **** 1111'redact
Returns a copy with values under secret-looking keys replaced (for logs and telemetry).
Plain objects, arrays and Maps are copied; other objects are kept by reference. Cycles become
'[Circular]'; unsafe keys (__proto__…) are dropped.
function redact<T>(value: T, options?: RedactOptions): T;Example:
redact({ user: 'ada', password: 'x', headers: { Authorization: 'Bearer t' } })// { user: 'ada', password: '[REDACTED]', headers: { Authorization: '[REDACTED]' } }Constants
Section titled “Constants”DEFAULT_REDACT_KEYS
Default secret-looking key pattern for {@link redact}.
const DEFAULT_REDACT_KEYS: readonly RegExp[];Example:
redact({ name: 'Ada', ssn: '123-45-6789', apiKey: 'k1' }, { keys: [...DEFAULT_REDACT_KEYS, /ssn/i] }); // => { name: 'Ada', ssn: '[REDACTED]', apiKey: '[REDACTED]' }Interfaces
Section titled “Interfaces”MaskPiiOptions
interface MaskPiiOptions { readonly email?: boolean; readonly phone?: boolean; readonly card?: boolean; readonly iban?: boolean; readonly ssn?: boolean; readonly maskChar?: string; readonly maxLength?: number;}RedactOptions
interface RedactOptions { readonly keys?: readonly (RegExp | string)[]; readonly replacement?: unknown; readonly deep?: boolean; readonly maxDepth?: number; readonly maskStrings?: boolean | MaskPiiOptions;}Records
Section titled “Records”Functions
Section titled “Functions”cleanRecords
Cleaning pipeline for imported rows: normalize -> drop empty rows -> dedupe -> filter -> map.
function cleanRecords<T = Record<string, unknown>>(records: readonly Readonly<Record<string, unknown>>[], options?: CleanRecordsOptions<T>): T[];Example:
const rows = [{ name: ' Ada ', email: 'ada@example.com' }, { name: '', email: '' }, { name: 'Ada L.', email: 'ada@example.com' }];cleanRecords(rows, { normalize: { emptyToNull: true }, dedupeBy: (r) => r.email, keep: 'last' }); // => [{ name: 'Ada L.', email: 'ada@example.com' }]coerceRecordTypes
Converts record fields from strings to the listed types (CSV/form import). date yields a
Date from an ISO 8601 string; json parses JSON text (max 100 000 characters).
function coerceRecordTypes(record: Readonly<Record<string, unknown>>, types: Readonly<Record<string, CoerceType>>, options?: CoerceOptions): Record<string, unknown>;Example:
coerceRecordTypes({ n: '4', ok: 'yes' }, { n: 'integer', ok: 'boolean' }) // => { n: 4, ok: true }dedupeBy
Removes duplicates by a key (compared with SameValueZero), in O(n).
function dedupeBy<T>(records: readonly T[], keyFn: (record: T, index: number) => unknown, options?: DedupeOptions): T[];Example:
dedupeBy([{ id: 1, v: 'a' }, { id: 1, v: 'b' }], (r) => r.id, { keep: 'last' }) // => [{ id: 1, v: 'b' }]normalizeRecord
Normalizes one flat record: trims strings, strips invisible characters, optionally collapses
whitespace, maps empty strings to null, normalizes keys, and converts listed fields.
function normalizeRecord(record: Readonly<Record<string, unknown>>, rules?: NormalizeRecordRules): Record<string, unknown>;Example:
normalizeRecord({ ' Age ': ' 42 ', Name: ' Ada L ' }, { lowercaseKeys: true, collapseWhitespace: true, toNumber: ['age'] })// { age: 42, name: 'Ada L' }removeEmpty
Removes empty strings, empty arrays and empty plain objects (and, by default, nullish values).
With deep, containers that become empty after cleaning are removed too.
function removeEmpty<T>(value: T, options?: RemoveEmptyOptions): T;Example:
removeEmpty({ a: '', b: [], c: {}, d: 0 }) // => { d: 0 }removeNullish
Removes null and undefined properties and array items (top level unless deep).
function removeNullish<T>(value: T, options?: DeepOptions): T;Example:
removeNullish({ a: 1, b: null, c: undefined }) // => { a: 1 }trimStrings
Trims every string; with deep (default true) also inside nested plain objects and arrays.
function trimStrings<T>(value: T, options?: DeepOptions): T;Example:
trimStrings({ name: ' Ada ', tags: [' x '] }) // => { name: 'Ada', tags: ['x'] }Interfaces
Section titled “Interfaces”CleanRecordsOptions
interface CleanRecordsOptions<T> { readonly normalize?: NormalizeRecordRules | false; readonly dropEmpty?: boolean; readonly dedupeBy?: (record: Record<string, unknown>, index: number) => unknown; readonly keep?: 'first' | 'last'; readonly filter?: (record: Record<string, unknown>) => boolean; readonly map?: (record: Record<string, unknown>) => T;}CoerceOptions
interface CoerceOptions { readonly onInvalid?: 'null' | 'keep' | 'omit'; readonly strip?: boolean;}DedupeOptions
interface DedupeOptions { readonly keep?: 'first' | 'last';}DeepOptions
interface DeepOptions { readonly deep?: boolean; readonly maxDepth?: number;}NormalizeRecordRules
interface NormalizeRecordRules { readonly trim?: boolean; readonly collapseWhitespace?: boolean; readonly stripInvisible?: boolean; readonly emptyToNull?: boolean; readonly lowercaseKeys?: boolean; readonly trimKeys?: boolean; readonly toNumber?: readonly string[]; readonly toBoolean?: readonly string[]; readonly lowercase?: readonly string[];}RemoveEmptyOptions
interface RemoveEmptyOptions extends DeepOptions { readonly whitespace?: boolean; readonly nullish?: boolean;}CoerceType
Target types for {@link coerceRecordTypes}.
type CoerceType = 'string' | 'number' | 'integer' | 'boolean' | 'date' | 'json';Functions
Section titled “Functions”collapseWhitespace
Trims and collapses runs of whitespace (any Unicode \s) to a single space.
function collapseWhitespace(text: string): string;Example:
collapseWhitespace(' a \n b ') // => 'a b'normalizeEmail
Trims and lowercases the domain of an e-mail address. Returns undefined when the input
has no single @ separating non-empty parts or exceeds 254 characters.
function normalizeEmail(email: string, options?: NormalizeEmailOptions): string | undefined;Example:
normalizeEmail(' John.Doe@Example.COM ') // => 'John.Doe@example.com'normalizePhone
Normalizes a phone number to E.164 (+<digits>, 8-15 digits) by digits only; no carrier or
numbering-plan logic. Returns undefined when the result cannot be E.164.
function normalizePhone(input: string, options?: NormalizePhoneOptions): string | undefined;Example:
normalizePhone('(020) 7946 0958', { defaultCountryCode: '44' }) // => '+442079460958'normalizeUnicode
Unicode normalization (NFC default). Use NFKC to fold compatibility forms
(full-width letters, ligatures) before comparisons or search.
function normalizeUnicode(text: string, form?: 'NFC' | 'NFD' | 'NFKC' | 'NFKD'): string;Example:
normalizeUnicode('full', 'NFKC') // => 'full'safeFileName
Makes a string safe to use as a single file name on Windows, macOS and Linux: strips path
separators, control and invisible characters, reserved device names (CON, NUL, COM1…),
trailing dots/spaces, and leading dots; truncates to maxLength UTF-8 bytes keeping the
extension.
function safeFileName(name: string, options?: SafeFileNameOptions): string;Example:
safeFileName('../../etc/passwd') // => 'etc_passwd'Example:
safeFileName('CON.txt') // => '_CON.txt'sanitizeSearchQuery
Cleans free-text search input: strips control and invisible characters, folds compatibility forms, collapses whitespace, and limits length. Not an injection defense: always use parameterized queries.
function sanitizeSearchQuery(query: string, options?: SanitizeSearchQueryOptions): string;Example:
sanitizeSearchQuery(' hello world ') // => 'hello world'stripControl
Removes C0/C1 control characters. Tab, LF and CR are kept unless keepWhitespace is false.
function stripControl(text: string, options?: { readonly keepWhitespace?: boolean;}): string;Example:
stripControl('a\u0000b') // => 'ab'stripInvisible
Removes zero-width characters and bidi control characters (U+202A-202E, U+2066-2069), which can hide or reorder text (Trojan Source). Variation selectors are kept (emoji).
function stripInvisible(text: string, options?: StripInvisibleOptions): string;Example:
stripInvisible('ad' + String.fromCharCode(0x200b) + 'min') // => 'admin'truncateForLog
Serializes any value for logging and truncates it to maxLength code units (default 1000),
appending ...[+N] with the number of characters removed. Handles cycles and BigInt.
function truncateForLog(value: unknown, maxLength?: number): string;Example:
truncateForLog('x'.repeat(2000), 10) // => 'xx…[+1998]'Interfaces
Section titled “Interfaces”NormalizeEmailOptions
interface NormalizeEmailOptions { readonly lowercaseLocal?: boolean; readonly gmail?: boolean; readonly stripPlusTag?: boolean;}NormalizePhoneOptions
interface NormalizePhoneOptions { readonly defaultCountryCode?: string; readonly stripTrunkZero?: boolean;}SafeFileNameOptions
interface SafeFileNameOptions { readonly maxLength?: number; readonly replacement?: string; readonly allowLeadingDot?: boolean; readonly fallback?: string;}SanitizeSearchQueryOptions
interface SanitizeSearchQueryOptions { readonly maxLength?: number; readonly fold?: boolean;}StripInvisibleOptions
interface StripInvisibleOptions { readonly keepJoiners?: boolean;}