@link-foundation/sensitive-data-sanitizer 0.19.3 → 0.20.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/CHANGELOG.md +6 -0
- package/README.md +8 -1
- package/bin/sensitive-data-sanitizer.js +49 -28
- package/docs/API.md +192 -1
- package/docs/COVERAGE.md +3 -3
- package/docs/RULES.md +217 -0
- package/package.json +1 -1
- package/src/allow-list.js +93 -0
- package/src/confidence.js +24 -9
- package/src/context.js +121 -25
- package/src/credentials.js +44 -0
- package/src/crypto-addresses.js +74 -0
- package/src/detection.js +21 -6
- package/src/document-person-cues.js +1 -1
- package/src/encrypt.js +95 -0
- package/src/entities.js +4 -3
- package/src/fake.js +29 -1
- package/src/files.js +57 -12
- package/src/gitleaks.js +82 -27
- package/src/identifier-detector.js +36 -0
- package/src/identity.js +14 -0
- package/src/index.d.ts +116 -1
- package/src/index.js +7 -0
- package/src/national-checks.js +209 -0
- package/src/national.js +310 -92
- package/src/pattern-detector.js +219 -0
- package/src/personal-context.js +3 -2
- package/src/personal-evidence.js +233 -0
- package/src/personal.js +42 -10
- package/src/projection.js +85 -26
- package/src/rule-catalog.js +424 -0
- package/src/sanitizer.js +48 -3
- package/src/sarif.js +127 -0
- package/src/token-checksum.js +49 -0
- package/src/transforms.js +108 -25
- package/src/vendor/betterleaks/LICENSE +21 -0
- package/src/vendor/betterleaks/provenance.json +11 -0
- package/src/vendor/betterleaks/rules-0.json +463 -0
- package/src/vendor/betterleaks/rules-1.json +454 -0
- package/src/vendor/betterleaks/rules-2.json +461 -0
- package/src/vendor/betterleaks/rules-3.json +455 -0
- package/src/vendor/betterleaks/rules-4.json +483 -0
- package/src/vendor/betterleaks/rules-5.json +35 -0
- package/src/vendor/entities/aws.json +1 -1
- package/src/vendor/entities/azure.json +6 -2
- package/src/vendor/entities/google.json +2 -1
package/src/index.d.ts
CHANGED
|
@@ -100,6 +100,20 @@ export interface SanitizerOptions {
|
|
|
100
100
|
minConfidence?: number;
|
|
101
101
|
/** Alias for minConfidence; specifying both requires equal values. */
|
|
102
102
|
threshold?: number;
|
|
103
|
+
/** Per-type thresholds (e.g. `{ PERSON: 0.8 }`) that override minConfidence. */
|
|
104
|
+
minConfidenceByType?: Record<string, number>;
|
|
105
|
+
/**
|
|
106
|
+
* Values kept as-is: a string must equal the whole detected value and a
|
|
107
|
+
* RegExp must fully match it. Declared knownPersonal/knownSecrets values and
|
|
108
|
+
* credentials (unless allowCredentialDisable) are never allow-listed.
|
|
109
|
+
*/
|
|
110
|
+
allowList?: (string | RegExp)[];
|
|
111
|
+
/**
|
|
112
|
+
* Honour scanner suppression comments (`gitleaks:allow`, `pragma: allowlist
|
|
113
|
+
* secret`, `pragma: allowlist nextline secret`, `NOSONAR`, ...) under the
|
|
114
|
+
* same rules as allowList.
|
|
115
|
+
*/
|
|
116
|
+
inlineAllow?: boolean;
|
|
103
117
|
/** Explicit network/callback verification; credentials never qualify. */
|
|
104
118
|
verifyPublic?: (candidate: {
|
|
105
119
|
type: PublicEntity['type'];
|
|
@@ -122,6 +136,7 @@ export type DebugEvent =
|
|
|
122
136
|
| { event: 'detected'; count: number }
|
|
123
137
|
| { event: 'document-experience' | 'proper-name-prose'; count: number }
|
|
124
138
|
| { event: 'residual-redaction'; round: number }
|
|
139
|
+
| { event: 'template-value'; rule: string }
|
|
125
140
|
| { event: 'two-pass-confirmation'; type: string }
|
|
126
141
|
| { event: 'propagation-skipped'; reason: 'length'; type: string }
|
|
127
142
|
| { event: 'gitleaks-rule'; rule: string; length: number; ms: number };
|
|
@@ -292,6 +307,17 @@ export type Transformation =
|
|
|
292
307
|
keepEnd?: number;
|
|
293
308
|
marker?: string;
|
|
294
309
|
minLength?: number;
|
|
310
|
+
/** One masking character, default `*`; not combinable with marker. */
|
|
311
|
+
char?: string;
|
|
312
|
+
/** Characters left unmasked, e.g. `'-'` for `****-****-1111`. */
|
|
313
|
+
ignore?: string;
|
|
314
|
+
ignoreClasses?: (
|
|
315
|
+
'digits' | 'upper' | 'lower' | 'punctuation' | 'whitespace'
|
|
316
|
+
)[];
|
|
317
|
+
/** Mask at most this many maskable characters. */
|
|
318
|
+
count?: number;
|
|
319
|
+
/** Apply `count` from the end instead of the start. */
|
|
320
|
+
fromEnd?: boolean;
|
|
295
321
|
}
|
|
296
322
|
| { mode: 'pseudonym' | 'format-preserving'; key: string }
|
|
297
323
|
| {
|
|
@@ -308,7 +334,9 @@ export type Transformation =
|
|
|
308
334
|
>;
|
|
309
335
|
}
|
|
310
336
|
| { mode: 'date-shift'; days: number }
|
|
311
|
-
| { mode: 'bucket'; size: number }
|
|
337
|
+
| { mode: 'bucket'; size: number }
|
|
338
|
+
/** Deterministic AES-256-GCM `[ENCRYPTED:TYPE:…]`; key >= 16 UTF-8 bytes. */
|
|
339
|
+
| { mode: 'encrypt'; key: string };
|
|
312
340
|
|
|
313
341
|
/** Contains original personal data; never publish this object or its files. */
|
|
314
342
|
export interface RestoreMap {
|
|
@@ -392,6 +420,93 @@ export declare const entityCatalogs: Record<
|
|
|
392
420
|
'google' | 'azure' | 'aws',
|
|
393
421
|
{ source: string; sha256: string; names: string[] }
|
|
394
422
|
>;
|
|
423
|
+
/** Built-in identifier rule; `context` is tested near each `pattern` match. */
|
|
424
|
+
export interface IdentifierRule {
|
|
425
|
+
type: string;
|
|
426
|
+
/** Global regular expression for candidate values. */
|
|
427
|
+
pattern: RegExp;
|
|
428
|
+
/** Non-global label/context expression, 64 characters before to 32 after. */
|
|
429
|
+
context: RegExp;
|
|
430
|
+
/** Checksum or issuing-format check that raises confidence. */
|
|
431
|
+
checksum: (value: string) => boolean;
|
|
432
|
+
/** Distinctive shape that is reported without context. */
|
|
433
|
+
plausible?: (value: string) => boolean;
|
|
434
|
+
/** Mandatory gate; candidates failing it are never reported. */
|
|
435
|
+
valid?: (value: string) => boolean;
|
|
436
|
+
}
|
|
437
|
+
export declare const nationalRules: readonly IdentifierRule[];
|
|
438
|
+
/** Format and checksum validators; they never prove issuance. */
|
|
439
|
+
export declare const identifierChecks: Readonly<
|
|
440
|
+
Record<string, (value: string, ...args: number[]) => boolean>
|
|
441
|
+
> & {
|
|
442
|
+
digitsOf(value: string): string;
|
|
443
|
+
};
|
|
444
|
+
/** Wrap identifier rules as a Detector for `detectors` options. */
|
|
445
|
+
export declare function createIdentifierDetector(
|
|
446
|
+
rules: readonly IdentifierRule[],
|
|
447
|
+
options?: { id?: string }
|
|
448
|
+
): Detector;
|
|
449
|
+
/** Restore `[ENCRYPTED:TYPE:…]` tokens; throws ERR_MAP on a wrong key. */
|
|
450
|
+
export declare function decrypt(
|
|
451
|
+
text: string,
|
|
452
|
+
key: string,
|
|
453
|
+
options?: { json?: boolean }
|
|
454
|
+
): string;
|
|
455
|
+
export interface PatternSpec {
|
|
456
|
+
type: string;
|
|
457
|
+
category?: Category;
|
|
458
|
+
/** Finding rule id, default `custom-pattern`. */
|
|
459
|
+
rule?: string;
|
|
460
|
+
/** Default base confidence, 0.6. */
|
|
461
|
+
confidence?: number;
|
|
462
|
+
patterns?: (
|
|
463
|
+
| RegExp
|
|
464
|
+
| string
|
|
465
|
+
| { regex: RegExp | string; confidence?: number; group?: number }
|
|
466
|
+
)[];
|
|
467
|
+
/** Whole-word terms (case-insensitive unless caseSensitive). */
|
|
468
|
+
denyList?: string[];
|
|
469
|
+
caseSensitive?: boolean;
|
|
470
|
+
denyListConfidence?: number;
|
|
471
|
+
/** Hotwords that adjust confidence when found near a match. */
|
|
472
|
+
context?: string[];
|
|
473
|
+
/** Characters searched for context, default `{ before: 64, after: 16 }`. */
|
|
474
|
+
contextWindow?: number | { before?: number; after?: number };
|
|
475
|
+
/** Added when context is found (default 0.35); negative lowers it. */
|
|
476
|
+
contextBoost?: number;
|
|
477
|
+
/** false rejects the match; a number in 0..1 sets its confidence. */
|
|
478
|
+
validate?: (value: string) => boolean | number | undefined;
|
|
479
|
+
}
|
|
480
|
+
/** Declarative regex/deny-list recognizers (Presidio/Google custom infoType style). */
|
|
481
|
+
export declare function createPatternDetector(
|
|
482
|
+
specs: PatternSpec | readonly PatternSpec[],
|
|
483
|
+
options?: { id?: string }
|
|
484
|
+
): Detector;
|
|
485
|
+
export interface SarifLog {
|
|
486
|
+
$schema: string;
|
|
487
|
+
version: '2.1.0';
|
|
488
|
+
runs: unknown[];
|
|
489
|
+
}
|
|
490
|
+
/** SARIF 2.1.0 log of rules and locations; never contains matched values. */
|
|
491
|
+
export declare function toSarif(
|
|
492
|
+
report:
|
|
493
|
+
| {
|
|
494
|
+
complete?: boolean;
|
|
495
|
+
results: { path: string; findings: Finding[] }[];
|
|
496
|
+
skipped?: { path: string; reason: string }[];
|
|
497
|
+
}
|
|
498
|
+
| { path: string; findings: Finding[] }[]
|
|
499
|
+
| Finding[],
|
|
500
|
+
options?: { version?: string; path?: string }
|
|
501
|
+
): SarifLog;
|
|
502
|
+
/**
|
|
503
|
+
* Offline CRC32/base62 check of GitHub (gh[opsur]_) and npm (npm_) tokens:
|
|
504
|
+
* true when the checksum matches, false when it does not, undefined for
|
|
505
|
+
* other formats.
|
|
506
|
+
*/
|
|
507
|
+
export declare function tokenChecksum(value: string): boolean | undefined;
|
|
508
|
+
/** Builds a checksum-valid token from a prefix ending in `_` and a 30-character body. */
|
|
509
|
+
export declare function tokenWithChecksum(prefix: string, body: string): string;
|
|
395
510
|
export declare function createWikidataVerifier(options?: {
|
|
396
511
|
fetch?: typeof fetch;
|
|
397
512
|
language?: string;
|
package/src/index.js
CHANGED
|
@@ -26,6 +26,13 @@ export {
|
|
|
26
26
|
} from './external.js';
|
|
27
27
|
|
|
28
28
|
export { entityCatalogs } from './entities.js';
|
|
29
|
+
export { nationalRules } from './national.js';
|
|
30
|
+
export * as identifierChecks from './national-checks.js';
|
|
31
|
+
export { createIdentifierDetector } from './identifier-detector.js';
|
|
32
|
+
export { decrypt } from './encrypt.js';
|
|
33
|
+
export { createPatternDetector } from './pattern-detector.js';
|
|
34
|
+
export { toSarif } from './sarif.js';
|
|
35
|
+
export { tokenChecksum, tokenWithChecksum } from './token-checksum.js';
|
|
29
36
|
export {
|
|
30
37
|
sanitizeStream,
|
|
31
38
|
sanitizeStreamToFile,
|
package/src/national-checks.js
CHANGED
|
@@ -127,3 +127,212 @@ export function southAfricanId(value) {
|
|
|
127
127
|
shortLuhn(value)
|
|
128
128
|
);
|
|
129
129
|
}
|
|
130
|
+
const repeated = (digits) => /^(\d)\1+$/.test(digits);
|
|
131
|
+
// US ABA routing transit number: Federal Reserve prefix and 3-7-1 weights.
|
|
132
|
+
export function abaRouting(value) {
|
|
133
|
+
const d = digitsOf(value),
|
|
134
|
+
prefix = Number(d.slice(0, 2));
|
|
135
|
+
return (
|
|
136
|
+
d.length === 9 &&
|
|
137
|
+
!repeated(d) &&
|
|
138
|
+
(prefix <= 12 ||
|
|
139
|
+
(prefix >= 21 && prefix <= 32) ||
|
|
140
|
+
(prefix >= 61 && prefix <= 72) ||
|
|
141
|
+
prefix === 80) &&
|
|
142
|
+
sum(d, [3, 7, 1, 3, 7, 1, 3, 7, 1]) % 10 === 0
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
const dniLetters = 'TRWAGMYFPDXBNJZSQVHLCKE';
|
|
146
|
+
// Spanish NIE: X/Y/Z stands for 0/1/2 before the DNI mod-23 letter.
|
|
147
|
+
export function spanishNie(value) {
|
|
148
|
+
const v = value.toUpperCase();
|
|
149
|
+
return (
|
|
150
|
+
/^[XYZ]\d{7}[A-Z]$/.test(v) &&
|
|
151
|
+
dniLetters[Number(`${'XYZ'.indexOf(v[0])}${v.slice(1, 8)}`) % 23] === v[8]
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
// Australian Business Number: subtract one from the first digit, mod 89.
|
|
155
|
+
export function abn(value) {
|
|
156
|
+
const d = digitsOf(value).split('').map(Number);
|
|
157
|
+
if (d.length !== 11 || repeated(d.join(''))) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
d[0] -= 1;
|
|
161
|
+
return sum(d, [10, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19]) % 89 === 0;
|
|
162
|
+
}
|
|
163
|
+
// Australian Company Number: weighted complement mod 10.
|
|
164
|
+
export function acn(value) {
|
|
165
|
+
const d = digitsOf(value);
|
|
166
|
+
return (
|
|
167
|
+
d.length === 9 &&
|
|
168
|
+
!repeated(d) &&
|
|
169
|
+
(10 - (sum(d, [8, 7, 6, 5, 4, 3, 2, 1]) % 10)) % 10 === Number(d[8])
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
// Australian Medicare card: first digit 2-6, ninth digit is the check digit.
|
|
173
|
+
export function medicare(value) {
|
|
174
|
+
const d = digitsOf(value);
|
|
175
|
+
return (
|
|
176
|
+
/^[2-6]\d{9,10}$/.test(d) &&
|
|
177
|
+
sum(d, [1, 3, 7, 9, 1, 3, 7, 9]) % 10 === Number(d[8])
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
const verhoeffD = [
|
|
181
|
+
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
|
182
|
+
[1, 2, 3, 4, 0, 6, 7, 8, 9, 5],
|
|
183
|
+
[2, 3, 4, 0, 1, 7, 8, 9, 5, 6],
|
|
184
|
+
[3, 4, 0, 1, 2, 8, 9, 5, 6, 7],
|
|
185
|
+
[4, 0, 1, 2, 3, 9, 5, 6, 7, 8],
|
|
186
|
+
[5, 9, 8, 7, 6, 0, 4, 3, 2, 1],
|
|
187
|
+
[6, 5, 9, 8, 7, 1, 0, 4, 3, 2],
|
|
188
|
+
[7, 6, 5, 9, 8, 2, 1, 0, 4, 3],
|
|
189
|
+
[8, 7, 6, 5, 9, 3, 2, 1, 0, 4],
|
|
190
|
+
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0],
|
|
191
|
+
];
|
|
192
|
+
const verhoeffP = [
|
|
193
|
+
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
|
194
|
+
[1, 5, 7, 6, 2, 8, 3, 0, 9, 4],
|
|
195
|
+
[5, 8, 0, 3, 7, 9, 6, 1, 4, 2],
|
|
196
|
+
[8, 9, 1, 6, 0, 4, 3, 5, 2, 7],
|
|
197
|
+
[9, 4, 5, 3, 1, 2, 6, 8, 7, 0],
|
|
198
|
+
[4, 2, 8, 6, 5, 7, 3, 9, 0, 1],
|
|
199
|
+
[2, 7, 9, 3, 8, 0, 6, 4, 1, 5],
|
|
200
|
+
[7, 0, 4, 6, 9, 1, 3, 2, 5, 8],
|
|
201
|
+
];
|
|
202
|
+
export function verhoeff(value) {
|
|
203
|
+
const d = digitsOf(value);
|
|
204
|
+
let check = 0;
|
|
205
|
+
for (let i = 0; i < d.length; i++) {
|
|
206
|
+
check = verhoeffD[check][verhoeffP[i % 8][Number(d[d.length - 1 - i])]];
|
|
207
|
+
}
|
|
208
|
+
return d.length > 0 && !repeated(d) && check === 0;
|
|
209
|
+
}
|
|
210
|
+
// Indian Aadhaar: 12 digits, never starting with 0 or 1, Verhoeff check.
|
|
211
|
+
export const aadhaar = (value) =>
|
|
212
|
+
/^[2-9]\d{11}$/.test(digitsOf(value)) && verhoeff(value);
|
|
213
|
+
const base36 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
214
|
+
// Indian GSTIN: state code, PAN, entity number, "Z", mod-36 check character.
|
|
215
|
+
export function gstin(value) {
|
|
216
|
+
const v = value.toUpperCase();
|
|
217
|
+
if (!/^\d{2}[A-Z]{5}\d{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/.test(v)) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
let total = 0;
|
|
221
|
+
for (let i = 0; i < 14; i++) {
|
|
222
|
+
const product = base36.indexOf(v[i]) * (i % 2 === 0 ? 1 : 2);
|
|
223
|
+
total += Math.floor(product / 36) + (product % 36);
|
|
224
|
+
}
|
|
225
|
+
return base36[(36 - (total % 36)) % 36] === v[14];
|
|
226
|
+
}
|
|
227
|
+
// Finnish henkilötunnus: DDMMYY, century sign, individual number, mod 31.
|
|
228
|
+
export function finnishIdentityCode(value) {
|
|
229
|
+
const v = value.toUpperCase(),
|
|
230
|
+
match = /^(\d{6})([-+A-FU-Y])(\d{3})([0-9A-Y])$/.exec(v);
|
|
231
|
+
if (!match) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
const [, date, , individual, check] = match;
|
|
235
|
+
const swapped = `${date.slice(4, 6)}${date.slice(2, 4)}${date.slice(0, 2)}`;
|
|
236
|
+
return (
|
|
237
|
+
datePart(swapped) &&
|
|
238
|
+
'0123456789ABCDEFHJKLMNPRSTUVWXY'[Number(date + individual) % 31] === check
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
// Thai national identification number: 13 digits, weighted mod 11.
|
|
242
|
+
export function thaiId(value) {
|
|
243
|
+
const d = digitsOf(value);
|
|
244
|
+
return (
|
|
245
|
+
/^[1-8]\d{12}$/.test(d) &&
|
|
246
|
+
(11 - (sum(d, [13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2]) % 11)) % 10 ===
|
|
247
|
+
Number(d[12])
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
// US DEA registration: registrant letter, name letter or 9, six digits, check.
|
|
251
|
+
export function dea(value) {
|
|
252
|
+
const v = value.toUpperCase();
|
|
253
|
+
if (!/^[ABCDEFGHJKLMPRSTUX][A-Z9]\d{7}$/.test(v)) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
const d = v.slice(2);
|
|
257
|
+
return (
|
|
258
|
+
(Number(d[0]) +
|
|
259
|
+
Number(d[2]) +
|
|
260
|
+
Number(d[4]) +
|
|
261
|
+
2 * (Number(d[1]) + Number(d[3]) + Number(d[5]))) %
|
|
262
|
+
10 ===
|
|
263
|
+
Number(d[6])
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
const regions = new Intl.DisplayNames(['en'], {
|
|
267
|
+
type: 'region',
|
|
268
|
+
fallback: 'none',
|
|
269
|
+
});
|
|
270
|
+
// ISO 3166 alpha-2 region known to the runtime's Unicode CLDR data.
|
|
271
|
+
export const regionCode = (code) =>
|
|
272
|
+
/^[A-Z]{2}$/.test(code) && code !== 'ZZ' && regions.of(code) !== undefined;
|
|
273
|
+
// SWIFT/BIC: bank, ISO country, location, optional branch.
|
|
274
|
+
export const swiftCode = (value) =>
|
|
275
|
+
/^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}(?:[A-Z0-9]{3})?$/.test(value) &&
|
|
276
|
+
regionCode(value.slice(4, 6));
|
|
277
|
+
// ISO 3779 / FMVSS 115 vehicle identification number: position 9 check digit.
|
|
278
|
+
export function vin(value) {
|
|
279
|
+
const v = value.toUpperCase();
|
|
280
|
+
if (!/^[A-HJ-NPR-Z\d]{17}$/.test(v) || !/\d/.test(v) || !/[A-Z]/.test(v)) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
const weights = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2];
|
|
284
|
+
let total = 0;
|
|
285
|
+
for (let i = 0; i < 17; i++) {
|
|
286
|
+
const char = v[i];
|
|
287
|
+
// Transliteration: A-H 1-8, J-N 1-5, P 7, R 9, S-Z 2-9 (I, O, Q unused).
|
|
288
|
+
const code = /\d/.test(char)
|
|
289
|
+
? Number(char)
|
|
290
|
+
: Number('12345678 12345 7 923456789'[char.charCodeAt(0) - 65]);
|
|
291
|
+
total += code * weights[i];
|
|
292
|
+
}
|
|
293
|
+
const check = total % 11;
|
|
294
|
+
return (check === 10 ? 'X' : String(check)) === v[8];
|
|
295
|
+
}
|
|
296
|
+
// German Steuerliche Identifikationsnummer: ISO 7064 MOD 11,10; no leading 0.
|
|
297
|
+
// ISO 7064 MOD 11,10 over all digits but the last, which is the check digit.
|
|
298
|
+
function mod11x10(d) {
|
|
299
|
+
let product = 10;
|
|
300
|
+
for (const digit of d.slice(0, -1)) {
|
|
301
|
+
const total = (Number(digit) + product) % 10 || 10;
|
|
302
|
+
product = (total * 2) % 11;
|
|
303
|
+
}
|
|
304
|
+
return (11 - product) % 10 === Number(d.at(-1));
|
|
305
|
+
}
|
|
306
|
+
export function germanTaxId(value) {
|
|
307
|
+
const d = digitsOf(value);
|
|
308
|
+
return /^[1-9]\d{10}$/.test(d) && mod11x10(d);
|
|
309
|
+
}
|
|
310
|
+
// German USt-IdNr: DE plus eight digits and a MOD 11,10 check digit.
|
|
311
|
+
export function germanVat(value) {
|
|
312
|
+
const v = value.replace(/[\s.-]/g, '').toUpperCase();
|
|
313
|
+
return /^DE[1-9]\d{8}$/.test(v) && mod11x10(v.slice(2));
|
|
314
|
+
}
|
|
315
|
+
// UPS 1Z tracking number: letters map to (code - 63) mod 10, even positions
|
|
316
|
+
// are doubled, and the last character is the check digit.
|
|
317
|
+
export function upsTracking(value) {
|
|
318
|
+
const v = value.replace(/\s/g, '').toUpperCase();
|
|
319
|
+
if (!/^1Z[0-9A-Z]{15}\d$/.test(v)) {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
let total = 0;
|
|
323
|
+
for (let i = 2; i < 17; i++) {
|
|
324
|
+
const code = /\d/.test(v[i]) ? Number(v[i]) : (v.charCodeAt(i) - 63) % 10;
|
|
325
|
+
total += i % 2 === 1 ? code * 2 : code;
|
|
326
|
+
}
|
|
327
|
+
return (10 - (total % 10)) % 10 === Number(v[17]);
|
|
328
|
+
}
|
|
329
|
+
// UAE Emirates ID: 784, birth year, serial and a Luhn check digit.
|
|
330
|
+
export const emiratesId = (value) =>
|
|
331
|
+
/^784(?:19|20)\d{10}$/.test(digitsOf(value)) && shortLuhn(value);
|
|
332
|
+
// Saudi national ID (1…) and Iqama (2…) numbers use Luhn over 10 digits.
|
|
333
|
+
export const saudiId = (value) =>
|
|
334
|
+
/^[12]\d{9}$/.test(digitsOf(value)) && shortLuhn(value);
|
|
335
|
+
// Israeli Teudat Zehut: up to 9 digits, left-padded, Luhn-weighted 1,2,1,2…
|
|
336
|
+
export const israeliId = (value) =>
|
|
337
|
+
/^\d{5,9}$/.test(digitsOf(value)) &&
|
|
338
|
+
shortLuhn(digitsOf(value).padStart(9, '0'));
|