@studio-foundation/anonymizer 0.4.0-beta → 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,6 +1,6 @@
1
1
  # @studio-foundation/anonymizer
2
2
 
3
- **Studio** is an agentic pipeline runtime that executes multi-stage LLM workflows with structural 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.
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
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
 
@@ -48,7 +48,7 @@ user data → anonymize() → [PERSON_1], [EMAIL_1] → LLM → deanonymize()
48
48
  | `phone` | `PHONE_N` | US phone numbers (10 digits, various formats) |
49
49
  | `ssn` | `SSN_N` | Social security numbers (ddd-dd-dddd) |
50
50
  | `credit_card` | `CREDIT_CARD_N` | 16-digit card numbers |
51
- | `address` | `ADDRESS_N` | Reserved (detection not yet implemented) |
51
+ | `address` | `ADDRESS_N` | Reserved — delegated to future NER detector (not regex-detected) |
52
52
 
53
53
  ## Detection strategy
54
54
 
@@ -58,7 +58,13 @@ Two-phase detection on each call:
58
58
 
59
59
  2. **Person names (best effort)**: salutation-gated pattern (`Dear X`, `Hi X`, `Mr. X`, etc.). Only catches explicitly addressed names, not bare occurrences.
60
60
 
61
- 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.
62
68
 
63
69
  ## Token consistency
64
70
 
@@ -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.4.0-beta",
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": [