@anuma/sdk 1.0.0 → 1.1.1

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.
Files changed (87) hide show
  1. package/README.md +54 -53
  2. package/dist/api/spec.cjs +30 -0
  3. package/dist/api/spec.d.mts +3 -0
  4. package/dist/api/spec.d.ts +3 -0
  5. package/dist/api/spec.mjs +12 -0
  6. package/dist/chunk-4ETHWSXJ.mjs +25 -0
  7. package/dist/client/index.cjs +2344 -0
  8. package/dist/client/index.d.mts +10368 -0
  9. package/dist/client/index.d.ts +10368 -0
  10. package/dist/client/index.mjs +2168 -0
  11. package/dist/clientConfig-WITO4T6L.mjs +12 -0
  12. package/dist/constants/index.cjs +30 -0
  13. package/dist/constants/index.d.mts +20 -0
  14. package/dist/constants/index.d.ts +20 -0
  15. package/dist/constants/index.mjs +5 -0
  16. package/dist/design/index.cjs +1523 -0
  17. package/dist/design/index.d.mts +561 -0
  18. package/dist/design/index.d.ts +561 -0
  19. package/dist/design/index.mjs +1468 -0
  20. package/dist/expo/chunk-3HZFU3PV.mjs +5283 -0
  21. package/dist/expo/chunk-DFVCS3QQ.mjs +246 -0
  22. package/dist/{react/chunk-MJJIYFAX.mjs → expo/chunk-GYKJOWEO.mjs} +13 -1
  23. package/dist/expo/chunk-Y7XT2VNQ.mjs +646 -0
  24. package/dist/expo/clientConfig-VPOGQRNP.mjs +12 -0
  25. package/dist/expo/consolidate-HEMQGOUD.mjs +131 -0
  26. package/dist/expo/index.cjs +16904 -6750
  27. package/dist/expo/index.d.mts +5755 -1474
  28. package/dist/expo/index.d.ts +5755 -1474
  29. package/dist/expo/index.mjs +9718 -6182
  30. package/dist/expo/recall-MCB3TVBW.mjs +8 -0
  31. package/dist/index.cjs +2992 -42
  32. package/dist/index.d.mts +7719 -1835
  33. package/dist/index.d.ts +7719 -1835
  34. package/dist/index.mjs +2859 -39
  35. package/dist/next/index.cjs +6 -2
  36. package/dist/next/index.d.mts +24 -1
  37. package/dist/next/index.d.ts +24 -1
  38. package/dist/next/index.mjs +6 -2
  39. package/dist/pii/transformers.cjs +126 -0
  40. package/dist/pii/transformers.d.mts +103 -0
  41. package/dist/pii/transformers.d.ts +103 -0
  42. package/dist/pii/transformers.mjs +90 -0
  43. package/dist/react/chunk-DFVCS3QQ.mjs +246 -0
  44. package/dist/react/chunk-ELDXIXRE.mjs +5656 -0
  45. package/dist/{expo/chunk-MJJIYFAX.mjs → react/chunk-GYKJOWEO.mjs} +13 -1
  46. package/dist/react/chunk-TP6CJZ25.mjs +645 -0
  47. package/dist/react/clientConfig-VPOGQRNP.mjs +12 -0
  48. package/dist/react/consolidate-IJH7LD2T.mjs +131 -0
  49. package/dist/react/index.cjs +25683 -11143
  50. package/dist/react/index.d.mts +7399 -1393
  51. package/dist/react/index.d.ts +7399 -1393
  52. package/dist/react/index.mjs +16709 -9196
  53. package/dist/react/recall-CSOMIT4H.mjs +8 -0
  54. package/dist/server/chunk-FMGZTVXQ.mjs +62 -0
  55. package/dist/server/chunk-G3DC7QBH.mjs +19457 -0
  56. package/dist/server/chunk-PXN5Q3MD.mjs +643 -0
  57. package/dist/server/chunk-RCCKRTBI.mjs +230 -0
  58. package/dist/server/clientConfig-GMADOBPK.mjs +12 -0
  59. package/dist/server/consolidate-A2ZIX5WF.mjs +131 -0
  60. package/dist/server/index.cjs +32161 -0
  61. package/dist/server/index.d.mts +7319 -0
  62. package/dist/server/index.d.ts +7319 -0
  63. package/dist/server/index.mjs +11217 -0
  64. package/dist/server/recall-LFYTTIIY.mjs +8 -0
  65. package/dist/tools/index.cjs +12682 -34
  66. package/dist/tools/index.d.mts +2733 -18
  67. package/dist/tools/index.d.ts +2733 -18
  68. package/dist/tools/index.mjs +12565 -33
  69. package/dist/utils/index.cjs +165 -0
  70. package/dist/utils/index.d.mts +14 -0
  71. package/dist/utils/index.d.ts +14 -0
  72. package/dist/utils/index.mjs +145 -0
  73. package/dist/vercel/index.d.mts +0 -3
  74. package/dist/vercel/index.d.ts +0 -3
  75. package/package.json +127 -16
  76. package/dist/expo/chunk-LJYAMK62.mjs +0 -25
  77. package/dist/expo/chunk-PJCZO4BQ.mjs +0 -25
  78. package/dist/expo/clientConfig-2MI4KULF.mjs +0 -10
  79. package/dist/expo/clientConfig-QDAXFL3W.mjs +0 -10
  80. package/dist/expo/clientConfig-R4IOW7I2.mjs +0 -10
  81. package/dist/react/chunk-LJYAMK62.mjs +0 -25
  82. package/dist/react/chunk-PJCZO4BQ.mjs +0 -25
  83. package/dist/react/clientConfig-2MI4KULF.mjs +0 -10
  84. package/dist/react/clientConfig-QDAXFL3W.mjs +0 -10
  85. package/dist/react/clientConfig-R4IOW7I2.mjs +0 -10
  86. package/dist/tools/chunk-KDFGY4SK.mjs +0 -13
  87. package/dist/tools/clientConfig-RMDOT5YM.mjs +0 -10
@@ -10,7 +10,18 @@ var __decorateClass = (decorators, target, key, kind) => {
10
10
  };
11
11
 
12
12
  // src/clientConfig.ts
13
- var BASE_URL = "https://portal.anuma-dev.ai";
13
+ var DEFAULT_BASE_URL = "https://portal.anuma-dev.ai";
14
+ function readGlobal(key) {
15
+ if (typeof globalThis === "undefined") return void 0;
16
+ const value = globalThis[key];
17
+ return typeof value === "string" && value.length > 0 ? value : void 0;
18
+ }
19
+ function resolveBaseUrl() {
20
+ const fromProcess = typeof process !== "undefined" ? process.env.API_URL || process.env.NEXT_PUBLIC_API_URL || process.env.EXPO_PUBLIC_API_URL : void 0;
21
+ const fromGlobal = readGlobal("API_URL") ?? readGlobal("NEXT_PUBLIC_API_URL") ?? readGlobal("EXPO_PUBLIC_API_URL");
22
+ return (fromProcess || fromGlobal || DEFAULT_BASE_URL).replace(/\/+$/, "");
23
+ }
24
+ var BASE_URL = resolveBaseUrl();
14
25
  var MCP_R2_DOMAIN = "4cf0e0ea50b97e72386fcf2f92a2d4e8.r2.cloudflarestorage.com";
15
26
  var createClientConfig = (config) => ({
16
27
  ...config,
@@ -19,6 +30,7 @@ var createClientConfig = (config) => ({
19
30
 
20
31
  export {
21
32
  __decorateClass,
33
+ resolveBaseUrl,
22
34
  BASE_URL,
23
35
  MCP_R2_DOMAIN,
24
36
  createClientConfig
@@ -0,0 +1,645 @@
1
+ // src/lib/pii/patterns.ts
2
+ function luhnCheck(digits) {
3
+ let sum = 0;
4
+ let alternate = false;
5
+ for (let i = digits.length - 1; i >= 0; i--) {
6
+ let n = parseInt(digits[i], 10);
7
+ if (alternate) {
8
+ n *= 2;
9
+ if (n > 9) n -= 9;
10
+ }
11
+ sum += n;
12
+ alternate = !alternate;
13
+ }
14
+ return sum % 10 === 0;
15
+ }
16
+ var PII_PATTERNS = [
17
+ // Email — standard RFC-ish pattern.
18
+ // Quantifiers are bounded (local-part ≤ 64, domain ≤ 255, TLD ≤ 24 —
19
+ // all at or above real RFC 5321 limits) so the engine cannot backtrack
20
+ // quadratically on adversarial input like "a.a.a.a…" with no "@".
21
+ {
22
+ category: "EMAIL",
23
+ regex: /\b[a-zA-Z0-9._%+-]{1,64}@[a-zA-Z0-9.-]{1,255}\.[a-zA-Z]{2,24}\b/g
24
+ },
25
+ // SSN — US Social Security Number, dashed form (must run before phone)
26
+ {
27
+ category: "SSN",
28
+ regex: /\b\d{3}-\d{2}-\d{4}\b/g,
29
+ validate: (match) => {
30
+ const parts = match.split("-");
31
+ const area = parseInt(parts[0], 10);
32
+ return area !== 0 && area !== 666 && area < 900;
33
+ }
34
+ },
35
+ // SSN — dashless or space-separated form. Only redacted with an "SSN" /
36
+ // "social security" cue nearby: a bare 9-digit run is too ambiguous
37
+ // (ZIP+4, account numbers, etc.) to redact unconditionally.
38
+ {
39
+ category: "SSN",
40
+ regex: /\b\d{3}[ ]?\d{2}[ ]?\d{4}\b/g,
41
+ context: /(?:\bssn\b|\bsocial\s+security(?:\s+(?:number|no\.?|#))?\b)[^\d\n]{0,12}$/i,
42
+ validate: (match) => {
43
+ const digits = match.replace(/\D/g, "");
44
+ if (digits.length !== 9) return false;
45
+ const area = parseInt(digits.slice(0, 3), 10);
46
+ return area !== 0 && area !== 666 && area < 900;
47
+ }
48
+ },
49
+ // Credit card numbers — 13-19 contiguous digits or groups of 4 separated by spaces/dashes
50
+ // Must run before phone to avoid partial matches
51
+ {
52
+ category: "CREDIT_CARD",
53
+ regex: /\b\d{4}[\s-]\d{4}[\s-]\d{4}[\s-]\d{1,7}\b|\b\d{13,19}\b/g,
54
+ validate: (match) => {
55
+ const digits = match.replace(/\D/g, "");
56
+ if (digits.length < 13 || digits.length > 19) return false;
57
+ return luhnCheck(digits);
58
+ }
59
+ },
60
+ // IPv4 addresses (must run before phone to avoid partial matches)
61
+ {
62
+ category: "IP_ADDRESS",
63
+ regex: /\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b/g,
64
+ validate: (match) => {
65
+ return match !== "0.0.0.0" && match !== "127.0.0.1" && match !== "255.255.255.255";
66
+ }
67
+ },
68
+ // IPv6 addresses — full and compressed (::) forms. Each alternative requires
69
+ // either a full 8-group address or a "::" compression, so ordinary
70
+ // colon-separated text (timestamps like 12:34:56) does not match. Loopback
71
+ // (::1) and unspecified (::) are excluded.
72
+ {
73
+ category: "IP_ADDRESS",
74
+ regex: /(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:))/g,
75
+ validate: (match) => match !== "::1" && match !== "::"
76
+ },
77
+ // Phone numbers. The separated form requires separator characters (dashes,
78
+ // spaces, dots, parens) to avoid matching bare digit sequences that are
79
+ // actually credit cards, IPs, or years. A second alternative matches E.164
80
+ // (a leading "+" then 10–15 digits, e.g. +14155552671), which is
81
+ // unambiguous; a bare 10-digit run with no "+" and no separators is
82
+ // intentionally not matched to avoid false positives.
83
+ {
84
+ category: "PHONE",
85
+ regex: /(?:\+\d{1,3}[\s.-])?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}\b|\+\d{10,15}\b/g
86
+ },
87
+ // API keys / secrets — long token with a key-like prefix. The validator
88
+ // requires entropy (at least one digit, or a mix of upper- and lower-case)
89
+ // so long all-lowercase prose identifiers like
90
+ // "accessibility_is_important_for_everyone" or
91
+ // "access-control-list-management-feature" are not redacted. Real keys and
92
+ // tokens essentially always contain digits or mixed case.
93
+ {
94
+ category: "API_KEY",
95
+ regex: /\b(?:sk|pk|api|key|token|secret|bearer|access)[_-]?[a-zA-Z0-9_-]{20,}\b/gi,
96
+ validate: (match) => {
97
+ const hasDigit = /\d/.test(match);
98
+ const hasMixedCase = /[a-z]/.test(match) && /[A-Z]/.test(match);
99
+ return hasDigit || hasMixedCase;
100
+ }
101
+ },
102
+ // US street addresses — house number + Title-Case street name + suffix.
103
+ // Intentionally NOT case-insensitive: the `[A-Z][a-z]+` guard requires
104
+ // properly-capitalized street words, so "123 main street", "100 MAIN
105
+ // STREET" and prose like "3 blocks down the road" / "5 minutes to drive"
106
+ // are not treated as addresses. Whitespace is `[ \t]` only (no newline
107
+ // spanning), and the suffix must end the segment (punctuation / newline /
108
+ // end of string) so "Driveway" or "Wayfair" don't match on the suffix.
109
+ {
110
+ category: "US_ADDRESS",
111
+ regex: /\b\d{1,5}[ \t]+(?:[A-Z][a-z]+[ \t]+){1,3}(?:St(?:reet)?|Ave(?:nue)?|Blvd|Boulevard|Dr(?:ive)?|Ln|Lane|Rd|Road|Ct|Court|Pl(?:ace)?|Way|Cir(?:cle)?|Pkwy|Parkway|Hwy|Highway)\.?(?=[\s,.;:!?)]|$)/g,
112
+ validate: (match) => {
113
+ return !/^0/.test(match.trim());
114
+ }
115
+ },
116
+ // Dates of birth — common US date formats. Only redacted when a birth-date
117
+ // cue ("DOB", "born", "birthday", "date of birth") appears just before the
118
+ // date, so ordinary calendar dates ("meeting 12/25/2024", deadlines,
119
+ // invoices) are left intact for the model. A calendar validator also rejects
120
+ // impossible dates like 02/31/1990.
121
+ {
122
+ category: "DATE_OF_BIRTH",
123
+ regex: /\b(?:(?:0?[1-9]|1[0-2])[/-](?:0?[1-9]|[12]\d|3[01])[/-](?:19|20)\d{2})\b/g,
124
+ context: /(?:\bdob\b|\bd\.?o\.?b\.?|\bborn\b|\bbirth\s?date\b|\bbirthdate\b|\bbirthday\b|\bdate\s+of\s+birth\b)[^\d\n]{0,12}$/i,
125
+ validate: (match) => {
126
+ const m = match.match(/(\d{1,2})[/-](\d{1,2})[/-](\d{4})/);
127
+ if (!m) return false;
128
+ const month = parseInt(m[1], 10);
129
+ const day = parseInt(m[2], 10);
130
+ const year = parseInt(m[3], 10);
131
+ const isLeap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
132
+ const daysInMonth = [31, isLeap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
133
+ return month >= 1 && month <= 12 && day >= 1 && day <= daysInMonth[month - 1];
134
+ }
135
+ }
136
+ ];
137
+
138
+ // src/lib/pii/redactor.ts
139
+ function escapeRegExp(value) {
140
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
141
+ }
142
+ function isWordChar(ch) {
143
+ return /[A-Za-z0-9_]/.test(ch) || ch.charCodeAt(0) > 127;
144
+ }
145
+ var PiiRedactor = class {
146
+ constructor(options = {}) {
147
+ /** Maps original PII value → placeholder tag. */
148
+ this.valueToPlaceholder = /* @__PURE__ */ new Map();
149
+ /** Maps placeholder tag → original PII value (reverse lookup for de-anonymization). */
150
+ this.placeholderToValue = /* @__PURE__ */ new Map();
151
+ /** Next index per category for placeholder numbering. */
152
+ this.categoryCounters = /* @__PURE__ */ new Map();
153
+ /**
154
+ * Matches any placeholder-shaped token for this redactor's categories —
155
+ * bracketed `[EMAIL_1]` OR bare `EMAIL_1`, case-insensitive — capturing the
156
+ * body. Drives {@link restoreForStorage}: the extraction / consolidation
157
+ * models echo placeholders mangled (brackets dropped, re-cased), and a single
158
+ * pass with this matcher restores the assigned ones and flags the rest in one
159
+ * go. Built lazily from the union of pattern categories AND any categories
160
+ * minted at runtime (e.g. NER `PERSON`/`LOCATION`/`ORG`): `undefined` = stale
161
+ * (rebuild on next use), `null` = built but there are no categories.
162
+ */
163
+ this.storagePattern = void 0;
164
+ /**
165
+ * Cached body→value lookups for {@link restoreForStorage}, built lazily from
166
+ * the assigned placeholders and invalidated on each mint. `exact` is keyed by
167
+ * the literal body (collision-proof); `ci` by upper-cased body for re-cased
168
+ * echoes, minus any `ciCollisions` where two distinct bodies share a form.
169
+ */
170
+ this.storageLookup = null;
171
+ /** Whether the non-text-content bypass warning has already been emitted. */
172
+ this.warnedNonText = false;
173
+ /** Whether the NER-detector-failure warning has already been emitted. */
174
+ this.warnedNerFailure = false;
175
+ this.nerDetector = options.nerDetector;
176
+ if (options.patterns) {
177
+ this.patterns = options.patterns;
178
+ } else {
179
+ let base = PII_PATTERNS;
180
+ if (options.excludeCategories?.length) {
181
+ const excluded = new Set(options.excludeCategories);
182
+ base = base.filter((p) => !excluded.has(p.category));
183
+ }
184
+ this.patterns = options.extraPatterns ? [...base, ...options.extraPatterns] : base;
185
+ }
186
+ this.patternCategories = [...new Set(this.patterns.map((p) => String(p.category)))];
187
+ }
188
+ /**
189
+ * Build the {@link storagePattern} from the union of pattern categories and
190
+ * any categories minted at runtime (NER entities arrive only when redaction
191
+ * runs, so this can't be fixed at construction). Bracketed `[EMAIL_1]` OR
192
+ * bare `EMAIL_1`, capturing the body, case-insensitive, global. `\d+` (not a
193
+ * fixed body) spans any index; `\b` on the bare arm keeps it off prose like
194
+ * "email 1 of 3".
195
+ */
196
+ buildStoragePattern() {
197
+ const categories = [.../* @__PURE__ */ new Set([...this.patternCategories, ...this.categoryCounters.keys()])];
198
+ if (categories.length === 0) return null;
199
+ const categoryAlt = categories.sort((a, b) => b.length - a.length).map(escapeRegExp).join("|");
200
+ return new RegExp(`\\[((?:${categoryAlt})_\\d+)\\]|\\b((?:${categoryAlt})_\\d+)\\b`, "gi");
201
+ }
202
+ /**
203
+ * Returns the current number of unique PII values tracked.
204
+ */
205
+ get size() {
206
+ return this.valueToPlaceholder.size;
207
+ }
208
+ /**
209
+ * Returns a snapshot of all placeholder → original value mappings.
210
+ * Useful for debugging or UI display. This is a copy — mutating it (or
211
+ * later redactions) does not affect the returned map and vice versa.
212
+ */
213
+ getMappings() {
214
+ return new Map(this.placeholderToValue);
215
+ }
216
+ /**
217
+ * Get or create a placeholder for a PII value.
218
+ * The same value always gets the same placeholder within a redactor instance.
219
+ */
220
+ getPlaceholder(category, value) {
221
+ const normalizedValue = value.trim();
222
+ const existing = this.valueToPlaceholder.get(normalizedValue);
223
+ if (existing) return existing;
224
+ const existingCount = this.categoryCounters.get(category);
225
+ const count = (existingCount ?? 0) + 1;
226
+ this.categoryCounters.set(category, count);
227
+ if (existingCount === void 0) this.storagePattern = void 0;
228
+ const placeholder = `[${category}_${count}]`;
229
+ this.valueToPlaceholder.set(normalizedValue, placeholder);
230
+ this.placeholderToValue.set(placeholder, normalizedValue);
231
+ this.storageLookup = null;
232
+ return placeholder;
233
+ }
234
+ /**
235
+ * Find all matches of a single pattern in `text`, applying its `validate` and
236
+ * `context` guards. Matches are non-overlapping and ascending (the global
237
+ * regex advances `lastIndex` past each match). Shared by the sync {@link scan}
238
+ * (sequential, per-pattern) and the async {@link collectRegexSpans} (span)
239
+ * paths so the detection rules live in exactly one place.
240
+ */
241
+ findMatches(text, pattern) {
242
+ const regex = new RegExp(pattern.regex.source, pattern.regex.flags);
243
+ const found = [];
244
+ let m;
245
+ while ((m = regex.exec(text)) !== null) {
246
+ const value = m[0];
247
+ if (pattern.validate && !pattern.validate(value)) continue;
248
+ if (pattern.context) {
249
+ const before = text.slice(Math.max(0, m.index - 40), m.index);
250
+ if (!pattern.context.test(before)) continue;
251
+ }
252
+ found.push({ match: value, index: m.index });
253
+ }
254
+ return found;
255
+ }
256
+ /**
257
+ * Scan text for PII and rebuild it, replacing each match with whatever
258
+ * `makeReplacement(category, value)` returns. Shared by `redactText`
259
+ * (numbered, stateful placeholders) and `maskText` (unnumbered, stateless).
260
+ *
261
+ * Patterns are applied sequentially: each scans the text already rewritten by
262
+ * earlier patterns, so an earlier (more specific) pattern claims a region
263
+ * before a later one can — placeholders are inert and match no pattern.
264
+ */
265
+ scan(text, makeReplacement) {
266
+ const matches = [];
267
+ let redacted = text;
268
+ for (const pattern of this.patterns) {
269
+ const found = this.findMatches(redacted, pattern);
270
+ if (found.length === 0) continue;
271
+ let result = "";
272
+ let cursor = 0;
273
+ for (const { match, index } of found) {
274
+ const placeholder = makeReplacement(pattern.category, match);
275
+ matches.push({ category: pattern.category, original: match, placeholder });
276
+ result += redacted.slice(cursor, index) + placeholder;
277
+ cursor = index + match.length;
278
+ }
279
+ result += redacted.slice(cursor);
280
+ redacted = result;
281
+ }
282
+ return { text: redacted, matches };
283
+ }
284
+ /**
285
+ * Scan and redact PII from a single text string, using numbered, reversible
286
+ * placeholders ([EMAIL_1], …) tracked on this instance.
287
+ */
288
+ redactText(text) {
289
+ return this.scan(text, (category, value) => this.getPlaceholder(category, value));
290
+ }
291
+ /**
292
+ * Mask PII with unnumbered, NON-reversible tokens ([EMAIL], [SSN], …) without
293
+ * mutating this instance's state. Use for one-way purposes where the value is
294
+ * never restored — notably embedding inputs: it keeps real PII off the
295
+ * embeddings endpoint while staying deterministic, so the same content always
296
+ * masks identically and search stays consistent across conversations.
297
+ */
298
+ maskText(text) {
299
+ return this.scan(text, (category) => `[${category}]`).text;
300
+ }
301
+ // ───────────────────────────────────────────────────────────────────────
302
+ // Async (regex + NER) path. Mirrors the sync API but additionally folds in
303
+ // an optional {@link NerDetector}'s spans. When no detector is configured,
304
+ // each async method delegates to its sync counterpart and is byte-for-byte
305
+ // identical — so it is a safe drop-in.
306
+ // ───────────────────────────────────────────────────────────────────────
307
+ /** All regex matches as spans on the ORIGINAL text (priority 0). Uses the
308
+ * same {@link findMatches} guards as the sync scan. */
309
+ collectRegexSpans(text) {
310
+ const spans = [];
311
+ this.patterns.forEach((pattern, patternIndex) => {
312
+ for (const { match, index } of this.findMatches(text, pattern)) {
313
+ spans.push({
314
+ start: index,
315
+ end: index + match.length,
316
+ category: pattern.category,
317
+ priority: 0,
318
+ order: patternIndex
319
+ });
320
+ }
321
+ });
322
+ return spans;
323
+ }
324
+ /**
325
+ * Normalize raw NER spans: clamp to the text bounds, drop empty/categoryless
326
+ * spans, and snap each to whole-word boundaries. Snapping prevents a detector
327
+ * that tagged only part of a word (a common WordPiece artifact — e.g. "Strip"
328
+ * of "Stripe") from producing a corrupting partial placeholder like
329
+ * `[ORG_1]e`.
330
+ */
331
+ normalizeNerSpans(text, raw) {
332
+ if (!Array.isArray(raw)) return [];
333
+ const out = [];
334
+ raw.forEach((s, i) => {
335
+ if (!Number.isFinite(s.start) || !Number.isFinite(s.end)) return;
336
+ let start = Math.max(0, Math.min(Math.trunc(s.start), text.length));
337
+ let end = Math.max(0, Math.min(Math.trunc(s.end), text.length));
338
+ if (end <= start) return;
339
+ while (start > 0 && isWordChar(text[start - 1])) start--;
340
+ while (end < text.length && isWordChar(text[end])) end++;
341
+ const category = String(s.category ?? "").trim();
342
+ if (!category) return;
343
+ out.push({ start, end, category, priority: 1, order: i });
344
+ });
345
+ return out;
346
+ }
347
+ /** Run the configured detector defensively: failures degrade to regex-only
348
+ * (fail-open on detection, not on protection) with a one-time warning. */
349
+ async detectNerSpans(text) {
350
+ if (!this.nerDetector) return [];
351
+ try {
352
+ return this.normalizeNerSpans(text, await this.nerDetector.detect(text));
353
+ } catch (err) {
354
+ this.warnNerFailure(err);
355
+ return [];
356
+ }
357
+ }
358
+ /**
359
+ * Resolve overlaps by placing higher-priority spans first and dropping any
360
+ * span that overlaps one already placed. Regex (priority 0) is placed before
361
+ * NER (priority 1), and within regex by ascending pattern index — so regex
362
+ * always wins on overlap and the regex-only result reproduces the sync scan's
363
+ * sequential "earlier pattern wins". O(n²) but n (spans per message) is tiny.
364
+ *
365
+ * The returned spans stay in `(priority, order, start)` order — i.e. mint
366
+ * order — which {@link rebuildSpans} relies on.
367
+ */
368
+ resolveOverlaps(spans) {
369
+ const ordered = [...spans].sort(
370
+ (a, b) => a.priority - b.priority || a.order - b.order || a.start - b.start
371
+ );
372
+ const placed = [];
373
+ for (const s of ordered) {
374
+ if (!placed.some((p) => s.start < p.end && p.start < s.end)) placed.push(s);
375
+ }
376
+ return placed;
377
+ }
378
+ /**
379
+ * Rebuild `text` from resolved (non-overlapping) spans. `spans` must arrive in
380
+ * mint order — `(priority, order, start)`, as {@link resolveOverlaps} returns —
381
+ * so placeholders are MINTED by iterating it directly (regex categories first,
382
+ * in pattern then document order, matching the sync scan's numbering). The
383
+ * string is then EMITTED in document order.
384
+ */
385
+ rebuildSpans(text, spans, makeReplacement) {
386
+ const placeholderByStart = /* @__PURE__ */ new Map();
387
+ const matches = [];
388
+ for (const s of spans) {
389
+ const value = text.slice(s.start, s.end);
390
+ const placeholder = makeReplacement(s.category, value);
391
+ placeholderByStart.set(s.start, placeholder);
392
+ matches.push({ category: s.category, original: value, placeholder });
393
+ }
394
+ const emitOrder = [...spans].sort((a, b) => a.start - b.start);
395
+ let result = "";
396
+ let cursor = 0;
397
+ for (const s of emitOrder) {
398
+ result += text.slice(cursor, s.start) + (placeholderByStart.get(s.start) ?? "");
399
+ cursor = s.end;
400
+ }
401
+ result += text.slice(cursor);
402
+ return { text: result, matches };
403
+ }
404
+ /** Merge regex + NER spans for `text` into a resolved, non-overlapping set. */
405
+ async detectAllSpans(text) {
406
+ const nerPromise = this.detectNerSpans(text);
407
+ const regexSpans = this.collectRegexSpans(text);
408
+ const ner = await nerPromise;
409
+ return this.resolveOverlaps([...regexSpans, ...ner]);
410
+ }
411
+ /**
412
+ * Async counterpart to {@link redactText}: redacts structured PII (regex) AND
413
+ * unstructured PII (the configured {@link NerDetector}) with numbered,
414
+ * reversible placeholders. With no detector, identical to `redactText`.
415
+ */
416
+ async redactTextAsync(text) {
417
+ if (!this.nerDetector) return this.redactText(text);
418
+ const spans = await this.detectAllSpans(text);
419
+ return this.rebuildSpans(
420
+ text,
421
+ spans,
422
+ (category, value) => this.getPlaceholder(category, value)
423
+ );
424
+ }
425
+ /**
426
+ * Async counterpart to {@link maskText}: stateless `[CATEGORY]` masking over
427
+ * regex + NER matches. With no detector, identical to `maskText`.
428
+ */
429
+ async maskTextAsync(text) {
430
+ if (!this.nerDetector) return this.maskText(text);
431
+ const spans = await this.detectAllSpans(text);
432
+ return this.rebuildSpans(text, spans, (category) => `[${category}]`).text;
433
+ }
434
+ /**
435
+ * Async counterpart to {@link redactMessages}. Text parts are redacted
436
+ * sequentially so placeholder numbering stays deterministic across the
437
+ * conversation. With no detector, identical to `redactMessages`.
438
+ */
439
+ async redactMessagesAsync(messages) {
440
+ if (!this.nerDetector) return this.redactMessages(messages);
441
+ const allMatches = [];
442
+ const redactedMessages = [];
443
+ for (const msg of messages) {
444
+ if (!msg.content) {
445
+ redactedMessages.push(msg);
446
+ continue;
447
+ }
448
+ const newContent = [];
449
+ for (const part of msg.content) {
450
+ if (part.type === "text" && part.text) {
451
+ const result = await this.redactTextAsync(part.text);
452
+ allMatches.push(...result.matches);
453
+ newContent.push(result.matches.length > 0 ? { ...part, text: result.text } : part);
454
+ } else {
455
+ if (part.type !== "text") this.warnNonTextBypass();
456
+ newContent.push(part);
457
+ }
458
+ }
459
+ redactedMessages.push({ ...msg, content: newContent });
460
+ }
461
+ return { messages: redactedMessages, matches: allMatches };
462
+ }
463
+ /**
464
+ * Redact PII from an array of LlmapiMessage objects.
465
+ * Returns new message objects — originals are not mutated.
466
+ *
467
+ * All roles' text content is redacted, not just user/system: tool results
468
+ * can return PII fetched from external systems, and assistant history is
469
+ * persisted with original (de-anonymized) values, so both must be re-redacted
470
+ * before they go back over the wire. Placeholders the model already emitted
471
+ * are inert here (they don't match any PII pattern), so re-redaction is safe.
472
+ */
473
+ redactMessages(messages) {
474
+ const allMatches = [];
475
+ const redactedMessages = messages.map((msg) => {
476
+ if (!msg.content) return msg;
477
+ const newContent = msg.content.map((part) => {
478
+ if (part.type === "text" && part.text) {
479
+ const result = this.redactText(part.text);
480
+ allMatches.push(...result.matches);
481
+ if (result.matches.length > 0) {
482
+ return { ...part, text: result.text };
483
+ }
484
+ } else if (part.type !== "text") {
485
+ this.warnNonTextBypass();
486
+ }
487
+ return part;
488
+ });
489
+ return { ...msg, content: newContent };
490
+ });
491
+ return { messages: redactedMessages, matches: allMatches };
492
+ }
493
+ /** Emit the non-text bypass warning at most once per redactor instance. */
494
+ warnNonTextBypass() {
495
+ if (this.warnedNonText) return;
496
+ this.warnedNonText = true;
497
+ console.warn(
498
+ "[PiiRedactor] A message contains non-text content (images, files, or attachments) that is NOT scanned for PII \u2014 only text parts are redacted."
499
+ );
500
+ }
501
+ /** Emit the NER-detector-failure warning at most once per redactor instance. */
502
+ warnNerFailure(err) {
503
+ if (this.warnedNerFailure) return;
504
+ this.warnedNerFailure = true;
505
+ console.warn(
506
+ "[PiiRedactor] The NER detector threw; falling back to regex-only redaction for this call's unstructured PII. The detector is still retried on subsequent calls; this warning is emitted only once.",
507
+ err
508
+ );
509
+ }
510
+ /**
511
+ * Restore original PII values in text that contains placeholders.
512
+ * Used to de-anonymize LLM responses before displaying to the user.
513
+ *
514
+ * Exact: matches the literal `[EMAIL_1]` form only. The storage paths use
515
+ * {@link restoreForStorage} instead, which tolerates the mangled echoes the
516
+ * extraction models produce.
517
+ */
518
+ deAnonymize(text) {
519
+ if (!text.includes("[")) return text;
520
+ let restored = text;
521
+ for (const [placeholder, original] of this.placeholderToValue) {
522
+ if (restored.includes(placeholder)) {
523
+ restored = restored.split(placeholder).join(original);
524
+ }
525
+ }
526
+ return restored;
527
+ }
528
+ /**
529
+ * De-anonymize for PERSISTENCE (auto-extraction / consolidation), tolerant of
530
+ * the ways the extraction models mangle a placeholder when echoing it back:
531
+ * dropped brackets (`EMAIL_1`) and changed case (`email_1` / `Email_1`).
532
+ *
533
+ * Returns the restored text plus `unresolved` — true when a placeholder-shaped
534
+ * token for one of this redactor's categories was NOT assigned during
535
+ * redaction (a model hallucination, or an ambiguous re-cased collision).
536
+ * Callers drop/degrade such facts so an opaque token never reaches the vault.
537
+ *
538
+ * One left-to-right pass over the ORIGINAL text. Two properties matter:
539
+ * - a restored value is never re-scanned — so a real value that happens to
540
+ * contain a `<CATEGORY>_<n>` substring (e.g. the email `ssn_1@example.com`,
541
+ * or an API key containing `EMAIL_1`) is neither corrupted nor false-flagged;
542
+ * - `unresolved` is derived from the lookup miss during this pass, not from a
543
+ * regex re-scan of the output — so it can't trip over a restored value.
544
+ */
545
+ restoreForStorage(text) {
546
+ if (this.storagePattern === void 0) this.storagePattern = this.buildStoragePattern();
547
+ if (!this.storagePattern || this.placeholderToValue.size === 0) {
548
+ return { text, unresolved: false };
549
+ }
550
+ this.storageLookup ?? (this.storageLookup = this.buildStorageLookup());
551
+ const { exact, ci, ciCollisions } = this.storageLookup;
552
+ let unresolved = false;
553
+ const restored = text.replace(
554
+ this.storagePattern,
555
+ (match, bracketed, bare) => {
556
+ const body = bracketed ?? bare ?? "";
557
+ const exactHit = exact.get(body);
558
+ if (exactHit !== void 0) return exactHit;
559
+ const key = body.toUpperCase();
560
+ if (!ciCollisions.has(key)) {
561
+ const ciHit = ci.get(key);
562
+ if (ciHit !== void 0) return ciHit;
563
+ }
564
+ unresolved = true;
565
+ return match;
566
+ }
567
+ );
568
+ return { text: restored, unresolved };
569
+ }
570
+ /**
571
+ * Build the {@link restoreForStorage} body→value lookups from the assigned
572
+ * placeholders. `exact` (literal body) is collision-proof; `ci` (upper-cased
573
+ * body) handles re-cased echoes, except where two distinct bodies share an
574
+ * upper-cased form — those are recorded in `ciCollisions` so a re-cased echo
575
+ * of them is left unresolved rather than resolved to the wrong value.
576
+ */
577
+ buildStorageLookup() {
578
+ const exact = /* @__PURE__ */ new Map();
579
+ const ci = /* @__PURE__ */ new Map();
580
+ const ciCollisions = /* @__PURE__ */ new Set();
581
+ for (const [placeholder, value] of this.placeholderToValue) {
582
+ const body = placeholder.slice(1, -1);
583
+ exact.set(body, value);
584
+ const key = body.toUpperCase();
585
+ if (ci.has(key) && ci.get(key) !== value) ciCollisions.add(key);
586
+ else ci.set(key, value);
587
+ }
588
+ return { exact, ci, ciCollisions };
589
+ }
590
+ /**
591
+ * Reset all state. Useful for testing or when starting a fresh conversation.
592
+ */
593
+ clear() {
594
+ this.valueToPlaceholder.clear();
595
+ this.placeholderToValue.clear();
596
+ this.categoryCounters.clear();
597
+ this.storageLookup = null;
598
+ this.storagePattern = void 0;
599
+ this.warnedNonText = false;
600
+ this.warnedNerFailure = false;
601
+ }
602
+ };
603
+ function isPiiRedactor(value) {
604
+ return typeof value === "object" && value !== null && typeof value.redactMessages === "function" && typeof value.deAnonymize === "function";
605
+ }
606
+ function resolvePiiRedactor(piiRedaction) {
607
+ if (piiRedaction === true) return new PiiRedactor();
608
+ if (!piiRedaction) return void 0;
609
+ if (isPiiRedactor(piiRedaction)) return piiRedaction;
610
+ console.warn(
611
+ "[PiiRedactor] `piiRedaction` is neither `true` nor a PiiRedactor instance; PII redaction is DISABLED for this request."
612
+ );
613
+ return void 0;
614
+ }
615
+ function createStreamingDeAnonymizer(redactor, emit) {
616
+ let buffer = "";
617
+ const trailingFragment = /\[[A-Za-z0-9_]*$/;
618
+ return {
619
+ push(chunk) {
620
+ if (!chunk) return;
621
+ buffer += chunk;
622
+ const m = buffer.match(trailingFragment);
623
+ if (!m || m.index === void 0) {
624
+ emit(redactor.deAnonymize(buffer));
625
+ buffer = "";
626
+ } else if (m.index > 0) {
627
+ emit(redactor.deAnonymize(buffer.slice(0, m.index)));
628
+ buffer = buffer.slice(m.index);
629
+ }
630
+ },
631
+ flush() {
632
+ if (buffer) {
633
+ emit(redactor.deAnonymize(buffer));
634
+ buffer = "";
635
+ }
636
+ }
637
+ };
638
+ }
639
+
640
+ export {
641
+ PiiRedactor,
642
+ isPiiRedactor,
643
+ resolvePiiRedactor,
644
+ createStreamingDeAnonymizer
645
+ };
@@ -0,0 +1,12 @@
1
+ import {
2
+ BASE_URL,
3
+ MCP_R2_DOMAIN,
4
+ createClientConfig,
5
+ resolveBaseUrl
6
+ } from "./chunk-GYKJOWEO.mjs";
7
+ export {
8
+ BASE_URL,
9
+ MCP_R2_DOMAIN,
10
+ createClientConfig,
11
+ resolveBaseUrl
12
+ };