gheim 0.1.1 → 0.1.3

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/dist/esm/index.js CHANGED
@@ -1,5 +1,10 @@
1
- import { createRequire } from "node:module";
2
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined")
5
+ return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
3
8
 
4
9
  // src/core/sentinels.ts
5
10
  var LABEL_TO_TAG = {
@@ -37,98 +42,6 @@ function isPossibleSentinelPrefix(buffer) {
37
42
  return PARTIAL_SENTINEL_RE.test(buffer);
38
43
  }
39
44
 
40
- // src/normalizers.ts
41
- import { createRequire as createRequire2 } from "node:module";
42
- var _require = null;
43
- var _requireResolved = false;
44
- function _getRequire() {
45
- if (_requireResolved)
46
- return _require;
47
- _requireResolved = true;
48
- try {
49
- _require = createRequire2(import.meta.url);
50
- } catch {
51
- _require = null;
52
- }
53
- return _require;
54
- }
55
- function loadSync(name, errMsg) {
56
- const req = _getRequire();
57
- if (!req)
58
- throw new Error(errMsg);
59
- try {
60
- return req(name);
61
- } catch {
62
- throw new Error(errMsg);
63
- }
64
- }
65
- function loadPhoneLib() {
66
- return loadSync("libphonenumber-js", "the e164 normalizer requires libphonenumber-js. " + "Install with: bun add libphonenumber-js");
67
- }
68
- function e164(opts = {}) {
69
- const lib = loadPhoneLib();
70
- return (s) => {
71
- try {
72
- const parsed = lib.parsePhoneNumberFromString(s, opts.region);
73
- if (!parsed || !parsed.isValid())
74
- return null;
75
- return parsed.format("E.164");
76
- } catch {
77
- return null;
78
- }
79
- };
80
- }
81
- function loadChrono(opts) {
82
- const mod = loadSync("chrono-node", "the isoDate normalizer requires chrono-node. " + "Install with: bun add chrono-node");
83
- if (opts.locale) {
84
- const localeParser = mod[opts.locale];
85
- if (localeParser)
86
- return localeParser;
87
- }
88
- return mod;
89
- }
90
- function isoDate(opts = {}) {
91
- const parser = loadChrono(opts);
92
- return (s) => {
93
- const d = parser.parseDate(s);
94
- if (!d)
95
- return null;
96
- return d.toISOString().slice(0, 10);
97
- };
98
- }
99
- function germanTransliteration() {
100
- const table = {
101
- ä: "ae",
102
- Ä: "ae",
103
- ö: "oe",
104
- Ö: "oe",
105
- ü: "ue",
106
- Ü: "ue",
107
- ß: "ss"
108
- };
109
- return (s) => {
110
- let out = "";
111
- for (const ch of s.normalize("NFKC")) {
112
- out += table[ch] ?? ch;
113
- }
114
- return out.toLowerCase().split(/\s+/).filter(Boolean).join(" ");
115
- };
116
- }
117
- var BUILTINS = {
118
- e164: () => e164(),
119
- iso_date: () => isoDate(),
120
- german: () => germanTransliteration()
121
- };
122
- function resolveNormalizer(spec) {
123
- if (typeof spec === "function")
124
- return spec;
125
- const factory = BUILTINS[spec];
126
- if (!factory) {
127
- throw new Error(`unknown built-in normalizer: ${JSON.stringify(spec)}. ` + `Available: ${Object.keys(BUILTINS).join(", ")}, or pass a callable.`);
128
- }
129
- return factory();
130
- }
131
-
132
45
  // src/core/session.ts
133
46
  function normalizeSurface(s) {
134
47
  return s.normalize("NFKC").toLowerCase().split(/\s+/).filter(Boolean).join(" ");
@@ -141,8 +54,8 @@ class Session {
141
54
  resolvedNormalizers = {};
142
55
  constructor(opts = {}) {
143
56
  if (opts.normalizers) {
144
- for (const [label, spec] of Object.entries(opts.normalizers)) {
145
- this.resolvedNormalizers[label] = resolveNormalizer(spec);
57
+ for (const [label, fn] of Object.entries(opts.normalizers)) {
58
+ this.resolvedNormalizers[label] = fn;
146
59
  }
147
60
  }
148
61
  }
@@ -166,8 +79,11 @@ class Session {
166
79
  return sentinel;
167
80
  }
168
81
  applySpans(text, spans) {
82
+ return this.applySpansDetailed(text, spans).redacted;
83
+ }
84
+ applySpansDetailed(text, spans) {
169
85
  if (spans.length === 0)
170
- return text;
86
+ return { redacted: text, merged: [] };
171
87
  const sorted = [...spans].sort((a, b) => a.start - b.start || b.end - b.start - (a.end - a.start));
172
88
  const merged = mergeAdjacent(text, sorted);
173
89
  const out = [];
@@ -183,7 +99,7 @@ class Session {
183
99
  cursor = end;
184
100
  }
185
101
  out.push(text.slice(cursor));
186
- return out.join("");
102
+ return { redacted: out.join(""), merged };
187
103
  }
188
104
  restore(text) {
189
105
  if (Object.keys(this.mapping).length === 0)
@@ -381,8 +297,10 @@ class LocalDetector {
381
297
  chunkTokens;
382
298
  chunkOverlapTokens;
383
299
  batchSize;
300
+ actualDevice = null;
384
301
  _pipeline = null;
385
302
  _loadPromise = null;
303
+ _onProgress;
386
304
  constructor(opts = {}) {
387
305
  this.model = opts.model ?? DEFAULT_MODEL2;
388
306
  this.device = opts.device ?? "auto";
@@ -391,11 +309,13 @@ class LocalDetector {
391
309
  this.chunkTokens = opts.chunkTokens ?? DEFAULT_CHUNK_TOKENS;
392
310
  this.chunkOverlapTokens = opts.chunkOverlapTokens ?? DEFAULT_OVERLAP_TOKENS;
393
311
  this.batchSize = opts.batchSize ?? DEFAULT_BATCH_SIZE;
312
+ this._onProgress = opts.onProgress;
394
313
  if (this.chunkOverlapTokens >= this.chunkTokens) {
395
314
  throw new Error(`chunkOverlapTokens (${this.chunkOverlapTokens}) must be smaller than chunkTokens (${this.chunkTokens})`);
396
315
  }
397
316
  if (opts.pipelineInstance) {
398
317
  this._pipeline = opts.pipelineInstance;
318
+ this.actualDevice = this.device === "auto" ? null : this.device;
399
319
  }
400
320
  }
401
321
  async load() {
@@ -404,24 +324,70 @@ class LocalDetector {
404
324
  if (this._loadPromise)
405
325
  return this._loadPromise;
406
326
  this._loadPromise = (async () => {
407
- let mod;
408
- try {
409
- mod = await import("@huggingface/transformers");
410
- } catch (e) {
411
- throw new Error("LocalDetector requires `@huggingface/transformers`. Install with: bun add @huggingface/transformers");
327
+ const mod = await loadTransformersJs();
328
+ const sequence = await this._planDeviceSequence();
329
+ let lastErr;
330
+ for (let i = 0;i < sequence.length; i++) {
331
+ const dev = sequence[i] ?? "wasm";
332
+ try {
333
+ const pipe = await mod.pipeline("token-classification", this.model, {
334
+ device: dev,
335
+ dtype: this.dtype,
336
+ progress_callback: this._onProgress ? (raw) => this._emitProgress(raw, dev) : undefined
337
+ });
338
+ this._pipeline = pipe;
339
+ this.actualDevice = dev;
340
+ return pipe;
341
+ } catch (e) {
342
+ lastErr = e;
343
+ if (i < sequence.length - 1)
344
+ continue;
345
+ throw e;
346
+ }
412
347
  }
413
- const pipe = await mod.pipeline("token-classification", this.model, {
414
- device: this.device,
415
- dtype: this.dtype
416
- });
417
- this._pipeline = pipe;
418
- return pipe;
348
+ throw lastErr ?? new Error("LocalDetector.load(): no candidate backends");
419
349
  })();
420
350
  return this._loadPromise;
421
351
  }
352
+ async _planDeviceSequence() {
353
+ if (this.device !== "auto")
354
+ return [this.device];
355
+ const seq = [];
356
+ const nav = typeof navigator !== "undefined" ? navigator : null;
357
+ if (nav?.gpu?.requestAdapter) {
358
+ try {
359
+ const adapter = await nav.gpu.requestAdapter();
360
+ if (adapter)
361
+ seq.push("webgpu");
362
+ } catch {}
363
+ }
364
+ seq.push("wasm");
365
+ return seq;
366
+ }
367
+ _emitProgress(raw, device) {
368
+ if (!this._onProgress)
369
+ return;
370
+ const event = {
371
+ phase: raw.status ?? "progress",
372
+ device
373
+ };
374
+ if (raw.file)
375
+ event.file = raw.file;
376
+ if (raw.status === "progress") {
377
+ if (raw.loaded != null)
378
+ event.loadedBytes = raw.loaded;
379
+ if (raw.total != null)
380
+ event.totalBytes = raw.total;
381
+ if (raw.progress != null) {
382
+ const norm = raw.progress > 1 ? raw.progress / 100 : raw.progress;
383
+ event.fraction = Math.max(0, Math.min(1, norm));
384
+ }
385
+ }
386
+ this._onProgress(event);
387
+ }
422
388
  async detect(text, opts) {
423
389
  if (opts?.model && opts.model !== this.model) {
424
- throw new Error(`LocalDetector was constructed with model='${this.model}'; per-call model override ('${opts.model}') not supported.`);
390
+ throw new Error(`LocalDetector was constructed with model='${this.model}'; ` + `per-call model override ('${opts.model}') not supported.`);
425
391
  }
426
392
  if (!text)
427
393
  return [];
@@ -430,7 +396,7 @@ class LocalDetector {
430
396
  }
431
397
  async detectBatch(texts, opts) {
432
398
  if (opts?.model && opts.model !== this.model) {
433
- throw new Error(`LocalDetector was constructed with model='${this.model}'; per-call model override ('${opts.model}') not supported.`);
399
+ throw new Error(`LocalDetector was constructed with model='${this.model}'; ` + `per-call model override ('${opts.model}') not supported.`);
434
400
  }
435
401
  if (texts.length === 0)
436
402
  return [];
@@ -591,6 +557,221 @@ function chunkByChars(text, chunkChars, overlapChars) {
591
557
  function normalizeWord(word) {
592
558
  return word.replace(/##/g, "").trim();
593
559
  }
560
+ async function loadTransformersJs() {
561
+ try {
562
+ return await import("@huggingface/transformers");
563
+ } catch {
564
+ throw new Error("LocalDetector requires `@huggingface/transformers`. Install with: bun add @huggingface/transformers");
565
+ }
566
+ }
567
+ // src/detectors/composite.ts
568
+ function luhnOk(s) {
569
+ const digits = [];
570
+ for (const c of s) {
571
+ if (c >= "0" && c <= "9")
572
+ digits.push(Number(c));
573
+ }
574
+ if (digits.length < 13 || digits.length > 19)
575
+ return false;
576
+ let total = 0;
577
+ for (let i = 0;i < digits.length; i++) {
578
+ let d = digits[digits.length - 1 - i] ?? 0;
579
+ if (i % 2 === 1) {
580
+ d *= 2;
581
+ if (d > 9)
582
+ d -= 9;
583
+ }
584
+ total += d;
585
+ }
586
+ return total % 10 === 0;
587
+ }
588
+ function ibanChOk(raw) {
589
+ const s = raw.replace(/[\s-]/g, "");
590
+ if (s.length !== 21 || !s.startsWith("CH") || !/^\d+$/.test(s.slice(2))) {
591
+ return false;
592
+ }
593
+ const rearranged = s.slice(4) + s.slice(0, 4);
594
+ let numeric = "";
595
+ for (const c of rearranged) {
596
+ numeric += c >= "A" && c <= "Z" ? String(c.charCodeAt(0) - 55) : c;
597
+ }
598
+ let rem = 0;
599
+ for (const ch of numeric) {
600
+ rem = (rem * 10 + Number(ch)) % 97;
601
+ }
602
+ return rem === 1;
603
+ }
604
+ function ahvOk(s) {
605
+ const digits = [];
606
+ for (const c of s) {
607
+ if (c >= "0" && c <= "9")
608
+ digits.push(Number(c));
609
+ }
610
+ if (digits.length !== 13)
611
+ return false;
612
+ if (digits[0] !== 7 || digits[1] !== 5 || digits[2] !== 6)
613
+ return false;
614
+ let total = 0;
615
+ for (let i = 0;i < 12; i++) {
616
+ total += (digits[i] ?? 0) * (i % 2 === 0 ? 1 : 3);
617
+ }
618
+ const check = (10 - total % 10) % 10;
619
+ return digits[12] === check;
620
+ }
621
+ function vatCheOk(s) {
622
+ const digits = [];
623
+ for (const c of s) {
624
+ if (c >= "0" && c <= "9")
625
+ digits.push(Number(c));
626
+ }
627
+ if (digits.length < 9)
628
+ return false;
629
+ const weights = [5, 4, 3, 2, 7, 6, 5, 4];
630
+ let total = 0;
631
+ for (let i = 0;i < 8; i++) {
632
+ total += (digits[i] ?? 0) * (weights[i] ?? 0);
633
+ }
634
+ let expected = 11 - total % 11;
635
+ if (expected === 11)
636
+ expected = 0;
637
+ else if (expected === 10)
638
+ return false;
639
+ return digits[8] === expected;
640
+ }
641
+ var REGEXES = [
642
+ {
643
+ label: "private_email",
644
+ pattern: /\b[\w._%+\-]+@[\w.\-]+\.[A-Za-z]{2,}\b/g
645
+ },
646
+ {
647
+ label: "private_url",
648
+ pattern: /https?:\/\/[^\s)>\]]+/g
649
+ },
650
+ {
651
+ label: "private_phone",
652
+ pattern: /(?:\+41|0041|0)\s?(?:\(0\)\s?)?\d{2}[\s\-/.]?\d{3}[\s\-/.]?\d{2}[\s\-/.]?\d{2}\b/g
653
+ },
654
+ {
655
+ label: "account_number",
656
+ pattern: /\bCH\d{2}(?:[\s-]?\d){17}\b/g,
657
+ validator: ibanChOk
658
+ },
659
+ {
660
+ label: "account_number",
661
+ pattern: /\b756\.\d{4}\.\d{4}\.\d{2}\b/g,
662
+ validator: ahvOk
663
+ },
664
+ {
665
+ label: "account_number",
666
+ pattern: /\b756\d{10}\b/g,
667
+ validator: ahvOk
668
+ },
669
+ {
670
+ label: "account_number",
671
+ pattern: /\bCHE-\d{3}\.\d{3}\.\d{3}(?:\s+(?:MWST|TVA|IVA))?\b/g,
672
+ validator: vatCheOk
673
+ },
674
+ {
675
+ label: "account_number",
676
+ pattern: /\b(?:\d[\s-]?){15,16}\d\b/g,
677
+ validator: luhnOk
678
+ },
679
+ {
680
+ label: "private_date",
681
+ pattern: /\b(?:\d{1,2}[./]\d{1,2}[./]\d{4}|\d{4}[./-]\d{1,2}[./-]\d{1,2})\b/g
682
+ },
683
+ {
684
+ label: "secret",
685
+ pattern: /\bsk-(?:proj-)?[A-Za-z0-9_\-]{20,}\b/g
686
+ },
687
+ {
688
+ label: "secret",
689
+ pattern: /\bghp_[A-Za-z0-9]{36}\b/g
690
+ },
691
+ {
692
+ label: "secret",
693
+ pattern: /\bAKIA[A-Z0-9]{16}\b/g
694
+ }
695
+ ];
696
+ var URL_TRAIL_TRIM = '.,;:!?")>]';
697
+ function trimUrl(text, start, end) {
698
+ while (end > start && URL_TRAIL_TRIM.includes(text[end - 1] ?? ""))
699
+ end--;
700
+ return [start, end];
701
+ }
702
+ function findRegexSpans(text) {
703
+ const candidates = [];
704
+ for (const { label, pattern, validator } of REGEXES) {
705
+ pattern.lastIndex = 0;
706
+ let m;
707
+ while ((m = pattern.exec(text)) !== null) {
708
+ let start = m.index;
709
+ let end = m.index + m[0].length;
710
+ if (label === "private_url")
711
+ [start, end] = trimUrl(text, start, end);
712
+ const value = text.slice(start, end);
713
+ if (!value.trim())
714
+ continue;
715
+ if (validator && !validator(value))
716
+ continue;
717
+ candidates.push({
718
+ start,
719
+ end,
720
+ label,
721
+ text: value,
722
+ score: 1
723
+ });
724
+ }
725
+ }
726
+ candidates.sort((a, b) => a.start !== b.start ? a.start - b.start : b.end - b.start - (a.end - a.start));
727
+ const out = [];
728
+ let lastEnd = -1;
729
+ for (const sp of candidates) {
730
+ if (sp.start < lastEnd)
731
+ continue;
732
+ out.push(sp);
733
+ lastEnd = sp.end;
734
+ }
735
+ return out;
736
+ }
737
+ function maskSpans(text, spans) {
738
+ if (spans.length === 0)
739
+ return text;
740
+ const chars = [...text];
741
+ for (const sp of spans) {
742
+ for (let i = sp.start;i < sp.end && i < chars.length; i++) {
743
+ chars[i] = " ";
744
+ }
745
+ }
746
+ return chars.join("");
747
+ }
748
+
749
+ class CompositeDetector {
750
+ model;
751
+ constructor(opts) {
752
+ if (!opts || !opts.model) {
753
+ throw new Error("CompositeDetector requires { model: Detector } — pass any object with " + "a .detect(text) method (e.g. new LocalDetector({...})).");
754
+ }
755
+ this.model = opts.model;
756
+ }
757
+ async detect(text, opts) {
758
+ if (!text)
759
+ return [];
760
+ const regexSpans = findRegexSpans(text);
761
+ const masked = maskSpans(text, regexSpans);
762
+ const modelSpans = await this.model.detect(masked, opts);
763
+ const out = [...regexSpans];
764
+ const regexRanges = regexSpans.map((s) => [s.start, s.end]);
765
+ for (const sp of modelSpans) {
766
+ const overlaps = regexRanges.some(([rs, re]) => sp.start < re && rs < sp.end);
767
+ if (overlaps)
768
+ continue;
769
+ out.push(sp);
770
+ }
771
+ out.sort((a, b) => a.start !== b.start ? a.start - b.start : b.end - b.start - (a.end - a.start));
772
+ return out;
773
+ }
774
+ }
594
775
  // src/detectors/index.ts
595
776
  function defaultDetector() {
596
777
  const hasKey = typeof process !== "undefined" && Boolean(process.env?.GHEIM_API_KEY);
@@ -645,10 +826,11 @@ async function* deanonymizeStream(chunks, session) {
645
826
  }
646
827
 
647
828
  // src/index.ts
648
- var VERSION = "0.1.0";
829
+ var VERSION = "0.1.3";
649
830
  export {
650
831
  renderSentinel,
651
832
  parseSentinel,
833
+ mergeAdjacent,
652
834
  labelTag,
653
835
  isPossibleSentinelPrefix,
654
836
  defaultDetector,
@@ -664,5 +846,6 @@ export {
664
846
  DEFAULT_MODEL as REMOTE_DEFAULT_MODEL,
665
847
  LocalDetector,
666
848
  LABEL_TO_TAG,
667
- DEFAULT_BASE_URL
849
+ DEFAULT_BASE_URL,
850
+ CompositeDetector
668
851
  };