euredact 0.3.7 → 0.3.9

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.
@@ -40,11 +40,16 @@ const MATH_AFTER = /^\s*[=+\-×÷*/]/;
40
40
  const COUNTRY_PREFIX_HYPHEN = /(?:^|[^A-Za-z0-9])[A-Z]{1,2}-\s*$/;
41
41
  const SEQUENTIAL_PATTERNS = /^(?:0{6,}|1234567890?|0123456789|9876543210?|1111111111?|000000000|123456789)$/;
42
42
  // YEAR_PATTERN kept for reference but replaced by RECENT_YEAR in suppressYearAsPostal
43
- // The leading \b is load-bearing. Without it the short alternatives match the
44
- // *tail* of an ordinary word: "NIS" matched "tālru**nis** ", the Latvian for
45
- // "telephone", so every Latvian phone number was suppressed as though it sat
46
- // behind a Belgian national-ID label. 653 misses from a missing word boundary.
47
- const ID_LABEL_BEFORE = /\b(?:BSN|RR|NN|NIR|INSZ|NISS|NIS|Steuer-?ID|TIN|NIF|NIE|SSN|rijksregisternummer|numéro\s*national|national\s*number|matricule|Ausweisnummer|Personalausweis|Versichertennummer|KVNR|KV-Nr|Steuernummer|St\.\-Nr|StNr|Finanzamt\s+ist|Ondernemingen\s+onder\s+nummer|ondernemingsnummer|numéro\s*d'entreprise|enterprise\s*number|Kruispuntbank)\s*[:.]?\s*$/i;
43
+ // `ID_LABEL_BEFORE` and `suppressPhoneAfterIdLabel` lived here. Their labels are
44
+ // now typed entries in `cues.ts`, and a phone-shaped span behind one is
45
+ // *relabelled* rather than dropped.
46
+ //
47
+ // Dropping was the wrong verb. The span is found either way, so removing the
48
+ // claim decided only whether the value was masked — and the answer was "no":
49
+ // "Rijksregisternummer: 85.03.19-284.73" produced no detection at all, a
50
+ // redaction library printing in full an identifier it had recognised and
51
+ // rejected. The 653-miss word-boundary lesson recorded here moved with the
52
+ // labels; see the module comment in `cues.ts`.
48
53
  const SERVICE_NUMBER = /^0800[\-\s]/;
49
54
  const DATE_PATTERN_FULL = /^\d{2}[-/.]\d{2}[-/.]\d{4}$|^\d{4}[-/.]\d{2}[-/.]\d{2}$/;
50
55
  const PASSPORT_CONTEXT_BEFORE = /(?:Reisepass|passport|passeport|paspoort|Bisheriger\s+Reisepass)\s*(?:Nr\.?|Nummer|nummer|number|n[°o])?\s*[:.]?\s*$/i;
@@ -338,11 +343,52 @@ function suppressYearAsPostal(text, match) {
338
343
  // Suppress: recent years without postal context are almost never postal codes
339
344
  return true;
340
345
  }
341
- function suppressPhoneAfterIdLabel(text, match) {
342
- if (match.patternDef.entityType !== EntityType.PHONE)
346
+ // ── Postal code: disqualified by the word in front of it ───────────────
347
+ //
348
+ // The mirror image of `rules/cues.ts`. There a label promotes a type; here a
349
+ // word refuses one. Both were reported together: a bare four-digit run becomes
350
+ // a POSTAL_CODE as soon as *any* real postal code establishes the country,
351
+ // which is to say in almost every real document, because POSTAL_CONTEXT_NEAR
352
+ // above then rescues it from suppressYearAsPostal. Measured on the training
353
+ // corpus: 55 bare years in prose ("Opgericht in 2016", "Fondée en 2017") plus
354
+ // telephone extensions ("(toest. 3841)", "(poste interne 3318)", "(ext. 2219)").
355
+ /**
356
+ * Prepositions that can only be temporal. Nothing is ever located "since 2018",
357
+ * so these disqualify a postal code on their own.
358
+ */
359
+ const YEAR_WORD_BEFORE = /(?<![A-Za-z0-9_])(?:sinds|since|depuis|seit|siden|sedan|desde|vuonna|anno|dal)\s+$/i;
360
+ /**
361
+ * "in", "en" and "im" are *both* temporal and locative, and in exactly the
362
+ * countries whose postal codes are year-shaped. Belgian 2000 is Antwerp and
363
+ * 2018 is one of its districts, so "Rustige ligging in 2018, vlakbij openbaar
364
+ * vervoer" is an address, not a date — an earlier version of this rule
365
+ * suppressed it and lost a real postal code, which is the worse error for a
366
+ * redaction tool. So an ambiguous preposition needs a founding or payment
367
+ * participle in front of it, which is what every case in the report had:
368
+ * "Opgericht in 2016", "Fondée en 2017", "versé en 2025".
369
+ */
370
+ const YEAR_VERB_BEFORE = /(?<![A-Za-z0-9_])(?:opgericht|gesticht|opgestart|founded|established|created|fond[ée]e?|cr[ée][ée]e?|gegründet|gegruendet|errichtet|grundlagt|grundad|perustettu|fundada|fundado|fondata|costituita|vers[ée]|betaald|uitbetaald|ausgezahlt|paid|geboren|born)\s+(?:in|en|im|op|the)\s+$/i;
371
+ /**
372
+ * A capitalised word immediately after keeps the candidate: "in 2000 Antwerpen"
373
+ * really is a postal code, and so is the "AZ" of a Dutch "1105 AZ". Without
374
+ * this the rule would suppress the very addresses it is meant to leave alone.
375
+ */
376
+ const TOWN_AFTER = /^\s*[A-ZÀ-ÞĀ-Ž]/;
377
+ /**
378
+ * A telephone extension marker. "poste interne 3318" puts one qualifier word
379
+ * between the marker and the number, so allow exactly one.
380
+ */
381
+ const EXTENSION_BEFORE = /(?<![A-Za-z0-9_])(?:toest(?:el)?|ext|extension|poste|durchwahl|doorkiesnummer|tst|nebenstelle)\.?\s*(?:[a-zà-ÿ]{2,10}\s*)?[:.]?\s*$/i;
382
+ /** Suppress a postal code the word in front of it rules out. */
383
+ function suppressPostalAfterDisqualifyingWord(text, match) {
384
+ if (match.patternDef.entityType !== EntityType.POSTAL_CODE)
343
385
  return false;
344
- const [before] = getContext(text, match.start, match.end);
345
- return ID_LABEL_BEFORE.test(before);
386
+ const before = text.slice(Math.max(0, match.start - 24), match.start);
387
+ if (EXTENSION_BEFORE.test(before))
388
+ return true;
389
+ return RECENT_YEAR.test(match.text.trim())
390
+ && (YEAR_WORD_BEFORE.test(before) || YEAR_VERB_BEFORE.test(before))
391
+ && !TOWN_AFTER.test(text.slice(match.end, match.end + 3));
346
392
  }
347
393
  function suppressPhoneServiceNumber(_text, match) {
348
394
  if (match.patternDef.entityType !== EntityType.PHONE)
@@ -788,11 +834,11 @@ function suppressRequiresContext(text, match) {
788
834
  return !match.patternDef.contextKeywords.some(kw => context.includes(kw.toLowerCase()));
789
835
  }
790
836
  const TYPE_SUPPRESSORS = {
791
- [EntityType.PHONE]: [suppressCurrency, suppressUnits, suppressReference, suppressMath, suppressPhoneAfterIdLabel, suppressPhoneServiceNumber, suppressPhoneDateOverlap, suppressPhoneAsNumberRange],
837
+ [EntityType.PHONE]: [suppressCurrency, suppressUnits, suppressReference, suppressMath, suppressPhoneServiceNumber, suppressPhoneDateOverlap, suppressPhoneAsNumberRange],
792
838
  [EntityType.NATIONAL_ID]: [suppressCurrency, suppressUnits, suppressReference, suppressLegal, suppressMath, suppressNatidAsPassport, suppressSeNatidAsOrg],
793
839
  [EntityType.SSN]: [suppressCurrency, suppressUnits, suppressReference, suppressMath],
794
840
  [EntityType.TAX_ID]: [suppressCurrency, suppressUnits, suppressReference, suppressMath, suppressTaxidAsIpAddress],
795
- [EntityType.POSTAL_CODE]: [suppressCurrency, suppressUnits, suppressMath, suppressLegal, suppressYearAsPostal, suppressPostalInsideIban, suppressPostalAsHouseNumber, suppressPostalInLongerIdentifier, suppressReference],
841
+ [EntityType.POSTAL_CODE]: [suppressCurrency, suppressUnits, suppressMath, suppressLegal, suppressYearAsPostal, suppressPostalAfterDisqualifyingWord, suppressPostalInsideIban, suppressPostalAsHouseNumber, suppressPostalInLongerIdentifier, suppressReference],
796
842
  [EntityType.BIC]: [suppressBicWithoutEvidence],
797
843
  [EntityType.BANK_ACCOUNT]: [suppressReference],
798
844
  [EntityType.LICENSE_PLATE]: [suppressPlateInCompound, suppressDePlateUnknownDistrict, suppressPlateAsCurrencyAmount],
@@ -32,6 +32,16 @@ export declare enum EntityType {
32
32
  HEALTH_INSURANCE = "HEALTH_INSURANCE",
33
33
  HEALTHCARE_PROVIDER = "HEALTHCARE_PROVIDER",
34
34
  CHAMBER_OF_COMMERCE = "CHAMBER_OF_COMMERCE",
35
+ /**
36
+ * Employee, badge or customer number tied to a person.
37
+ *
38
+ * Emitted **only** when an explicit label names it — see `rules/cues.ts`.
39
+ * There is no pattern for an internal identifier because there is no shape
40
+ * for one; without the label, a digit run is not distinguishable from any
41
+ * other. The type exists so that a labelled employee number is filed
42
+ * correctly instead of being claimed by the phone pattern.
43
+ */
44
+ INTERNAL_ID = "INTERNAL_ID",
35
45
  IMEI = "IMEI",
36
46
  GPS_COORDINATES = "GPS_COORDINATES",
37
47
  UUID = "UUID",
package/dist/esm/types.js CHANGED
@@ -33,6 +33,16 @@ export var EntityType;
33
33
  EntityType["HEALTH_INSURANCE"] = "HEALTH_INSURANCE";
34
34
  EntityType["HEALTHCARE_PROVIDER"] = "HEALTHCARE_PROVIDER";
35
35
  EntityType["CHAMBER_OF_COMMERCE"] = "CHAMBER_OF_COMMERCE";
36
+ /**
37
+ * Employee, badge or customer number tied to a person.
38
+ *
39
+ * Emitted **only** when an explicit label names it — see `rules/cues.ts`.
40
+ * There is no pattern for an internal identifier because there is no shape
41
+ * for one; without the label, a digit run is not distinguishable from any
42
+ * other. The type exists so that a labelled employee number is filed
43
+ * correctly instead of being claimed by the phone pattern.
44
+ */
45
+ EntityType["INTERNAL_ID"] = "INTERNAL_ID";
36
46
  EntityType["IMEI"] = "IMEI";
37
47
  EntityType["GPS_COORDINATES"] = "GPS_COORDINATES";
38
48
  EntityType["UUID"] = "UUID";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "euredact",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "European PII redaction SDK -- fast, zero-dependency, rule-based detection and redaction of personal data across 31 European countries",
5
5
  "homepage": "https://euredact.dev",
6
6
  "license": "Apache-2.0",