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:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.