@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 +9 -3
- package/dist/detection/provider.d.ts +18 -0
- package/dist/detection/provider.d.ts.map +1 -0
- package/dist/detection/provider.js +3 -0
- package/dist/detection/provider.js.map +1 -0
- package/dist/detection/regex-detector.d.ts +13 -0
- package/dist/detection/regex-detector.d.ts.map +1 -0
- package/dist/detection/regex-detector.js +19 -0
- package/dist/detection/regex-detector.js.map +1 -0
- package/dist/detection/regex-matcher.d.ts +9 -0
- package/dist/detection/regex-matcher.d.ts.map +1 -0
- package/dist/detection/regex-matcher.js +110 -0
- package/dist/detection/regex-matcher.js.map +1 -0
- package/dist/detector.d.ts +5 -0
- package/dist/detector.d.ts.map +1 -1
- package/dist/detector.js +12 -82
- package/dist/detector.js.map +1 -1
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -1
- package/dist/index.js.map +1 -1
- package/dist/tokenizer.d.ts +9 -2
- package/dist/tokenizer.d.ts.map +1 -1
- package/dist/tokenizer.js +19 -20
- package/dist/tokenizer.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @studio-foundation/anonymizer
|
|
2
2
|
|
|
3
|
-
**Studio** is
|
|
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 (
|
|
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.
|
|
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 @@
|
|
|
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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"}
|
package/dist/detector.d.ts
CHANGED
|
@@ -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
|
package/dist/detector.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"detector.d.ts","sourceRoot":"","sources":["../src/detector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
package/dist/detector.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"detector.js","sourceRoot":"","sources":["../src/detector.ts"],"names":[],"mappings":";;
|
|
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.
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,
|
|
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":";;;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/tokenizer.d.ts
CHANGED
|
@@ -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:
|
|
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).
|
package/dist/tokenizer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tokenizer.d.ts","sourceRoot":"","sources":["../src/tokenizer.ts"],"names":[],"mappings":"AAAA,
|
|
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
|
|
27
|
-
this.counters.
|
|
28
|
-
|
|
29
|
-
const token = `${
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
}
|
package/dist/tokenizer.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tokenizer.js","sourceRoot":"","sources":["../src/tokenizer.ts"],"names":[],"mappings":";;;
|
|
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
|
-
"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": [
|