@studio-foundation/anonymizer 0.3.0-beta.6 → 0.5.2

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
@@ -1,20 +1,25 @@
1
1
  # @studio-foundation/anonymizer
2
2
 
3
- PII detection and anonymization library. Replaces sensitive data with consistent tokens before sending to LLMs, with a keymap to restore the original values afterward.
3
+ **Studio** is a declarative YAML runtime for AI agents. It orchestrates multi-stage agent workflows with structured output validation and automatic retry. This package is the **anonymizer**: a PII detection and anonymization library that replaces sensitive data with consistent tokens before sending to LLMs, with a keymap to restore the original values afterward.
4
4
 
5
- ## Role
5
+ anonymizer sits at the bottom of the stack, a pure utility with zero `@studio-foundation/*` dependencies. It's usable standalone in any TypeScript project, and it's what powers `anonymize: true` in Studio agent configs.
6
6
 
7
- anonymizer sits at the bottom of the stack — a pure utility with zero `@studio/*` dependencies. The runner wraps it in `AnonymizationMiddleware` and injects it transparently into the agent execution loop.
7
+ - Homepage: https://github.com/studio-foundation/studio
8
+ - Full docs: [README](https://github.com/studio-foundation/studio#readme) · [CONCEPTS](https://github.com/studio-foundation/studio/blob/main/CONCEPTS.md)
9
+ - Use via the CLI: [`@studio-foundation/cli`](https://www.npmjs.com/package/@studio-foundation/cli)
8
10
 
9
- ```
10
- user data → anonymize() → [PERSON_1], [EMAIL_1] → LLM → deanonymize() → original values
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install @studio-foundation/anonymizer
15
+ # or
16
+ pnpm add @studio-foundation/anonymizer
11
17
  ```
12
18
 
13
- ## Key exports
19
+ ## Quick start
14
20
 
15
21
  ```typescript
16
- import { anonymize, deanonymize, Tokenizer } from '@studio-foundation/anonymizer';
17
- import type { PIICategory, PIIDetectionResult, AnonymizerOptions } from '@studio-foundation/anonymizer';
22
+ import { anonymize, deanonymize } from '@studio-foundation/anonymizer';
18
23
 
19
24
  // Anonymize a string
20
25
  const { text, keymap } = anonymize('Hi Marie, call me at 555-867-5309');
@@ -30,26 +35,36 @@ const { text: text2, keymap: keymap2 } = anonymize(nextChunk, { seedKeymap: keym
30
35
  // PERSON_1 still maps to "Marie" across calls
31
36
  ```
32
37
 
38
+ ```
39
+ user data → anonymize() → [PERSON_1], [EMAIL_1] → LLM → deanonymize() → original values
40
+ ```
41
+
33
42
  ## PII categories
34
43
 
35
44
  | Category | Token format | What it detects |
36
45
  |----------|-------------|-----------------|
37
- | `person` | `PERSON_N` | Names after salutations (Dear, Hi, Mr., Dr., etc.) best effort |
46
+ | `person` | `PERSON_N` | Names after salutations (Dear, Hi, Mr., Dr., etc.) (best effort) |
38
47
  | `email` | `EMAIL_N` | Email addresses |
39
48
  | `phone` | `PHONE_N` | US phone numbers (10 digits, various formats) |
40
49
  | `ssn` | `SSN_N` | Social security numbers (ddd-dd-dddd) |
41
50
  | `credit_card` | `CREDIT_CARD_N` | 16-digit card numbers |
42
- | `address` | `ADDRESS_N` | Reserved (detection not yet implemented) |
51
+ | `address` | `ADDRESS_N` | Reserved — delegated to future NER detector (not regex-detected) |
43
52
 
44
53
  ## Detection strategy
45
54
 
46
55
  Two-phase detection on each call:
47
56
 
48
- 1. **Regex (high precision)** email, phone, SSN, credit card. Structural patterns anchored to avoid false positives (e.g. SSN regex uses strict hyphen format to avoid matching phone fragments).
57
+ 1. **Regex (high precision)**: email, phone, SSN, credit card. Structural patterns anchored to avoid false positives (e.g. SSN regex uses strict hyphen format to avoid matching phone fragments).
49
58
 
50
- 2. **Person names (best effort)** salutation-gated pattern (`Dear X`, `Hi X`, `Mr. X`, etc.). Only catches explicitly addressed names not bare occurrences.
59
+ 2. **Person names (best effort)**: salutation-gated pattern (`Dear X`, `Hi X`, `Mr. X`, etc.). Only catches explicitly addressed names, not bare occurrences.
51
60
 
52
- Spans are non-overlapping. If two patterns match the same range, the first one wins and the position is marked occupied.
61
+ Spans are non-overlapping. Overlaps are resolved by an explicit priority
62
+ (credit_card > ssn > email > phone > person, most-specific wins): the
63
+ highest-priority match takes the whole span and overlapping lower-priority
64
+ matches are dropped. Detection is pluggable via the `DetectionProvider`
65
+ interface (`detect(text) → Promise<Span[]>`); `RegexDetector` is the built-in
66
+ provider. Person detection covers FR + EN salutations. `address` is not
67
+ detected by regex — it is delegated to a future NER detector.
53
68
 
54
69
  ## Token consistency
55
70
 
@@ -81,10 +96,16 @@ The runner wraps this in `AnonymizationMiddleware` (in `runner/src/middleware/an
81
96
  2. Tool results are anonymized before being injected back into context
82
97
  3. The accumulated keymap is written to `.studio/runs/anonymization/<run-id>.keymap.json` for post-run inspection
83
98
 
84
- The middleware is wired by the engine and passed to `runAgent()` user code doesn't call `anonymize()` directly.
99
+ The middleware is wired by the engine and passed to `runAgent()`, user code doesn't call `anonymize()` directly when using the Studio CLI.
100
+
101
+ ## For contributors
102
+
103
+ Internal rules that govern this package:
104
+
105
+ - **Zero `@studio-foundation/*` dependencies.** This package must stay a pure utility.
106
+ - `anonymize()` is stateless, the `Tokenizer` is created fresh each call (or seeded via `seedKeymap`).
107
+ - Person detection is always best-effort and non-fatal, failures are silently skipped.
85
108
 
86
- ## Rules
109
+ ## License
87
110
 
88
- - **Zero `@studio/*` dependencies.** This package must stay a pure utility.
89
- - `anonymize()` is stateless — the `Tokenizer` is created fresh each call (or seeded via `seedKeymap`).
90
- - Person detection is always best-effort and non-fatal — failures are silently skipped.
111
+ AGPL-3.0-only
@@ -0,0 +1,18 @@
1
+ /**
2
+ * A detected PII span — POSITIONS only. The kernel reconstructs the value via
3
+ * text.slice(start, end). `type` is a free string so a future network provider
4
+ * (Presidio/NER) can return its own vocabulary without changing this interface.
5
+ */
6
+ export interface Span {
7
+ start: number;
8
+ end: number;
9
+ type: string;
10
+ }
11
+ /**
12
+ * Pluggable PII detection. Async by default so the same contract covers an
13
+ * in-process detector (regex) and a future network provider (Presidio HTTP).
14
+ */
15
+ export interface DetectionProvider {
16
+ detect(text: string): Promise<Span[]>;
17
+ }
18
+ //# sourceMappingURL=provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/detection/provider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,IAAI;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;CACvC"}
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/detection/provider.ts"],"names":[],"mappings":""}
@@ -0,0 +1,13 @@
1
+ import type { DetectionProvider, Span } from './provider.js';
2
+ /**
3
+ * Base in-process detector. Covers the types where regex is reliable
4
+ * (formatted identifiers): email, phone, ssn, credit_card, and salutation-based
5
+ * person (FR + EN).
6
+ *
7
+ * address is intentionally delegated to a future NER detector — see PRIORITY in
8
+ * regex-matcher.ts. Do NOT add an address regex here.
9
+ */
10
+ export declare class RegexDetector implements DetectionProvider {
11
+ detect(text: string): Promise<Span[]>;
12
+ }
13
+ //# sourceMappingURL=regex-detector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"regex-detector.d.ts","sourceRoot":"","sources":["../../src/detection/regex-detector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAG7D;;;;;;;GAOG;AACH,qBAAa,aAAc,YAAW,iBAAiB;IAC/C,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;CAG5C"}
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RegexDetector = void 0;
4
+ const regex_matcher_js_1 = require("./regex-matcher.js");
5
+ /**
6
+ * Base in-process detector. Covers the types where regex is reliable
7
+ * (formatted identifiers): email, phone, ssn, credit_card, and salutation-based
8
+ * person (FR + EN).
9
+ *
10
+ * address is intentionally delegated to a future NER detector — see PRIORITY in
11
+ * regex-matcher.ts. Do NOT add an address regex here.
12
+ */
13
+ class RegexDetector {
14
+ async detect(text) {
15
+ return (0, regex_matcher_js_1.matchPII)(text).map(({ start, end, type }) => ({ start, end, type }));
16
+ }
17
+ }
18
+ exports.RegexDetector = RegexDetector;
19
+ //# sourceMappingURL=regex-detector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"regex-detector.js","sourceRoot":"","sources":["../../src/detection/regex-detector.ts"],"names":[],"mappings":";;;AACA,yDAA8C;AAE9C;;;;;;;GAOG;AACH,MAAa,aAAa;IACxB,KAAK,CAAC,MAAM,CAAC,IAAY;QACvB,OAAO,IAAA,2BAAQ,EAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAC9E,CAAC;CACF;AAJD,sCAIC"}
@@ -0,0 +1,9 @@
1
+ export type PIIType = 'email' | 'phone' | 'ssn' | 'credit_card' | 'person';
2
+ export interface RegexMatch {
3
+ start: number;
4
+ end: number;
5
+ type: PIIType;
6
+ }
7
+ export declare function resolveByPriority(candidates: RegexMatch[]): RegexMatch[];
8
+ export declare function matchPII(text: string): RegexMatch[];
9
+ //# sourceMappingURL=regex-matcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"regex-matcher.d.ts","sourceRoot":"","sources":["../../src/detection/regex-matcher.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,KAAK,GAAG,aAAa,GAAG,QAAQ,CAAC;AAE3E,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,OAAO,CAAC;CACf;AAyFD,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAaxE;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,EAAE,CAMnD"}
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ // Shared PRIVATE regex internal for the anonymizer package.
3
+ // Returns POSITIONS only — never the extracted substring. The kernel
4
+ // reconstructs the value via text.slice(start, end); see detector.ts.
5
+ // NOT exported from index.ts.
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.resolveByPriority = resolveByPriority;
8
+ exports.matchPII = matchPII;
9
+ // Most-specific format wins. credit_card beats ssn on the dangerous digit-run
10
+ // collision; email is near-unambiguous (the @) so it stays high; phone is the
11
+ // most permissive of the formatted types so it runs late; person is a
12
+ // salutation heuristic, last, and only claims what the others leave.
13
+ //
14
+ // address: intentionally NOT here. Addresses are the least regex-able PII type;
15
+ // a number+word+street-suffix pattern false-positives on non-addresses, and in
16
+ // an email classifier a false positive (tokenizing legitimate text) is strictly
17
+ // worse than a miss. Address detection is delegated to a future NER detector
18
+ // (horizon 2). Do NOT add an address regex here.
19
+ const PRIORITY = ['credit_card', 'ssn', 'email', 'phone', 'person'];
20
+ const FORMAT_PATTERNS = {
21
+ credit_card: /\b(?:\d{4}[-\s]?){3}\d{4}\b/g,
22
+ ssn: /\b\d{3}-\d{2}-\d{4}\b/g,
23
+ email: /\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b/g,
24
+ phone: /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/g,
25
+ };
26
+ // Person detection is salutation-anchored (not a pure format), so it inherits
27
+ // the deployment language. The first deployment is French-language; an EN-only list
28
+ // would silently miss French names and leak the most sensitive PII (parents and
29
+ // minors). FR + EN salutations ship together.
30
+ //
31
+ // Case-sensitivity is SPLIT across two passes, because a single regex cannot
32
+ // be case-insensitive in one part and case-sensitive in another (the `i` flag
33
+ // is all-or-nothing, and `i` on the name capture is exactly the bug: `[A-Z]`
34
+ // would then also match lowercase, tokenizing `bonjour marie` as a person):
35
+ //
36
+ // 1. SALUTATION_TRIGGER (`/gi`) — case-insensitive so `bonjour` / `Bonjour` /
37
+ // `BONJOUR` all fire. It consumes the trigger *and* the whitespace after
38
+ // it, leaving lastIndex at the first character of the name.
39
+ // 2. NAME (`/y`, sticky, NO `i`) — case-SENSITIVE, anchored to start exactly
40
+ // where the trigger ended. It requires an initial uppercase on every word,
41
+ // so a lowercase word after a salutation produces no name. A false positive
42
+ // (tokenizing legitimate text) is strictly worse than a miss in an email
43
+ // classifier, so we fail closed.
44
+ //
45
+ // (ES2025 inline flags `(?i:…)` would express this in one regex, but V8 on the
46
+ // supported runtime does not yet parse them.)
47
+ //
48
+ // `\b...` anchors the trigger to a word boundary. `m\.` REQUIRES the trailing
49
+ // period so a word merely ending in "m" before a period (e.g. "forum.") does
50
+ // not fire.
51
+ const SALUTATION_TRIGGER = /\b(?:dear|hi|hello|greetings|hey(?:\s+there)?|mr\.?|mrs\.?|ms\.?|dr\.?|prof\.?|bonjour|bonsoir|all[oô]|salut|ch[eè]re?s?|madame|monsieur|mme\.?|m\.)\s+/gi;
52
+ const NAME = /[A-Z][a-zA-ZÀ-ÿ'\-]+(?:\s+[A-Z][a-zA-ZÀ-ÿ'\-]+)*/y;
53
+ function gatherPersonCandidates(text) {
54
+ const out = [];
55
+ SALUTATION_TRIGGER.lastIndex = 0;
56
+ let m;
57
+ while ((m = SALUTATION_TRIGGER.exec(text)) !== null) {
58
+ const start = m.index + m[0].length;
59
+ // Sticky: matches only if a Title-cased name begins exactly at `start`.
60
+ NAME.lastIndex = start;
61
+ const nameMatch = NAME.exec(text);
62
+ if (!nameMatch)
63
+ continue;
64
+ out.push({ start, end: start + nameMatch[0].length, type: 'person' });
65
+ }
66
+ return out;
67
+ }
68
+ function gatherFormatCandidates(text) {
69
+ const out = [];
70
+ for (const type of Object.keys(FORMAT_PATTERNS)) {
71
+ const re = FORMAT_PATTERNS[type];
72
+ re.lastIndex = 0;
73
+ let m;
74
+ while ((m = re.exec(text)) !== null) {
75
+ out.push({ start: m.index, end: m.index + m[0].length, type });
76
+ }
77
+ }
78
+ return out;
79
+ }
80
+ function rangeOccupied(occupied, start, end) {
81
+ // True interval intersection: ANY position in [start, end) being occupied
82
+ // means overlap. Never a start-only check.
83
+ for (let i = start; i < end; i++) {
84
+ if (occupied.has(i))
85
+ return true;
86
+ }
87
+ return false;
88
+ }
89
+ function resolveByPriority(candidates) {
90
+ const rank = (t) => PRIORITY.indexOf(t);
91
+ const ordered = [...candidates].sort((a, b) => rank(a.type) - rank(b.type) || a.start - b.start);
92
+ const occupied = new Set();
93
+ const accepted = [];
94
+ for (const c of ordered) {
95
+ if (rangeOccupied(occupied, c.start, c.end))
96
+ continue;
97
+ for (let i = c.start; i < c.end; i++)
98
+ occupied.add(i);
99
+ accepted.push(c);
100
+ }
101
+ return accepted.sort((a, b) => a.start - b.start);
102
+ }
103
+ function matchPII(text) {
104
+ const candidates = [
105
+ ...gatherFormatCandidates(text),
106
+ ...gatherPersonCandidates(text),
107
+ ];
108
+ return resolveByPriority(candidates);
109
+ }
110
+ //# sourceMappingURL=regex-matcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"regex-matcher.js","sourceRoot":"","sources":["../../src/detection/regex-matcher.ts"],"names":[],"mappings":"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"}
@@ -2,6 +2,11 @@ import type { PIISpan } from './types.js';
2
2
  /**
3
3
  * Detect PII spans in text.
4
4
  * Returns non-overlapping spans sorted by position.
5
+ *
6
+ * Thin adapter over the shared regex-matcher internal: the matcher owns the
7
+ * patterns, the canonical type vocabulary, and overlap resolution (positions
8
+ * only). The value is reconstructed HERE — never inside the internal. PIIType
9
+ * is a subset of PIICategory, so the type assignment is direct.
5
10
  */
6
11
  export declare function detectPII(text: string): PIISpan[];
7
12
  //# sourceMappingURL=detector.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"detector.d.ts","sourceRoot":"","sources":["../src/detector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAe,OAAO,EAAE,MAAM,YAAY,CAAC;AA0BvD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CAyBjD"}
1
+ {"version":3,"file":"detector.d.ts","sourceRoot":"","sources":["../src/detector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAG1C;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CAOjD"}
package/dist/detector.js CHANGED
@@ -1,92 +1,22 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.detectPII = detectPII;
4
- // Regex patterns for structural PII (high precision)
5
- const PATTERNS = [
6
- {
7
- category: 'email',
8
- regex: /\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b/g,
9
- },
10
- {
11
- category: 'phone',
12
- // US phone: must be at least 10 digits. Require optional country code + area code.
13
- // Anchored to avoid matching partial SSN-like strings.
14
- regex: /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/g,
15
- },
16
- {
17
- category: 'ssn',
18
- // SSN: exactly ddd-dd-dddd with hyphens (strict format, not dots or spaces
19
- // to avoid collision with phone numbers already consumed)
20
- regex: /\b\d{3}-\d{2}-\d{4}\b/g,
21
- },
22
- {
23
- category: 'credit_card',
24
- regex: /\b(?:\d{4}[-\s]?){3}\d{4}\b/g,
25
- },
26
- ];
4
+ const regex_matcher_js_1 = require("./detection/regex-matcher.js");
27
5
  /**
28
6
  * Detect PII spans in text.
29
7
  * Returns non-overlapping spans sorted by position.
8
+ *
9
+ * Thin adapter over the shared regex-matcher internal: the matcher owns the
10
+ * patterns, the canonical type vocabulary, and overlap resolution (positions
11
+ * only). The value is reconstructed HERE — never inside the internal. PIIType
12
+ * is a subset of PIICategory, so the type assignment is direct.
30
13
  */
31
14
  function detectPII(text) {
32
- const spans = [];
33
- const occupied = new Set();
34
- // Phase 1: Structural PII via regex
35
- for (const { category, regex } of PATTERNS) {
36
- regex.lastIndex = 0;
37
- let match;
38
- while ((match = regex.exec(text)) !== null) {
39
- const start = match.index;
40
- const end = start + match[0].length;
41
- if (isOccupied(occupied, start, end))
42
- continue;
43
- markOccupied(occupied, start, end);
44
- spans.push({ start, end, category, value: match[0] });
45
- }
46
- }
47
- // Phase 2: Person names — best-effort via @redactpii/node
48
- try {
49
- detectPersons(text, occupied, spans);
50
- }
51
- catch {
52
- // Person detection is best-effort; silently skip on any error
53
- }
54
- return spans.sort((a, b) => a.start - b.start);
55
- }
56
- function isOccupied(occupied, start, end) {
57
- for (let i = start; i < end; i++) {
58
- if (occupied.has(i))
59
- return true;
60
- }
61
- return false;
62
- }
63
- function markOccupied(occupied, start, end) {
64
- for (let i = start; i < end; i++) {
65
- occupied.add(i);
66
- }
67
- }
68
- function detectPersons(text, occupied, spans) {
69
- // The @redactpii/node NAME pattern only catches names after salutations
70
- // (Dear, Hi, Hello, Hey, etc.) — not bare names. We replicate that pattern
71
- // here and use it directly, avoiding the ESM/CJS import complexity.
72
- // This gives best-effort detection for explicitly addressed names.
73
- const salutationPattern = /(?:dear|hi|hello|greetings|hey(?:\s+there)?|mr\.?|mrs\.?|ms\.?|dr\.?|prof\.?)\s+([A-Z][a-zA-ZÀ-ÿ'\-]+(?:\s+[A-Z][a-zA-ZÀ-ÿ'\-]+)*)/gi;
74
- let match;
75
- salutationPattern.lastIndex = 0;
76
- while ((match = salutationPattern.exec(text)) !== null) {
77
- // match[1] is the captured name group
78
- const nameValue = match[1];
79
- if (!nameValue)
80
- continue;
81
- // Find the actual start position of the captured name within the full match
82
- const fullMatchStart = match.index;
83
- const nameOffset = match[0].indexOf(nameValue);
84
- const start = fullMatchStart + nameOffset;
85
- const end = start + nameValue.length;
86
- if (isOccupied(occupied, start, end))
87
- continue;
88
- markOccupied(occupied, start, end);
89
- spans.push({ start, end, category: 'person', value: nameValue });
90
- }
15
+ return (0, regex_matcher_js_1.matchPII)(text).map(({ start, end, type }) => ({
16
+ start,
17
+ end,
18
+ category: type,
19
+ value: text.slice(start, end),
20
+ }));
91
21
  }
92
22
  //# sourceMappingURL=detector.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"detector.js","sourceRoot":"","sources":["../src/detector.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"detector.js","sourceRoot":"","sources":["../src/detector.ts"],"names":[],"mappings":";;AAYA,8BAOC;AAlBD,mEAAwD;AAExD;;;;;;;;GAQG;AACH,SAAgB,SAAS,CAAC,IAAY;IACpC,OAAO,IAAA,2BAAQ,EAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;QACnD,KAAK;QACL,GAAG;QACH,QAAQ,EAAE,IAAI;QACd,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC;KAC9B,CAAC,CAAC,CAAC;AACN,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,12 +1,25 @@
1
1
  import type { PIIDetectionResult, AnonymizerOptions } from './types.js';
2
+ import type { DetectionProvider } from './detection/provider.js';
2
3
  export type { PIICategory, PIIDetectionResult, AnonymizerOptions } from './types.js';
3
4
  export { Tokenizer } from './tokenizer.js';
5
+ export { RegexDetector } from './detection/regex-detector.js';
6
+ export type { DetectionProvider, Span } from './detection/provider.js';
4
7
  /**
5
8
  * Anonymize PII in text. Returns anonymized text + keymap (token → original).
6
9
  * Same PII value always gets the same token within a call.
7
10
  * Pass seedKeymap to maintain consistency across multiple calls.
8
11
  */
9
12
  export declare function anonymize(text: string, options?: AnonymizerOptions): PIIDetectionResult;
13
+ /**
14
+ * Anonymize PII using a pluggable {@link DetectionProvider} instead of the
15
+ * built-in regex detector. The provider returns positional spans only; the
16
+ * value is reconstructed HERE via text.slice and tokenized with `span.type` as
17
+ * the category. Async to cover network providers (e.g. Presidio HTTP).
18
+ *
19
+ * Same contract as {@link anonymize}: pass seedKeymap to keep tokens consistent
20
+ * across calls (a value already mapped reuses its token).
21
+ */
22
+ export declare function anonymizeWithProvider(text: string, provider: DetectionProvider, options?: AnonymizerOptions): Promise<PIIDetectionResult>;
10
23
  /**
11
24
  * Restore original PII values from keymap.
12
25
  * Tokens not in the keymap are left unchanged.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAExE,YAAY,EAAE,WAAW,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACrF,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,kBAAkB,CAyBvF;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAQhF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAe,MAAM,YAAY,CAAC;AACrF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,YAAY,EAAE,WAAW,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACrF,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,YAAY,EAAE,iBAAiB,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAEvE;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,kBAAkB,CAyBvF;AAED;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,iBAAiB,EAC3B,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,kBAAkB,CAAC,CA2B7B;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAQhF"}
package/dist/index.js CHANGED
@@ -1,12 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Tokenizer = void 0;
3
+ exports.RegexDetector = exports.Tokenizer = void 0;
4
4
  exports.anonymize = anonymize;
5
+ exports.anonymizeWithProvider = anonymizeWithProvider;
5
6
  exports.deanonymize = deanonymize;
6
7
  const detector_js_1 = require("./detector.js");
7
8
  const tokenizer_js_1 = require("./tokenizer.js");
8
9
  var tokenizer_js_2 = require("./tokenizer.js");
9
10
  Object.defineProperty(exports, "Tokenizer", { enumerable: true, get: function () { return tokenizer_js_2.Tokenizer; } });
11
+ var regex_detector_js_1 = require("./detection/regex-detector.js");
12
+ Object.defineProperty(exports, "RegexDetector", { enumerable: true, get: function () { return regex_detector_js_1.RegexDetector; } });
10
13
  /**
11
14
  * Anonymize PII in text. Returns anonymized text + keymap (token → original).
12
15
  * Same PII value always gets the same token within a call.
@@ -34,6 +37,39 @@ function anonymize(text, options) {
34
37
  }
35
38
  return { text: result, keymap: tokenizer.getKeymap() };
36
39
  }
40
+ /**
41
+ * Anonymize PII using a pluggable {@link DetectionProvider} instead of the
42
+ * built-in regex detector. The provider returns positional spans only; the
43
+ * value is reconstructed HERE via text.slice and tokenized with `span.type` as
44
+ * the category. Async to cover network providers (e.g. Presidio HTTP).
45
+ *
46
+ * Same contract as {@link anonymize}: pass seedKeymap to keep tokens consistent
47
+ * across calls (a value already mapped reuses its token).
48
+ */
49
+ async function anonymizeWithProvider(text, provider, options) {
50
+ const spans = await provider.detect(text);
51
+ const filtered = options?.categories
52
+ ? spans.filter(s => options.categories.includes(s.type))
53
+ : spans;
54
+ const tokenizer = new tokenizer_js_1.Tokenizer();
55
+ if (options?.seedKeymap && Object.keys(options.seedKeymap).length > 0) {
56
+ tokenizer.loadKeymap(options.seedKeymap);
57
+ }
58
+ if (filtered.length === 0) {
59
+ return { text, keymap: tokenizer.getKeymap() };
60
+ }
61
+ // Replace spans right-to-left so earlier offsets stay valid as we splice.
62
+ const sortedDesc = [...filtered].sort((a, b) => b.start - a.start);
63
+ let result = text;
64
+ for (const span of sortedDesc) {
65
+ const value = text.slice(span.start, span.end);
66
+ // span.type is a free string (the provider's vocabulary); the Tokenizer
67
+ // derives a clean prefix from it — no cast, no undefined_N tokens.
68
+ const token = tokenizer.tokenize(value, span.type);
69
+ result = result.slice(0, span.start) + token + result.slice(span.end);
70
+ }
71
+ return { text: result, keymap: tokenizer.getKeymap() };
72
+ }
37
73
  /**
38
74
  * Restore original PII values from keymap.
39
75
  * Tokens not in the keymap are left unchanged.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAYA,8BAyBC;AAMD,kCAQC;AAnDD,+CAA0C;AAC1C,iDAA2C;AAI3C,+CAA2C;AAAlC,yGAAA,SAAS,OAAA;AAElB;;;;GAIG;AACH,SAAgB,SAAS,CAAC,IAAY,EAAE,OAA2B;IACjE,MAAM,KAAK,GAAG,IAAA,uBAAS,EAAC,IAAI,CAAC,CAAC;IAC9B,MAAM,QAAQ,GAAG,OAAO,EAAE,UAAU;QAClC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,UAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;QAC7D,CAAC,CAAC,KAAK,CAAC;IAEV,MAAM,SAAS,GAAG,IAAI,wBAAS,EAAE,CAAC;IAClC,uDAAuD;IACvD,IAAI,OAAO,EAAE,UAAU,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtE,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAC3C,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,SAAS,EAAE,EAAE,CAAC;IACjD,CAAC;IAED,mEAAmE;IACnE,MAAM,UAAU,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;IACnE,IAAI,MAAM,GAAG,IAAI,CAAC;IAClB,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,MAAM,KAAK,GAAG,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC5D,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACxE,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC,SAAS,EAAE,EAAE,CAAC;AACzD,CAAC;AAED;;;GAGG;AACH,SAAgB,WAAW,CAAC,IAAY,EAAE,MAA8B;IACtE,IAAI,MAAM,GAAG,IAAI,CAAC;IAClB,yEAAyE;IACzE,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IAChF,KAAK,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,MAAM,EAAE,CAAC;QACvC,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IAC9C,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -1,12 +1,19 @@
1
- import type { PIICategory } from './types.js';
2
1
  export declare class Tokenizer {
3
2
  private inverse;
4
3
  private keymap;
5
4
  private counters;
6
5
  /**
7
6
  * Get or create a consistent sequential token for a PII value.
7
+ *
8
+ * `category` is a free string: the built-in detector emits the six PIICategory
9
+ * values, but a pluggable DetectionProvider (STU-397) may return its own
10
+ * vocabulary (e.g. "organization", "iban"). The token prefix is the category
11
+ * uppercased — for the built-in categories that is exactly the old fixed
12
+ * mapping (person → PERSON, credit_card → CREDIT_CARD), so existing tokens are
13
+ * unchanged — and the counter is keyed by the lowercased category so distinct
14
+ * types never collide on a single prefix.
8
15
  */
9
- tokenize(value: string, category: PIICategory): string;
16
+ tokenize(value: string, category: string): string;
10
17
  getKeymap(): Record<string, string>;
11
18
  /**
12
19
  * Load an existing keymap (for cross-stage continuity).
@@ -1 +1 @@
1
- {"version":3,"file":"tokenizer.d.ts","sourceRoot":"","sources":["../src/tokenizer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAW9C,qBAAa,SAAS;IAEpB,OAAO,CAAC,OAAO,CAA6B;IAE5C,OAAO,CAAC,MAAM,CAA6B;IAE3C,OAAO,CAAC,QAAQ,CAAkC;IAElD;;OAEG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,GAAG,MAAM;IAetD,SAAS,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAInC;;;OAGG;IACH,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI;CAiBjD"}
1
+ {"version":3,"file":"tokenizer.d.ts","sourceRoot":"","sources":["../src/tokenizer.ts"],"names":[],"mappings":"AAAA,qBAAa,SAAS;IAEpB,OAAO,CAAC,OAAO,CAA6B;IAE5C,OAAO,CAAC,MAAM,CAA6B;IAE3C,OAAO,CAAC,QAAQ,CAA6B;IAE7C;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAejD,SAAS,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAInC;;;OAGG;IACH,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI;CAgBjD"}
package/dist/tokenizer.js CHANGED
@@ -1,32 +1,32 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Tokenizer = void 0;
4
- const CATEGORY_PREFIX = {
5
- person: 'PERSON',
6
- email: 'EMAIL',
7
- phone: 'PHONE',
8
- address: 'ADDRESS',
9
- ssn: 'SSN',
10
- credit_card: 'CREDIT_CARD',
11
- };
12
4
  class Tokenizer {
13
5
  // original value → token
14
6
  inverse = new Map();
15
7
  // token → original value
16
8
  keymap = new Map();
17
- // category → counter
9
+ // category (lowercased) → counter
18
10
  counters = new Map();
19
11
  /**
20
12
  * Get or create a consistent sequential token for a PII value.
13
+ *
14
+ * `category` is a free string: the built-in detector emits the six PIICategory
15
+ * values, but a pluggable DetectionProvider (STU-397) may return its own
16
+ * vocabulary (e.g. "organization", "iban"). The token prefix is the category
17
+ * uppercased — for the built-in categories that is exactly the old fixed
18
+ * mapping (person → PERSON, credit_card → CREDIT_CARD), so existing tokens are
19
+ * unchanged — and the counter is keyed by the lowercased category so distinct
20
+ * types never collide on a single prefix.
21
21
  */
22
22
  tokenize(value, category) {
23
23
  const existing = this.inverse.get(value);
24
24
  if (existing)
25
25
  return existing;
26
- const counter = (this.counters.get(category) ?? 0) + 1;
27
- this.counters.set(category, counter);
28
- const prefix = CATEGORY_PREFIX[category];
29
- const token = `${prefix}_${counter}`;
26
+ const key = category.toLowerCase();
27
+ const counter = (this.counters.get(key) ?? 0) + 1;
28
+ this.counters.set(key, counter);
29
+ const token = `${category.toUpperCase()}_${counter}`;
30
30
  this.inverse.set(value, token);
31
31
  this.keymap.set(token, value);
32
32
  return token;
@@ -42,15 +42,14 @@ class Tokenizer {
42
42
  for (const [token, value] of Object.entries(keymap)) {
43
43
  this.keymap.set(token, value);
44
44
  this.inverse.set(value, token);
45
- // Restore counter state from token name (e.g. PERSON_3 → counter = 3)
45
+ // Restore counter state from token name (e.g. PERSON_3 → counter = 3).
46
+ // The prefix lowercased is the counter key, mirroring tokenize().
46
47
  const match = token.match(/^([A-Z_]+)_(\d+)$/);
47
48
  if (match) {
48
- const cat = Object.entries(CATEGORY_PREFIX).find(([, p]) => p === match[1])?.[0];
49
- if (cat) {
50
- const n = parseInt(match[2], 10);
51
- if ((this.counters.get(cat) ?? 0) < n) {
52
- this.counters.set(cat, n);
53
- }
49
+ const key = match[1].toLowerCase();
50
+ const n = parseInt(match[2], 10);
51
+ if ((this.counters.get(key) ?? 0) < n) {
52
+ this.counters.set(key, n);
54
53
  }
55
54
  }
56
55
  }
@@ -1 +1 @@
1
- {"version":3,"file":"tokenizer.js","sourceRoot":"","sources":["../src/tokenizer.ts"],"names":[],"mappings":";;;AAEA,MAAM,eAAe,GAAgC;IACnD,MAAM,EAAE,QAAQ;IAChB,KAAK,EAAE,OAAO;IACd,KAAK,EAAE,OAAO;IACd,OAAO,EAAE,SAAS;IAClB,GAAG,EAAE,KAAK;IACV,WAAW,EAAE,aAAa;CAC3B,CAAC;AAEF,MAAa,SAAS;IACpB,yBAAyB;IACjB,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC5C,yBAAyB;IACjB,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC3C,qBAAqB;IACb,QAAQ,GAAG,IAAI,GAAG,EAAuB,CAAC;IAElD;;OAEG;IACH,QAAQ,CAAC,KAAa,EAAE,QAAqB;QAC3C,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACzC,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAE9B,MAAM,OAAO,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QACvD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAErC,MAAM,MAAM,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;QACzC,MAAM,KAAK,GAAG,GAAG,MAAM,IAAI,OAAO,EAAE,CAAC;QAErC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QAC/B,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QAC9B,OAAO,KAAK,CAAC;IACf,CAAC;IAED,SAAS;QACP,OAAO,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACzC,CAAC;IAED;;;OAGG;IACH,UAAU,CAAC,MAA8B;QACvC,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YACpD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YAC9B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YAC/B,sEAAsE;YACtE,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAC;YAC/C,IAAI,KAAK,EAAE,CAAC;gBACV,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAA4B,CAAC;gBAC5G,IAAI,GAAG,EAAE,CAAC;oBACR,MAAM,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;oBACjC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;wBACtC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;oBAC5B,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;CACF;AAnDD,8BAmDC"}
1
+ {"version":3,"file":"tokenizer.js","sourceRoot":"","sources":["../src/tokenizer.ts"],"names":[],"mappings":";;;AAAA,MAAa,SAAS;IACpB,yBAAyB;IACjB,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC5C,yBAAyB;IACjB,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC3C,kCAAkC;IAC1B,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE7C;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,KAAa,EAAE,QAAgB;QACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACzC,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAE9B,MAAM,GAAG,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;QACnC,MAAM,OAAO,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QAClD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAEhC,MAAM,KAAK,GAAG,GAAG,QAAQ,CAAC,WAAW,EAAE,IAAI,OAAO,EAAE,CAAC;QAErD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QAC/B,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QAC9B,OAAO,KAAK,CAAC;IACf,CAAC;IAED,SAAS;QACP,OAAO,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACzC,CAAC;IAED;;;OAGG;IACH,UAAU,CAAC,MAA8B;QACvC,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YACpD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YAC9B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YAC/B,uEAAuE;YACvE,kEAAkE;YAClE,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAC;YAC/C,IAAI,KAAK,EAAE,CAAC;gBACV,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;gBACnC,MAAM,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBACjC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;oBACtC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;gBAC5B,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;CACF;AA1DD,8BA0DC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@studio-foundation/anonymizer",
3
- "version": "0.3.0-beta.6",
4
- "description": "PII detection and anonymization with consistent token mapping",
3
+ "version": "0.5.2",
4
+ "description": "PII detection and anonymization for LLM workflows, with consistent token mapping. Used by Studio.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "files": [