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,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.
|
||||
Reference in New Issue
Block a user