@lazymac/korean-pii-validate
v0.2.0
Published
Zero-dependency Korean PII checksum validator — RRN (주민등록번호), BRN (사업자등록번호), Korean mobile phone, masked-RRN. Pure JS, works in Node 18+, Bun, Deno, and modern browsers.
Downloads
283
Maintainers
Readme
@lazymac/korean-pii-validate
Zero-dependency Korean PII checksum validator. Catches 주민등록번호 (RRN), 사업자등록번호 (BRN), and Korean mobile phone numbers before you log them, send them to an LLM, paste them into a chat, or commit them to a public repo.
Pure JavaScript. Works in Node 18+ (CJS interop on Node 22+), Bun, Deno, and modern browsers. No telemetry, no network calls, ~6 KB unpacked, ~1 ms per 1000 validations.
Why this exists
Naive regex-based PII scanners (/\d{6}-\d{7}/) hit obvious shapes — and false-positive constantly on order numbers, tracking codes, anonymized samples. The fix is not more regex: it's the official NTS / RRN checksum layered on top.
This package does the layered detection: shape match → checksum validate → return what you can mask vs. what you can leave alone.
Install
npm install @lazymac/korean-pii-validateUsage
Validate a single value
import { validateRRN, validateBRN, validateKoreanPhone } from '@lazymac/korean-pii-validate';
validateRRN('901101-1234564');
// { valid: true, gender: 'M', foreign: false, birthYear: 1990, birthDate: '1990-11-01' }
validateBRN('220-81-62517');
// { valid: true, formatted: '220-81-62517' }
validateKoreanPhone('010-1234-5678');
// { valid: true, kind: 'mobile', legacy: false, prefix: '010', formatted: '010-1234-5678' }Validate a masked RRN (개인정보보호법 §29)
Korean privacy law mandates that display systems hide everything past the
century/gender flag — 901101-1****** instead of 901101-1234564. You usually
can't recompute the checksum but you can still confirm the date + flag are
sane before trusting the row.
import { validateMaskedRRN } from '@lazymac/korean-pii-validate';
validateMaskedRRN('901101-1******');
// { valid: true, masked: true, gender: 'M', foreign: false, birthYear: 1990, birthDate: '1990-11-01' }
validateMaskedRRN('901301-1******'); // bad month
// { valid: false, reason: 'invalid birth date: 1990-13-01' }Accepts *, x/X, ?, •, _ as mask characters.
Detect and redact across a document
import { detectAll, redactValid } from '@lazymac/korean-pii-validate';
const text = 'Customer 김철수 (901101-1234564), phone 010-1234-5678, vendor BRN 220-81-62517.';
detectAll(text);
// [
// { kind: 'rrn', start: 14, end: 28, raw: '901101-1234564', valid: true, detail: {...} },
// { kind: 'phone', start: 36, end: 49, raw: '010-1234-5678', valid: true, detail: {...} },
// { kind: 'brn', start: 62, end: 74, raw: '220-81-62517', valid: true, detail: {...} },
// ]
redactValid(text, '[REDACTED]');
// {
// redacted: 'Customer 김철수 ([REDACTED]), phone [REDACTED], vendor BRN [REDACTED].',
// matches: [...]
// }redactValid only masks checksum-valid matches. False positives (regex-shaped strings that don't pass the official checksum) are left intact — they're almost always order numbers, tracking IDs, or test fixtures.
CLI
# single value
$ npx korean-pii-validate "220-81-62517"
{ "valid": true, "formatted": "220-81-62517", "kind": "brn" }
# pipe a document, get JSON matches
$ cat customer_dump.txt | npx korean-pii-validate --scan
[ { "kind": "rrn", "start": 142, "end": 156, "raw": "...", "valid": true, ... } ]Exit code 0 if valid, 1 if not. Easy to drop into pre-commit hooks.
What it validates
| Type | Format | Algorithm | Status |
|---|---|---|---|
| RRN (주민등록번호) | YYMMDD-CXXXXXC₂ | Weighted sum [2,3,4,5,6,7,8,9,2,3,4,5], (11 − sum%11) % 10 | ✅ checksum + date + century/gender flag |
| BRN (사업자등록번호) | XXX-XX-XXXXX | NTS algorithm: weighted [1,3,7,1,3,7,1,3,5] + ⌊d₈·5/10⌋, (10 − sum%10) % 10 | ✅ checksum |
| Korean mobile | 010/011/.../019-XXX(X)-XXXX | Shape only (no carrier checksum exists) | ✅ shape + prefix |
| VOIP | 070-XXXX-XXXX | Shape only | ✅ classified separately |
What it does NOT do
- No carrier liveness check —
010-1234-5678can be syntactically valid but unallocated. Real activation status requires a paid KISA lookup. - No business-active check for BRN — checksum-valid does not mean the company is currently registered. Use the NTS
homepage.htmlAPI for that (rate-limited, requires your own key). - No name-validation — there is no checksum on Korean names. We don't try.
- No log / no upload / no analytics. This runs locally. Verify by opening DevTools → Network and watching nothing happen.
API reference
validateRRN(input) → RRNResult (discriminated union — narrows after if (r.valid))
validateMaskedRRN(input) → MaskedRRNResult
validateBRN(input) → BRNResult
validateKoreanPhone(input) → PhoneResult
findRRNCandidates(text) / findMaskedRRNCandidates(text) / findBRNCandidates(text) / findPhoneCandidates(text)
detectAll(text) → PIIMatch[] (sorted by position, non-overlapping)
redactValid(text, mask='•••••') → { redacted, matches }
Hyphenated and non-hyphenated forms are both accepted. validateRRN, validateBRN, validateKoreanPhone return { valid: false, reason: '...' } with a human-readable reason on rejection.
Tests
npm test # 27 cases, zero depsLicense
MIT © Daniel Choi
