Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
# @suaveplan/data-masking - PII/sensitive data masking for objects and strings: field-level masking rules, nested object traversal, format-preserving masking, tokenization, and FNV-1a hashing.
|
||||
|
||||
## Summary
|
||||
|
||||
PII/sensitive data masking for objects and strings with field-level rules,
|
||||
nested traversal, eight masking strategies, and OpenTelemetry instrumentation.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [masker](./src/core/masker/object-masker.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [object-masker](./src/core/masker/object-masker.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [errors](./src/errors/data-masking-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [data-masking-error](./src/errors/data-masking-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [schemas](./src/schemas/mask-rule.schema.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [mask-rule.schema](./src/schemas/mask-rule.schema.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [credit-card](./src/strategies/credit-card.md) — The `credit-card` masking strategy produces the PCI-DSS-compatible truncation format for payment card numbers: last 4 d…
|
||||
- [email](./src/strategies/email.md) — The `email` masking strategy anonymises email addresses while keeping the output immediately recognisable as an email a…
|
||||
- [full](./src/strategies/full.md) — The `full` masking strategy completely replaces an input string with a configurable replacement sentinel. It is the mos…
|
||||
- [hash](./src/strategies/hash.md) — The `hash` masking strategy produces a deterministic, fixed-length 8-character hexadecimal token from any input string…
|
||||
- [partial](./src/strategies/partial.md) — The `partial` masking strategy preserves a configurable number of characters at the start and end of an input string wh…
|
||||
- [phone](./src/strategies/phone.md) — The `phone` masking strategy anonymises phone numbers while preserving their formatting separators, making masked value…
|
||||
- [preserve-format](./src/strategies/preserve-format.md) — The `preserve-format` masking strategy replaces every alphanumeric character with a placeholder of the same character c…
|
||||
- [tokenize](./src/strategies/tokenize.md) — The `tokenize` masking strategy replaces a sensitive value with a structured opaque token of the form `TOKEN_<STRATEGY>…
|
||||
- [types](./src/types/mask-types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [mask-types](./src/types/mask-types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [utils](./src/utils/path-matcher.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [path-matcher](./src/utils/path-matcher.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
<!-- end:auto-docs --> - [object-masker](./src/core/masker/object-masker.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/data-masking-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [data-masking-error](./src/errors/data-masking-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/mask-rule.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [mask-rule.schema](./src/schemas/mask-rule.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [credit-card](./src/strategies/credit-card.md) — The `credit-card` masking strategy produces the PCI-DSS-compatible truncation format for payment card numbers: last 4 d…
|
||||
- [email](./src/strategies/email.md) — The `email` masking strategy anonymises email addresses while keeping the output immediately recognisable as an email a…
|
||||
- [full](./src/strategies/full.md) — The `full` masking strategy completely replaces an input string with a configurable replacement sentinel. It is the mos…
|
||||
- [hash](./src/strategies/hash.md) — The `hash` masking strategy produces a deterministic, fixed-length 8-character hexadecimal token from any input string…
|
||||
- [partial](./src/strategies/partial.md) — The `partial` masking strategy preserves a configurable number of characters at the start and end of an input string wh…
|
||||
- [phone](./src/strategies/phone.md) — The `phone` masking strategy anonymises phone numbers while preserving their formatting separators, making masked value…
|
||||
- [preserve-format](./src/strategies/preserve-format.md) — The `preserve-format` masking strategy replaces every alphanumeric character with a placeholder of the same character c…
|
||||
- [tokenize](./src/strategies/tokenize.md) — The `tokenize` masking strategy replaces a sensitive value with a structured opaque token of the form `TOKEN_<STRATEGY>…
|
||||
- [types](./src/types/mask-types.md) — <!-- begin:auto-docs --> _(no submodules)_- [mask-types](./src/types/mask-types.md) — <!-- begin:auto-docs --> _(no submodules)_- [utils](./src/utils/path-matcher.md) — <!-- begin:auto-docs --> _(no submodules)_- [path-matcher](./src/utils/path-matcher.md) — <!-- begin:auto-docs --> _(no submodules)_- [object-masker](./src/core/masker/object-masker.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/data-masking-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [data-masking-error](./src/errors/data-masking-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/mask-rule.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [mask-rule.schema](./src/schemas/mask-rule.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [credit-card](./src/strategies/credit-card.md) — The `credit-card` masking strategy produces the PCI-DSS-compatible truncation format for payment card numbers: last 4 d…
|
||||
- [email](./src/strategies/email.md) — The `email` masking strategy anonymises email addresses while keeping the output immediately recognisable as an email a…
|
||||
- [full](./src/strategies/full.md) — The `full` masking strategy completely replaces an input string with a configurable replacement sentinel. It is the mos…
|
||||
- [hash](./src/strategies/hash.md) — The `hash` masking strategy produces a deterministic, fixed-length 8-character hexadecimal token from any input string…
|
||||
- [partial](./src/strategies/partial.md) — The `partial` masking strategy preserves a configurable number of characters at the start and end of an input string wh…
|
||||
- [phone](./src/strategies/phone.md) — The `phone` masking strategy anonymises phone numbers while preserving their formatting separators, making masked value…
|
||||
- [preserve-format](./src/strategies/preserve-format.md) — The `preserve-format` masking strategy replaces every alphanumeric character with a placeholder of the same character c…
|
||||
- [tokenize](./src/strategies/tokenize.md) — The `tokenize` masking strategy replaces a sensitive value with a structured opaque token of the form `TOKEN_<STRATEGY>…
|
||||
- [types](./src/types/mask-types.md) — <!-- begin:auto-docs --> _(no submodules)_- [mask-types](./src/types/mask-types.md) — <!-- begin:auto-docs --> _(no submodules)_- [utils](./src/utils/path-matcher.md) — <!-- begin:auto-docs --> _(no submodules)_- [path-matcher](./src/utils/path-matcher.md) — <!-- begin:auto-docs --> _(no submodules)_- [object-masker](./src/core/masker/object-masker.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/data-masking-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [data-masking-error](./src/errors/data-masking-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/mask-rule.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [mask-rule.schema](./src/schemas/mask-rule.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [credit-card](./src/strategies/credit-card.md) — The `credit-card` masking strategy produces the PCI-DSS-compatible truncation format for payment card numbers: last 4 d…
|
||||
- [email](./src/strategies/email.md) — The `email` masking strategy anonymises email addresses while keeping the output immediately recognisable as an email a…
|
||||
- [full](./src/strategies/full.md) — The `full` masking strategy completely replaces an input string with a configurable replacement sentinel. It is the mos…
|
||||
- [hash](./src/strategies/hash.md) — The `hash` masking strategy produces a deterministic, fixed-length 8-character hexadecimal token from any input string…
|
||||
- [partial](./src/strategies/partial.md) — The `partial` masking strategy preserves a configurable number of characters at the start and end of an input string wh…
|
||||
- [phone](./src/strategies/phone.md) — The `phone` masking strategy anonymises phone numbers while preserving their formatting separators, making masked value…
|
||||
- [preserve-format](./src/strategies/preserve-format.md) — The `preserve-format` masking strategy replaces every alphanumeric character with a placeholder of the same character c…
|
||||
- [tokenize](./src/strategies/tokenize.md) — The `tokenize` masking strategy replaces a sensitive value with a structured opaque token of the form `TOKEN_<STRATEGY>…
|
||||
- [types](./src/types/mask-types.md) — <!-- begin:auto-docs --> _(no submodules)_- [mask-types](./src/types/mask-types.md) — <!-- begin:auto-docs --> _(no submodules)_- [utils](./src/utils/path-matcher.md) — <!-- begin:auto-docs --> _(no submodules)_- [path-matcher](./src/utils/path-matcher.md) — <!-- begin:auto-docs --> _(no submodules)_- [object-masker](./src/core/masker/object-masker.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/data-masking-error.md) — <!-- begin:auto-docs --> _(no submodules)_ - [data-masking-error](./src/errors/data-masking-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/mask-rule.schema.md) — <!-- begin:auto-docs --> _(no submodules)_ - [mask-rule.schema](./src/schemas/mask-rule.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [credit-card](./src/strategies/credit-card.md) — The `credit-card` masking strategy produces the PCI-DSS-compatible truncation format for payment card numbers: last 4 d…
|
||||
- [email](./src/strategies/email.md) — The `email` masking strategy anonymises email addresses while keeping the output immediately recognisable as an email a…
|
||||
- [full](./src/strategies/full.md) — The `full` masking strategy completely replaces an input string with a configurable replacement sentinel. It is the mos…
|
||||
- [hash](./src/strategies/hash.md) — The `hash` masking strategy produces a deterministic, fixed-length 8-character hexadecimal token from any input string…
|
||||
- [partial](./src/strategies/partial.md) — The `partial` masking strategy preserves a configurable number of characters at the start and end of an input string wh…
|
||||
- [phone](./src/strategies/phone.md) — The `phone` masking strategy anonymises phone numbers while preserving their formatting separators, making masked value…
|
||||
- [preserve-format](./src/strategies/preserve-format.md) — The `preserve-format` masking strategy replaces every alphanumeric character with a placeholder of the same character c…
|
||||
- [tokenize](./src/strategies/tokenize.md) — The `tokenize` masking strategy replaces a sensitive value with a structured opaque token of the form `TOKEN_<STRATEGY>…
|
||||
- [types](./src/types/mask-types.md) — <!-- begin:auto-docs --> _(no submodules)_ - [mask-types](./src/types/mask-types.md) — <!-- begin:auto-docs --> _(no submodules)_- [utils](./src/utils/path-matcher.md) — <!-- begin:auto-docs --> _(no submodules)_ - [path-matcher](./src/utils/path-matcher.md) — <!-- begin:auto-docs --> _(no submodules)_
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/data-masking
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import { maskObject } from '@suaveplan/data-masking';
|
||||
|
||||
const result = maskObject(
|
||||
{
|
||||
user: { name: 'John Doe', email: 'john@example.com', ssn: '123-45-6789' },
|
||||
payment: { cardNumber: '4111-1111-1111-1111', cvv: '123' },
|
||||
},
|
||||
{
|
||||
rules: [
|
||||
{ fields: ['user.email'], strategy: 'email' },
|
||||
{ fields: ['user.ssn', 'payment.cvv'], strategy: 'full' },
|
||||
{ fields: ['payment.cardNumber'], strategy: 'credit-card' },
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
console.log(result.masked.user.email); // 'j***@e***.com'
|
||||
console.log(result.masked.payment.cardNumber); // '****-****-****-1111'
|
||||
console.log(result.fieldsAffected); // ['payment.cardNumber', 'payment.cvv', 'user.email', 'user.ssn']
|
||||
```
|
||||
|
||||
## Strategies
|
||||
|
||||
| Strategy | Description | Example output |
|
||||
|-------------------|------------------------------------------|-------------------------|
|
||||
| `full` | Replace entire value with `***` | `***` |
|
||||
| `partial` | Keep first/last N chars, mask middle | `sk_***...****789` |
|
||||
| `email` | Format-preserving email mask | `j***@e***.com` |
|
||||
| `phone` | Keep last 4 digits | `***-***-1234` |
|
||||
| `credit-card` | Keep last 4 digits | `****-****-****-1234` |
|
||||
| `hash` | FNV-1a 32-bit hex (deterministic) | `4f9f2cab` |
|
||||
| `tokenize` | Opaque token (not reversible) | `TOKEN_TOKENIZE_0` |
|
||||
| `preserve-format` | Replace alnum with type placeholder | `XXXXX000` |
|
||||
|
||||
## Field Path Patterns
|
||||
|
||||
Rules use dot-notation paths with optional glob wildcards:
|
||||
|
||||
```ts
|
||||
{ fields: ['user.email'] } // exact match
|
||||
{ fields: ['user.*'] } // single-segment wildcard
|
||||
{ fields: ['user.**'] } // multi-segment wildcard (any depth)
|
||||
{ fields: ['**.email'] } // any path ending in .email
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
| Subpath | Contents |
|
||||
|---------------------|--------------------------------------------|
|
||||
| `@suaveplan/data-masking` | Main exports (maskObject, maskString, maskValue, types, schemas) |
|
||||
| `@suaveplan/data-masking/masker` | [object-masker](src/core/masker/object-masker.md) — core traversal engine |
|
||||
| `@suaveplan/data-masking/strategies` | [full](src/strategies/full.md), [partial](src/strategies/partial.md), [email](src/strategies/email.md), [phone](src/strategies/phone.md), [credit-card](src/strategies/credit-card.md), [hash](src/strategies/hash.md), [tokenize](src/strategies/tokenize.md), [preserve-format](src/strategies/preserve-format.md) |
|
||||
| `@suaveplan/data-masking/errors` | [DataMaskingError](src/errors/data-masking-error.md), `DATA_MASKING_CODES` |
|
||||
| `@suaveplan/data-masking/schemas` | Zod schemas for `MaskRule`, `MaskOptions` |
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,128 @@
|
||||
# object-masker
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The `object-masker` module is the core engine of `@suaveplan/data-masking`. It
|
||||
provides three exports: `maskValue` for dispatching a masking strategy to a
|
||||
single string, `maskObject` for traversing and masking a nested plain-object
|
||||
graph, and `maskString` for detecting and masking embedded PII patterns inside
|
||||
free-form text. All traversal is non-destructive — the input is never mutated;
|
||||
a deep structural clone is produced instead.
|
||||
|
||||
OpenTelemetry instrumentation records a counter per strategy (`data_masking.mask.total`)
|
||||
and a histogram of overall `maskObject` call duration (`data_masking.mask.duration_ms`),
|
||||
enabling SREs to monitor masking throughput and latency in production.
|
||||
|
||||
## Features
|
||||
|
||||
- `maskValue`: uniform dispatch to all eight masking strategies.
|
||||
- `maskObject`: deep-clone traversal with `deep` and `maskArrays` controls.
|
||||
- `maskString`: regex-based PII detection for email, phone, and credit-card
|
||||
patterns embedded in free-form strings.
|
||||
- Non-mutating: original object/string always preserved.
|
||||
- OTel counter + histogram on every operation.
|
||||
- Sorted `fieldsAffected` list for deterministic output.
|
||||
- `DataMaskingError` on strategy dispatch failure.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { maskObject } from '@suaveplan/data-masking/masker';
|
||||
|
||||
const result = maskObject(
|
||||
{ user: { name: 'Alice', email: 'alice@example.com' } },
|
||||
{ rules: [{ fields: ['user.email'], strategy: 'email' }] },
|
||||
);
|
||||
// result.masked.user.email → 'a***@e***.com'
|
||||
// result.fieldsAffected → ['user.email']
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { maskObject, maskString, maskValue } from '@suaveplan/data-masking/masker';
|
||||
|
||||
// Full object masking with wildcards
|
||||
const data = {
|
||||
user: { name: 'John Doe', email: 'john@example.com', ssn: '123-45-6789' },
|
||||
payment: { cardNumber: '4111-1111-1111-1111', cvv: '123' },
|
||||
};
|
||||
|
||||
const result = maskObject(data, {
|
||||
rules: [
|
||||
{ fields: ['user.email'], strategy: 'email' },
|
||||
{ fields: ['user.ssn', 'payment.cvv'], strategy: 'full' },
|
||||
{ fields: ['payment.cardNumber'], strategy: 'credit-card' },
|
||||
],
|
||||
deep: true,
|
||||
maskArrays: true,
|
||||
});
|
||||
// result.masked.user.email → 'j***@e***.com'
|
||||
// result.masked.user.ssn → '***'
|
||||
// result.masked.payment.cardNumber → '****-****-****-1111'
|
||||
// result.masked.payment.cvv → '***'
|
||||
// result.fieldsAffected.length → 4
|
||||
|
||||
// Free-form string masking
|
||||
const log = 'User john@example.com called from +1-555-123-4567';
|
||||
const masked = maskString(log, [
|
||||
{ fields: ['*'], strategy: 'email' },
|
||||
{ fields: ['*'], strategy: 'phone' },
|
||||
]);
|
||||
// masked → 'User j***@e***.com called from *-***-***-4567'
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `maskValue(value, strategy, rule)`
|
||||
|
||||
```ts
|
||||
function maskValue(value: string, strategy: MaskStrategy, rule: MaskRule): string;
|
||||
```
|
||||
|
||||
Dispatches `value` to the named strategy. Wraps any thrown error in a
|
||||
`DataMaskingError` with code `DATA_MASKING.MASK_FAILED`.
|
||||
|
||||
### `maskObject<T>(obj, options)`
|
||||
|
||||
```ts
|
||||
function maskObject<T extends Record<string, unknown>>(
|
||||
obj: T,
|
||||
options: MaskOptions,
|
||||
): MaskResult<T>;
|
||||
```
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|------------------|---------------|---------|------------------------------------|
|
||||
| `obj` | `T` | — | Source plain object. |
|
||||
| `options.rules` | `MaskRule[]` | — | Ordered masking rules. |
|
||||
| `options.deep` | `boolean` | `true` | Recurse into nested objects. |
|
||||
| `options.maskArrays` | `boolean` | `true` | Mask array element values. |
|
||||
|
||||
**Returns** `MaskResult<T>` with `masked` clone and sorted `fieldsAffected`.
|
||||
|
||||
### `maskString(str, rules)`
|
||||
|
||||
```ts
|
||||
function maskString(str: string, rules: MaskRule[]): string;
|
||||
```
|
||||
|
||||
Scans `str` for embedded credit-card, email, and phone patterns and replaces
|
||||
each detected match using the first rule with a matching strategy.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Object traversal uses plain `for...of` over `Object.keys` to avoid
|
||||
prototype-chain pollution; `isPlainObject` guards against class instances,
|
||||
`Date`, `RegExp`, etc., which are returned as-is.
|
||||
- Array element paths use bracket notation `field[0]` to distinguish them
|
||||
from nested object paths in the `fieldsAffected` list.
|
||||
- The OTel `Meter` is obtained lazily on first call and cached module-wide to
|
||||
avoid re-creating metric instruments on each invocation.
|
||||
- `performance.now()` is used for duration measurement (available in Node.js
|
||||
16+ and Bun natively; no polyfill required for the suaveplan target).
|
||||
- `maskString` applies rules in category order (credit-card → email → phone)
|
||||
to avoid double-masking where patterns overlap.
|
||||
@@ -0,0 +1,120 @@
|
||||
# data-masking-error
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides a stable error-code registry and a typed `DataMaskingError` class for
|
||||
all failures that originate inside `@suaveplan/data-masking`. Keeping errors
|
||||
isolated to a single file makes it easy for callers to perform narrow
|
||||
`instanceof` checks and to discriminate specific failure modes via the `code`
|
||||
property without resorting to fragile message-string matching.
|
||||
|
||||
The package is intentionally a leaf node — it carries zero `@suaveplan/*`
|
||||
runtime dependencies — so `DataMaskingError` extends the native `Error` class
|
||||
directly rather than `SuaveplanError`.
|
||||
|
||||
## Features
|
||||
|
||||
- `DATA_MASKING_CODES` constant object: two stable string codes exported as
|
||||
`as const` for full TypeScript narrowing.
|
||||
- `DataMaskingCode` union type derived from `DATA_MASKING_CODES`.
|
||||
- `DataMaskingError` class: extends `Error`, sets `name = "DataMaskingError"`,
|
||||
exposes a readonly `code: DataMaskingCode` property.
|
||||
- Tree-shakeable — no side-effects, compatible with `"sideEffects": false`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
DataMaskingError,
|
||||
DATA_MASKING_CODES,
|
||||
} from '@suaveplan/data-masking/errors';
|
||||
|
||||
try {
|
||||
throw new DataMaskingError(
|
||||
'Strategy "unknown" is not recognised',
|
||||
DATA_MASKING_CODES.INVALID_RULE,
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof DataMaskingError) {
|
||||
console.error(err.code); // 'DATA_MASKING.INVALID_RULE'
|
||||
console.error(err.name); // 'DataMaskingError'
|
||||
console.error(err.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
DataMaskingError,
|
||||
DATA_MASKING_CODES,
|
||||
type DataMaskingCode,
|
||||
} from '@suaveplan/data-masking/errors';
|
||||
|
||||
function handleMaskingError(err: unknown): void {
|
||||
if (!(err instanceof DataMaskingError)) throw err;
|
||||
|
||||
const actions: Record<DataMaskingCode, () => void> = {
|
||||
[DATA_MASKING_CODES.INVALID_RULE]: () =>
|
||||
console.warn('Rule validation failed — check your MaskRule definitions'),
|
||||
[DATA_MASKING_CODES.MASK_FAILED]: () =>
|
||||
console.error('Masking strategy threw during execution'),
|
||||
};
|
||||
|
||||
actions[err.code]?.();
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `DATA_MASKING_CODES`
|
||||
|
||||
```ts
|
||||
const DATA_MASKING_CODES: {
|
||||
readonly INVALID_RULE: 'DATA_MASKING.INVALID_RULE';
|
||||
readonly MASK_FAILED: 'DATA_MASKING.MASK_FAILED';
|
||||
};
|
||||
```
|
||||
|
||||
Stable string constants that identify each error category. Values are
|
||||
intentionally namespaced with `DATA_MASKING.` to avoid collisions in shared
|
||||
error-monitoring pipelines.
|
||||
|
||||
### `DataMaskingCode`
|
||||
|
||||
```ts
|
||||
type DataMaskingCode = 'DATA_MASKING.INVALID_RULE' | 'DATA_MASKING.MASK_FAILED';
|
||||
```
|
||||
|
||||
Union type derived from `DATA_MASKING_CODES`. Use this for exhaustive `switch`
|
||||
statements or discriminated-union narrowing.
|
||||
|
||||
### `DataMaskingError`
|
||||
|
||||
```ts
|
||||
class DataMaskingError extends Error {
|
||||
readonly code: DataMaskingCode;
|
||||
constructor(message: string, code: DataMaskingCode): DataMaskingError;
|
||||
}
|
||||
```
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|-------------------|----------------------------------------|
|
||||
| `name` | `string` | Always `"DataMaskingError"`. |
|
||||
| `code` | `DataMaskingCode` | The stable machine-readable error code.|
|
||||
| `message`| `string` | Human-readable description of failure. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The `name` property is set in the constructor body (not via a static field)
|
||||
to ensure correct behaviour after `Object.setPrototypeOf` in transpiled ES5
|
||||
environments, though suaveplan targets ES2022+ natively.
|
||||
- No stack-trace manipulation is performed — the default `Error` constructor
|
||||
captures a stack trace automatically in V8 and Bun runtimes.
|
||||
- `DATA_MASKING_CODES` is declared `as const` so TypeScript infers the narrow
|
||||
string-literal type for each member; this enables exhaustive checks on
|
||||
`DataMaskingCode` without a separate enum.
|
||||
@@ -0,0 +1,122 @@
|
||||
# mask-rule.schema
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides Zod schemas for all public configuration types in
|
||||
`@suaveplan/data-masking`. Runtime validation of `MaskRule` and `MaskOptions`
|
||||
objects is important when rules originate from external sources such as API
|
||||
request bodies, configuration files, or database records. Using these schemas
|
||||
before calling `maskObject` prevents silent mis-configurations — for example, a
|
||||
rule with an empty `fields` array or an unrecognised `strategy` string — from
|
||||
causing confusing runtime behaviour.
|
||||
|
||||
Exporting the schemas as a dedicated subpath (`./schemas`) allows tree-shaking
|
||||
in production bundles that never parse external rule definitions.
|
||||
|
||||
## Features
|
||||
|
||||
- `MaskStrategySchema` — exhaustive `z.enum` of all eight strategy strings.
|
||||
- `MaskRuleSchema` — validates `fields` (non-empty array of non-empty strings),
|
||||
`strategy`, and optional `keepLast`, `keepFirst`, `replacement` fields.
|
||||
- `MaskOptionsSchema` — validates `rules` (at least one required), `deep`,
|
||||
`maskArrays`.
|
||||
- Input types (`MaskStrategyInput`, `MaskRuleInput`, `MaskOptionsInput`)
|
||||
exported for use in form and API validation pipelines.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { MaskRuleSchema } from '@suaveplan/data-masking/schemas';
|
||||
|
||||
const parsed = MaskRuleSchema.safeParse({
|
||||
fields: ['user.email'],
|
||||
strategy: 'email',
|
||||
});
|
||||
|
||||
if (!parsed.success) {
|
||||
console.error(parsed.error.issues);
|
||||
} else {
|
||||
console.log(parsed.data.strategy); // 'email'
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { MaskOptionsSchema } from '@suaveplan/data-masking/schemas';
|
||||
import { maskObject } from '@suaveplan/data-masking';
|
||||
|
||||
function safeMaskObject(
|
||||
data: Record<string, unknown>,
|
||||
rawOptions: unknown,
|
||||
) {
|
||||
const options = MaskOptionsSchema.parse(rawOptions); // throws ZodError on invalid input
|
||||
return maskObject(data, options);
|
||||
}
|
||||
|
||||
// Use from an API request body
|
||||
const result = safeMaskObject(userData, req.body.maskOptions);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `MaskStrategySchema`
|
||||
|
||||
```ts
|
||||
const MaskStrategySchema: z.ZodEnum<[
|
||||
'full', 'partial', 'email', 'phone',
|
||||
'credit-card', 'hash', 'tokenize', 'preserve-format'
|
||||
]>;
|
||||
```
|
||||
|
||||
Validates that a string is one of the eight supported masking strategy
|
||||
identifiers.
|
||||
|
||||
### `MaskRuleSchema`
|
||||
|
||||
```ts
|
||||
const MaskRuleSchema: z.ZodObject<{
|
||||
fields: z.ZodArray<z.ZodString>; // min 1 item, each min 1 char
|
||||
strategy: typeof MaskStrategySchema;
|
||||
keepLast?: z.ZodNumber; // int, non-negative
|
||||
keepFirst?: z.ZodNumber; // int, non-negative
|
||||
replacement?: z.ZodString;
|
||||
}>;
|
||||
```
|
||||
|
||||
### `MaskOptionsSchema`
|
||||
|
||||
```ts
|
||||
const MaskOptionsSchema: z.ZodObject<{
|
||||
rules: z.ZodArray<typeof MaskRuleSchema>; // min 1 item
|
||||
deep?: z.ZodBoolean;
|
||||
maskArrays?: z.ZodBoolean;
|
||||
}>;
|
||||
```
|
||||
|
||||
### Input types
|
||||
|
||||
```ts
|
||||
type MaskStrategyInput = z.input<typeof MaskStrategySchema>;
|
||||
type MaskRuleInput = z.input<typeof MaskRuleSchema>;
|
||||
type MaskOptionsInput = z.input<typeof MaskOptionsSchema>;
|
||||
```
|
||||
|
||||
These are the raw (un-coerced) input types, useful for TypeScript annotations
|
||||
in form handlers and API route definitions before calling `.parse()`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `z.number().int().nonnegative()` is used for `keepLast` and `keepFirst` to
|
||||
reject fractional or negative values that would produce nonsensical results
|
||||
in the `partial` strategy.
|
||||
- `z.array(...).min(1)` on both `fields` and `rules` prevents silent no-op
|
||||
configurations — a rule with no fields or an options object with no rules
|
||||
would be a programming error.
|
||||
- The schema file is excluded from coverage measurement (via `*.schema.ts`
|
||||
exclusion in `vitest.config.ts`) because it is declarative Zod schema
|
||||
definition code; meaningful coverage comes from integration tests of the
|
||||
schemas via `safeParse`/`parse`, not line-by-line execution tracing.
|
||||
@@ -0,0 +1,82 @@
|
||||
# credit-card strategy
|
||||
|
||||
## Purpose
|
||||
|
||||
The `credit-card` masking strategy produces the PCI-DSS-compatible truncation
|
||||
format for payment card numbers: last 4 digits visible, all preceding digit
|
||||
positions replaced with `*`, formatting separators preserved. This aligns
|
||||
with the PCI DSS 4.0 requirement that primary account numbers (PANs) must be
|
||||
rendered unreadable in logs and displays, while retaining just enough
|
||||
information for card identification and customer service operations.
|
||||
|
||||
## Features
|
||||
|
||||
- Last-4-digit preservation regardless of separator style.
|
||||
- Hyphens, spaces, and other non-digit characters pass through unchanged.
|
||||
- Handles formatted (`4111-1111-1111-1111`) and raw (`4111111111111111`) inputs.
|
||||
- No regex capture groups — single linear pass, O(n) complexity.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { maskCreditCard } from '@suaveplan/data-masking/strategies/credit-card';
|
||||
|
||||
const masked = maskCreditCard('4111-1111-1111-1111', {
|
||||
fields: ['cardNumber'],
|
||||
strategy: 'credit-card',
|
||||
});
|
||||
// masked → '****-****-****-1111'
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { maskCreditCard } from '@suaveplan/data-masking/strategies/credit-card';
|
||||
|
||||
// No separators
|
||||
maskCreditCard('5500005555555559', { fields: ['pan'], strategy: 'credit-card' });
|
||||
// → '************5559'
|
||||
|
||||
// Space-separated format
|
||||
maskCreditCard('4111 1111 1111 1111', { fields: ['pan'], strategy: 'credit-card' });
|
||||
// → '**** **** **** 1111'
|
||||
|
||||
// Amex 15-digit
|
||||
maskCreditCard('3714 496353 98431', { fields: ['pan'], strategy: 'credit-card' });
|
||||
// → '**** ****** *8431'
|
||||
|
||||
// No digits at all → passthrough
|
||||
maskCreditCard('CARD-ON-FILE', { fields: ['pan'], strategy: 'credit-card' });
|
||||
// → 'CARD-ON-FILE'
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `maskCreditCard(value, rule)`
|
||||
|
||||
```ts
|
||||
function maskCreditCard(value: string, rule: MaskRule): string;
|
||||
```
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------------|-----------------------------------------------------------|
|
||||
| `value` | `string` | The original card number in any format. |
|
||||
| `rule` | `MaskRule` | Driving rule (no fields consumed by credit-card strategy).|
|
||||
|
||||
**Returns** the masked card number with last 4 digits preserved and all other
|
||||
digit positions replaced by `*`.
|
||||
|
||||
**Does not throw.** Returns the original value unchanged when no digit
|
||||
characters are found.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The algorithm is identical in structure to the `phone` strategy: extract all
|
||||
digits to find the `keepFrom` threshold, then rebuild the string in a single
|
||||
pass. The two strategies are kept as separate files to allow divergent
|
||||
evolution (e.g. future BIN-preservation or Luhn-check validation in the
|
||||
credit-card variant).
|
||||
- `Math.max(0, digits.length - 4)` ensures cards with 4 or fewer digits are
|
||||
returned unchanged, consistent with the principle of least surprise.
|
||||
- The `rule` parameter is silently voided to satisfy the uniform strategy
|
||||
interface; no `MaskRule` fields are read.
|
||||
@@ -0,0 +1,75 @@
|
||||
# email strategy
|
||||
|
||||
## Purpose
|
||||
|
||||
The `email` masking strategy anonymises email addresses while keeping the
|
||||
output immediately recognisable as an email address. Retaining the first
|
||||
character of the local part and of the domain name — plus the full TLD — gives
|
||||
operations and support teams just enough context to identify a domain category
|
||||
(e.g. `.com`, `.io`, `.co.uk`) and the rough shape of an address without
|
||||
exposing personally identifiable information. This is the standard form used
|
||||
in GDPR-compliant audit logs and support ticketing systems.
|
||||
|
||||
## Features
|
||||
|
||||
- Preserves first character of the local-part (`u***@...`).
|
||||
- Preserves first character of the domain name (`...@d***...`).
|
||||
- Preserves the TLD verbatim (`.com`, `.co.uk`, etc.).
|
||||
- Gracefully handles malformed inputs (no `@` sign) by returning `"***"`.
|
||||
- No external dependencies — pure string slicing.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { maskEmail } from '@suaveplan/data-masking/strategies/email';
|
||||
|
||||
const masked = maskEmail('john.doe@example.com', { fields: ['email'], strategy: 'email' });
|
||||
// masked → 'j***@e***.com'
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { maskEmail } from '@suaveplan/data-masking/strategies/email';
|
||||
|
||||
// Long TLD preserved
|
||||
maskEmail('alice@subdomain.company.co.uk', { fields: ['email'], strategy: 'email' });
|
||||
// → 'a***@s***.co.uk'
|
||||
|
||||
// No @ in input → fallback
|
||||
maskEmail('not-an-email', { fields: ['email'], strategy: 'email' });
|
||||
// → '***'
|
||||
|
||||
// Single-char local part
|
||||
maskEmail('x@y.io', { fields: ['email'], strategy: 'email' });
|
||||
// → 'x***@y***.io'
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `maskEmail(value, rule)`
|
||||
|
||||
```ts
|
||||
function maskEmail(value: string, rule: MaskRule): string;
|
||||
```
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------------|---------------------------------------------------------|
|
||||
| `value` | `string` | The original email address. |
|
||||
| `rule` | `MaskRule` | Driving rule (no fields are read from it for email). |
|
||||
|
||||
**Returns** a string in the form `u***@d***.tld`, or `"***"` when `value`
|
||||
contains no `@` character.
|
||||
|
||||
**Does not throw.**
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The TLD is identified as everything from the **last** `.` in the domain
|
||||
portion, which handles multi-part TLDs like `.co.uk` correctly.
|
||||
- If the domain portion contains no `.` (uncommon in practice) the TLD is
|
||||
treated as empty and the output becomes `d***`.
|
||||
- The `rule` parameter is accepted to satisfy the uniform strategy interface
|
||||
but none of its fields are consumed by the email strategy.
|
||||
- Masking is applied to the parsed components, not via regex substitution,
|
||||
which avoids regex-denial-of-service risks on adversarially crafted inputs.
|
||||
@@ -0,0 +1,67 @@
|
||||
# full strategy
|
||||
|
||||
## Purpose
|
||||
|
||||
The `full` masking strategy completely replaces an input string with a
|
||||
configurable replacement sentinel. It is the most aggressive masking mode
|
||||
available in `@suaveplan/data-masking`: no characters from the original value
|
||||
are preserved, so the output reveals nothing about length, structure, or
|
||||
content. This strategy is appropriate for highly sensitive fields such as
|
||||
passwords, SSNs, or security codes where even partial exposure is unacceptable.
|
||||
|
||||
## Features
|
||||
|
||||
- Zero information leakage — original value is discarded entirely.
|
||||
- Customisable replacement via `MaskRule.replacement`; defaults to `"***"`.
|
||||
- Constant-time execution (no iteration over the input string).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { maskFull } from '@suaveplan/data-masking/strategies/full';
|
||||
|
||||
const masked = maskFull('super-secret-password', { fields: ['password'], strategy: 'full' });
|
||||
// masked → '***'
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { maskFull } from '@suaveplan/data-masking/strategies/full';
|
||||
|
||||
// Custom replacement sentinel
|
||||
const masked = maskFull('123-45-6789', {
|
||||
fields: ['ssn'],
|
||||
strategy: 'full',
|
||||
replacement: '[REDACTED]',
|
||||
});
|
||||
// masked → '[REDACTED]'
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `maskFull(value, rule)`
|
||||
|
||||
```ts
|
||||
function maskFull(value: string, rule: MaskRule): string;
|
||||
```
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------------|---------------------------------------------------------|
|
||||
| `value` | `string` | The original value to mask (immediately discarded). |
|
||||
| `rule` | `MaskRule` | Driving rule; `rule.replacement` used when provided. |
|
||||
|
||||
**Returns** `rule.replacement` if set, otherwise `"***"`.
|
||||
|
||||
**Does not throw.**
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The `value` parameter is accepted but immediately voided to satisfy the
|
||||
uniform `mask(value, rule): string` strategy interface.
|
||||
- `rule.replacement` may be any string, including the empty string `""`.
|
||||
Callers that want a custom sentinel (e.g. `"[REDACTED]"`, `"N/A"`) set this
|
||||
field on their `MaskRule`.
|
||||
- Unlike `partial`, `email`, or `preserve-format` strategies, the `full`
|
||||
strategy cannot be reversed or detected — it is appropriate for
|
||||
security-critical fields that must not leak data to downstream systems.
|
||||
@@ -0,0 +1,79 @@
|
||||
# hash strategy
|
||||
|
||||
## Purpose
|
||||
|
||||
The `hash` masking strategy produces a deterministic, fixed-length 8-character
|
||||
hexadecimal token from any input string using the FNV-1a 32-bit hash algorithm.
|
||||
Unlike the `full` or `tokenize` strategies, the `hash` strategy preserves the
|
||||
property that identical input values produce identical output tokens, which
|
||||
enables de-duplication and correlation in anonymised log streams without
|
||||
exposing the original data. This is valuable when troubleshooting session
|
||||
issues where the same user ID must be traceable across log entries.
|
||||
|
||||
**Security caveat:** FNV-1a is not cryptographically secure. It must not be
|
||||
used for password hashing, MAC generation, or any context where collision
|
||||
resistance is a security requirement. For those use cases, use
|
||||
`@suaveplan/crypto-hash` with `sha256` or `argon2id`.
|
||||
|
||||
## Features
|
||||
|
||||
- Deterministic: same input → same 8-char hex output, every time.
|
||||
- Fixed-length output (8 hex digits = 32-bit hash).
|
||||
- Zero external dependencies — pure JavaScript arithmetic via `Math.imul`.
|
||||
- Fast: O(n) in string length with very low constant factor.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { maskHash } from '@suaveplan/data-masking/strategies/hash';
|
||||
|
||||
const masked = maskHash('user_12345', { fields: ['userId'], strategy: 'hash' });
|
||||
// masked → e.g. '2c7b4f8a' (deterministic for this input)
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { maskHash } from '@suaveplan/data-masking/strategies/hash';
|
||||
|
||||
// Identical inputs → identical outputs (log correlation)
|
||||
const a = maskHash('john@example.com', { fields: ['email'], strategy: 'hash' });
|
||||
const b = maskHash('john@example.com', { fields: ['email'], strategy: 'hash' });
|
||||
// a === b → true
|
||||
|
||||
// Different inputs → (almost certainly) different outputs
|
||||
const c = maskHash('jane@example.com', { fields: ['email'], strategy: 'hash' });
|
||||
// a === c → false (very likely)
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `maskHash(value, rule)`
|
||||
|
||||
```ts
|
||||
function maskHash(value: string, rule: MaskRule): string;
|
||||
```
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------------|-----------------------------------------------------|
|
||||
| `value` | `string` | The original string to hash. |
|
||||
| `rule` | `MaskRule` | Driving rule (no fields consumed by hash strategy). |
|
||||
|
||||
**Returns** an 8-character lowercase hexadecimal string.
|
||||
|
||||
**Does not throw.**
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The FNV-1a 32-bit variant uses offset basis `0x811c9dc5` and prime
|
||||
`0x01000193` per the canonical specification.
|
||||
- `Math.imul` is used for the multiplication step to get correct 32-bit
|
||||
integer multiplication semantics in JavaScript (standard `*` uses IEEE 754
|
||||
doubles which can lose low bits beyond 53-bit precision).
|
||||
- The `>>> 0` unsigned right-shift converts the result back from signed Int32
|
||||
to unsigned Uint32 before the next iteration, matching the expected
|
||||
unsigned arithmetic of FNV-1a.
|
||||
- Character codes are read with `charCodeAt` (UTF-16 code units), which is
|
||||
consistent and fast. Full Unicode codepoint splitting (via `codePointAt`)
|
||||
is not applied because the goal is a cheap stable hash, not an
|
||||
encoding-canonical one.
|
||||
@@ -0,0 +1,85 @@
|
||||
# partial strategy
|
||||
|
||||
## Purpose
|
||||
|
||||
The `partial` masking strategy preserves a configurable number of characters at
|
||||
the start and end of an input string while replacing the middle portion with
|
||||
asterisks. This is the most commonly needed masking shape for fields like
|
||||
API keys, account numbers, and reference codes where enough context must remain
|
||||
for humans to identify a value in logs without exposing the sensitive body.
|
||||
|
||||
## Features
|
||||
|
||||
- Configurable `keepFirst` (default `0`) and `keepLast` (default `4`) character
|
||||
counts via `MaskRule`.
|
||||
- Safe handling of short strings: when `keepFirst + keepLast >= length` the
|
||||
original value is returned unchanged rather than producing an empty or
|
||||
misleading result.
|
||||
- No external dependencies — pure string slicing.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { maskPartial } from '@suaveplan/data-masking/strategies/partial';
|
||||
|
||||
// Keep last 4 characters (default keepLast = 4, keepFirst = 0)
|
||||
const masked = maskPartial('sk_live_abc123xyz789', {
|
||||
fields: ['apiKey'],
|
||||
strategy: 'partial',
|
||||
});
|
||||
// masked → '****************789' (last 3 chars shown — "789" ends value)
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { maskPartial } from '@suaveplan/data-masking/strategies/partial';
|
||||
|
||||
// Keep first 2 and last 4 characters
|
||||
const masked = maskPartial('4111111111111111', {
|
||||
fields: ['cardNumber'],
|
||||
strategy: 'partial',
|
||||
keepFirst: 2,
|
||||
keepLast: 4,
|
||||
});
|
||||
// masked → '41**********1111'
|
||||
|
||||
// Short string — no masking applied
|
||||
const short = maskPartial('abc', {
|
||||
fields: ['code'],
|
||||
strategy: 'partial',
|
||||
keepFirst: 2,
|
||||
keepLast: 2,
|
||||
});
|
||||
// short → 'abc' (keepFirst + keepLast >= length)
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `maskPartial(value, rule)`
|
||||
|
||||
```ts
|
||||
function maskPartial(value: string, rule: MaskRule): string;
|
||||
```
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|------------------|------------|---------|------------------------------------------|
|
||||
| `value` | `string` | — | The original string. |
|
||||
| `rule.keepFirst` | `number` | `0` | Characters to keep from the start. |
|
||||
| `rule.keepLast` | `number` | `4` | Characters to keep from the end. |
|
||||
|
||||
**Returns** the masked string, or the original value unchanged if it is too
|
||||
short to mask.
|
||||
|
||||
**Does not throw.**
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The mask character is always `"*"`. A future extension could expose a
|
||||
`maskChar` option on `MaskRule` if needed.
|
||||
- `keepFirst + keepLast >= len` guard prevents edge cases where `slice` would
|
||||
produce overlapping or inverted ranges, which can silently produce empty
|
||||
strings in JavaScript.
|
||||
- Middle length is computed as `len - keepFirst - keepLast` to give a
|
||||
mask block that exactly fills the hidden region, preserving total length
|
||||
and allowing callers to spot-check that masking was applied.
|
||||
@@ -0,0 +1,80 @@
|
||||
# phone strategy
|
||||
|
||||
## Purpose
|
||||
|
||||
The `phone` masking strategy anonymises phone numbers while preserving their
|
||||
formatting separators, making masked values easy to read in logs and audit
|
||||
trails. Only the last 4 digits are kept; all other digit positions are
|
||||
replaced with `*`. Non-digit characters (hyphens, spaces, parentheses, dots,
|
||||
`+`) pass through unchanged, so the output retains the same visual structure
|
||||
as the input regardless of locale or formatting convention.
|
||||
|
||||
## Features
|
||||
|
||||
- Last-4-digit preservation regardless of phone format.
|
||||
- Separator characters (`-`, ` `, `(`, `)`, `.`, `+`) are preserved verbatim.
|
||||
- Handles international formats (`+1-…`), US formats (`(555) 123-4567`), and
|
||||
unformatted digit strings (`5551234567`).
|
||||
- No regex-based capture groups or lookaheads — single linear pass.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { maskPhone } from '@suaveplan/data-masking/strategies/phone';
|
||||
|
||||
const masked = maskPhone('+1-555-123-4567', { fields: ['phone'], strategy: 'phone' });
|
||||
// masked → '*-***-***-4567'
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { maskPhone } from '@suaveplan/data-masking/strategies/phone';
|
||||
|
||||
// US format with parentheses
|
||||
maskPhone('(555) 123-4567', { fields: ['phone'], strategy: 'phone' });
|
||||
// → '(***) ***-4567'
|
||||
|
||||
// E.164 international
|
||||
maskPhone('+447911123456', { fields: ['phone'], strategy: 'phone' });
|
||||
// → '+*********3456'
|
||||
|
||||
// Unformatted
|
||||
maskPhone('5551234567', { fields: ['phone'], strategy: 'phone' });
|
||||
// → '******4567'
|
||||
|
||||
// No digits at all → passthrough
|
||||
maskPhone('N/A', { fields: ['phone'], strategy: 'phone' });
|
||||
// → 'N/A'
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `maskPhone(value, rule)`
|
||||
|
||||
```ts
|
||||
function maskPhone(value: string, rule: MaskRule): string;
|
||||
```
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------------|----------------------------------------------------------|
|
||||
| `value` | `string` | The original phone number in any format. |
|
||||
| `rule` | `MaskRule` | Driving rule (no fields are consumed by phone strategy). |
|
||||
|
||||
**Returns** the masked phone string with last 4 digits preserved and all other
|
||||
digit positions replaced by `*`.
|
||||
|
||||
**Does not throw.** Returns the original value unchanged when no digit
|
||||
characters are found.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The strategy extracts all digits first to determine the `keepFrom` boundary
|
||||
(total digits minus 4), then makes a single linear pass over the original
|
||||
string to rebuild the result. This avoids multi-pass scanning and keeps
|
||||
the logic O(n).
|
||||
- `Math.max(0, digits.length - 4)` ensures that strings with 4 or fewer digits
|
||||
are returned with all digits preserved (no masking applied), matching the
|
||||
principle of least surprise.
|
||||
- The `rule` parameter is accepted to satisfy the uniform strategy interface
|
||||
but none of its fields are consumed.
|
||||
@@ -0,0 +1,79 @@
|
||||
# preserve-format strategy
|
||||
|
||||
## Purpose
|
||||
|
||||
The `preserve-format` masking strategy replaces every alphanumeric character
|
||||
with a placeholder of the same character class — digits become `0`, letters
|
||||
become `X` — while leaving all punctuation, spaces, and special characters
|
||||
unchanged. The result has the same total length and the same structural
|
||||
"skeleton" as the original, which allows downstream systems (parsers, validators,
|
||||
format checkers) to continue operating on the masked data without seeing any
|
||||
real content. This is particularly useful in schema-validation pipelines that
|
||||
process PII-bearing fields and must remain functional during testing against
|
||||
production-shaped data.
|
||||
|
||||
## Features
|
||||
|
||||
- Length-preserving: output has exactly the same character count as input.
|
||||
- Character-class-preserving: digits → `0`, letters → `X`, other → verbatim.
|
||||
- No regex — single character-by-character pass, safe on all Unicode inputs
|
||||
(only ASCII alphanumerics are substituted; non-ASCII passes through).
|
||||
- Zero external dependencies.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { maskPreserveFormat } from '@suaveplan/data-masking/strategies/preserve-format';
|
||||
|
||||
const masked = maskPreserveFormat('Hello123', { fields: ['code'], strategy: 'preserve-format' });
|
||||
// masked → 'XXXXX000'
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { maskPreserveFormat } from '@suaveplan/data-masking/strategies/preserve-format';
|
||||
|
||||
// Phone with separators
|
||||
maskPreserveFormat('+1-555-123-4567', { fields: ['phone'], strategy: 'preserve-format' });
|
||||
// → '+0-000-000-0000'
|
||||
|
||||
// Mixed alphanumeric code
|
||||
maskPreserveFormat('AB12-CD34-EF56', { fields: ['serial'], strategy: 'preserve-format' });
|
||||
// → 'XX00-XX00-XX00'
|
||||
|
||||
// Non-ASCII pass-through (accented chars preserved)
|
||||
maskPreserveFormat('café 42', { fields: ['label'], strategy: 'preserve-format' });
|
||||
// → 'XXXX 00' (é is non-ASCII and passes through as-is)
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `maskPreserveFormat(value, rule)`
|
||||
|
||||
```ts
|
||||
function maskPreserveFormat(value: string, rule: MaskRule): string;
|
||||
```
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------------|----------------------------------------------------------------|
|
||||
| `value` | `string` | The original string to format-preserve. |
|
||||
| `rule` | `MaskRule` | Driving rule (no fields consumed by preserve-format strategy). |
|
||||
|
||||
**Returns** a same-length string with alphanumeric chars replaced by `0` / `X`
|
||||
and all other chars preserved.
|
||||
|
||||
**Does not throw.**
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Only ASCII alphanumerics (`a-z`, `A-Z`, `0-9`) are replaced. Non-ASCII
|
||||
characters (including accented letters, CJK, emoji) pass through unchanged.
|
||||
This is intentional: the strategy targets structured English-language data
|
||||
formats (phone numbers, reference codes, account identifiers) where only
|
||||
ASCII alphanumerics carry sensitive content.
|
||||
- The `for...of` iteration correctly handles multi-code-unit Unicode characters
|
||||
(surrogate pairs) because JavaScript iterates by codepoint in `for...of`.
|
||||
The ASCII range check ensures non-ASCII characters are passed through intact.
|
||||
- The `rule` parameter is silently voided to satisfy the uniform strategy
|
||||
interface.
|
||||
@@ -0,0 +1,91 @@
|
||||
# tokenize strategy
|
||||
|
||||
## Purpose
|
||||
|
||||
The `tokenize` masking strategy replaces a sensitive value with a structured
|
||||
opaque token of the form `TOKEN_<STRATEGY>_<N>`, where `<N>` is a
|
||||
monotonically incrementing counter scoped to the current process lifetime.
|
||||
This is the simplest form of tokenisation — sufficient for audit trails and
|
||||
structured logs where the original value must be fully obscured but a unique
|
||||
reference per occurrence is still useful.
|
||||
|
||||
**Non-reversibility warning:** This implementation does NOT maintain a mapping
|
||||
from token to original value. Each call produces a new token (the counter
|
||||
increments on every invocation), and the counter resets when the process
|
||||
restarts. True reversible tokenisation requires a persistent, encrypted token
|
||||
vault that maps tokens to their plaintext originals. That capability is
|
||||
intentionally outside the scope of `@suaveplan/data-masking`.
|
||||
|
||||
## Features
|
||||
|
||||
- Structured token format: `TOKEN_<STRATEGY>_<index>`.
|
||||
- Monotonically increasing per-process counter — no random dependency.
|
||||
- Deterministic in tests when the counter is reset via `_resetTokenCounter()`.
|
||||
- Zero external dependencies.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { maskTokenize, _resetTokenCounter } from '@suaveplan/data-masking/strategies/tokenize';
|
||||
|
||||
_resetTokenCounter(); // deterministic start for tests
|
||||
|
||||
const t1 = maskTokenize('john@example.com', { fields: ['email'], strategy: 'tokenize' });
|
||||
// t1 → 'TOKEN_TOKENIZE_0'
|
||||
|
||||
const t2 = maskTokenize('jane@example.com', { fields: ['email'], strategy: 'tokenize' });
|
||||
// t2 → 'TOKEN_TOKENIZE_1'
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { maskTokenize, _resetTokenCounter } from '@suaveplan/data-masking/strategies/tokenize';
|
||||
|
||||
_resetTokenCounter();
|
||||
|
||||
// The strategy tag is embedded in the token for provenance
|
||||
const token = maskTokenize('4111111111111111', { fields: ['pan'], strategy: 'tokenize' });
|
||||
// token → 'TOKEN_TOKENIZE_0'
|
||||
|
||||
// Confirm it's a token (quick prefix check)
|
||||
console.log(token.startsWith('TOKEN_')); // true
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `maskTokenize(value, rule)`
|
||||
|
||||
```ts
|
||||
function maskTokenize(value: string, rule: MaskRule): string;
|
||||
```
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------------|---------------------------------------------------------------|
|
||||
| `value` | `string` | The original value (silently discarded). |
|
||||
| `rule` | `MaskRule` | Driving rule; `rule.strategy` is embedded in the token tag. |
|
||||
|
||||
**Returns** a token string of the form `TOKEN_<STRATEGY>_<N>`.
|
||||
|
||||
**Does not throw.**
|
||||
|
||||
### `_resetTokenCounter()`
|
||||
|
||||
```ts
|
||||
function _resetTokenCounter(): void;
|
||||
```
|
||||
|
||||
Resets the internal token counter to `0`. **For testing only.** Production
|
||||
code must not call this function.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The counter is a module-level variable, not a class instance, to satisfy
|
||||
the `mask(value, rule): string` strategy interface without requiring callers
|
||||
to instantiate a strategy object or inject state.
|
||||
- Hyphens in the strategy name are converted to underscores in the token tag
|
||||
(`credit-card` → `CREDIT_CARD`) for identifier-safe token strings.
|
||||
- Truly reversible tokenisation would require a key-value store (database or
|
||||
in-memory map with encryption) mapping each token back to its plaintext.
|
||||
That is deliberately deferred to a higher-level package and explicitly
|
||||
documented as out-of-scope here.
|
||||
@@ -0,0 +1,130 @@
|
||||
# mask-types
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Central type definition module for `@suaveplan/data-masking`. All public-facing
|
||||
interfaces (`MaskRule`, `MaskOptions`, `MaskResult`) and the `MaskStrategy`
|
||||
union type live here to provide a single authoritative source of truth for the
|
||||
package's data model and to break circular-import hazards between strategy
|
||||
implementations and the masker core.
|
||||
|
||||
This file ships zero runtime code — it is a types-only module. Importing it
|
||||
has no observable side-effects, making it tree-shakeable in type-erasure builds.
|
||||
|
||||
## Features
|
||||
|
||||
- `MaskStrategy` union: eight masking modes as a string literal union.
|
||||
- `MaskRule` interface: field path list, strategy, and per-strategy options.
|
||||
- `MaskOptions` interface: traversal controls (`deep`, `maskArrays`).
|
||||
- `MaskResult<T>` generic interface: masked clone + affected path list.
|
||||
- Full JSDoc on every property for IDE hover documentation.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type { MaskRule, MaskOptions } from '@suaveplan/data-masking';
|
||||
|
||||
const options: MaskOptions = {
|
||||
rules: [
|
||||
{ fields: ['user.email'], strategy: 'email' },
|
||||
{ fields: ['user.ssn'], strategy: 'full' },
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type { MaskRule, MaskResult, MaskStrategy } from '@suaveplan/data-masking';
|
||||
|
||||
// Build a typed rule map
|
||||
const SENSITIVE_FIELDS: Record<string, MaskStrategy> = {
|
||||
'user.email': 'email',
|
||||
'user.phone': 'phone',
|
||||
'payment.cardNumber': 'credit-card',
|
||||
'payment.cvv': 'full',
|
||||
};
|
||||
|
||||
const rules: MaskRule[] = Object.entries(SENSITIVE_FIELDS).map(
|
||||
([field, strategy]) => ({ fields: [field], strategy }),
|
||||
);
|
||||
|
||||
// Use with maskObject result
|
||||
function logAffected(result: MaskResult<unknown>): void {
|
||||
console.log('Masked fields:', result.fieldsAffected.join(', '));
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `MaskStrategy`
|
||||
|
||||
```ts
|
||||
type MaskStrategy =
|
||||
| 'full' | 'partial' | 'email' | 'phone'
|
||||
| 'credit-card' | 'hash' | 'tokenize' | 'preserve-format';
|
||||
```
|
||||
|
||||
String-literal union of all supported masking strategy identifiers.
|
||||
|
||||
### `MaskRule`
|
||||
|
||||
```ts
|
||||
interface MaskRule {
|
||||
fields: string[];
|
||||
strategy: MaskStrategy;
|
||||
keepLast?: number;
|
||||
keepFirst?: number;
|
||||
replacement?: string;
|
||||
}
|
||||
```
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|---------------|----------------|---------|-------------------------------------------------|
|
||||
| `fields` | `string[]` | — | Dot-notation paths / glob patterns. |
|
||||
| `strategy` | `MaskStrategy` | — | Which masking strategy to apply. |
|
||||
| `keepLast` | `number` | `4` | Chars to preserve at end (`partial` only). |
|
||||
| `keepFirst` | `number` | `0` | Chars to preserve at start (`partial` only). |
|
||||
| `replacement` | `string` | `"***"` | Custom replacement string (`full` only). |
|
||||
|
||||
### `MaskOptions`
|
||||
|
||||
```ts
|
||||
interface MaskOptions {
|
||||
rules: MaskRule[];
|
||||
deep?: boolean;
|
||||
maskArrays?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|--------------|--------------|---------|-------------------------------------------|
|
||||
| `rules` | `MaskRule[]` | — | Ordered list of masking rules. |
|
||||
| `deep` | `boolean` | `true` | Recurse into nested objects. |
|
||||
| `maskArrays` | `boolean` | `true` | Apply rules to array element values. |
|
||||
|
||||
### `MaskResult<T>`
|
||||
|
||||
```ts
|
||||
interface MaskResult<T = unknown> {
|
||||
masked: T;
|
||||
fieldsAffected: string[];
|
||||
}
|
||||
```
|
||||
|
||||
| Property | Type | Description |
|
||||
|------------------|------------|----------------------------------------------------|
|
||||
| `masked` | `T` | Deep clone of input with masking applied. |
|
||||
| `fieldsAffected` | `string[]` | Sorted dot-notation paths that were masked. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- This module is intentionally separated from strategy implementations to
|
||||
avoid circular imports. Strategy files import `MaskRule` from here; the
|
||||
masker core imports both strategies and types from here.
|
||||
- `exactOptionalPropertyTypes` is enabled in `tsconfig.base.json`, so all
|
||||
optional fields are typed as `T | undefined` rather than `T?` to avoid
|
||||
accidental `undefined` assignment bypasses.
|
||||
@@ -0,0 +1,88 @@
|
||||
# path-matcher
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Implements a lightweight glob-style matcher for dot-notation object-path
|
||||
strings used by `@suaveplan/data-masking`'s rule engine. When a `MaskRule`
|
||||
declares `fields: ['user.*']` the path matcher is responsible for deciding
|
||||
whether a path like `'user.email'` satisfies that pattern during object
|
||||
traversal. Zero external dependencies are required — the entire implementation
|
||||
is a recursive backtracking segment comparator that handles the three modes
|
||||
callers need in practice.
|
||||
|
||||
## Features
|
||||
|
||||
- Exact matching: `"user.email"` matches `"user.email"`.
|
||||
- Single-segment wildcard `*`: matches any one dot-separated segment.
|
||||
- Multi-segment wildcard `**`: matches zero or more consecutive dot-separated
|
||||
segments, enabling patterns like `"**.email"` or `"user.**"`.
|
||||
- Correct handling of leading, trailing, and adjacent wildcards through
|
||||
backtracking.
|
||||
- Pure function — no state, no side-effects, zero allocations beyond the
|
||||
initial `split`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { matchPath } from '@suaveplan/data-masking/utils/path-matcher';
|
||||
|
||||
matchPath('user.email', 'user.email'); // true — exact
|
||||
matchPath('user.name', 'user.email'); // false — no match
|
||||
matchPath('user.email', 'user.*'); // true — single wildcard
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { matchPath } from '@suaveplan/data-masking/utils/path-matcher';
|
||||
|
||||
// Match any path ending in .email regardless of depth
|
||||
matchPath('billing.contact.email', '**.email'); // true
|
||||
matchPath('email', '**.email'); // true (** matches zero segs)
|
||||
|
||||
// Match everything under user.*
|
||||
matchPath('user.address.city', 'user.**'); // true
|
||||
matchPath('user.address.city', 'user.*'); // false — * is single-segment only
|
||||
matchPath('user.address', 'user.*'); // true
|
||||
|
||||
// Combine wildcards
|
||||
matchPath('a.b.c.d', '*.b.**.d'); // true
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `matchPath(fieldPath, pattern)`
|
||||
|
||||
```ts
|
||||
function matchPath(fieldPath: string, pattern: string): boolean;
|
||||
```
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-------------|----------|--------------------------------------------------------|
|
||||
| `fieldPath` | `string` | Concrete dot-notation path, e.g. `"user.address.zip"`. |
|
||||
| `pattern` | `string` | Glob pattern with optional `*` / `**` wildcards. |
|
||||
|
||||
**Returns** `true` when `fieldPath` satisfies `pattern`, `false` otherwise.
|
||||
|
||||
**Does not throw.** Both empty strings are treated as single empty-segment
|
||||
paths and will only match each other or a lone `*` / `**` pattern.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The implementation splits both inputs on `"."` and feeds the resulting
|
||||
arrays into a recursive `matchSegments` helper. The recursion depth is
|
||||
bounded by `Math.max(pathSegments, patternSegments)`, which in practice is
|
||||
small (typically < 10 for real object paths).
|
||||
- The `**` wildcard uses a greedy-with-backtrack strategy: it first attempts
|
||||
to match zero path segments (advance only the pattern pointer), then
|
||||
progressively tries consuming one more path segment per recursive call.
|
||||
This guarantees correct results for patterns like `"**.email"` where `**`
|
||||
must match zero leading segments.
|
||||
- No regular expressions are used, avoiding edge-case escaping issues with
|
||||
field names that contain regex metacharacters (dots, brackets, etc.).
|
||||
- The function is intentionally kept free of any optional parameters or
|
||||
configuration — the three modes (exact, `*`, `**`) cover every use case
|
||||
encountered in PII field-path matching.
|
||||
Reference in New Issue
Block a user