us-ssn-tools 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +7 -0
- package/README.md +319 -0
- package/dist/generate.d.ts +23 -0
- package/dist/generate.js +84 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +14 -0
- package/dist/mask.d.ts +25 -0
- package/dist/mask.js +55 -0
- package/dist/normalize.d.ts +15 -0
- package/dist/normalize.js +62 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.js +13 -0
- package/dist/validate.d.ts +43 -0
- package/dist/validate.js +179 -0
- package/dist/yup.d.ts +9 -0
- package/dist/yup.js +80 -0
- package/dist/zod.d.ts +11 -0
- package/dist/zod.js +41 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright 2025 backupbrain@gmail.com
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# US Social Security Tools
|
|
2
|
+
|
|
3
|
+
A small, well-tested TypeScript library for **working with U.S. Social Security Numbers (SSNs)** in UI and backend code.
|
|
4
|
+
|
|
5
|
+
It provides:
|
|
6
|
+
|
|
7
|
+
* ✅ **Validation** (strict + typing-as-you-go)
|
|
8
|
+
* 🔁 **Normalization** (canonical `###-##-####` formatting)
|
|
9
|
+
* 🎭 **Masking** (UI-safe, best-effort, privacy-first)
|
|
10
|
+
* 🎲 **Generation** (pre-2011, post-2011, random, and publicly advertised SSNs)
|
|
11
|
+
* 🧩 **Zod & Yup adapters** for form validation
|
|
12
|
+
|
|
13
|
+
> **Design principle:**
|
|
14
|
+
> Validation enforces rules.
|
|
15
|
+
> Masking never leaks data.
|
|
16
|
+
> Normalization is deterministic and UI-friendly.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install us-ssn-tools
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
or
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
yarn add us-ssn-tools
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Imports
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import {
|
|
38
|
+
validateSsn,
|
|
39
|
+
normalizeSsnInput,
|
|
40
|
+
formatSsnFromDigits,
|
|
41
|
+
maskSsn,
|
|
42
|
+
generateSsn,
|
|
43
|
+
} from "us-ssn-tools";
|
|
44
|
+
|
|
45
|
+
import { zodSsnTyping, zodSsnSubmit } from "us-ssn-tools/zod";
|
|
46
|
+
import { yupSsnTyping, yupSsnSubmit } from "us-ssn-tools/yup";
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Validation
|
|
52
|
+
|
|
53
|
+
### `validateSsn(input, options)`
|
|
54
|
+
|
|
55
|
+
Validates an SSN according to U.S. rules.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
const result = validateSsn("123-45-6789");
|
|
59
|
+
|
|
60
|
+
if (result.ok) {
|
|
61
|
+
console.log(result.normalized); // "123-45-6789"
|
|
62
|
+
} else {
|
|
63
|
+
console.log(result.error); // e.g. "INVALID_AREA"
|
|
64
|
+
console.log(result.message); // human-readable explanation
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Options
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
type ValidateSsnOptions = {
|
|
72
|
+
allowNoDashes?: boolean; // default true
|
|
73
|
+
allowPartial?: boolean; // default false
|
|
74
|
+
ruleMode?: "pre2011" | "post2011" | "both"; // default "both"
|
|
75
|
+
};
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
#### Examples
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
validateSsn("123456789"); // ok (normalized to "123-45-6789")
|
|
82
|
+
|
|
83
|
+
validateSsn("9", { allowPartial: true });
|
|
84
|
+
// ❌ INVALID_AREA (impossible prefix)
|
|
85
|
+
|
|
86
|
+
validateSsn("773-12-3456", { ruleMode: "pre2011" });
|
|
87
|
+
// ❌ INVALID_AREA
|
|
88
|
+
|
|
89
|
+
validateSsn("773-12-3456", { ruleMode: "post2011" });
|
|
90
|
+
// ✅ ok
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Normalization
|
|
96
|
+
|
|
97
|
+
### `normalizeSsnInput(input, options)`
|
|
98
|
+
|
|
99
|
+
Parses and formats SSNs **without enforcing full validity**.
|
|
100
|
+
Ideal for **typing-as-you-go**.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const res = normalizeSsnInput("1234", { allowPartial: true });
|
|
104
|
+
|
|
105
|
+
if (res.ok) {
|
|
106
|
+
res.digits; // "1234"
|
|
107
|
+
res.normalized; // "123-4"
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Options
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
type NormalizeSsnOptions = {
|
|
115
|
+
allowPartial?: boolean;
|
|
116
|
+
allowNoDashes?: boolean;
|
|
117
|
+
};
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Examples
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
normalizeSsnInput("123456789");
|
|
124
|
+
// → { digits: "123456789", normalized: "123-45-6789" }
|
|
125
|
+
|
|
126
|
+
normalizeSsnInput("123-45-6", { allowPartial: true });
|
|
127
|
+
// → { digits: "123456", normalized: "123-45-6" }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Formatting (UI helper)
|
|
133
|
+
|
|
134
|
+
### `formatSsnFromDigits(digits)`
|
|
135
|
+
|
|
136
|
+
Formats a **digit string** into SSN shape.
|
|
137
|
+
This function **does not validate**.
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
formatSsnFromDigits("123"); // "123"
|
|
141
|
+
formatSsnFromDigits("1234"); // "123-4"
|
|
142
|
+
formatSsnFromDigits("123456789"); // "123-45-6789"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Used internally by normalization and masking, but safe to use directly for UI.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Masking (UI-safe)
|
|
150
|
+
|
|
151
|
+
### `maskSsn(input, options)`
|
|
152
|
+
|
|
153
|
+
Masks digits while **never masking dashes**.
|
|
154
|
+
Designed for **privacy-safe UI rendering**, not validation.
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
maskSsn("123-45-6789");
|
|
158
|
+
// "***-**-****"
|
|
159
|
+
|
|
160
|
+
maskSsn("123-45-6789", { revealLast4: true });
|
|
161
|
+
// "***-**-6789"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Options
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
type MaskSsnOptions = {
|
|
168
|
+
allowPartial?: boolean; // default false
|
|
169
|
+
revealLast4?: boolean; // default false
|
|
170
|
+
maskChar?: string; // default "*"
|
|
171
|
+
dashMode?: "normalize" | "preserve"; // default "normalize"
|
|
172
|
+
allowNoDashes?: boolean; // default true
|
|
173
|
+
};
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Partial / typing behavior
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
maskSsn("123", { allowPartial: true });
|
|
180
|
+
// "***"
|
|
181
|
+
|
|
182
|
+
maskSsn("123-45-6", { allowPartial: true, revealLast4: true });
|
|
183
|
+
// "***-**-6"
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Important guarantees
|
|
187
|
+
|
|
188
|
+
* ✔️ **Digits are always masked** (even on invalid input)
|
|
189
|
+
* ✔️ Dashes are never masked
|
|
190
|
+
* ✔️ No validation required — safe for UI display
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Generation
|
|
195
|
+
|
|
196
|
+
### `generateSsn(options)`
|
|
197
|
+
Absolutely — that’s an important point, and it’s worth stating **clearly but professionally**, without sounding alarmist.
|
|
198
|
+
|
|
199
|
+
Here’s a **polished replacement** for the **Generation** section note that you can drop straight into the README.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Generation
|
|
204
|
+
|
|
205
|
+
Generates realistic-looking SSNs for testing, demos, and development.
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
generateSsn();
|
|
209
|
+
// e.g. "509-21-4837"
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### ⚠️ Important note on public usage
|
|
213
|
+
|
|
214
|
+
> **Only use `mode: "public"` for any SSNs that may be displayed publicly.**
|
|
215
|
+
|
|
216
|
+
SSNs generated with `"pre2011"`, `"post2011"`, or `"any"` modes are **syntactically valid** and may correspond to **real individuals**.
|
|
217
|
+
|
|
218
|
+
If such a value is:
|
|
219
|
+
|
|
220
|
+
* rendered in documentation
|
|
221
|
+
* shown in screenshots
|
|
222
|
+
* logged to public consoles
|
|
223
|
+
* included in sample data or demos
|
|
224
|
+
|
|
225
|
+
you risk **exposing a real person to identity theft** if the generated SSN happens to match an existing one.
|
|
226
|
+
|
|
227
|
+
To prevent this, the library includes a special `"public"` generation mode that returns **historically documented, non-random, publicly advertised SSNs** that are known to be unsafe for real-world use but safe for examples.
|
|
228
|
+
|
|
229
|
+
### Recommended usage
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
// ✅ Safe for docs, demos, screenshots, and public output
|
|
233
|
+
generateSsn({ mode: "public" });
|
|
234
|
+
|
|
235
|
+
// ❌ Do NOT use in any public or user-visible context
|
|
236
|
+
generateSsn({ mode: "any" });
|
|
237
|
+
generateSsn({ mode: "pre2011" });
|
|
238
|
+
generateSsn({ mode: "post2011" });
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Use non-public modes **only** for:
|
|
242
|
+
|
|
243
|
+
* internal testing
|
|
244
|
+
* private development environments
|
|
245
|
+
* automated test data that is never exposed
|
|
246
|
+
|
|
247
|
+
This distinction exists to protect real people, not just to satisfy validation rules.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Zod Adapters
|
|
252
|
+
|
|
253
|
+
### Typing (partial + normalized)
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
const schema = zodSsnTyping();
|
|
257
|
+
|
|
258
|
+
schema.parse("1234"); // "123-4"
|
|
259
|
+
schema.parse("9"); // ❌ throws (impossible prefix)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Submit (strict)
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
const schema = zodSsnSubmit();
|
|
266
|
+
|
|
267
|
+
schema.parse("123456789"); // "123-45-6789"
|
|
268
|
+
schema.parse("123-45-6"); // ❌ throws
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Yup Adapters
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
await yupSsnTyping().validate("1234");
|
|
277
|
+
// "123-4"
|
|
278
|
+
|
|
279
|
+
await yupSsnSubmit().validate("123456789");
|
|
280
|
+
// "123-45-6789"
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Testing & Guarantees
|
|
286
|
+
|
|
287
|
+
* ✔️ 100% table-driven Jest tests
|
|
288
|
+
* ✔️ Deterministic RNG support
|
|
289
|
+
* ✔️ Strict separation between:
|
|
290
|
+
|
|
291
|
+
* validation
|
|
292
|
+
* formatting
|
|
293
|
+
* masking
|
|
294
|
+
* generation
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Non-Goals
|
|
299
|
+
|
|
300
|
+
* ❌ No storage or encryption
|
|
301
|
+
* ❌ No non-US SSNs (yet)
|
|
302
|
+
* ❌ No implicit trimming or mutation of user input
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## License
|
|
307
|
+
|
|
308
|
+
ISC
|
|
309
|
+
|
|
310
|
+
## Support This Project
|
|
311
|
+
|
|
312
|
+
If you find this project useful, consider supporting me to help keep it maintained and improved:
|
|
313
|
+
|
|
314
|
+
- [Sponsor on GitHub](https://github.com/sponsors/backupbrain)
|
|
315
|
+
- [Buy Me a Coffee](https://www.buymeacoffee.com/backupbrain)
|
|
316
|
+
- [Ko-fi](https://ko-fi.com/backupbrain)
|
|
317
|
+
- [Thanks.dev](https://thanks.dev/u/gh/backupbrain)
|
|
318
|
+
|
|
319
|
+
Your support is greatly appreciated!
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type GenerateSsnMode = 'pre2011' | 'post2011' | 'any' | 'public';
|
|
2
|
+
export interface GenerateSsnOptions {
|
|
3
|
+
/**
|
|
4
|
+
* What kind of SSN to generate.
|
|
5
|
+
* - "any" (default): may produce either pre2011-valid or post2011-valid
|
|
6
|
+
* - "pre2011": uses stricter pre-2011 area rules
|
|
7
|
+
* - "post2011": uses post-2011 relaxed area rules
|
|
8
|
+
* - "public": returns one of the publicly-advertised SSNs (intentionally invalid)
|
|
9
|
+
*/
|
|
10
|
+
mode?: GenerateSsnMode;
|
|
11
|
+
/** Output format (default: "dashed") */
|
|
12
|
+
format?: 'dashed' | 'digits';
|
|
13
|
+
/**
|
|
14
|
+
* Optional RNG injection for determinism in tests.
|
|
15
|
+
* Must return a float in [0, 1).
|
|
16
|
+
*/
|
|
17
|
+
rng?: () => number;
|
|
18
|
+
/**
|
|
19
|
+
* For mode="public": choose a specific advertised SSN, otherwise random.
|
|
20
|
+
*/
|
|
21
|
+
publicValue?: '078-05-1120' | '721-07-4426' | '219-09-9999';
|
|
22
|
+
}
|
|
23
|
+
export declare function generateSsn(opts?: GenerateSsnOptions): string;
|
package/dist/generate.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateSsn = generateSsn;
|
|
4
|
+
const PUBLICLY_ADVERTISED = [
|
|
5
|
+
'078-05-1120',
|
|
6
|
+
'721-07-4426',
|
|
7
|
+
'219-09-9999',
|
|
8
|
+
];
|
|
9
|
+
function generateSsn(opts = {}) {
|
|
10
|
+
var _a, _b, _c, _d;
|
|
11
|
+
const mode = (_a = opts.mode) !== null && _a !== void 0 ? _a : 'any';
|
|
12
|
+
const format = (_b = opts.format) !== null && _b !== void 0 ? _b : 'dashed';
|
|
13
|
+
const rng = (_c = opts.rng) !== null && _c !== void 0 ? _c : defaultRng;
|
|
14
|
+
if (mode === 'public') {
|
|
15
|
+
const chosen = (_d = opts.publicValue) !== null && _d !== void 0 ? _d : PUBLICLY_ADVERTISED[randomInt(rng, 0, PUBLICLY_ADVERTISED.length - 1)];
|
|
16
|
+
return format === 'digits' ? chosen.replace(/-/g, '') : chosen;
|
|
17
|
+
}
|
|
18
|
+
// "any" => randomly choose pre or post
|
|
19
|
+
const effectiveMode = mode === 'any' ? (rng() < 0.5 ? 'pre2011' : 'post2011') : mode;
|
|
20
|
+
const area = generateArea(effectiveMode, rng);
|
|
21
|
+
const group = generateNonZeroFixedWidth(rng, 2); // 01..99 (not 00)
|
|
22
|
+
const serial = generateNonZeroFixedWidth(rng, 4); // 0001..9999 (not 0000)
|
|
23
|
+
const dashed = `${area}-${group}-${serial}`;
|
|
24
|
+
// Avoid accidentally returning a "publicly advertised" SSN unless explicitly requested.
|
|
25
|
+
if (PUBLICLY_ADVERTISED.includes(dashed)) {
|
|
26
|
+
return generateSsn(Object.assign(Object.assign({}, opts), { mode })); // retry (very unlikely)
|
|
27
|
+
}
|
|
28
|
+
return format === 'digits' ? dashed.replace(/-/g, '') : dashed;
|
|
29
|
+
}
|
|
30
|
+
/* ---------------------------- helpers ---------------------------- */
|
|
31
|
+
function generateArea(mode, rng) {
|
|
32
|
+
// Base rules always:
|
|
33
|
+
// - not 000
|
|
34
|
+
// - not 666
|
|
35
|
+
// - not 900-999
|
|
36
|
+
//
|
|
37
|
+
// Pre-2011 additionally:
|
|
38
|
+
// - not 734-749
|
|
39
|
+
// - not >= 773
|
|
40
|
+
//
|
|
41
|
+
// Strategy: generate until it passes constraints (fast, tiny rejection rate).
|
|
42
|
+
while (true) {
|
|
43
|
+
const n = randomInt(rng, 1, 899); // 001..899
|
|
44
|
+
if (n === 666)
|
|
45
|
+
continue;
|
|
46
|
+
if (mode === 'pre2011') {
|
|
47
|
+
if (n >= 734 && n <= 749)
|
|
48
|
+
continue;
|
|
49
|
+
if (n >= 773)
|
|
50
|
+
continue; // excludes 773..899
|
|
51
|
+
}
|
|
52
|
+
return pad3(n);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function generateNonZeroFixedWidth(rng, width) {
|
|
56
|
+
if (width === 2) {
|
|
57
|
+
// 01..99
|
|
58
|
+
const n = randomInt(rng, 1, 99);
|
|
59
|
+
return n.toString().padStart(2, '0');
|
|
60
|
+
}
|
|
61
|
+
// 0001..9999
|
|
62
|
+
const n = randomInt(rng, 1, 9999);
|
|
63
|
+
return n.toString().padStart(4, '0');
|
|
64
|
+
}
|
|
65
|
+
function pad3(n) {
|
|
66
|
+
return n.toString().padStart(3, '0');
|
|
67
|
+
}
|
|
68
|
+
function randomInt(rng, min, max) {
|
|
69
|
+
// inclusive min/max
|
|
70
|
+
const r = rng();
|
|
71
|
+
return Math.floor(r * (max - min + 1)) + min;
|
|
72
|
+
}
|
|
73
|
+
function defaultRng() {
|
|
74
|
+
var _a;
|
|
75
|
+
// Prefer crypto when available; fall back to Math.random.
|
|
76
|
+
// Works in browsers; in Node 19+ crypto.getRandomValues exists on globalThis.crypto.
|
|
77
|
+
const g = globalThis;
|
|
78
|
+
if ((_a = g.crypto) === null || _a === void 0 ? void 0 : _a.getRandomValues) {
|
|
79
|
+
const buf = new Uint32Array(1);
|
|
80
|
+
g.crypto.getRandomValues(buf);
|
|
81
|
+
return buf[0] / Math.pow(2, 32);
|
|
82
|
+
}
|
|
83
|
+
return Math.random();
|
|
84
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateSsn = exports.formatSsnFromDigits = exports.maskSsn = exports.normalizeSsnInput = exports.validateSsn = void 0;
|
|
4
|
+
// Exporting all functions
|
|
5
|
+
var validate_1 = require("./validate");
|
|
6
|
+
Object.defineProperty(exports, "validateSsn", { enumerable: true, get: function () { return validate_1.validateSsn; } });
|
|
7
|
+
var normalize_1 = require("./normalize");
|
|
8
|
+
Object.defineProperty(exports, "normalizeSsnInput", { enumerable: true, get: function () { return normalize_1.normalizeSsnInput; } });
|
|
9
|
+
var mask_1 = require("./mask");
|
|
10
|
+
Object.defineProperty(exports, "maskSsn", { enumerable: true, get: function () { return mask_1.maskSsn; } });
|
|
11
|
+
var utils_1 = require("./utils");
|
|
12
|
+
Object.defineProperty(exports, "formatSsnFromDigits", { enumerable: true, get: function () { return utils_1.formatSsnFromDigits; } });
|
|
13
|
+
var generate_1 = require("./generate");
|
|
14
|
+
Object.defineProperty(exports, "generateSsn", { enumerable: true, get: function () { return generate_1.generateSsn; } });
|
package/dist/mask.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface MaskSsnOptions {
|
|
2
|
+
/** Allow masking partial SSNs (typing-as-you-go). */
|
|
3
|
+
allowPartial?: boolean;
|
|
4
|
+
/** If true, reveal last 4 digits when present (>= 4 digits typed). */
|
|
5
|
+
revealLast4?: boolean;
|
|
6
|
+
/** Mask character (default: "*"). Only first char is used. */
|
|
7
|
+
maskChar?: string;
|
|
8
|
+
/** Accept digit-only input (default: true). */
|
|
9
|
+
allowNoDashes?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Controls dash output:
|
|
12
|
+
* - "preserve": keep dashes only if the input already contains '-'
|
|
13
|
+
* - "normalize": always insert dashes in ###-##-#### style (even for digits-only)
|
|
14
|
+
*
|
|
15
|
+
* Default: "normalize"
|
|
16
|
+
*/
|
|
17
|
+
dashMode?: 'preserve' | 'normalize';
|
|
18
|
+
/**
|
|
19
|
+
* If true (default), and allowPartial=true, invalid partial input will be masked
|
|
20
|
+
* in a best-effort way (digits masked, dashes preserved).
|
|
21
|
+
* If false, invalid input is returned unchanged.
|
|
22
|
+
*/
|
|
23
|
+
bestEffortOnInvalidPartial?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export declare function maskSsn(input: string, opts?: MaskSsnOptions): string;
|
package/dist/mask.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.maskSsn = maskSsn;
|
|
4
|
+
const normalize_1 = require("./normalize");
|
|
5
|
+
const utils_1 = require("./utils");
|
|
6
|
+
function maskSsn(input, opts = {}) {
|
|
7
|
+
var _a, _b, _c, _d, _e;
|
|
8
|
+
const allowPartial = (_a = opts.allowPartial) !== null && _a !== void 0 ? _a : false;
|
|
9
|
+
const revealLast4 = (_b = opts.revealLast4) !== null && _b !== void 0 ? _b : false;
|
|
10
|
+
const maskChar = ((_c = opts.maskChar) !== null && _c !== void 0 ? _c : '*').slice(0, 1) || '*';
|
|
11
|
+
const allowNoDashes = (_d = opts.allowNoDashes) !== null && _d !== void 0 ? _d : true;
|
|
12
|
+
const dashMode = (_e = opts.dashMode) !== null && _e !== void 0 ? _e : 'normalize';
|
|
13
|
+
const hadDashes = input.includes('-');
|
|
14
|
+
const normalized = (0, normalize_1.normalizeSsnInput)(input, { allowPartial, allowNoDashes });
|
|
15
|
+
if (!normalized.ok) {
|
|
16
|
+
// Masking is UI-safety, not validation: never return raw digits.
|
|
17
|
+
// Best-effort: mask digits, keep '-' unmasked, keep any other characters as-is.
|
|
18
|
+
const masked = maskBestEffort(input, { maskChar });
|
|
19
|
+
// If caller wants normalized dashes, we can try to normalize by extracting digits
|
|
20
|
+
// from the best-effort masked string is not possible (digits are already masked),
|
|
21
|
+
// so we simply return the best-effort output.
|
|
22
|
+
return masked;
|
|
23
|
+
}
|
|
24
|
+
const digits = normalized.digits; // 0..9 digits (partial) or 9 digits (full)
|
|
25
|
+
// Determine how many digits to reveal (last 4) if present.
|
|
26
|
+
const total = digits.length;
|
|
27
|
+
// digits.length = total typed digits (0..9)
|
|
28
|
+
const serialTyped = Math.max(0, total - 5); // digits 6..9 => 1..4
|
|
29
|
+
const revealCount = revealLast4 ? Math.min(4, serialTyped) : 0;
|
|
30
|
+
const maskedCount = Math.max(0, total - revealCount);
|
|
31
|
+
const maskedDigits = maskChar.repeat(maskedCount) + digits.slice(maskedCount);
|
|
32
|
+
// Output formatting
|
|
33
|
+
if (dashMode === 'normalize') {
|
|
34
|
+
// Insert dashes as ###-##-#### prefix (even while typing).
|
|
35
|
+
return (0, utils_1.formatSsnFromDigits)(maskedDigits);
|
|
36
|
+
}
|
|
37
|
+
// dashMode === "preserve"
|
|
38
|
+
if (hadDashes) {
|
|
39
|
+
// Input had dashes -> keep dashed presentation (normalized positions).
|
|
40
|
+
return (0, utils_1.formatSsnFromDigits)(maskedDigits);
|
|
41
|
+
}
|
|
42
|
+
// Input had no dashes -> keep digits-only.
|
|
43
|
+
return maskedDigits;
|
|
44
|
+
}
|
|
45
|
+
function maskBestEffort(input, opts) {
|
|
46
|
+
const m = opts.maskChar;
|
|
47
|
+
let out = '';
|
|
48
|
+
for (const ch of input) {
|
|
49
|
+
if (ch >= '0' && ch <= '9')
|
|
50
|
+
out += m;
|
|
51
|
+
else
|
|
52
|
+
out += ch; // dashes and any other characters preserved
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface NormalizeSsnOptions {
|
|
2
|
+
allowNoDashes?: boolean;
|
|
3
|
+
allowPartial?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export type NormalizeSsnOk = {
|
|
6
|
+
ok: true;
|
|
7
|
+
digits: string;
|
|
8
|
+
normalized: string;
|
|
9
|
+
};
|
|
10
|
+
export type NormalizeSsnErr = {
|
|
11
|
+
ok: false;
|
|
12
|
+
message: string;
|
|
13
|
+
};
|
|
14
|
+
export type NormalizeSsnResult = NormalizeSsnOk | NormalizeSsnErr;
|
|
15
|
+
export declare function normalizeSsnInput(input: string, opts?: NormalizeSsnOptions): NormalizeSsnResult;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeSsnInput = normalizeSsnInput;
|
|
4
|
+
const utils_1 = require("./utils");
|
|
5
|
+
function normalizeSsnInput(input, opts = {}) {
|
|
6
|
+
var _a, _b;
|
|
7
|
+
const allowNoDashes = (_a = opts.allowNoDashes) !== null && _a !== void 0 ? _a : true;
|
|
8
|
+
const allowPartial = (_b = opts.allowPartial) !== null && _b !== void 0 ? _b : false;
|
|
9
|
+
const raw = input;
|
|
10
|
+
// Full (non-partial) normalization
|
|
11
|
+
if (!allowPartial) {
|
|
12
|
+
if (/^\d{3}-\d{2}-\d{4}$/.test(raw)) {
|
|
13
|
+
return {
|
|
14
|
+
ok: true,
|
|
15
|
+
digits: raw.replace(/-/g, ''),
|
|
16
|
+
normalized: raw,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (allowNoDashes && /^\d{9}$/.test(raw)) {
|
|
20
|
+
return {
|
|
21
|
+
ok: true,
|
|
22
|
+
digits: raw,
|
|
23
|
+
normalized: `${raw.slice(0, 3)}-${raw.slice(3, 5)}-${raw.slice(5)}`,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return { ok: false, message: 'Invalid SSN format.' };
|
|
27
|
+
}
|
|
28
|
+
// ---- Partial / typing-as-you-go normalization ----
|
|
29
|
+
if (!/^[0-9-]*$/.test(raw)) {
|
|
30
|
+
return { ok: false, message: "Only digits and '-' are allowed." };
|
|
31
|
+
}
|
|
32
|
+
let digits = '';
|
|
33
|
+
let sawDashAt3 = false;
|
|
34
|
+
let sawDashAt5 = false;
|
|
35
|
+
for (const ch of raw) {
|
|
36
|
+
if (ch >= '0' && ch <= '9') {
|
|
37
|
+
digits += ch;
|
|
38
|
+
if (digits.length > 9) {
|
|
39
|
+
return { ok: false, message: 'SSN is too long.' };
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
// ch === '-'
|
|
44
|
+
if (!allowNoDashes) {
|
|
45
|
+
return { ok: false, message: 'Dashes are not allowed.' };
|
|
46
|
+
}
|
|
47
|
+
if (digits.length === 3 && !sawDashAt3) {
|
|
48
|
+
sawDashAt3 = true;
|
|
49
|
+
}
|
|
50
|
+
else if (digits.length === 5 && !sawDashAt5) {
|
|
51
|
+
sawDashAt5 = true;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
return { ok: false, message: 'Dash is in an invalid position.' };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
ok: true,
|
|
59
|
+
digits,
|
|
60
|
+
normalized: (0, utils_1.formatSsnFromDigits)(digits),
|
|
61
|
+
};
|
|
62
|
+
}
|
package/dist/utils.d.ts
ADDED
package/dist/utils.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatSsnFromDigits = formatSsnFromDigits;
|
|
4
|
+
/**
|
|
5
|
+
* Format SSN from digits. Used for UI formatting.
|
|
6
|
+
*/
|
|
7
|
+
function formatSsnFromDigits(digits) {
|
|
8
|
+
if (digits.length <= 3)
|
|
9
|
+
return digits;
|
|
10
|
+
if (digits.length <= 5)
|
|
11
|
+
return `${digits.slice(0, 3)}-${digits.slice(3)}`;
|
|
12
|
+
return `${digits.slice(0, 3)}-${digits.slice(3, 5)}-${digits.slice(5)}`;
|
|
13
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type SsnValidationFailureReason = 'INVALID_FORMAT' | 'INVALID_AREA' | 'INVALID_GROUP' | 'INVALID_SERIAL' | 'PUBLICLY_ADVERTISED';
|
|
2
|
+
export type SsnRuleMode = 'pre2011' | 'post2011' | 'both';
|
|
3
|
+
export type SsnValidationOkResult = {
|
|
4
|
+
ok: true;
|
|
5
|
+
normalized: string;
|
|
6
|
+
};
|
|
7
|
+
export type SsnValidationErrorResult = {
|
|
8
|
+
ok: false;
|
|
9
|
+
error: SsnValidationFailureReason;
|
|
10
|
+
message: string;
|
|
11
|
+
};
|
|
12
|
+
export type SsnValidationResult = SsnValidationOkResult | SsnValidationErrorResult;
|
|
13
|
+
export interface ValidateSsnOptions {
|
|
14
|
+
/**
|
|
15
|
+
* If true, accept either "#########" or "###-##-####" as input.
|
|
16
|
+
* If false, require exact "###-##-####" (or partial prefix of it if allowPartial=true).
|
|
17
|
+
*/
|
|
18
|
+
allowNoDashes?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Which rule-set(s) to accept.
|
|
21
|
+
* - "pre2011": apply stricter pre–Jun 25 2011 area rules (734–749, >=773 invalid)
|
|
22
|
+
* - "post2011": do NOT apply those extra area restrictions
|
|
23
|
+
* - "both": accept either (default)
|
|
24
|
+
*
|
|
25
|
+
* Note: base rules (area 000/666/900-999, group 00, serial 0000, public list) still apply.
|
|
26
|
+
*/
|
|
27
|
+
ruleMode?: SsnRuleMode;
|
|
28
|
+
/**
|
|
29
|
+
* If true, allow "checking-as-you-go":
|
|
30
|
+
* - partial prefixes are accepted as long as they could still become valid
|
|
31
|
+
* - returned normalized string is the sanitized, dash-normalized prefix
|
|
32
|
+
*/
|
|
33
|
+
allowPartial?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Validates a US SSN with support for:
|
|
37
|
+
* - strict full validation
|
|
38
|
+
* - optional acceptance of no-dash input
|
|
39
|
+
* - rule modes: pre2011 | post2011 | both (default)
|
|
40
|
+
* - optional "checking-as-you-go" partial validation
|
|
41
|
+
*/ export declare function validateSsn(input: string, opts?: ValidateSsnOptions): SsnValidationResult;
|
|
42
|
+
/** Convenience boolean wrapper */
|
|
43
|
+
export declare function isValidSsn(input: string, opts?: ValidateSsnOptions): boolean;
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateSsn = validateSsn;
|
|
4
|
+
exports.isValidSsn = isValidSsn;
|
|
5
|
+
const normalize_1 = require("./normalize");
|
|
6
|
+
const PUBLICLY_ADVERTISED = new Set([
|
|
7
|
+
'078-05-1120',
|
|
8
|
+
'721-07-4426',
|
|
9
|
+
'219-09-9999',
|
|
10
|
+
]);
|
|
11
|
+
/**
|
|
12
|
+
* Validates a US SSN with support for:
|
|
13
|
+
* - strict full validation
|
|
14
|
+
* - optional acceptance of no-dash input
|
|
15
|
+
* - rule modes: pre2011 | post2011 | both (default)
|
|
16
|
+
* - optional "checking-as-you-go" partial validation
|
|
17
|
+
*/ function validateSsn(input, opts = {}) {
|
|
18
|
+
var _a, _b, _c;
|
|
19
|
+
const allowNoDashes = (_a = opts.allowNoDashes) !== null && _a !== void 0 ? _a : true;
|
|
20
|
+
const ruleMode = (_b = opts.ruleMode) !== null && _b !== void 0 ? _b : 'both';
|
|
21
|
+
const allowPartial = (_c = opts.allowPartial) !== null && _c !== void 0 ? _c : false;
|
|
22
|
+
const normalized = (0, normalize_1.normalizeSsnInput)(input, {
|
|
23
|
+
allowNoDashes,
|
|
24
|
+
allowPartial,
|
|
25
|
+
});
|
|
26
|
+
if (!normalized.ok) {
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
error: 'INVALID_FORMAT',
|
|
30
|
+
message: normalized.message,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const digits = normalized.digits;
|
|
34
|
+
const area = digits.slice(0, 3);
|
|
35
|
+
const group = digits.slice(3, 5);
|
|
36
|
+
const serial = digits.slice(5, 9);
|
|
37
|
+
if (!allowPartial && PUBLICLY_ADVERTISED.has(normalized.normalized)) {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
error: 'PUBLICLY_ADVERTISED',
|
|
41
|
+
message: 'This SSN is a known publicly advertised (and invalid) value.',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const ruleCheck = allowPartial
|
|
45
|
+
? validatePartialSegments(area, group, serial, ruleMode)
|
|
46
|
+
: validateFullSegments(area, group, serial, ruleMode);
|
|
47
|
+
if (!ruleCheck.ok)
|
|
48
|
+
return ruleCheck;
|
|
49
|
+
return { ok: true, normalized: normalized.normalized };
|
|
50
|
+
}
|
|
51
|
+
/** Convenience boolean wrapper */
|
|
52
|
+
function isValidSsn(input, opts) {
|
|
53
|
+
return validateSsn(input, opts).ok;
|
|
54
|
+
}
|
|
55
|
+
function normalizePrefix(digits) {
|
|
56
|
+
// digits: 0..9 chars
|
|
57
|
+
if (digits.length <= 3)
|
|
58
|
+
return digits;
|
|
59
|
+
if (digits.length <= 5)
|
|
60
|
+
return `${digits.slice(0, 3)}-${digits.slice(3)}`;
|
|
61
|
+
return `${digits.slice(0, 3)}-${digits.slice(3, 5)}-${digits.slice(5)}`;
|
|
62
|
+
}
|
|
63
|
+
/* -------------------------- Rules -------------------------- */
|
|
64
|
+
function validateFullSegments(area, group, serial, ruleMode) {
|
|
65
|
+
const areaNum = Number(area);
|
|
66
|
+
const groupNum = Number(group);
|
|
67
|
+
const serialNum = Number(serial);
|
|
68
|
+
// Base Rule 2: area cannot be 000, 666, or 900–999
|
|
69
|
+
if (areaNum === 0 || areaNum === 666 || areaNum >= 900) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
error: 'INVALID_AREA',
|
|
73
|
+
message: 'Area number is not allowed (000, 666, and 900–999 are invalid).',
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// Ruleset selection:
|
|
77
|
+
// - pre2011: apply extra reserved ranges
|
|
78
|
+
// - post2011: do not
|
|
79
|
+
// - both: accept if EITHER passes (so we only fail if it violates post rules AND pre rules)
|
|
80
|
+
const violatesPre2011 = (areaNum >= 734 && areaNum <= 749) || areaNum >= 773;
|
|
81
|
+
if (ruleMode === 'pre2011' && violatesPre2011) {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
error: 'INVALID_AREA',
|
|
85
|
+
message: 'Area number is not allowed under pre–June 25, 2011 rules (734–749 and >= 773 are invalid).',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (ruleMode === 'both') {
|
|
89
|
+
// If it violates pre2011, it's still acceptable as post2011.
|
|
90
|
+
// So no action needed.
|
|
91
|
+
}
|
|
92
|
+
// Rule 3: group cannot be 00
|
|
93
|
+
if (groupNum === 0) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
error: 'INVALID_GROUP',
|
|
97
|
+
message: 'Group number may not be 00.',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// Rule 4: serial cannot be 0000
|
|
101
|
+
if (serialNum === 0) {
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
error: 'INVALID_SERIAL',
|
|
105
|
+
message: 'Serial number may not be 0000.',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// Rule 7: publicly advertised checked in validateSsn (full only)
|
|
109
|
+
return { ok: true, normalized: `${area}-${group}-${serial}` };
|
|
110
|
+
}
|
|
111
|
+
function validatePartialSegments(area, group, serial, ruleMode) {
|
|
112
|
+
// AREA checks while typing:
|
|
113
|
+
// - if first digit is '9', area can never be valid (since 900–999 invalid)
|
|
114
|
+
if (area.length >= 1 && area[0] === '9') {
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
error: 'INVALID_AREA',
|
|
118
|
+
message: 'Area numbers starting with 9 are not allowed.',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
// - if area is complete (3 digits), enforce base rule 2 and (optionally) pre2011 when in pre-only mode
|
|
122
|
+
if (area.length === 3) {
|
|
123
|
+
const areaNum = Number(area);
|
|
124
|
+
if (areaNum === 0 || areaNum === 666 || areaNum >= 900) {
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
error: 'INVALID_AREA',
|
|
128
|
+
message: 'Area number is not allowed (000, 666, and 900–999 are invalid).',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const violatesPre2011 = (areaNum >= 734 && areaNum <= 749) || areaNum >= 773;
|
|
132
|
+
if (ruleMode === 'pre2011' && violatesPre2011) {
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
error: 'INVALID_AREA',
|
|
136
|
+
message: 'Area number is not allowed under pre–June 25, 2011 rules (734–749 and >= 773 are invalid).',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// ruleMode "both" accepts either; "post2011" ignores extra restrictions.
|
|
140
|
+
}
|
|
141
|
+
// GROUP checks while typing:
|
|
142
|
+
// Only enforce once group is complete (2 digits)
|
|
143
|
+
if (group.length === 2) {
|
|
144
|
+
const groupNum = Number(group);
|
|
145
|
+
if (groupNum === 0) {
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
error: 'INVALID_GROUP',
|
|
149
|
+
message: 'Group number may not be 00.',
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// SERIAL checks while typing:
|
|
154
|
+
// Only enforce once serial is complete (4 digits)
|
|
155
|
+
if (serial.length === 4) {
|
|
156
|
+
const serialNum = Number(serial);
|
|
157
|
+
if (serialNum === 0) {
|
|
158
|
+
return {
|
|
159
|
+
ok: false,
|
|
160
|
+
error: 'INVALID_SERIAL',
|
|
161
|
+
message: 'Serial number may not be 0000.',
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
// Now that we have a full SSN in partial mode, also block publicly advertised.
|
|
165
|
+
const fullNormalized = `${area}-${group}-${serial}`;
|
|
166
|
+
if (PUBLICLY_ADVERTISED.has(fullNormalized)) {
|
|
167
|
+
return {
|
|
168
|
+
ok: false,
|
|
169
|
+
error: 'PUBLICLY_ADVERTISED',
|
|
170
|
+
message: 'This SSN is a known publicly advertised (and invalid) value.',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// If we haven't hit a definitive violation yet, it's valid "so far".
|
|
175
|
+
return {
|
|
176
|
+
ok: true,
|
|
177
|
+
normalized: normalizePrefix((area + group + serial).slice(0, 9)),
|
|
178
|
+
};
|
|
179
|
+
}
|
package/dist/yup.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as yup from 'yup';
|
|
2
|
+
import { type ValidateSsnOptions } from './validate';
|
|
3
|
+
/**
|
|
4
|
+
* Yup schema for "typing": normalizes to a prefix and allows partial validity.
|
|
5
|
+
* Note: transform runs before test in Yup.
|
|
6
|
+
*/
|
|
7
|
+
export declare function yupSsnTyping(opts?: Omit<ValidateSsnOptions, 'allowPartial'>): yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
|
|
8
|
+
/** Yup schema for full submit */
|
|
9
|
+
export declare function yupSsnSubmit(opts?: Omit<ValidateSsnOptions, 'allowPartial'>): yup.StringSchema<string | undefined, yup.AnyObject, undefined, "">;
|
package/dist/yup.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.yupSsnTyping = yupSsnTyping;
|
|
37
|
+
exports.yupSsnSubmit = yupSsnSubmit;
|
|
38
|
+
const yup = __importStar(require("yup"));
|
|
39
|
+
const validate_1 = require("./validate");
|
|
40
|
+
/**
|
|
41
|
+
* Yup schema for "typing": normalizes to a prefix and allows partial validity.
|
|
42
|
+
* Note: transform runs before test in Yup.
|
|
43
|
+
*/
|
|
44
|
+
function yupSsnTyping(opts = {}) {
|
|
45
|
+
return yup
|
|
46
|
+
.string()
|
|
47
|
+
.transform((value) => {
|
|
48
|
+
const v = (value !== null && value !== void 0 ? value : '').toString();
|
|
49
|
+
const res = (0, validate_1.validateSsn)(v, Object.assign(Object.assign({}, opts), { allowPartial: true }));
|
|
50
|
+
return res.ok ? res.normalized : v; // keep original if invalid; test will fail
|
|
51
|
+
})
|
|
52
|
+
.test('ssn-typing-valid', 'Invalid SSN', function (value) {
|
|
53
|
+
const v = (value !== null && value !== void 0 ? value : '').toString();
|
|
54
|
+
const res = (0, validate_1.validateSsn)(v, Object.assign(Object.assign({}, opts), { allowPartial: true }));
|
|
55
|
+
return res.ok
|
|
56
|
+
? true
|
|
57
|
+
: this.createError({
|
|
58
|
+
message: res.message,
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** Yup schema for full submit */
|
|
63
|
+
function yupSsnSubmit(opts = {}) {
|
|
64
|
+
return yup
|
|
65
|
+
.string()
|
|
66
|
+
.transform((value) => {
|
|
67
|
+
const v = (value !== null && value !== void 0 ? value : '').toString();
|
|
68
|
+
const res = (0, validate_1.validateSsn)(v, Object.assign(Object.assign({}, opts), { allowPartial: false }));
|
|
69
|
+
return res.ok ? res.normalized : v;
|
|
70
|
+
})
|
|
71
|
+
.test('ssn-submit-valid', 'Invalid SSN', function (value) {
|
|
72
|
+
const v = (value !== null && value !== void 0 ? value : '').toString();
|
|
73
|
+
const res = (0, validate_1.validateSsn)(v, Object.assign(Object.assign({}, opts), { allowPartial: false }));
|
|
74
|
+
return res.ok
|
|
75
|
+
? true
|
|
76
|
+
: this.createError({
|
|
77
|
+
message: res.message,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
package/dist/zod.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type ValidateSsnOptions } from './validate';
|
|
3
|
+
/**
|
|
4
|
+
* Zod schema for "typing": normalizes on every parse and allows partial input.
|
|
5
|
+
* Output is a normalized prefix in ###-##-#### style (e.g. "1234" -> "123-4").
|
|
6
|
+
*/
|
|
7
|
+
export declare function zodSsnTyping(opts?: Omit<ValidateSsnOptions, 'allowPartial'>): z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
8
|
+
/**
|
|
9
|
+
* Zod schema for "submit": requires a full SSN and returns fully normalized ###-##-####.
|
|
10
|
+
*/
|
|
11
|
+
export declare function zodSsnSubmit(opts?: Omit<ValidateSsnOptions, 'allowPartial'>): z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
package/dist/zod.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.zodSsnTyping = zodSsnTyping;
|
|
4
|
+
exports.zodSsnSubmit = zodSsnSubmit;
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const validate_1 = require("./validate");
|
|
7
|
+
/**
|
|
8
|
+
* Zod schema for "typing": normalizes on every parse and allows partial input.
|
|
9
|
+
* Output is a normalized prefix in ###-##-#### style (e.g. "1234" -> "123-4").
|
|
10
|
+
*/
|
|
11
|
+
function zodSsnTyping(opts = {}) {
|
|
12
|
+
return zod_1.z.string().transform((val, ctx) => {
|
|
13
|
+
const res = (0, validate_1.validateSsn)(val, Object.assign(Object.assign({}, opts), { allowPartial: true }));
|
|
14
|
+
if (!res.ok) {
|
|
15
|
+
ctx.addIssue({
|
|
16
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
17
|
+
message: res.message,
|
|
18
|
+
params: { ssnError: res.error },
|
|
19
|
+
});
|
|
20
|
+
return zod_1.z.NEVER;
|
|
21
|
+
}
|
|
22
|
+
return res.normalized; // normalized prefix
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Zod schema for "submit": requires a full SSN and returns fully normalized ###-##-####.
|
|
27
|
+
*/
|
|
28
|
+
function zodSsnSubmit(opts = {}) {
|
|
29
|
+
return zod_1.z.string().transform((val, ctx) => {
|
|
30
|
+
const res = (0, validate_1.validateSsn)(val, Object.assign(Object.assign({}, opts), { allowPartial: false }));
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
ctx.addIssue({
|
|
33
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
34
|
+
message: res.message,
|
|
35
|
+
params: { ssnError: res.error },
|
|
36
|
+
});
|
|
37
|
+
return zod_1.z.NEVER;
|
|
38
|
+
}
|
|
39
|
+
return res.normalized; // full normalized ###-##-####
|
|
40
|
+
});
|
|
41
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "us-ssn-tools",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Validate, format, mask, and generate US social security numbers",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "npx jest",
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"prepublishOnly": "npm run build",
|
|
14
|
+
"lint": "eslint 'src/**/*.ts' 'tests/**/*.ts'",
|
|
15
|
+
"format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts'"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/backupbrain/us-ssn-tools.git"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"identity",
|
|
23
|
+
"social",
|
|
24
|
+
"security",
|
|
25
|
+
"ssn",
|
|
26
|
+
"social",
|
|
27
|
+
"security",
|
|
28
|
+
"number",
|
|
29
|
+
"us",
|
|
30
|
+
"social",
|
|
31
|
+
"security",
|
|
32
|
+
"number",
|
|
33
|
+
"identity",
|
|
34
|
+
"verification"
|
|
35
|
+
],
|
|
36
|
+
"author": "backupbrain@gmail.com",
|
|
37
|
+
"license": "ISC",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/backupbrain/us-ssn-tools/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/backupbrain/us-ssn-tools#readme",
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@eslint/js": "^9.39.2",
|
|
44
|
+
"@types/jest": "^30.0.0",
|
|
45
|
+
"@typescript-eslint/eslint-plugin": "^8.52.0",
|
|
46
|
+
"@typescript-eslint/parser": "^8.52.0",
|
|
47
|
+
"eslint": "^9.39.2",
|
|
48
|
+
"eslint-config-prettier": "^10.1.8",
|
|
49
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
50
|
+
"globals": "^17.0.0",
|
|
51
|
+
"jest": "^30.2.0",
|
|
52
|
+
"jiti": "^2.6.1",
|
|
53
|
+
"prettier": "^3.7.4",
|
|
54
|
+
"ts-jest": "^29.4.6",
|
|
55
|
+
"typescript-eslint": "^8.52.0",
|
|
56
|
+
"yup": "^1.7.1",
|
|
57
|
+
"zod": "^4.3.5"
|
|
58
|
+
}
|
|
59
|
+
}
|