euredact 0.2.0 → 0.3.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/README.md +71 -1
- package/dist/cjs/cache.js +2 -2
- package/dist/cjs/index.js +10 -1
- package/dist/cjs/normalizer.js +16 -18
- package/dist/cjs/rules/bicRegistry.js +239 -0
- package/dist/cjs/rules/countries/index.js +95 -28
- package/dist/cjs/rules/engine.js +131 -19
- package/dist/cjs/rules/matchers.js +3 -1
- package/dist/cjs/rules/suppressors.js +246 -9
- package/dist/cjs/rules/validators.js +62 -12
- package/dist/cjs/sdk.js +20 -0
- package/dist/cjs/types.js +18 -2
- package/dist/esm/cache.js +2 -2
- package/dist/esm/index.d.ts +3 -1
- package/dist/esm/index.js +5 -1
- package/dist/esm/normalizer.js +16 -18
- package/dist/esm/rules/bicRegistry.d.ts +50 -0
- package/dist/esm/rules/bicRegistry.js +233 -0
- package/dist/esm/rules/countries/index.js +95 -28
- package/dist/esm/rules/engine.d.ts +8 -0
- package/dist/esm/rules/engine.js +130 -19
- package/dist/esm/rules/matchers.d.ts +1 -1
- package/dist/esm/rules/matchers.js +3 -1
- package/dist/esm/rules/suppressors.js +246 -9
- package/dist/esm/rules/validators.d.ts +25 -0
- package/dist/esm/rules/validators.js +60 -11
- package/dist/esm/sdk.d.ts +7 -0
- package/dist/esm/sdk.js +20 -0
- package/dist/esm/types.d.ts +15 -1
- package/dist/esm/types.js +17 -1
- package/package.json +8 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.VALIDATORS = void 0;
|
|
3
|
+
exports.VALIDATORS = exports.ISO_3166_ALPHA2 = exports.IBAN_LENGTHS = void 0;
|
|
4
4
|
exports.validateIban = validateIban;
|
|
5
5
|
exports.validateBsn = validateBsn;
|
|
6
6
|
exports.validateBelgianNn = validateBelgianNn;
|
|
@@ -14,6 +14,7 @@ exports.validateGermanTaxId = validateGermanTaxId;
|
|
|
14
14
|
exports.validateFrenchNir = validateFrenchNir;
|
|
15
15
|
exports.validateVin = validateVin;
|
|
16
16
|
exports.validateBic = validateBic;
|
|
17
|
+
exports.validateE164 = validateE164;
|
|
17
18
|
exports.validateKvk = validateKvk;
|
|
18
19
|
exports.validateSwedishPnr = validateSwedishPnr;
|
|
19
20
|
exports.validateNorwegianFnr = validateNorwegianFnr;
|
|
@@ -46,21 +47,45 @@ exports.validateGreekAmka = validateGreekAmka;
|
|
|
46
47
|
function clean(candidate, chars = /[\s.\-]/g) {
|
|
47
48
|
return candidate.replace(chars, "");
|
|
48
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Total IBAN length per country (ISO 13616 registry). Also used to build the
|
|
52
|
+
* country-independent IBAN pattern in rules/countries, so an IBAN is detected
|
|
53
|
+
* on its own structure rather than on the caller's `countries` setting.
|
|
54
|
+
*/
|
|
55
|
+
exports.IBAN_LENGTHS = {
|
|
56
|
+
AL: 28, AD: 24, AT: 20, AZ: 28, BH: 22, BY: 28, BE: 16, BA: 20, BR: 29, BG: 22, CR: 22, HR: 21,
|
|
57
|
+
CY: 28, CZ: 24, DK: 18, DO: 28, TL: 23, EE: 20, FO: 18, FI: 18, FR: 27, GE: 22, DE: 22, GI: 23,
|
|
58
|
+
GR: 27, GL: 18, GT: 28, HU: 28, IS: 26, IQ: 23, IE: 22, IL: 23, IT: 27, JO: 30, KZ: 20, XK: 20,
|
|
59
|
+
KW: 30, LV: 21, LB: 28, LI: 21, LT: 20, LU: 20, MT: 31, MR: 27, MU: 30, MC: 27, MD: 24, ME: 22,
|
|
60
|
+
NL: 18, MK: 19, NO: 15, PK: 24, PS: 29, PL: 28, PT: 25, QA: 29, RO: 24, LC: 32, SM: 27, SA: 24,
|
|
61
|
+
RS: 22, SC: 31, SK: 24, SI: 19, ES: 24, SE: 24, CH: 21, TN: 24, TR: 26, UA: 29, AE: 23, GB: 22,
|
|
62
|
+
VA: 22, VG: 24,
|
|
63
|
+
};
|
|
64
|
+
/** ISO 3166-1 alpha-2 codes, plus XK (Kosovo, user-assigned but in banking use). */
|
|
65
|
+
exports.ISO_3166_ALPHA2 = new Set([
|
|
66
|
+
"AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", "AS", "AT", "AU", "AW", "AX", "AZ",
|
|
67
|
+
"BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BL", "BM", "BN", "BO", "BQ", "BR", "BS",
|
|
68
|
+
"BT", "BV", "BW", "BY", "BZ", "CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN",
|
|
69
|
+
"CO", "CR", "CU", "CV", "CW", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE",
|
|
70
|
+
"EG", "EH", "ER", "ES", "ET", "FI", "FJ", "FK", "FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF",
|
|
71
|
+
"GG", "GH", "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM",
|
|
72
|
+
"HN", "HR", "HT", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR", "IS", "IT", "JE", "JM",
|
|
73
|
+
"JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC",
|
|
74
|
+
"LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MF", "MG", "MH", "MK",
|
|
75
|
+
"ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA",
|
|
76
|
+
"NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PA", "PE", "PF", "PG",
|
|
77
|
+
"PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", "QA", "RE", "RO", "RS", "RU", "RW",
|
|
78
|
+
"SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SO", "SR", "SS",
|
|
79
|
+
"ST", "SV", "SX", "SY", "SZ", "TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO",
|
|
80
|
+
"TR", "TT", "TV", "TW", "TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI",
|
|
81
|
+
"VN", "VU", "WF", "WS", "XK", "YE", "YT", "ZA", "ZM", "ZW",
|
|
82
|
+
]);
|
|
49
83
|
function validateIban(candidate) {
|
|
50
84
|
const c = candidate.replace(/[\s\-]/g, "").toUpperCase();
|
|
51
85
|
if (c.length < 5 || !/^[A-Z]{2}/.test(c) || !/^\d{2}$/.test(c.slice(2, 4)))
|
|
52
86
|
return false;
|
|
53
|
-
const ibanLengths = {
|
|
54
|
-
AL: 28, AD: 24, AT: 20, AZ: 28, BH: 22, BY: 28, BE: 16, BA: 20, BR: 29, BG: 22, CR: 22, HR: 21,
|
|
55
|
-
CY: 28, CZ: 24, DK: 18, DO: 28, TL: 23, EE: 20, FO: 18, FI: 18, FR: 27, GE: 22, DE: 22, GI: 23,
|
|
56
|
-
GR: 27, GL: 18, GT: 28, HU: 28, IS: 26, IQ: 23, IE: 22, IL: 23, IT: 27, JO: 30, KZ: 20, XK: 20,
|
|
57
|
-
KW: 30, LV: 21, LB: 28, LI: 21, LT: 20, LU: 20, MT: 31, MR: 27, MU: 30, MC: 27, MD: 24, ME: 22,
|
|
58
|
-
NL: 18, MK: 19, NO: 15, PK: 24, PS: 29, PL: 28, PT: 25, QA: 29, RO: 24, LC: 32, SM: 27, SA: 24,
|
|
59
|
-
RS: 22, SC: 31, SK: 24, SI: 19, ES: 24, SE: 24, CH: 21, TN: 24, TR: 26, UA: 29, AE: 23, GB: 22,
|
|
60
|
-
VA: 22, VG: 24,
|
|
61
|
-
};
|
|
62
87
|
const country = c.slice(0, 2);
|
|
63
|
-
const expected =
|
|
88
|
+
const expected = exports.IBAN_LENGTHS[country];
|
|
64
89
|
if (expected !== undefined && c.length !== expected)
|
|
65
90
|
return false;
|
|
66
91
|
const rearranged = c.slice(4) + c.slice(0, 4);
|
|
@@ -206,13 +231,22 @@ function validateVin(candidate) {
|
|
|
206
231
|
return false;
|
|
207
232
|
return !/[IOQ]/.test(c);
|
|
208
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* BIC/SWIFT structural validation (ISO 9362): BBBB CC LL (PPP).
|
|
236
|
+
*
|
|
237
|
+
* Necessary but **not sufficient** — BIC has no check digit, and characters
|
|
238
|
+
* 5-6 of ordinary uppercase words are frequently valid country codes
|
|
239
|
+
* (`DRINGEND` -> `GE`, `HOSPITAL` -> `IT`). Emission additionally requires a
|
|
240
|
+
* registry hit or banking context; see `suppressBicWithoutEvidence`.
|
|
241
|
+
*/
|
|
209
242
|
function validateBic(candidate) {
|
|
210
243
|
const c = candidate.replace(/ /g, "").toUpperCase();
|
|
211
244
|
if (c.length !== 8 && c.length !== 11)
|
|
212
245
|
return false;
|
|
213
246
|
if (!/^[A-Z]{4}/.test(c))
|
|
214
247
|
return false;
|
|
215
|
-
|
|
248
|
+
// Country code must be a real ISO 3166-1 alpha-2 code
|
|
249
|
+
if (!exports.ISO_3166_ALPHA2.has(c.slice(4, 6)))
|
|
216
250
|
return false;
|
|
217
251
|
if (!/^[A-Z0-9]{2}$/.test(c.slice(6, 8)))
|
|
218
252
|
return false;
|
|
@@ -220,6 +254,21 @@ function validateBic(candidate) {
|
|
|
220
254
|
return false;
|
|
221
255
|
return true;
|
|
222
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* International phone number in E.164 form: `+` then 8-15 digits.
|
|
259
|
+
*
|
|
260
|
+
* A `+` prefix makes a phone number self-identifying, so this backs a
|
|
261
|
+
* country-independent pattern: the per-country patterns each hard-code one
|
|
262
|
+
* grouping and miss the others (`+43 664 8213 907` was invisible because the
|
|
263
|
+
* Austrian pattern expects an unbroken subscriber number).
|
|
264
|
+
*/
|
|
265
|
+
function validateE164(candidate) {
|
|
266
|
+
if (!candidate.trimStart().startsWith("+"))
|
|
267
|
+
return false;
|
|
268
|
+
// A trunk prefix written as "(0)" is not part of the E.164 digit count.
|
|
269
|
+
const digits = candidate.replace(/\(\s*0\s*\)/g, "").replace(/\D/g, "");
|
|
270
|
+
return digits.length >= 8 && digits.length <= 15;
|
|
271
|
+
}
|
|
223
272
|
function validateKvk(candidate) {
|
|
224
273
|
const c = clean(candidate);
|
|
225
274
|
return c.length === 8 && /^\d{8}$/.test(c);
|
|
@@ -666,6 +715,7 @@ exports.VALIDATORS = {
|
|
|
666
715
|
french_nir: validateFrenchNir,
|
|
667
716
|
vin: validateVin,
|
|
668
717
|
bic: validateBic,
|
|
718
|
+
e164: validateE164,
|
|
669
719
|
kvk: validateKvk,
|
|
670
720
|
swedish_pnr: validateSwedishPnr,
|
|
671
721
|
norwegian_fnr: validateNorwegianFnr,
|
package/dist/cjs/sdk.js
CHANGED
|
@@ -17,16 +17,36 @@ class ReferentialMapper {
|
|
|
17
17
|
}
|
|
18
18
|
return this.mapping.get(text);
|
|
19
19
|
}
|
|
20
|
+
clear() {
|
|
21
|
+
this.counters.clear();
|
|
22
|
+
this.mapping.clear();
|
|
23
|
+
}
|
|
20
24
|
}
|
|
25
|
+
const DEFAULT_MAX_INPUT_LENGTH = 10_485_760; // ~10 MB of text
|
|
21
26
|
class EuRedact {
|
|
22
27
|
engine = new engine_js_1.RuleEngine();
|
|
23
28
|
cache = new cache_js_1.ResultCache();
|
|
24
29
|
referentialMapper = new ReferentialMapper();
|
|
30
|
+
maxInputLength;
|
|
31
|
+
constructor(options) {
|
|
32
|
+
this.maxInputLength = options?.maxInputLength ?? DEFAULT_MAX_INPUT_LENGTH;
|
|
33
|
+
}
|
|
25
34
|
addCustomPattern(name, pattern) {
|
|
26
35
|
this.engine.addCustomPattern(name, pattern);
|
|
27
36
|
this.cache.clear();
|
|
28
37
|
}
|
|
38
|
+
/** Clear the result cache and referential integrity mappings.
|
|
39
|
+
* Call this in long-running processes to free PII from memory. */
|
|
40
|
+
clear() {
|
|
41
|
+
this.cache.clear();
|
|
42
|
+
this.referentialMapper.clear();
|
|
43
|
+
}
|
|
29
44
|
redact(text, options = {}) {
|
|
45
|
+
if (text.length > this.maxInputLength) {
|
|
46
|
+
throw new Error(`Input text length (${text.length.toLocaleString()} chars) exceeds the maximum ` +
|
|
47
|
+
`(${this.maxInputLength.toLocaleString()} chars). Split the input or ` +
|
|
48
|
+
`increase maxInputLength when constructing EuRedact.`);
|
|
49
|
+
}
|
|
30
50
|
const { countries = null, mode = "rules", referentialIntegrity = false, detectDates = false, cache = true, } = options;
|
|
31
51
|
const [normalizedText, offsetMapping] = (0, normalizer_js_1.normalize)(text);
|
|
32
52
|
const countriesTuple = countries
|
package/dist/cjs/types.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DetectionSource = exports.EntityType = void 0;
|
|
3
|
+
exports.DetectionSource = exports.LEGACY_TYPE_ALIASES = exports.EntityType = void 0;
|
|
4
4
|
var EntityType;
|
|
5
5
|
(function (EntityType) {
|
|
6
6
|
EntityType["NAME"] = "NAME";
|
|
7
7
|
EntityType["ADDRESS"] = "ADDRESS";
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Canonical name for a bank account identifier. `IBAN` is kept below as a
|
|
10
|
+
* legacy alias with the same value, so `EntityType.IBAN === EntityType.BANK_ACCOUNT`
|
|
11
|
+
* and existing code keeps working — but the emitted value, and therefore the
|
|
12
|
+
* `[BANK_ACCOUNT]` placeholder written into redacted text, is canonical.
|
|
13
|
+
*/
|
|
14
|
+
EntityType["BANK_ACCOUNT"] = "BANK_ACCOUNT";
|
|
15
|
+
/** @deprecated legacy alias of {@link EntityType.BANK_ACCOUNT} */
|
|
16
|
+
EntityType["IBAN"] = "BANK_ACCOUNT";
|
|
9
17
|
EntityType["BIC"] = "BIC";
|
|
10
18
|
EntityType["CREDIT_CARD"] = "CREDIT_CARD";
|
|
11
19
|
EntityType["PHONE"] = "PHONE";
|
|
@@ -35,6 +43,14 @@ var EntityType;
|
|
|
35
43
|
EntityType["SECRET"] = "SECRET";
|
|
36
44
|
EntityType["OTHER"] = "OTHER";
|
|
37
45
|
})(EntityType || (exports.EntityType = EntityType = {}));
|
|
46
|
+
/**
|
|
47
|
+
* Legacy type names accepted on input and mapped to their canonical form.
|
|
48
|
+
* Kept in sync with the pipeline canon (config/entity_types.json →
|
|
49
|
+
* legacy_aliases). The engine never *emits* these names.
|
|
50
|
+
*/
|
|
51
|
+
exports.LEGACY_TYPE_ALIASES = {
|
|
52
|
+
IBAN: "BANK_ACCOUNT",
|
|
53
|
+
};
|
|
38
54
|
var DetectionSource;
|
|
39
55
|
(function (DetectionSource) {
|
|
40
56
|
DetectionSource["RULES"] = "rules";
|
package/dist/esm/cache.js
CHANGED
|
@@ -18,10 +18,10 @@ export class ResultCache {
|
|
|
18
18
|
key(text, countries, mode) {
|
|
19
19
|
const sorted = [...countries].sort();
|
|
20
20
|
const raw = `${text}|${sorted.join("|")}|${mode}`;
|
|
21
|
-
//
|
|
21
|
+
// Include raw length to reduce collision risk alongside dual FNV-1a hashes
|
|
22
22
|
const h1 = fnv1aHash(raw);
|
|
23
23
|
const h2 = fnv1aHash(raw.slice(Math.floor(raw.length / 2)) + raw.slice(0, Math.floor(raw.length / 2)));
|
|
24
|
-
return h1
|
|
24
|
+
return `${raw.length}:${h1}${h2}`;
|
|
25
25
|
}
|
|
26
26
|
get(key) {
|
|
27
27
|
if (!this.enabled)
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
export { EntityType, DetectionSource } from "./types.js";
|
|
1
|
+
export { EntityType, DetectionSource, LEGACY_TYPE_ALIASES } from "./types.js";
|
|
2
2
|
export type { Detection, RedactResult, PatternDef, CountryConfig } from "./types.js";
|
|
3
3
|
export { EuRedact, type RedactOptions } from "./sdk.js";
|
|
4
4
|
export { COUNTRY_CONFIGS } from "./rules/countries/index.js";
|
|
5
|
+
export { setBicRegistry, getBicRegistry, SEED_BIC6_PREFIXES, type BicRegistryProvider } from "./rules/bicRegistry.js";
|
|
5
6
|
import { type RedactOptions } from "./sdk.js";
|
|
6
7
|
import type { RedactResult } from "./types.js";
|
|
7
8
|
export declare function availableCountries(): string[];
|
|
8
9
|
export declare function redact(text: string, options?: RedactOptions): RedactResult;
|
|
9
10
|
export declare function addCustomPattern(name: string, pattern: string): void;
|
|
11
|
+
export declare function clear(): void;
|
|
10
12
|
export declare function redactBatch(texts: string[], options?: RedactOptions): RedactResult[];
|
package/dist/esm/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export { EntityType, DetectionSource } from "./types.js";
|
|
1
|
+
export { EntityType, DetectionSource, LEGACY_TYPE_ALIASES } from "./types.js";
|
|
2
2
|
export { EuRedact } from "./sdk.js";
|
|
3
3
|
export { COUNTRY_CONFIGS } from "./rules/countries/index.js";
|
|
4
|
+
export { setBicRegistry, getBicRegistry, SEED_BIC6_PREFIXES } from "./rules/bicRegistry.js";
|
|
4
5
|
import { EuRedact } from "./sdk.js";
|
|
5
6
|
import { COUNTRY_CONFIGS } from "./rules/countries/index.js";
|
|
6
7
|
let _instance = null;
|
|
@@ -18,6 +19,9 @@ export function redact(text, options) {
|
|
|
18
19
|
export function addCustomPattern(name, pattern) {
|
|
19
20
|
getInstance().addCustomPattern(name, pattern);
|
|
20
21
|
}
|
|
22
|
+
export function clear() {
|
|
23
|
+
getInstance().clear();
|
|
24
|
+
}
|
|
21
25
|
export function redactBatch(texts, options) {
|
|
22
26
|
return getInstance().redactBatch(texts, options);
|
|
23
27
|
}
|
package/dist/esm/normalizer.js
CHANGED
|
@@ -5,27 +5,25 @@ export function normalize(text) {
|
|
|
5
5
|
return [normalized, buildOffsetMapping(text, normalized)];
|
|
6
6
|
}
|
|
7
7
|
function buildOffsetMapping(original, normalized) {
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
8
|
+
// Precompute cumulative NFD lengths for normalized characters
|
|
9
|
+
const normNfdCum = [0];
|
|
10
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
11
|
+
normNfdCum.push(normNfdCum[i] + normalized[i].normalize("NFD").length);
|
|
12
|
+
}
|
|
13
|
+
// Precompute cumulative NFD lengths for original characters
|
|
14
|
+
const origNfdCum = [0];
|
|
15
|
+
for (let i = 0; i < original.length; i++) {
|
|
16
|
+
origNfdCum.push(origNfdCum[i] + original[i].normalize("NFD").length);
|
|
17
|
+
}
|
|
18
|
+
// Two-pointer scan: map each normalized position to its original position
|
|
11
19
|
const normToOrig = [];
|
|
20
|
+
let origPtr = 0;
|
|
12
21
|
for (let normIdx = 0; normIdx < normalized.length; normIdx++) {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
nfdStart += normalized[i].normalize("NFD").length;
|
|
17
|
-
}
|
|
18
|
-
let targetOrig = 0;
|
|
19
|
-
let running = 0;
|
|
20
|
-
for (let oi = 0; oi < original.length; oi++) {
|
|
21
|
-
const nfdOc = original[oi].normalize("NFD");
|
|
22
|
-
if (running + nfdOc.length > nfdStart) {
|
|
23
|
-
targetOrig = oi;
|
|
24
|
-
break;
|
|
25
|
-
}
|
|
26
|
-
running += nfdOc.length;
|
|
22
|
+
const nfdStart = normNfdCum[normIdx];
|
|
23
|
+
while (origPtr + 1 < original.length && origNfdCum[origPtr + 1] <= nfdStart) {
|
|
24
|
+
origPtr++;
|
|
27
25
|
}
|
|
28
|
-
normToOrig.push(
|
|
26
|
+
normToOrig.push(origPtr);
|
|
29
27
|
}
|
|
30
28
|
return normToOrig;
|
|
31
29
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BIC registry — tier-1 membership testing for ISO 9362 bank identifier codes.
|
|
3
|
+
*
|
|
4
|
+
* BIC is the only bank identifier in the supported set with **no check digit**
|
|
5
|
+
* (IBAN has mod-97, VAT has country-specific checksums). Structure alone cannot
|
|
6
|
+
* separate a genuine BIC from an ordinary 8- or 11-character uppercase word,
|
|
7
|
+
* because characters 5-6 of everyday words are frequently valid ISO 3166
|
|
8
|
+
* country codes (`DRINGEND` -> `GE`, `HOSPITAL` -> `IT`). Registry membership
|
|
9
|
+
* is therefore the strongest precision signal available.
|
|
10
|
+
*
|
|
11
|
+
* Two sources are consulted, in order:
|
|
12
|
+
*
|
|
13
|
+
* 1. A deployment-supplied registry, installed via {@link setBicRegistry}.
|
|
14
|
+
* This is the path for anyone holding a licensed directory (SWIFTRef).
|
|
15
|
+
* 2. {@link SEED_BIC6_PREFIXES} — a small bundled set of institution+country
|
|
16
|
+
* prefixes for major European banks, compiled from publicly published bank
|
|
17
|
+
* data. No licensed directory is bundled.
|
|
18
|
+
*
|
|
19
|
+
* Matching is on the **BIC6 prefix** (4-letter institution + 2-letter country),
|
|
20
|
+
* so any location and branch code on a known institution is accepted. The
|
|
21
|
+
* prefix is the stable part of a BIC; branch codes churn as branches open and
|
|
22
|
+
* close.
|
|
23
|
+
*
|
|
24
|
+
* The registry is used as an *accept* signal, never as a filter: a candidate
|
|
25
|
+
* that misses it falls through to the context gate in `suppressors.ts` and is
|
|
26
|
+
* still detected when banking context is present. A stale list therefore costs
|
|
27
|
+
* a little recall on bare, contextless BICs — it never causes a leak. Annual
|
|
28
|
+
* review is sufficient.
|
|
29
|
+
*/
|
|
30
|
+
export declare const SEED_BIC6_PREFIXES: ReadonlySet<string>;
|
|
31
|
+
/** A membership test over BIC8/BIC11 codes or bare BIC6 prefixes. */
|
|
32
|
+
export type BicRegistryProvider = (bic: string) => boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Install a deployment-supplied BIC registry, consulted ahead of the seed list.
|
|
35
|
+
*
|
|
36
|
+
* Accepts a membership callable, any iterable of BIC strings, or `null` to
|
|
37
|
+
* remove a previously installed registry. Entries may be full BIC8/BIC11 codes
|
|
38
|
+
* or bare BIC6 prefixes; both are matched, case-insensitively and ignoring
|
|
39
|
+
* spaces.
|
|
40
|
+
*/
|
|
41
|
+
export declare function setBicRegistry(registry: BicRegistryProvider | Iterable<string> | null): void;
|
|
42
|
+
/** Return the installed registry provider, or null if none is installed. */
|
|
43
|
+
export declare function getBicRegistry(): BicRegistryProvider | null;
|
|
44
|
+
/**
|
|
45
|
+
* Tier 1: is `candidate` a BIC of a known institution?
|
|
46
|
+
*
|
|
47
|
+
* Checks the deployment-supplied registry first (full code, then BIC6 prefix),
|
|
48
|
+
* then the bundled seed prefixes.
|
|
49
|
+
*/
|
|
50
|
+
export declare function isRegisteredBic(candidate: string): boolean;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BIC registry — tier-1 membership testing for ISO 9362 bank identifier codes.
|
|
3
|
+
*
|
|
4
|
+
* BIC is the only bank identifier in the supported set with **no check digit**
|
|
5
|
+
* (IBAN has mod-97, VAT has country-specific checksums). Structure alone cannot
|
|
6
|
+
* separate a genuine BIC from an ordinary 8- or 11-character uppercase word,
|
|
7
|
+
* because characters 5-6 of everyday words are frequently valid ISO 3166
|
|
8
|
+
* country codes (`DRINGEND` -> `GE`, `HOSPITAL` -> `IT`). Registry membership
|
|
9
|
+
* is therefore the strongest precision signal available.
|
|
10
|
+
*
|
|
11
|
+
* Two sources are consulted, in order:
|
|
12
|
+
*
|
|
13
|
+
* 1. A deployment-supplied registry, installed via {@link setBicRegistry}.
|
|
14
|
+
* This is the path for anyone holding a licensed directory (SWIFTRef).
|
|
15
|
+
* 2. {@link SEED_BIC6_PREFIXES} — a small bundled set of institution+country
|
|
16
|
+
* prefixes for major European banks, compiled from publicly published bank
|
|
17
|
+
* data. No licensed directory is bundled.
|
|
18
|
+
*
|
|
19
|
+
* Matching is on the **BIC6 prefix** (4-letter institution + 2-letter country),
|
|
20
|
+
* so any location and branch code on a known institution is accepted. The
|
|
21
|
+
* prefix is the stable part of a BIC; branch codes churn as branches open and
|
|
22
|
+
* close.
|
|
23
|
+
*
|
|
24
|
+
* The registry is used as an *accept* signal, never as a filter: a candidate
|
|
25
|
+
* that misses it falls through to the context gate in `suppressors.ts` and is
|
|
26
|
+
* still detected when banking context is present. A stale list therefore costs
|
|
27
|
+
* a little recall on bare, contextless BICs — it never causes a leak. Annual
|
|
28
|
+
* review is sufficient.
|
|
29
|
+
*/
|
|
30
|
+
// BIC6 prefixes (institution + country) of major European institutions,
|
|
31
|
+
// compiled from publicly published bank data — not from a licensed directory.
|
|
32
|
+
// Kept deliberately small: this covers the banks that appear bare and
|
|
33
|
+
// contextless in real documents. Anything outside this list is still detected
|
|
34
|
+
// via the context gate.
|
|
35
|
+
export const SEED_BIC6_PREFIXES = new Set([
|
|
36
|
+
// --- Netherlands ---
|
|
37
|
+
"ABNANL", // ABN AMRO
|
|
38
|
+
"INGBNL", // ING
|
|
39
|
+
"RABONL", // Rabobank
|
|
40
|
+
"SNSBNL", // SNS Bank
|
|
41
|
+
"TRIONL", // Triodos
|
|
42
|
+
"BUNQNL", // bunq
|
|
43
|
+
"KNABNL", // Knab
|
|
44
|
+
"ASNBNL", // ASN Bank
|
|
45
|
+
"RBRBNL", // RegioBank
|
|
46
|
+
"FVLBNL", // Van Lanschot
|
|
47
|
+
"BITSNL", // BinckBank
|
|
48
|
+
"DEUTNL", // Deutsche Bank NL
|
|
49
|
+
// --- Belgium ---
|
|
50
|
+
"BBRUBE", // BNP Paribas Fortis
|
|
51
|
+
"GEBABE", // BNP Paribas Fortis (legacy)
|
|
52
|
+
"KREDBE", // KBC
|
|
53
|
+
"GKCCBE", // Belfius
|
|
54
|
+
"BPOTBE", // bpost bank
|
|
55
|
+
"ARSPBE", // Argenta
|
|
56
|
+
"AXABBE", // AXA Bank Belgium
|
|
57
|
+
"CREGBE", // Belfius (legacy)
|
|
58
|
+
"NICABE", // Crelan
|
|
59
|
+
"TRIOBE", // Triodos Belgium
|
|
60
|
+
"CTBKBE", // Banca Monte Paschi Belgio
|
|
61
|
+
"JVBABE", // Van Breda
|
|
62
|
+
// --- Germany ---
|
|
63
|
+
"DEUTDE", // Deutsche Bank
|
|
64
|
+
"COBADE", // Commerzbank
|
|
65
|
+
"PBNKDE", // Postbank
|
|
66
|
+
"GENODE", // Volksbanken / Raiffeisen
|
|
67
|
+
"MALADE", // Sparkassen
|
|
68
|
+
"HELADE", // Helaba / Sparkassen
|
|
69
|
+
"BYLADE", // BayernLB / Sparkassen
|
|
70
|
+
"SOLADE", // Sparkassen (Baden-Wurttemberg)
|
|
71
|
+
"WELADE", // Sparkassen (Westfalen)
|
|
72
|
+
"NOLADE", // Norddeutsche Landesbank
|
|
73
|
+
"SPKKDE", // Sparkasse
|
|
74
|
+
"HYVEDE", // UniCredit / HypoVereinsbank
|
|
75
|
+
"DRESDE", // Dresdner (legacy Commerzbank)
|
|
76
|
+
"INGDDE", // ING-DiBa
|
|
77
|
+
"NTSBDE", // N26
|
|
78
|
+
"SFRTDE", // Sutor / fintech BaaS
|
|
79
|
+
"TEDADE", // Telda / BaaS
|
|
80
|
+
// --- France ---
|
|
81
|
+
"BNPAFR", // BNP Paribas
|
|
82
|
+
"SOGEFR", // Societe Generale
|
|
83
|
+
"AGRIFR", // Credit Agricole
|
|
84
|
+
"CCBPFR", // Banque Populaire
|
|
85
|
+
"CEPAFR", // Caisse d'Epargne
|
|
86
|
+
"CMCIFR", // Credit Mutuel / CIC
|
|
87
|
+
"PSSTFR", // La Banque Postale
|
|
88
|
+
"BREDFR", // BRED Banque Populaire
|
|
89
|
+
"CRLYFR", // LCL
|
|
90
|
+
"HSBCFR", // HSBC France
|
|
91
|
+
"TRZOFR", // Treezor
|
|
92
|
+
"QNTOFR", // Qonto
|
|
93
|
+
// --- Austria ---
|
|
94
|
+
"GIBAAT", // Erste Bank / Sparkassen
|
|
95
|
+
"BKAUAT", // UniCredit Bank Austria
|
|
96
|
+
"RZBAAT", // Raiffeisen Bank International
|
|
97
|
+
"RLNWAT", // Raiffeisen NO-Wien
|
|
98
|
+
"OBKLAT", // Oberbank
|
|
99
|
+
"BAWAAT", // BAWAG P.S.K.
|
|
100
|
+
"VBOEAT", // Volksbank
|
|
101
|
+
"HYPTAT", // Hypo Tirol
|
|
102
|
+
"SPIHAT", // Sparkasse
|
|
103
|
+
// --- Luxembourg ---
|
|
104
|
+
"BCEELU", // Banque et Caisse d'Epargne de l'Etat
|
|
105
|
+
"BILLLU", // Banque Internationale a Luxembourg
|
|
106
|
+
"BGLLLU", // BGL BNP Paribas
|
|
107
|
+
"CCRALU", // Banque Raiffeisen
|
|
108
|
+
"BLUXLU", // Banque de Luxembourg
|
|
109
|
+
// --- Ireland / UK ---
|
|
110
|
+
"AIBKIE", // Allied Irish Banks
|
|
111
|
+
"BOFIIE", // Bank of Ireland
|
|
112
|
+
"ULSBIE", // Ulster Bank
|
|
113
|
+
"CITIIE", // Citibank Ireland
|
|
114
|
+
"BARCGB", // Barclays
|
|
115
|
+
"HBUKGB", // HSBC UK
|
|
116
|
+
"MIDLGB", // HSBC (legacy Midland)
|
|
117
|
+
"LOYDGB", // Lloyds
|
|
118
|
+
"NWBKGB", // NatWest
|
|
119
|
+
"RBOSGB", // Royal Bank of Scotland
|
|
120
|
+
"TSBSGB", // TSB
|
|
121
|
+
"REVOGB", // Revolut
|
|
122
|
+
"MONZGB", // Monzo
|
|
123
|
+
"STARGB", // Starling
|
|
124
|
+
"CHASGB", // JP Morgan Chase UK
|
|
125
|
+
// --- Nordics ---
|
|
126
|
+
"NDEADK", // Nordea Denmark
|
|
127
|
+
"DABADK", // Danske Bank
|
|
128
|
+
"JYBADK", // Jyske Bank
|
|
129
|
+
"SYBKDK", // Sydbank
|
|
130
|
+
"NDEASE", // Nordea Sweden
|
|
131
|
+
"ESSESE", // SEB
|
|
132
|
+
"HANDSE", // Handelsbanken
|
|
133
|
+
"SWEDSE", // Swedbank
|
|
134
|
+
"DNBANO", // DNB
|
|
135
|
+
"NDEANO", // Nordea Norway
|
|
136
|
+
"NDEAFI", // Nordea Finland
|
|
137
|
+
"OKOYFI", // OP Corporate Bank
|
|
138
|
+
"HELSFI", // Aktia
|
|
139
|
+
"NBIIIS", // Landsbankinn (NBIIISRE)
|
|
140
|
+
"ESJAIS", // Arion Bank (ESJAISRE)
|
|
141
|
+
"GLITIS", // Islandsbanki (GLITISRE)
|
|
142
|
+
"MPBAIS", // Kvika banki (MPBAISRE)
|
|
143
|
+
"KBNONO", // BNbank ASA (KBNONO22)
|
|
144
|
+
"PRBONO", // Boligbanken ASA (PRBONO22)
|
|
145
|
+
"AARBDE", // Aareal Bank AG (AARBDE5W)
|
|
146
|
+
// --- Southern / Central Europe ---
|
|
147
|
+
"UNCRIT", // UniCredit Italy
|
|
148
|
+
"BCITIT", // Intesa Sanpaolo
|
|
149
|
+
"PASCIT", // Monte dei Paschi
|
|
150
|
+
"BPMOIT", // Banco BPM
|
|
151
|
+
"CRPPIT", // Cassa di Risparmio
|
|
152
|
+
"BSCHES", // Banco Santander
|
|
153
|
+
"BBVAES", // BBVA
|
|
154
|
+
"CAIXES", // CaixaBank
|
|
155
|
+
"SABBES", // Banco Sabadell
|
|
156
|
+
"BKBKES", // Bankinter
|
|
157
|
+
"BCOMPT", // Millennium BCP
|
|
158
|
+
"CGDIPT", // Caixa Geral de Depositos
|
|
159
|
+
"BPIPPT", // Banco BPI
|
|
160
|
+
"NOVBPT", // Novo Banco
|
|
161
|
+
"BPKOPL", // PKO Bank Polski
|
|
162
|
+
"BREXPL", // mBank
|
|
163
|
+
"INGBPL", // ING Bank Slaski
|
|
164
|
+
"WBKPPL", // Santander Bank Polska
|
|
165
|
+
"KOMBCZ", // Komercni banka
|
|
166
|
+
"CEKOCZ", // CSOB
|
|
167
|
+
"GIBACZ", // Ceska sporitelna
|
|
168
|
+
"TATRSK", // Tatra banka
|
|
169
|
+
"SUBASK", // Vseobecna uverova banka
|
|
170
|
+
"GIBASK", // Slovenska sporitelna
|
|
171
|
+
"OTPVHU", // OTP Bank
|
|
172
|
+
"BUDAHU", // Budapest Bank
|
|
173
|
+
"MKKBHU", // MKB Bank
|
|
174
|
+
// --- Switzerland ---
|
|
175
|
+
"UBSWCH", // UBS
|
|
176
|
+
"CRESCH", // Credit Suisse
|
|
177
|
+
"POFICH", // PostFinance
|
|
178
|
+
"ZKBKCH", // Zurcher Kantonalbank
|
|
179
|
+
"RAIFCH", // Raiffeisen Switzerland
|
|
180
|
+
"MIGRCH", // Migros Bank
|
|
181
|
+
]);
|
|
182
|
+
let provider = null;
|
|
183
|
+
/**
|
|
184
|
+
* Install a deployment-supplied BIC registry, consulted ahead of the seed list.
|
|
185
|
+
*
|
|
186
|
+
* Accepts a membership callable, any iterable of BIC strings, or `null` to
|
|
187
|
+
* remove a previously installed registry. Entries may be full BIC8/BIC11 codes
|
|
188
|
+
* or bare BIC6 prefixes; both are matched, case-insensitively and ignoring
|
|
189
|
+
* spaces.
|
|
190
|
+
*/
|
|
191
|
+
export function setBicRegistry(registry) {
|
|
192
|
+
if (registry === null) {
|
|
193
|
+
provider = null;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (typeof registry === "function") {
|
|
197
|
+
provider = registry;
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const known = new Set();
|
|
201
|
+
for (const entry of registry) {
|
|
202
|
+
const e = String(entry).trim();
|
|
203
|
+
if (e && !e.startsWith("#"))
|
|
204
|
+
known.add(e.replace(/ /g, "").toUpperCase());
|
|
205
|
+
}
|
|
206
|
+
provider = (bic) => known.has(bic);
|
|
207
|
+
}
|
|
208
|
+
/** Return the installed registry provider, or null if none is installed. */
|
|
209
|
+
export function getBicRegistry() {
|
|
210
|
+
return provider;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Tier 1: is `candidate` a BIC of a known institution?
|
|
214
|
+
*
|
|
215
|
+
* Checks the deployment-supplied registry first (full code, then BIC6 prefix),
|
|
216
|
+
* then the bundled seed prefixes.
|
|
217
|
+
*/
|
|
218
|
+
export function isRegisteredBic(candidate) {
|
|
219
|
+
const c = candidate.replace(/ /g, "").toUpperCase();
|
|
220
|
+
if (c.length !== 8 && c.length !== 11)
|
|
221
|
+
return false;
|
|
222
|
+
const prefix = c.slice(0, 6);
|
|
223
|
+
if (provider !== null) {
|
|
224
|
+
try {
|
|
225
|
+
if (provider(c) || provider(prefix))
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// a broken provider must not break detection
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return SEED_BIC6_PREFIXES.has(prefix);
|
|
233
|
+
}
|