gheim 0.1.2 → 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/README.md CHANGED
@@ -38,33 +38,38 @@ The core package itself has no runtime dependencies.
38
38
 
39
39
  `LocalDetector` loads a token-classification model via
40
40
  [`@huggingface/transformers`](https://www.npmjs.com/package/@huggingface/transformers)
41
- (transformers.js). Two models are recommended depending on the
42
- deployment context. Both implement the same 33-class BIOES output
43
- schema and are interchangeable.
41
+ (transformers.js). The package's default model is
42
+ [`joelbarmettler/gheim-ch-560m`](https://huggingface.co/joelbarmettler/gheim-ch-560m)
43
+ — a 560M xlm-roberta-large fine-tune optimised for Swiss-market PII
44
+ (strict-span F1 0.916 on Swiss text, see
45
+ [MODEL_CARD.md](https://github.com/joelbarmettlerUZH/gheim/blob/main/MODEL_CARD.md)).
46
+ Any HuggingFace token-classification model that emits the same 33-class
47
+ BIOES schema can be substituted via `model:`.
44
48
 
45
49
  | Model | Best for | Parameters | Notes |
46
50
  |---|---|---:|---|
47
- | [`joelbarmettler/gheim-ch-560m`](https://huggingface.co/joelbarmettler/gheim-ch-560m) | Swiss-market text (de_CH, fr_CH, it_CH, rm, en) with CH-format account numbers (IBAN, AHV, VAT-CHE) | 560M | Apache 2.0. Test F1 = 0.916 on Swiss text. ONNX export ships in the `onnx/` subfolder. |
51
+ | [`joelbarmettler/gheim-ch-560m`](https://huggingface.co/joelbarmettler/gheim-ch-560m) **(default)** | Swiss-market text (de_CH, fr_CH, it_CH, rm, en) with CH-format account numbers (IBAN, AHV, VAT-CHE) | 560M | Apache 2.0. Test F1 0.916. Hub repo ships `onnx/model_quantized.onnx` only — load with `dtype: "q8"`. |
48
52
  | [`openai/privacy-filter`](https://huggingface.co/openai/privacy-filter) | English-first or general use, long-context (up to 128k tokens) | 1.4B (50M active, MoE) | Apache 2.0. Wider language coverage, larger weights. |
49
53
 
50
54
  ```ts
51
55
  import { LocalDetector } from "gheim";
52
56
 
53
- // Recommended for Swiss-market text:
54
- const det = new LocalDetector({ model: "joelbarmettler/gheim-ch-560m" });
57
+ // Default — Swiss-tuned, q8 ONNX. `device: "auto"` probes WebGPU in
58
+ // the browser and falls back to WASM; in Node it's WASM directly.
59
+ const det = new LocalDetector({ dtype: "q8" });
55
60
 
56
61
  // Alternative for English or general use:
57
62
  const detEn = new LocalDetector({ model: "openai/privacy-filter" });
58
63
  ```
59
64
 
60
- The package default `model` is `openai/privacy-filter`. Pass `model`
61
- explicitly to opt into the Swiss model.
62
-
63
65
  ## Drop-in OpenAI client
64
66
 
65
67
  ```ts
66
68
  import { OpenAI } from "gheim/openai";
67
69
 
70
+ // Same constructor shape as `openai`. `apiKey`, `baseURL`, etc. work
71
+ // at the top level (forwarded to the inner client). gheim-specific
72
+ // keys: `gheimDetector`, `gheimStrict`, `openaiClient`, `clientOptions`.
68
73
  const client = new OpenAI();
69
74
  const r = await client.chat.completions.create({
70
75
  model: "gpt-4o",
@@ -74,6 +79,17 @@ const r = await client.chat.completions.create({
74
79
  // OpenAI only ever saw "<PERSON_1>".
75
80
  ```
76
81
 
82
+ Custom endpoint or key (e.g. OpenRouter, local vLLM):
83
+
84
+ ```ts
85
+ import { OpenAI } from "gheim/openai";
86
+
87
+ const client = new OpenAI({
88
+ apiKey: process.env.OPENROUTER_API_KEY,
89
+ baseURL: "https://openrouter.ai/api/v1",
90
+ });
91
+ ```
92
+
77
93
  Streaming:
78
94
 
79
95
  ```ts
@@ -104,9 +120,9 @@ All non-chat OpenAI resources are on `client.raw` once `await client.ready()` re
104
120
  import { Session, LocalDetector, anonymizeText, deanonymizeText } from "gheim";
105
121
 
106
122
  const session = new Session();
107
- (session as any).detector = new LocalDetector({
108
- model: "joelbarmettler/gheim-ch-560m",
109
- });
123
+ (session as any).detector = new LocalDetector({ dtype: "q8" });
124
+ // ^ defaults to joelbarmettler/gheim-ch-560m
125
+
110
126
  const clean = await anonymizeText("Hi, my name is Joel", session);
111
127
  // ... call any LLM with clean ...
112
128
  const final = deanonymizeText(responseText, session);
@@ -155,12 +171,18 @@ client.raw.beta.assistants.create(...); // always works regardless of strict mo
155
171
  ```ts
156
172
  import { LocalDetector, RemoteDetector, defaultDetector } from "gheim";
157
173
 
158
- // Local: via @huggingface/transformers, on Node and Bun.
174
+ // Local: via @huggingface/transformers, on Node, Bun, and browsers.
175
+ // `device: "auto"` (default) probes navigator.gpu in the browser and
176
+ // falls back to WASM if WebGPU isn't usable; in Node it's WASM only.
159
177
  const local = new LocalDetector({
160
- model: "joelbarmettler/gheim-ch-560m",
161
178
  device: "auto",
162
- dtype: "fp32",
179
+ dtype: "q8",
180
+ onProgress: (e) => {
181
+ if (e.fraction != null) console.log(`loading: ${(e.fraction * 100) | 0}%`);
182
+ },
163
183
  });
184
+ await local.load();
185
+ console.log("backend in use:", local.actualDevice); // "webgpu" or "wasm"
164
186
 
165
187
  // Remote: against your own gheim-server or api.gheim.ch.
166
188
  const remote = new RemoteDetector({
@@ -174,10 +196,9 @@ const auto = defaultDetector();
174
196
 
175
197
  ## Runtime support
176
198
 
177
- Tested on Node 18+ and Bun 1.1+ with `device: "cpu"` (the default). The
178
- underlying transformers.js library also supports `device: "webgpu"` in
179
- modern browsers; the WebGPU path is not exercised in CI and should be
180
- considered best-effort.
199
+ Tested on Node 18+, Bun 1.1+, and modern browsers (Chrome 113+, Edge,
200
+ Safari 17+, Firefox via flag). Browser builds use WebGPU when available
201
+ and fall back to WebAssembly automatically.
181
202
 
182
203
  For Node servers, installing `onnxruntime-node` alongside
183
204
  `@huggingface/transformers` enables the native ONNX backend, which is
@@ -57,7 +57,8 @@ __export(exports_src, {
57
57
  REMOTE_DEFAULT_MODEL: () => DEFAULT_MODEL,
58
58
  LocalDetector: () => LocalDetector,
59
59
  LABEL_TO_TAG: () => LABEL_TO_TAG,
60
- DEFAULT_BASE_URL: () => DEFAULT_BASE_URL
60
+ DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
61
+ CompositeDetector: () => CompositeDetector
61
62
  });
62
63
  module.exports = __toCommonJS(exports_src);
63
64
 
@@ -619,6 +620,214 @@ async function loadTransformersJs() {
619
620
  throw new Error("LocalDetector requires `@huggingface/transformers`. Install with: bun add @huggingface/transformers");
620
621
  }
621
622
  }
623
+ // src/detectors/composite.ts
624
+ function luhnOk(s) {
625
+ const digits = [];
626
+ for (const c of s) {
627
+ if (c >= "0" && c <= "9")
628
+ digits.push(Number(c));
629
+ }
630
+ if (digits.length < 13 || digits.length > 19)
631
+ return false;
632
+ let total = 0;
633
+ for (let i = 0;i < digits.length; i++) {
634
+ let d = digits[digits.length - 1 - i] ?? 0;
635
+ if (i % 2 === 1) {
636
+ d *= 2;
637
+ if (d > 9)
638
+ d -= 9;
639
+ }
640
+ total += d;
641
+ }
642
+ return total % 10 === 0;
643
+ }
644
+ function ibanChOk(raw) {
645
+ const s = raw.replace(/[\s-]/g, "");
646
+ if (s.length !== 21 || !s.startsWith("CH") || !/^\d+$/.test(s.slice(2))) {
647
+ return false;
648
+ }
649
+ const rearranged = s.slice(4) + s.slice(0, 4);
650
+ let numeric = "";
651
+ for (const c of rearranged) {
652
+ numeric += c >= "A" && c <= "Z" ? String(c.charCodeAt(0) - 55) : c;
653
+ }
654
+ let rem = 0;
655
+ for (const ch of numeric) {
656
+ rem = (rem * 10 + Number(ch)) % 97;
657
+ }
658
+ return rem === 1;
659
+ }
660
+ function ahvOk(s) {
661
+ const digits = [];
662
+ for (const c of s) {
663
+ if (c >= "0" && c <= "9")
664
+ digits.push(Number(c));
665
+ }
666
+ if (digits.length !== 13)
667
+ return false;
668
+ if (digits[0] !== 7 || digits[1] !== 5 || digits[2] !== 6)
669
+ return false;
670
+ let total = 0;
671
+ for (let i = 0;i < 12; i++) {
672
+ total += (digits[i] ?? 0) * (i % 2 === 0 ? 1 : 3);
673
+ }
674
+ const check = (10 - total % 10) % 10;
675
+ return digits[12] === check;
676
+ }
677
+ function vatCheOk(s) {
678
+ const digits = [];
679
+ for (const c of s) {
680
+ if (c >= "0" && c <= "9")
681
+ digits.push(Number(c));
682
+ }
683
+ if (digits.length < 9)
684
+ return false;
685
+ const weights = [5, 4, 3, 2, 7, 6, 5, 4];
686
+ let total = 0;
687
+ for (let i = 0;i < 8; i++) {
688
+ total += (digits[i] ?? 0) * (weights[i] ?? 0);
689
+ }
690
+ let expected = 11 - total % 11;
691
+ if (expected === 11)
692
+ expected = 0;
693
+ else if (expected === 10)
694
+ return false;
695
+ return digits[8] === expected;
696
+ }
697
+ var REGEXES = [
698
+ {
699
+ label: "private_email",
700
+ pattern: /\b[\w._%+\-]+@[\w.\-]+\.[A-Za-z]{2,}\b/g
701
+ },
702
+ {
703
+ label: "private_url",
704
+ pattern: /https?:\/\/[^\s)>\]]+/g
705
+ },
706
+ {
707
+ label: "private_phone",
708
+ pattern: /(?:\+41|0041|0)\s?(?:\(0\)\s?)?\d{2}[\s\-/.]?\d{3}[\s\-/.]?\d{2}[\s\-/.]?\d{2}\b/g
709
+ },
710
+ {
711
+ label: "account_number",
712
+ pattern: /\bCH\d{2}(?:[\s-]?\d){17}\b/g,
713
+ validator: ibanChOk
714
+ },
715
+ {
716
+ label: "account_number",
717
+ pattern: /\b756\.\d{4}\.\d{4}\.\d{2}\b/g,
718
+ validator: ahvOk
719
+ },
720
+ {
721
+ label: "account_number",
722
+ pattern: /\b756\d{10}\b/g,
723
+ validator: ahvOk
724
+ },
725
+ {
726
+ label: "account_number",
727
+ pattern: /\bCHE-\d{3}\.\d{3}\.\d{3}(?:\s+(?:MWST|TVA|IVA))?\b/g,
728
+ validator: vatCheOk
729
+ },
730
+ {
731
+ label: "account_number",
732
+ pattern: /\b(?:\d[\s-]?){15,16}\d\b/g,
733
+ validator: luhnOk
734
+ },
735
+ {
736
+ label: "private_date",
737
+ pattern: /\b(?:\d{1,2}[./]\d{1,2}[./]\d{4}|\d{4}[./-]\d{1,2}[./-]\d{1,2})\b/g
738
+ },
739
+ {
740
+ label: "secret",
741
+ pattern: /\bsk-(?:proj-)?[A-Za-z0-9_\-]{20,}\b/g
742
+ },
743
+ {
744
+ label: "secret",
745
+ pattern: /\bghp_[A-Za-z0-9]{36}\b/g
746
+ },
747
+ {
748
+ label: "secret",
749
+ pattern: /\bAKIA[A-Z0-9]{16}\b/g
750
+ }
751
+ ];
752
+ var URL_TRAIL_TRIM = '.,;:!?")>]';
753
+ function trimUrl(text, start, end) {
754
+ while (end > start && URL_TRAIL_TRIM.includes(text[end - 1] ?? ""))
755
+ end--;
756
+ return [start, end];
757
+ }
758
+ function findRegexSpans(text) {
759
+ const candidates = [];
760
+ for (const { label, pattern, validator } of REGEXES) {
761
+ pattern.lastIndex = 0;
762
+ let m;
763
+ while ((m = pattern.exec(text)) !== null) {
764
+ let start = m.index;
765
+ let end = m.index + m[0].length;
766
+ if (label === "private_url")
767
+ [start, end] = trimUrl(text, start, end);
768
+ const value = text.slice(start, end);
769
+ if (!value.trim())
770
+ continue;
771
+ if (validator && !validator(value))
772
+ continue;
773
+ candidates.push({
774
+ start,
775
+ end,
776
+ label,
777
+ text: value,
778
+ score: 1
779
+ });
780
+ }
781
+ }
782
+ candidates.sort((a, b) => a.start !== b.start ? a.start - b.start : b.end - b.start - (a.end - a.start));
783
+ const out = [];
784
+ let lastEnd = -1;
785
+ for (const sp of candidates) {
786
+ if (sp.start < lastEnd)
787
+ continue;
788
+ out.push(sp);
789
+ lastEnd = sp.end;
790
+ }
791
+ return out;
792
+ }
793
+ function maskSpans(text, spans) {
794
+ if (spans.length === 0)
795
+ return text;
796
+ const chars = [...text];
797
+ for (const sp of spans) {
798
+ for (let i = sp.start;i < sp.end && i < chars.length; i++) {
799
+ chars[i] = " ";
800
+ }
801
+ }
802
+ return chars.join("");
803
+ }
804
+
805
+ class CompositeDetector {
806
+ model;
807
+ constructor(opts) {
808
+ if (!opts || !opts.model) {
809
+ throw new Error("CompositeDetector requires { model: Detector } — pass any object with " + "a .detect(text) method (e.g. new LocalDetector({...})).");
810
+ }
811
+ this.model = opts.model;
812
+ }
813
+ async detect(text, opts) {
814
+ if (!text)
815
+ return [];
816
+ const regexSpans = findRegexSpans(text);
817
+ const masked = maskSpans(text, regexSpans);
818
+ const modelSpans = await this.model.detect(masked, opts);
819
+ const out = [...regexSpans];
820
+ const regexRanges = regexSpans.map((s) => [s.start, s.end]);
821
+ for (const sp of modelSpans) {
822
+ const overlaps = regexRanges.some(([rs, re]) => sp.start < re && rs < sp.end);
823
+ if (overlaps)
824
+ continue;
825
+ out.push(sp);
826
+ }
827
+ out.sort((a, b) => a.start !== b.start ? a.start - b.start : b.end - b.start - (a.end - a.start));
828
+ return out;
829
+ }
830
+ }
622
831
  // src/detectors/index.ts
623
832
  function defaultDetector() {
624
833
  const hasKey = typeof process !== "undefined" && Boolean(process.env?.GHEIM_API_KEY);
@@ -673,4 +882,4 @@ async function* deanonymizeStream(chunks, session) {
673
882
  }
674
883
 
675
884
  // src/index.ts
676
- var VERSION = "0.1.2";
885
+ var VERSION = "0.1.3";
@@ -543,6 +543,214 @@ async function loadTransformersJs() {
543
543
  throw new Error("LocalDetector requires `@huggingface/transformers`. Install with: bun add @huggingface/transformers");
544
544
  }
545
545
  }
546
+ // src/detectors/composite.ts
547
+ function luhnOk(s) {
548
+ const digits = [];
549
+ for (const c of s) {
550
+ if (c >= "0" && c <= "9")
551
+ digits.push(Number(c));
552
+ }
553
+ if (digits.length < 13 || digits.length > 19)
554
+ return false;
555
+ let total = 0;
556
+ for (let i = 0;i < digits.length; i++) {
557
+ let d = digits[digits.length - 1 - i] ?? 0;
558
+ if (i % 2 === 1) {
559
+ d *= 2;
560
+ if (d > 9)
561
+ d -= 9;
562
+ }
563
+ total += d;
564
+ }
565
+ return total % 10 === 0;
566
+ }
567
+ function ibanChOk(raw) {
568
+ const s = raw.replace(/[\s-]/g, "");
569
+ if (s.length !== 21 || !s.startsWith("CH") || !/^\d+$/.test(s.slice(2))) {
570
+ return false;
571
+ }
572
+ const rearranged = s.slice(4) + s.slice(0, 4);
573
+ let numeric = "";
574
+ for (const c of rearranged) {
575
+ numeric += c >= "A" && c <= "Z" ? String(c.charCodeAt(0) - 55) : c;
576
+ }
577
+ let rem = 0;
578
+ for (const ch of numeric) {
579
+ rem = (rem * 10 + Number(ch)) % 97;
580
+ }
581
+ return rem === 1;
582
+ }
583
+ function ahvOk(s) {
584
+ const digits = [];
585
+ for (const c of s) {
586
+ if (c >= "0" && c <= "9")
587
+ digits.push(Number(c));
588
+ }
589
+ if (digits.length !== 13)
590
+ return false;
591
+ if (digits[0] !== 7 || digits[1] !== 5 || digits[2] !== 6)
592
+ return false;
593
+ let total = 0;
594
+ for (let i = 0;i < 12; i++) {
595
+ total += (digits[i] ?? 0) * (i % 2 === 0 ? 1 : 3);
596
+ }
597
+ const check = (10 - total % 10) % 10;
598
+ return digits[12] === check;
599
+ }
600
+ function vatCheOk(s) {
601
+ const digits = [];
602
+ for (const c of s) {
603
+ if (c >= "0" && c <= "9")
604
+ digits.push(Number(c));
605
+ }
606
+ if (digits.length < 9)
607
+ return false;
608
+ const weights = [5, 4, 3, 2, 7, 6, 5, 4];
609
+ let total = 0;
610
+ for (let i = 0;i < 8; i++) {
611
+ total += (digits[i] ?? 0) * (weights[i] ?? 0);
612
+ }
613
+ let expected = 11 - total % 11;
614
+ if (expected === 11)
615
+ expected = 0;
616
+ else if (expected === 10)
617
+ return false;
618
+ return digits[8] === expected;
619
+ }
620
+ var REGEXES = [
621
+ {
622
+ label: "private_email",
623
+ pattern: /\b[\w._%+\-]+@[\w.\-]+\.[A-Za-z]{2,}\b/g
624
+ },
625
+ {
626
+ label: "private_url",
627
+ pattern: /https?:\/\/[^\s)>\]]+/g
628
+ },
629
+ {
630
+ label: "private_phone",
631
+ pattern: /(?:\+41|0041|0)\s?(?:\(0\)\s?)?\d{2}[\s\-/.]?\d{3}[\s\-/.]?\d{2}[\s\-/.]?\d{2}\b/g
632
+ },
633
+ {
634
+ label: "account_number",
635
+ pattern: /\bCH\d{2}(?:[\s-]?\d){17}\b/g,
636
+ validator: ibanChOk
637
+ },
638
+ {
639
+ label: "account_number",
640
+ pattern: /\b756\.\d{4}\.\d{4}\.\d{2}\b/g,
641
+ validator: ahvOk
642
+ },
643
+ {
644
+ label: "account_number",
645
+ pattern: /\b756\d{10}\b/g,
646
+ validator: ahvOk
647
+ },
648
+ {
649
+ label: "account_number",
650
+ pattern: /\bCHE-\d{3}\.\d{3}\.\d{3}(?:\s+(?:MWST|TVA|IVA))?\b/g,
651
+ validator: vatCheOk
652
+ },
653
+ {
654
+ label: "account_number",
655
+ pattern: /\b(?:\d[\s-]?){15,16}\d\b/g,
656
+ validator: luhnOk
657
+ },
658
+ {
659
+ label: "private_date",
660
+ pattern: /\b(?:\d{1,2}[./]\d{1,2}[./]\d{4}|\d{4}[./-]\d{1,2}[./-]\d{1,2})\b/g
661
+ },
662
+ {
663
+ label: "secret",
664
+ pattern: /\bsk-(?:proj-)?[A-Za-z0-9_\-]{20,}\b/g
665
+ },
666
+ {
667
+ label: "secret",
668
+ pattern: /\bghp_[A-Za-z0-9]{36}\b/g
669
+ },
670
+ {
671
+ label: "secret",
672
+ pattern: /\bAKIA[A-Z0-9]{16}\b/g
673
+ }
674
+ ];
675
+ var URL_TRAIL_TRIM = '.,;:!?")>]';
676
+ function trimUrl(text, start, end) {
677
+ while (end > start && URL_TRAIL_TRIM.includes(text[end - 1] ?? ""))
678
+ end--;
679
+ return [start, end];
680
+ }
681
+ function findRegexSpans(text) {
682
+ const candidates = [];
683
+ for (const { label, pattern, validator } of REGEXES) {
684
+ pattern.lastIndex = 0;
685
+ let m;
686
+ while ((m = pattern.exec(text)) !== null) {
687
+ let start = m.index;
688
+ let end = m.index + m[0].length;
689
+ if (label === "private_url")
690
+ [start, end] = trimUrl(text, start, end);
691
+ const value = text.slice(start, end);
692
+ if (!value.trim())
693
+ continue;
694
+ if (validator && !validator(value))
695
+ continue;
696
+ candidates.push({
697
+ start,
698
+ end,
699
+ label,
700
+ text: value,
701
+ score: 1
702
+ });
703
+ }
704
+ }
705
+ candidates.sort((a, b) => a.start !== b.start ? a.start - b.start : b.end - b.start - (a.end - a.start));
706
+ const out = [];
707
+ let lastEnd = -1;
708
+ for (const sp of candidates) {
709
+ if (sp.start < lastEnd)
710
+ continue;
711
+ out.push(sp);
712
+ lastEnd = sp.end;
713
+ }
714
+ return out;
715
+ }
716
+ function maskSpans(text, spans) {
717
+ if (spans.length === 0)
718
+ return text;
719
+ const chars = [...text];
720
+ for (const sp of spans) {
721
+ for (let i = sp.start;i < sp.end && i < chars.length; i++) {
722
+ chars[i] = " ";
723
+ }
724
+ }
725
+ return chars.join("");
726
+ }
727
+
728
+ class CompositeDetector {
729
+ model;
730
+ constructor(opts) {
731
+ if (!opts || !opts.model) {
732
+ throw new Error("CompositeDetector requires { model: Detector } — pass any object with " + "a .detect(text) method (e.g. new LocalDetector({...})).");
733
+ }
734
+ this.model = opts.model;
735
+ }
736
+ async detect(text, opts) {
737
+ if (!text)
738
+ return [];
739
+ const regexSpans = findRegexSpans(text);
740
+ const masked = maskSpans(text, regexSpans);
741
+ const modelSpans = await this.model.detect(masked, opts);
742
+ const out = [...regexSpans];
743
+ const regexRanges = regexSpans.map((s) => [s.start, s.end]);
744
+ for (const sp of modelSpans) {
745
+ const overlaps = regexRanges.some(([rs, re]) => sp.start < re && rs < sp.end);
746
+ if (overlaps)
747
+ continue;
748
+ out.push(sp);
749
+ }
750
+ out.sort((a, b) => a.start !== b.start ? a.start - b.start : b.end - b.start - (a.end - a.start));
751
+ return out;
752
+ }
753
+ }
546
754
  // src/detectors/index.ts
547
755
  function defaultDetector() {
548
756
  const hasKey = typeof process !== "undefined" && Boolean(process.env?.GHEIM_API_KEY);
package/dist/esm/index.js CHANGED
@@ -564,6 +564,214 @@ async function loadTransformersJs() {
564
564
  throw new Error("LocalDetector requires `@huggingface/transformers`. Install with: bun add @huggingface/transformers");
565
565
  }
566
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
+ }
567
775
  // src/detectors/index.ts
568
776
  function defaultDetector() {
569
777
  const hasKey = typeof process !== "undefined" && Boolean(process.env?.GHEIM_API_KEY);
@@ -618,7 +826,7 @@ async function* deanonymizeStream(chunks, session) {
618
826
  }
619
827
 
620
828
  // src/index.ts
621
- var VERSION = "0.1.2";
829
+ var VERSION = "0.1.3";
622
830
  export {
623
831
  renderSentinel,
624
832
  parseSentinel,
@@ -638,5 +846,6 @@ export {
638
846
  DEFAULT_MODEL as REMOTE_DEFAULT_MODEL,
639
847
  LocalDetector,
640
848
  LABEL_TO_TAG,
641
- DEFAULT_BASE_URL
849
+ DEFAULT_BASE_URL,
850
+ CompositeDetector
642
851
  };
@@ -500,6 +500,214 @@ async function loadTransformersJs() {
500
500
  throw new Error("LocalDetector requires `@huggingface/transformers`. Install with: bun add @huggingface/transformers");
501
501
  }
502
502
  }
503
+ // src/detectors/composite.ts
504
+ function luhnOk(s) {
505
+ const digits = [];
506
+ for (const c of s) {
507
+ if (c >= "0" && c <= "9")
508
+ digits.push(Number(c));
509
+ }
510
+ if (digits.length < 13 || digits.length > 19)
511
+ return false;
512
+ let total = 0;
513
+ for (let i = 0;i < digits.length; i++) {
514
+ let d = digits[digits.length - 1 - i] ?? 0;
515
+ if (i % 2 === 1) {
516
+ d *= 2;
517
+ if (d > 9)
518
+ d -= 9;
519
+ }
520
+ total += d;
521
+ }
522
+ return total % 10 === 0;
523
+ }
524
+ function ibanChOk(raw) {
525
+ const s = raw.replace(/[\s-]/g, "");
526
+ if (s.length !== 21 || !s.startsWith("CH") || !/^\d+$/.test(s.slice(2))) {
527
+ return false;
528
+ }
529
+ const rearranged = s.slice(4) + s.slice(0, 4);
530
+ let numeric = "";
531
+ for (const c of rearranged) {
532
+ numeric += c >= "A" && c <= "Z" ? String(c.charCodeAt(0) - 55) : c;
533
+ }
534
+ let rem = 0;
535
+ for (const ch of numeric) {
536
+ rem = (rem * 10 + Number(ch)) % 97;
537
+ }
538
+ return rem === 1;
539
+ }
540
+ function ahvOk(s) {
541
+ const digits = [];
542
+ for (const c of s) {
543
+ if (c >= "0" && c <= "9")
544
+ digits.push(Number(c));
545
+ }
546
+ if (digits.length !== 13)
547
+ return false;
548
+ if (digits[0] !== 7 || digits[1] !== 5 || digits[2] !== 6)
549
+ return false;
550
+ let total = 0;
551
+ for (let i = 0;i < 12; i++) {
552
+ total += (digits[i] ?? 0) * (i % 2 === 0 ? 1 : 3);
553
+ }
554
+ const check = (10 - total % 10) % 10;
555
+ return digits[12] === check;
556
+ }
557
+ function vatCheOk(s) {
558
+ const digits = [];
559
+ for (const c of s) {
560
+ if (c >= "0" && c <= "9")
561
+ digits.push(Number(c));
562
+ }
563
+ if (digits.length < 9)
564
+ return false;
565
+ const weights = [5, 4, 3, 2, 7, 6, 5, 4];
566
+ let total = 0;
567
+ for (let i = 0;i < 8; i++) {
568
+ total += (digits[i] ?? 0) * (weights[i] ?? 0);
569
+ }
570
+ let expected = 11 - total % 11;
571
+ if (expected === 11)
572
+ expected = 0;
573
+ else if (expected === 10)
574
+ return false;
575
+ return digits[8] === expected;
576
+ }
577
+ var REGEXES = [
578
+ {
579
+ label: "private_email",
580
+ pattern: /\b[\w._%+\-]+@[\w.\-]+\.[A-Za-z]{2,}\b/g
581
+ },
582
+ {
583
+ label: "private_url",
584
+ pattern: /https?:\/\/[^\s)>\]]+/g
585
+ },
586
+ {
587
+ label: "private_phone",
588
+ pattern: /(?:\+41|0041|0)\s?(?:\(0\)\s?)?\d{2}[\s\-/.]?\d{3}[\s\-/.]?\d{2}[\s\-/.]?\d{2}\b/g
589
+ },
590
+ {
591
+ label: "account_number",
592
+ pattern: /\bCH\d{2}(?:[\s-]?\d){17}\b/g,
593
+ validator: ibanChOk
594
+ },
595
+ {
596
+ label: "account_number",
597
+ pattern: /\b756\.\d{4}\.\d{4}\.\d{2}\b/g,
598
+ validator: ahvOk
599
+ },
600
+ {
601
+ label: "account_number",
602
+ pattern: /\b756\d{10}\b/g,
603
+ validator: ahvOk
604
+ },
605
+ {
606
+ label: "account_number",
607
+ pattern: /\bCHE-\d{3}\.\d{3}\.\d{3}(?:\s+(?:MWST|TVA|IVA))?\b/g,
608
+ validator: vatCheOk
609
+ },
610
+ {
611
+ label: "account_number",
612
+ pattern: /\b(?:\d[\s-]?){15,16}\d\b/g,
613
+ validator: luhnOk
614
+ },
615
+ {
616
+ label: "private_date",
617
+ pattern: /\b(?:\d{1,2}[./]\d{1,2}[./]\d{4}|\d{4}[./-]\d{1,2}[./-]\d{1,2})\b/g
618
+ },
619
+ {
620
+ label: "secret",
621
+ pattern: /\bsk-(?:proj-)?[A-Za-z0-9_\-]{20,}\b/g
622
+ },
623
+ {
624
+ label: "secret",
625
+ pattern: /\bghp_[A-Za-z0-9]{36}\b/g
626
+ },
627
+ {
628
+ label: "secret",
629
+ pattern: /\bAKIA[A-Z0-9]{16}\b/g
630
+ }
631
+ ];
632
+ var URL_TRAIL_TRIM = '.,;:!?")>]';
633
+ function trimUrl(text, start, end) {
634
+ while (end > start && URL_TRAIL_TRIM.includes(text[end - 1] ?? ""))
635
+ end--;
636
+ return [start, end];
637
+ }
638
+ function findRegexSpans(text) {
639
+ const candidates = [];
640
+ for (const { label, pattern, validator } of REGEXES) {
641
+ pattern.lastIndex = 0;
642
+ let m;
643
+ while ((m = pattern.exec(text)) !== null) {
644
+ let start = m.index;
645
+ let end = m.index + m[0].length;
646
+ if (label === "private_url")
647
+ [start, end] = trimUrl(text, start, end);
648
+ const value = text.slice(start, end);
649
+ if (!value.trim())
650
+ continue;
651
+ if (validator && !validator(value))
652
+ continue;
653
+ candidates.push({
654
+ start,
655
+ end,
656
+ label,
657
+ text: value,
658
+ score: 1
659
+ });
660
+ }
661
+ }
662
+ candidates.sort((a, b) => a.start !== b.start ? a.start - b.start : b.end - b.start - (a.end - a.start));
663
+ const out = [];
664
+ let lastEnd = -1;
665
+ for (const sp of candidates) {
666
+ if (sp.start < lastEnd)
667
+ continue;
668
+ out.push(sp);
669
+ lastEnd = sp.end;
670
+ }
671
+ return out;
672
+ }
673
+ function maskSpans(text, spans) {
674
+ if (spans.length === 0)
675
+ return text;
676
+ const chars = [...text];
677
+ for (const sp of spans) {
678
+ for (let i = sp.start;i < sp.end && i < chars.length; i++) {
679
+ chars[i] = " ";
680
+ }
681
+ }
682
+ return chars.join("");
683
+ }
684
+
685
+ class CompositeDetector {
686
+ model;
687
+ constructor(opts) {
688
+ if (!opts || !opts.model) {
689
+ throw new Error("CompositeDetector requires { model: Detector } — pass any object with " + "a .detect(text) method (e.g. new LocalDetector({...})).");
690
+ }
691
+ this.model = opts.model;
692
+ }
693
+ async detect(text, opts) {
694
+ if (!text)
695
+ return [];
696
+ const regexSpans = findRegexSpans(text);
697
+ const masked = maskSpans(text, regexSpans);
698
+ const modelSpans = await this.model.detect(masked, opts);
699
+ const out = [...regexSpans];
700
+ const regexRanges = regexSpans.map((s) => [s.start, s.end]);
701
+ for (const sp of modelSpans) {
702
+ const overlaps = regexRanges.some(([rs, re]) => sp.start < re && rs < sp.end);
703
+ if (overlaps)
704
+ continue;
705
+ out.push(sp);
706
+ }
707
+ out.sort((a, b) => a.start !== b.start ? a.start - b.start : b.end - b.start - (a.end - a.start));
708
+ return out;
709
+ }
710
+ }
503
711
  // src/detectors/index.ts
504
712
  function defaultDetector() {
505
713
  const hasKey = typeof process !== "undefined" && Boolean(process.env?.GHEIM_API_KEY);
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Composite detector: regex front-end + model on the remainder.
3
+ *
4
+ * Production-ready Swiss PII detector that combines two complementary
5
+ * strategies — a port of the Python sibling
6
+ * (`gheim.detectors.composite.CompositeDetector`) with parity-tested
7
+ * regexes and checksum validators.
8
+ *
9
+ * 1. Deterministic regex front-end for structured PII categories where
10
+ * shape is unambiguous and checksums verify identity:
11
+ * - IBAN-CH (mod-97-10), AHV (EAN-13 mod-10), VAT-CHE (mod-11-10),
12
+ * Visa/Mastercard (Luhn)
13
+ * - Email, URL, Swiss phone (+41 / 0041 / 0...), explicit dates
14
+ * (DD.MM.YYYY, YYYY-MM-DD)
15
+ * - API tokens (sk-, ghp_, AKIA...)
16
+ *
17
+ * Spans found here are "trusted" — checksums catch most surface-form
18
+ * imitations; the model is never asked.
19
+ *
20
+ * 2. The fine-tuned model on whatever's left after regex masking.
21
+ * Persons, addresses, and edge-case dates fall here. The model's
22
+ * "single-category mode" failure (where an IBAN/phone-shaped
23
+ * sequence suppresses person/date detection in the same input) is
24
+ * bypassed because the regex already absorbed those tokens — the
25
+ * model sees them as whitespace.
26
+ *
27
+ * Wraps the same `Detector` interface (`detect(text) → Span[]`) as
28
+ * `LocalDetector` so it drops into any code path that already accepts a
29
+ * detector.
30
+ */
31
+ import type { Detector, Span } from "./base.ts";
32
+ /** Options for {@link CompositeDetector}. */
33
+ export interface CompositeDetectorOptions {
34
+ /** Inner detector — usually a {@link LocalDetector}. Anything with
35
+ * a `detect(text) → Promise<Span[]>` method works. */
36
+ model: Detector;
37
+ }
38
+ /** Regex front-end (with checksum validation) + model on the remainder.
39
+ *
40
+ * @example
41
+ * import { LocalDetector, CompositeDetector } from "gheim";
42
+ * const composite = new CompositeDetector({
43
+ * model: new LocalDetector({ dtype: "q8" }),
44
+ * });
45
+ * const spans = await composite.detect(swissLetter);
46
+ * // → IBAN/phone/email caught by regex (checksum-validated);
47
+ * // person/address/dates caught by the model on the masked remainder.
48
+ */
49
+ export declare class CompositeDetector implements Detector {
50
+ readonly model: Detector;
51
+ constructor(opts: CompositeDetectorOptions);
52
+ /** Two-pass detection: regex first (with checksum), then model on
53
+ * the masked remainder. Returns a single overlap-resolved span list. */
54
+ detect(text: string, opts?: {
55
+ model?: string;
56
+ }): Promise<Span[]>;
57
+ }
58
+ //# sourceMappingURL=composite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../../src/detectors/composite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAiNhD,6CAA6C;AAC7C,MAAM,WAAW,wBAAwB;IACvC;2DACuD;IACvD,KAAK,EAAE,QAAQ,CAAC;CACjB;AAED;;;;;;;;;;GAUG;AACH,qBAAa,iBAAkB,YAAW,QAAQ;IAChD,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;gBAEb,IAAI,EAAE,wBAAwB;IAU1C;6EACyE;IACnE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;CAmBvE"}
@@ -3,6 +3,8 @@ export { DEFAULT_BASE_URL, DEFAULT_MODEL as REMOTE_DEFAULT_MODEL, RemoteDetector
3
3
  export type { RemoteDetectorOptions } from "./remote.ts";
4
4
  export { LocalDetector } from "./local.ts";
5
5
  export type { LocalDetectorOptions, LocalDetectorLoadEvent } from "./local.ts";
6
+ export { CompositeDetector } from "./composite.ts";
7
+ export type { CompositeDetectorOptions } from "./composite.ts";
6
8
  import type { Detector } from "./base.ts";
7
9
  /** Pick a reasonable default: remote if `GHEIM_API_KEY` is set in env, else local. */
8
10
  export declare function defaultDetector(): Detector;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/detectors/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EACL,gBAAgB,EAChB,aAAa,IAAI,oBAAoB,EACrC,cAAc,GACf,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,YAAY,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAE/E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAI1C,sFAAsF;AACtF,wBAAgB,eAAe,IAAI,QAAQ,CAI1C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/detectors/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EACL,gBAAgB,EAChB,aAAa,IAAI,oBAAoB,EACrC,cAAc,GACf,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,YAAY,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAC/E,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,YAAY,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAE/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAI1C,sFAAsF;AACtF,wBAAgB,eAAe,IAAI,QAAQ,CAI1C"}
@@ -13,11 +13,11 @@ export { LABEL_TO_TAG, SENTINEL_RE, isPossibleSentinelPrefix, labelTag, parseSen
13
13
  export type { Sentinel } from "./core/sentinels.ts";
14
14
  export { StreamDeanonymizer } from "./core/stream.ts";
15
15
  export type { Detector, Span } from "./detectors/base.ts";
16
- export { DEFAULT_BASE_URL, REMOTE_DEFAULT_MODEL, RemoteDetector, LocalDetector, defaultDetector, } from "./detectors/index.ts";
17
- export type { RemoteDetectorOptions, LocalDetectorOptions, LocalDetectorLoadEvent, } from "./detectors/index.ts";
16
+ export { DEFAULT_BASE_URL, REMOTE_DEFAULT_MODEL, RemoteDetector, LocalDetector, CompositeDetector, defaultDetector, } from "./detectors/index.ts";
17
+ export type { RemoteDetectorOptions, LocalDetectorOptions, LocalDetectorLoadEvent, CompositeDetectorOptions, } from "./detectors/index.ts";
18
18
  export { anonymizeMessages, anonymizeText, deanonymizeStream, deanonymizeText, } from "./plain.ts";
19
19
  export type { ChatMessage } from "./plain.ts";
20
- export declare const VERSION = "0.1.2";
20
+ export declare const VERSION = "0.1.3";
21
21
  export { mergeAdjacent } from "./core/session.ts";
22
22
  export type { MergedSpan } from "./core/session.ts";
23
23
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,EACL,YAAY,EACZ,WAAW,EACX,wBAAwB,EACxB,QAAQ,EACR,aAAa,EACb,cAAc,GACf,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,YAAY,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,cAAc,EACd,aAAa,EACb,eAAe,GAChB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,qBAAqB,EACrB,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAK9C,eAAO,MAAM,OAAO,UAAU,CAAC;AAE/B,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,EACL,YAAY,EACZ,WAAW,EACX,wBAAwB,EACxB,QAAQ,EACR,aAAa,EACb,cAAc,GACf,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,YAAY,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,cAAc,EACd,aAAa,EACb,iBAAiB,EACjB,eAAe,GAChB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,qBAAqB,EACrB,oBAAoB,EACpB,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAK9C,eAAO,MAAM,OAAO,UAAU,CAAC;AAE/B,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gheim",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "PII round-trip for LLM APIs: anonymize before the request, de-anonymize the stream on the way back.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Joel Barmettler <joel.barmettler@gmail.com>",