@medusasec/sensitive-spans 0.1.0

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.
@@ -0,0 +1,720 @@
1
+ // src/shared/text.ts
2
+ function shannonEntropy(token) {
3
+ if (token.length === 0) return 0;
4
+ const counts = /* @__PURE__ */ new Map();
5
+ for (const ch of token) counts.set(ch, (counts.get(ch) ?? 0) + 1);
6
+ let h = 0;
7
+ for (const c of counts.values()) {
8
+ const p = c / token.length;
9
+ h -= p * Math.log2(p);
10
+ }
11
+ return h;
12
+ }
13
+
14
+ // src/tier0/validators.ts
15
+ function luhnValid(digits) {
16
+ if (!/^\d{13,19}$/.test(digits)) return false;
17
+ let sum = 0;
18
+ let dbl = false;
19
+ for (let i = digits.length - 1; i >= 0; i--) {
20
+ let d = digits.charCodeAt(i) - 48;
21
+ if (dbl) {
22
+ d *= 2;
23
+ if (d > 9) d -= 9;
24
+ }
25
+ sum += d;
26
+ dbl = !dbl;
27
+ }
28
+ return sum % 10 === 0;
29
+ }
30
+ function ibanValid(iban) {
31
+ const s = iban.replace(/\s/g, "").toUpperCase();
32
+ if (!/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(s)) return false;
33
+ const rearranged = s.slice(4) + s.slice(0, 4);
34
+ let rem = 0;
35
+ for (const ch of rearranged) {
36
+ const v = ch >= "0" && ch <= "9" ? ch : (ch.charCodeAt(0) - 55).toString();
37
+ for (const digit of v) rem = (rem * 10 + (digit.charCodeAt(0) - 48)) % 97;
38
+ }
39
+ return rem === 1;
40
+ }
41
+ function ssnValid(ssn) {
42
+ const m = ssn.replace(/-/g, "");
43
+ if (!/^\d{9}$/.test(m)) return false;
44
+ const area = Number(m.slice(0, 3));
45
+ const group = Number(m.slice(3, 5));
46
+ const serial = Number(m.slice(5));
47
+ if (area === 0 || area === 666 || area >= 900) return false;
48
+ if (group === 0 || serial === 0) return false;
49
+ return true;
50
+ }
51
+ function abaRoutingValid(digits) {
52
+ if (!/^\d{9}$/.test(digits)) return false;
53
+ const d = [...digits].map(Number);
54
+ const sum = 3 * (d[0] + d[3] + d[6]) + 7 * (d[1] + d[4] + d[7]) + (d[2] + d[5] + d[8]);
55
+ return sum % 10 === 0 && sum > 0;
56
+ }
57
+ function jwtValid(token) {
58
+ const head = token.split(".")[0];
59
+ if (!head) return false;
60
+ try {
61
+ const b64 = head.replace(/-/g, "+").replace(/_/g, "/");
62
+ const json = JSON.parse(atob(b64.padEnd(Math.ceil(b64.length / 4) * 4, "=")));
63
+ return typeof json === "object" && json !== null && "alg" in json;
64
+ } catch {
65
+ return false;
66
+ }
67
+ }
68
+
69
+ // src/tier0/patterns.ts
70
+ var CONTEXT_TERMS = /\b(password|passwd|pwd|passphrase|secret|key|token|credential|apikey|api[_-]?key|bearer|auth|ssn|social\s+security|card|cvv|cvc|iban|account|routing|wire|ach|swift|salary|dob|birth|policy|claim|coverage|deductible|premium|copay|insured|insurance|beneficiary|member|reimburs)\b/i;
71
+ var LIVE_PAYMENT_CONTEXT = /\b(cvv|cvc|exp|expir\w*|pin|billing)\b/i;
72
+ var CONTEXT_WINDOW = 40;
73
+ var DUMMY_VALUES = [
74
+ /AKIAIOSFODNN7EXAMPLE/,
75
+ /wJalrXUtnFEMI\/K7MDENG\/bPxRfiCYEXAMPLEKEY/,
76
+ /^4111[ -]?1111[ -]?1111[ -]?1111$/,
77
+ /^4242[ -]?4242[ -]?4242[ -]?4242$/,
78
+ /^sk-x{4,}/i,
79
+ /^sk-\.{3,}/,
80
+ /^(123-45-6789|078-05-1120|219-09-9999)$/,
81
+ // canonical fake SSNs
82
+ /^xox[baprs]-(x{4,}|0{10,})/i
83
+ ];
84
+ var PATTERNS = [
85
+ {
86
+ id: "AWS_ACCESS_KEY",
87
+ label: "AWS access key ID",
88
+ regex: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g,
89
+ baseScore: 0.95,
90
+ deterministicOnMatch: true
91
+ },
92
+ {
93
+ id: "AWS_SECRET",
94
+ label: "AWS secret access key (candidate)",
95
+ // 40-char base64-ish token; generic on purpose — precision comes from
96
+ // entropy + context, or Tier 1 arbitration.
97
+ regex: /\b[A-Za-z0-9/+=]{40}\b/g,
98
+ baseScore: 0.35,
99
+ entropyCheck: true
100
+ },
101
+ {
102
+ id: "OPENAI_KEY",
103
+ label: "OpenAI API key",
104
+ regex: /\bsk-(?:proj-|svcacct-)?[A-Za-z0-9_-]{20,}\b/g,
105
+ baseScore: 0.9,
106
+ // Legacy keys carry an unambiguous infix.
107
+ validator: (m) => m.includes("T3BlbkFJ")
108
+ },
109
+ {
110
+ id: "ANTHROPIC_KEY",
111
+ label: "Anthropic API key",
112
+ regex: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g,
113
+ baseScore: 0.95,
114
+ deterministicOnMatch: true
115
+ },
116
+ {
117
+ id: "GITHUB_TOKEN",
118
+ label: "GitHub token",
119
+ regex: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b/g,
120
+ baseScore: 0.95,
121
+ deterministicOnMatch: true
122
+ },
123
+ {
124
+ id: "SLACK_TOKEN",
125
+ label: "Slack token",
126
+ regex: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g,
127
+ baseScore: 0.95,
128
+ deterministicOnMatch: true
129
+ },
130
+ {
131
+ id: "PRIVATE_KEY_BLOCK",
132
+ label: "Private key material",
133
+ regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY(?: BLOCK)?-----/g,
134
+ baseScore: 1,
135
+ deterministicOnMatch: true
136
+ },
137
+ {
138
+ id: "JWT",
139
+ label: "JSON Web Token",
140
+ regex: /\beyJ[\w-]{10,}\.[\w-]{10,}\.[\w-]{5,}\b/g,
141
+ baseScore: 0.7,
142
+ validator: jwtValid,
143
+ dropOnInvalid: true
144
+ },
145
+ {
146
+ id: "CREDIT_CARD",
147
+ label: "Payment card number",
148
+ regex: /\b(?:\d[ -]?){12,18}\d\b/g,
149
+ baseScore: 0.3,
150
+ validator: (m) => luhnValid(m.replace(/[ -]/g, "")),
151
+ dropOnInvalid: true,
152
+ deterministicOnMatch: true
153
+ // deterministic *iff* validator passed
154
+ },
155
+ {
156
+ id: "SSN",
157
+ label: "US Social Security number",
158
+ regex: /\b\d{3}-\d{2}-\d{4}\b/g,
159
+ baseScore: 0.5,
160
+ validator: ssnValid,
161
+ dropOnInvalid: true
162
+ },
163
+ {
164
+ id: "SSN_LOOSE",
165
+ label: "US Social Security number (unformatted)",
166
+ regex: /\b\d{9}\b/g,
167
+ baseScore: 0.35,
168
+ validator: ssnValid,
169
+ dropOnInvalid: true,
170
+ requiresContext: true
171
+ },
172
+ {
173
+ id: "IBAN",
174
+ label: "IBAN bank account",
175
+ // Allows the conventional 4-char grouping with spaces; validator strips them.
176
+ regex: /\b[A-Z]{2}\d{2}(?: ?[A-Z0-9]){11,32}\b/g,
177
+ baseScore: 0.4,
178
+ validator: ibanValid,
179
+ dropOnInvalid: true,
180
+ deterministicOnMatch: true
181
+ },
182
+ {
183
+ id: "STRIPE_KEY",
184
+ label: "Stripe API key",
185
+ regex: /\b[sr]k_(?:live|test)_[A-Za-z0-9]{16,}\b/g,
186
+ baseScore: 0.95,
187
+ deterministicOnMatch: true
188
+ },
189
+ {
190
+ id: "WEBHOOK_SECRET",
191
+ label: "Webhook signing secret",
192
+ regex: /\bwhsec_[A-Za-z0-9]{16,}\b/g,
193
+ baseScore: 0.95,
194
+ deterministicOnMatch: true
195
+ },
196
+ {
197
+ id: "GOOGLE_API_KEY",
198
+ label: "Google API key",
199
+ regex: /\bAIza[0-9A-Za-z_-]{30,}\b/g,
200
+ baseScore: 0.95,
201
+ deterministicOnMatch: true
202
+ },
203
+ {
204
+ id: "SENDGRID_KEY",
205
+ label: "SendGrid API key",
206
+ regex: /\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g,
207
+ baseScore: 0.95,
208
+ deterministicOnMatch: true
209
+ },
210
+ {
211
+ id: "CONNECTION_STRING",
212
+ label: "Credentials in connection string",
213
+ // user:password@ inside a database/broker URL.
214
+ regex: /\b(?:postgres(?:ql)?|mysql|mariadb|mongodb(?:\+srv)?|redis|amqps?|mssql):\/\/[^\s/@:]+:[^\s@]+@/gi,
215
+ baseScore: 0.95,
216
+ deterministicOnMatch: true
217
+ },
218
+ {
219
+ id: "ROUTING_NUMBER",
220
+ label: "US bank routing number",
221
+ regex: /\b\d{9}\b/g,
222
+ baseScore: 0.45,
223
+ validator: abaRoutingValid,
224
+ dropOnInvalid: true,
225
+ requiresContext: true
226
+ // "routing", "account", "wire", "ACH" nearby
227
+ },
228
+ {
229
+ id: "INSURANCE_ID",
230
+ label: "Insurance policy / claim / member ID",
231
+ // Prefixed identifier (POL-, CLM-, HMO-, SF-2025-…). Requires insurance
232
+ // vocabulary nearby so ordinary "AB-1234"-shaped tokens don't fire.
233
+ regex: /\b[A-Z]{2,4}-\d{3,}[A-Z0-9-]*\b/g,
234
+ baseScore: 0.6,
235
+ requiresContext: true
236
+ },
237
+ {
238
+ id: "HEX_SECRET",
239
+ label: "Hex-encoded secret token",
240
+ regex: /\b[a-f0-9]{32,64}\b/g,
241
+ baseScore: 0.4,
242
+ entropyCheck: true,
243
+ requiresContext: true
244
+ // only counts near "token"/"secret"/"auth"/"key"
245
+ },
246
+ // ---- prompt-injection / jailbreak (destination is an AI tool) ----
247
+ // Object nouns are deliberately narrow so ordinary revision requests
248
+ // ("ignore the typo", "forget the previous outline") never fire.
249
+ {
250
+ id: "INJECTION_OVERRIDE",
251
+ label: "Prompt-injection: instruction override",
252
+ // "ignore/disregard/forget the previous/all instructions", plus
253
+ // "new instructions supersede/override prior ones".
254
+ regex: /\b(?:ignore|disregard|forget|drop)\b[^.\n]{0,50}\b(?:previous|prior|above|earlier|all|any|your)\s+(?:\w+\s+){0,2}?(?:instructions?|rules?|guidelines?|prompts?|polic(?:y|ies)|restrictions?|safety|filters?)\b|\bnew\s+instructions?\b[^.\n]{0,40}\b(?:supersede|override|replace|ignore)\b|\bsystem\s+override\b/gi,
255
+ baseScore: 0.8
256
+ },
257
+ {
258
+ id: "INJECTION_EXFIL",
259
+ label: "Prompt-injection: system-prompt exfiltration",
260
+ regex: /\b(?:print|reveal|repeat|output|show|display|prefix|leak|tell|respond\s+only\s+with)\b[^.\n]{0,60}\b(?:system|hidden|initial|developer|restricted|your)\s+(?:\w+\s+){0,2}?(?:prompt|message|instructions?|configuration|<system>|tags)\b|\brepeat\s+everything\s+above\b/gi,
261
+ baseScore: 0.8
262
+ },
263
+ {
264
+ id: "INJECTION_SUSPEND",
265
+ label: "Prompt-injection: rules-no-longer-apply",
266
+ // "the rules ... don't apply anymore", "guidelines no longer matter" — the
267
+ // AI's constraints, asserted void. Allows words between (oblique phrasings).
268
+ // Scored in the warn band (not a hard block) since it's fuzzier; the
269
+ // arbiter adjudicates.
270
+ regex: /\b(?:rules?|guidelines?|restrictions?|instructions?|constraints?|limits?|filters?|guardrails?|polic(?:y|ies))\b[^.\n]{0,40}\b(?:no\s+longer|don'?t|do\s+not|doesn'?t|does\s+not)\s+(?:apply|exist|matter|count|hold)\b/gi,
271
+ baseScore: 0.65
272
+ },
273
+ {
274
+ id: "INJECTION_BYPASS",
275
+ label: "Prompt-injection: bypass safeguards",
276
+ // "without the usual checks", "answer freely", "do what I say" — freedom /
277
+ // safeguard-bypass assertions directed at the assistant.
278
+ regex: /\b(?:without|bypass\w*|skip\w*|ignoring|no)\s+(?:the\s+)?(?:usual\s+)?(?:checks?|rules?|restrictions?|filters?|guardrails?|limits?|safeguards?|constraints?|safety)\b|\b(?:answer|respond|reply|comply)\s+(?:freely|without\s+(?:any\s+)?restrictions?)\b|\bdo\s+(?:whatever|what)\s+I\s+say\b/gi,
279
+ baseScore: 0.65
280
+ },
281
+ {
282
+ id: "INJECTION_PERSONA",
283
+ label: "Prompt-injection: jailbreak persona",
284
+ // Object nouns kept specific so "explain prompt injection to defend my bot"
285
+ // never matches.
286
+ regex: /\b(?:jailbr(?:eak|oken)|you\s+are\s+now\s+(?:DAN|an?\s+\w+\s+(?:with\s+no|without))|(?:developer|god|dan)\s+mode|no\s+(?:ethical\s+)?(?:restrictions?|filters?|rules|guidelines)|unfiltered\s+(?:model|ai|version)|(?:guidelines|rules|restrictions|content\s+polic\w+)\s+(?:do(?:n'?t| not)\s+(?:exist|apply)|doesn'?t\s+exist)|roleplay\s+as\s+an?\s+ai\s+with\s+no|bypass\b[^.\n]{0,40}\b(?:safety|filter|restriction|guardrail)\w*)/gi,
287
+ baseScore: 0.75
288
+ },
289
+ {
290
+ id: "GENERIC_SECRET",
291
+ label: "Credential assignment",
292
+ // No leading \b: must also match prefixed names like db_password, ACCOUNT_KEY.
293
+ regex: /(?:secret|passwd|password|passphrase|token|api[_-]?key|account[_-]?key|signing[_-]?key)\s*[:=]\s*["']?[^\s"']{8,}/gi,
294
+ baseScore: 0.75
295
+ }
296
+ ];
297
+ var EMAIL_REGEX = /\b[\w.+-]+@[\w-]+\.[A-Za-z]{2,}\b/g;
298
+ var EMAIL_BULK_THRESHOLD = 10;
299
+ var EMAIL_BULK_SCORE = 0.5;
300
+
301
+ // src/tier0/health.ts
302
+ var SIGNALS = [
303
+ // Drug names by distinctive pharmacological suffix + common exact names.
304
+ {
305
+ name: "drug",
306
+ re: /\b(?:\w+(?:cillin|mycin|micin|floxacin|pril|sartan|statin|parin|azole|prazole|olol|tidine|dipine|codone|morphine|barbital)|metformin|insulin|gabapentin|pregabalin|levetiracetam|lithium|sertraline|escitalopram|fluoxetine|methotrexate|levothyroxine|atorvastatin|lisinopril|carvedilol|albuterol|oxycodone|paxlovid|warfarin|prednisone|amoxicillin)\b/i
307
+ },
308
+ // Clinical abbreviations / dosing / record markers.
309
+ {
310
+ name: "clinical",
311
+ re: /\b(?:MRN|Dx|Rx|Hx|Tx|BID|TID|QID|PRN|A1c|LDL|HDL|TSH|EF|PCR|MRI|CT scan|ICD-?10|NYHA|ER\+|stage\s+(?:I|II|III|IV)\b|\d+\s?(?:mg|mcg|mL|units?)\b)/
312
+ },
313
+ // Diagnoses / conditions / procedures.
314
+ {
315
+ name: "condition",
316
+ re: /\b(?:diabet\w+|cancer|carcinoma|adenocarcinoma|melanoma|leukemia|lymphoma|tumou?r|HIV|hepatitis|asthma|epilep\w+|seizure|hypertens\w+|hypothyroid\w*|depress\w+|anxiety|schizophren\w+|psychosis|herniation|arthritis|dialysis|chemotherapy|biopsy|anaphylaxis|gestational|appendectomy|antiretroviral|metastat\w+)\b/i
317
+ },
318
+ // Care context.
319
+ {
320
+ name: "care",
321
+ re: /\b(?:patient|diagnos\w+|prescrib\w+|discharge summary|oncolog\w+|cardiolog\w+|pathology|inpatient|outpatient|prognos\w+|symptom\w*|admitted|referral|transplant|surgical)\b/i
322
+ }
323
+ ];
324
+ var HEALTH_BASE_SCORE = 0.75;
325
+ function scanHealth(text) {
326
+ let hitClasses = 0;
327
+ let minStart = Infinity;
328
+ let maxEnd = -1;
329
+ for (const sig of SIGNALS) {
330
+ sig.re.lastIndex = 0;
331
+ const m = sig.re.exec(text);
332
+ if (m) {
333
+ hitClasses++;
334
+ minStart = Math.min(minStart, m.index);
335
+ maxEnd = Math.max(maxEnd, m.index + m[0].length);
336
+ const all = [...text.matchAll(new RegExp(sig.re.source, sig.re.flags.replace("g", "") + "g"))];
337
+ for (const a of all) {
338
+ minStart = Math.min(minStart, a.index ?? minStart);
339
+ maxEnd = Math.max(maxEnd, (a.index ?? 0) + a[0].length);
340
+ }
341
+ }
342
+ }
343
+ if (hitClasses < 2) return null;
344
+ return {
345
+ start: Math.max(0, minStart),
346
+ end: Math.min(text.length, maxEnd),
347
+ label: "HEALTH_CLINICAL",
348
+ score: HEALTH_BASE_SCORE,
349
+ tier: 0,
350
+ deterministic: false
351
+ };
352
+ }
353
+
354
+ // src/tier0/code.ts
355
+ var SIGNATURES = [
356
+ /\b(?:def|func|function|fn|sub)\s+\w+\s*\(/,
357
+ // function definition
358
+ /\b(?:public|private|protected|static)\s+[\w<>[\]]+\s+\w+\s*\([^)]*\)\s*\{/,
359
+ // typed method
360
+ /\b(?:const|let|var)\s+\w+\s*=\s*[^;\n]+[;({]/,
361
+ // assignment to call/expr
362
+ /=>\s*[{(]/,
363
+ // arrow function body
364
+ /\)\s*=>/,
365
+ // arrow params
366
+ /\)\s*\{\s*$/m,
367
+ // block-opening line
368
+ /\b(?:SELECT|INSERT|UPDATE|DELETE|CREATE(?:\s+OR\s+REPLACE)?|WITH|DROP|ALTER)\b[\s\S]{0,80}\b(?:FROM|INTO|SET|TABLE|WHERE|VALUES|FUNCTION|AS|USING)\b/i,
369
+ // SQL
370
+ /^#!\/\S+/m,
371
+ // shebang
372
+ /\bresource\s+"[^"]+"\s+"[^"]+"\s*\{/,
373
+ // terraform/HCL
374
+ /@\w+(?:\.\w+)?\s*\([^)]*\)\s*$/m,
375
+ // decorator/route line
376
+ /\b(?:import|from)\s+[\w.]+\s+import\b/,
377
+ // python import
378
+ /\bdb\.(?:execute|query|drop_all|transfer|debit|credit)\s*\(/
379
+ // db call idioms
380
+ ];
381
+ var MIN_SIGNATURES = 1;
382
+ function scanCode(text) {
383
+ let matches = 0;
384
+ let minStart = Infinity;
385
+ let maxEnd = -1;
386
+ for (const re of SIGNATURES) {
387
+ const m = re.exec(text);
388
+ if (m) {
389
+ matches++;
390
+ minStart = Math.min(minStart, m.index);
391
+ maxEnd = Math.max(maxEnd, m.index + m[0].length);
392
+ }
393
+ }
394
+ if (matches < MIN_SIGNATURES) return null;
395
+ return {
396
+ start: Math.max(0, minStart),
397
+ end: Math.min(text.length, maxEnd),
398
+ label: "SOURCE_CODE",
399
+ score: 0.7,
400
+ tier: 0,
401
+ deterministic: false
402
+ };
403
+ }
404
+
405
+ // src/tier0/deobfuscate.ts
406
+ var BASE64_BLOB = /\b[A-Za-z0-9+/]{16,}={0,2}\b/g;
407
+ function decodeBase64(blob) {
408
+ const stripped = blob.replace(/=+$/, "");
409
+ if (stripped.length < 16) return null;
410
+ try {
411
+ const bin = atob(blob);
412
+ if (bin.length < 6) return null;
413
+ let printable = 0;
414
+ for (let i = 0; i < bin.length; i++) {
415
+ const c = bin.charCodeAt(i);
416
+ if (c >= 32 && c < 127) printable++;
417
+ }
418
+ if (printable / bin.length < 0.85) return null;
419
+ return bin;
420
+ } catch {
421
+ return null;
422
+ }
423
+ }
424
+ function decodeRegions(text, max = 8) {
425
+ const out = [];
426
+ BASE64_BLOB.lastIndex = 0;
427
+ for (const m of text.matchAll(BASE64_BLOB)) {
428
+ const decoded = decodeBase64(m[0]);
429
+ if (decoded && decoded !== m[0]) {
430
+ out.push({ start: m.index ?? 0, end: (m.index ?? 0) + m[0].length, decoded });
431
+ if (out.length >= max) break;
432
+ }
433
+ }
434
+ return out;
435
+ }
436
+
437
+ // src/tier0/engine.ts
438
+ var clamp01 = (x) => Math.max(0, Math.min(1, x));
439
+ function windowAround(text, start, end) {
440
+ const lo = Math.max(0, start - CONTEXT_WINDOW);
441
+ const hi = Math.min(text.length, end + CONTEXT_WINDOW);
442
+ return text.slice(lo, start) + " " + text.slice(end, hi);
443
+ }
444
+ function isDummy(match) {
445
+ return DUMMY_VALUES.some((re) => re.test(match));
446
+ }
447
+ function activePatterns(policy) {
448
+ const disabled = new Set(policy.disabledPatterns);
449
+ const custom = policy.customPatterns.map((c) => ({
450
+ id: c.id,
451
+ label: c.id,
452
+ regex: new RegExp(c.pattern, "g"),
453
+ baseScore: c.baseScore,
454
+ deterministicOnMatch: c.deterministic
455
+ }));
456
+ return [...PATTERNS.filter((p) => !disabled.has(p.id)), ...custom];
457
+ }
458
+ function runPatterns(text, patterns, state) {
459
+ for (const spec of patterns) {
460
+ spec.regex.lastIndex = 0;
461
+ for (const m of text.matchAll(spec.regex)) {
462
+ const value = m[0];
463
+ const start = m.index ?? 0;
464
+ const end = start + value.length;
465
+ const around = windowAround(text, start, end);
466
+ if (isDummy(value) && !LIVE_PAYMENT_CONTEXT.test(around)) continue;
467
+ const contextNearby = CONTEXT_TERMS.test(around);
468
+ if (spec.requiresContext && !contextNearby) continue;
469
+ const validated = spec.validator ? spec.validator(value) : void 0;
470
+ if (validated === false && spec.dropOnInvalid) continue;
471
+ let score = spec.baseScore;
472
+ let deterministic = false;
473
+ if (validated === true) {
474
+ deterministic = spec.deterministicOnMatch !== false;
475
+ score = Math.max(score, 0.95);
476
+ } else if (spec.deterministicOnMatch && !spec.validator) {
477
+ deterministic = true;
478
+ }
479
+ if (contextNearby) score += 0.25;
480
+ if (spec.entropyCheck) {
481
+ const h = shannonEntropy(value.slice(0, 64));
482
+ if (h > 4.2 && value.length >= 20) score += 0.3;
483
+ else if (h < 3) score -= 0.3;
484
+ if (h > 4.2 !== contextNearby) state.conflicted = true;
485
+ }
486
+ score = clamp01(score);
487
+ if (score <= 0) continue;
488
+ state.hits.push({ start, end, label: spec.id, score, tier: 0, deterministic });
489
+ }
490
+ }
491
+ }
492
+ function scanTier0(text, policy) {
493
+ const state = { hits: [], conflicted: false };
494
+ const patterns = activePatterns(policy);
495
+ runPatterns(text, patterns, state);
496
+ for (const region of decodeRegions(text)) {
497
+ const sub = { hits: [], conflicted: false };
498
+ runPatterns(region.decoded, patterns, sub);
499
+ const injectionInside = /INJECTION_/.test(sub.hits.map((h) => h.label).join(",")) || scanHealth(region.decoded);
500
+ if (sub.hits.length > 0 || injectionInside) {
501
+ const best = Math.max(0.85, ...sub.hits.map((h) => h.score));
502
+ state.hits.push({
503
+ start: region.start,
504
+ end: region.end,
505
+ label: "ENCODED_PAYLOAD",
506
+ score: best,
507
+ tier: 0,
508
+ deterministic: false
509
+ });
510
+ }
511
+ }
512
+ const health = scanHealth(text);
513
+ if (health) state.hits.push(health);
514
+ const code = scanCode(text);
515
+ if (code) state.hits.push(code);
516
+ const hits = state.hits;
517
+ let conflicted = state.conflicted;
518
+ EMAIL_REGEX.lastIndex = 0;
519
+ const emails = /* @__PURE__ */ new Set();
520
+ for (const m of text.matchAll(EMAIL_REGEX)) emails.add(m[0].toLowerCase());
521
+ if (emails.size >= EMAIL_BULK_THRESHOLD) {
522
+ hits.push({
523
+ start: 0,
524
+ end: text.length,
525
+ label: "EMAIL_BULK",
526
+ score: EMAIL_BULK_SCORE,
527
+ tier: 0,
528
+ deterministic: false
529
+ });
530
+ }
531
+ const score = hits.reduce((mx, h) => Math.max(mx, h.score), 0);
532
+ const deterministic = hits.some((h) => h.deterministic);
533
+ return { score, deterministic, hits, conflicted };
534
+ }
535
+
536
+ // src/transplant/adapter.ts
537
+ var LABEL_MAP = {
538
+ // secret
539
+ AWS_ACCESS_KEY: { category: "SECRET" },
540
+ AWS_SECRET: { category: "SECRET" },
541
+ OPENAI_KEY: { category: "SECRET" },
542
+ ANTHROPIC_KEY: { category: "SECRET" },
543
+ GITHUB_TOKEN: { category: "SECRET" },
544
+ SLACK_TOKEN: { category: "SECRET" },
545
+ PRIVATE_KEY_BLOCK: { category: "SECRET" },
546
+ JWT: { category: "SECRET" },
547
+ GENERIC_SECRET: { category: "SECRET" },
548
+ STRIPE_KEY: { category: "SECRET" },
549
+ WEBHOOK_SECRET: { category: "SECRET" },
550
+ GOOGLE_API_KEY: { category: "SECRET" },
551
+ SENDGRID_KEY: { category: "SECRET" },
552
+ CONNECTION_STRING: { category: "SECRET" },
553
+ HEX_SECRET: { category: "SECRET" },
554
+ "api key or secret token": { category: "SECRET" },
555
+ password: { category: "SECRET" },
556
+ // financial
557
+ CREDIT_CARD: { category: "FINANCIAL" },
558
+ IBAN: { category: "FINANCIAL" },
559
+ ROUTING_NUMBER: { category: "FINANCIAL" },
560
+ "credit card number": { category: "FINANCIAL" },
561
+ "bank account number": { category: "FINANCIAL" },
562
+ // pii
563
+ SSN: { category: "PII" },
564
+ SSN_LOOSE: { category: "PII" },
565
+ EMAIL_BULK: { category: "PII", intent: true },
566
+ "social security number": { category: "PII" },
567
+ "home address": { category: "PII" },
568
+ "date of birth": { category: "PII" },
569
+ "passport or license number": { category: "PII" },
570
+ "person name": { category: "PII" },
571
+ "email address": { category: "PII" },
572
+ "phone number": { category: "PII" },
573
+ // health
574
+ HEALTH_CLINICAL: { category: "HEALTH" },
575
+ "medical condition or diagnosis": { category: "HEALTH" },
576
+ medication: { category: "HEALTH" },
577
+ // insurance
578
+ INSURANCE_ID: { category: "INSURANCE" },
579
+ "insurance policy or claim number": { category: "INSURANCE" },
580
+ // code (whole-text intent)
581
+ SOURCE_CODE: { category: "CODE", intent: true },
582
+ "source code": { category: "CODE", intent: true },
583
+ // injection (whole-text intent)
584
+ INJECTION_OVERRIDE: { category: "INJECTION", intent: true },
585
+ INJECTION_EXFIL: { category: "INJECTION", intent: true },
586
+ INJECTION_PERSONA: { category: "INJECTION", intent: true },
587
+ INJECTION_SUSPEND: { category: "INJECTION", intent: true },
588
+ INJECTION_BYPASS: { category: "INJECTION", intent: true },
589
+ ENCODED_PAYLOAD: { category: "INJECTION", intent: true },
590
+ // multi-turn
591
+ MULTITURN_SPLIT_SECRET: { category: "SECRET", intent: true },
592
+ MULTITURN_BULK_PII: { category: "PII", intent: true }
593
+ };
594
+ function mapSpan(span, sourceText) {
595
+ const m = LABEL_MAP[span.label];
596
+ if (!m) return null;
597
+ const start = Math.max(0, span.start);
598
+ const end = Math.min(sourceText.length, span.end);
599
+ return {
600
+ category: m.category,
601
+ text: sourceText.slice(start, end),
602
+ start,
603
+ end,
604
+ confidence: Math.max(0, Math.min(1, span.score)),
605
+ ...m.intent ? { kind: "intent" } : {},
606
+ detector: "medusa2"
607
+ };
608
+ }
609
+ function dedupe(spans) {
610
+ const out = [];
611
+ for (const s of spans.sort((a, b) => a.start - b.start || b.confidence - a.confidence)) {
612
+ const prev = out.find(
613
+ (p) => p.category === s.category && s.start < p.end && s.end > p.start
614
+ );
615
+ if (prev) {
616
+ prev.start = Math.min(prev.start, s.start);
617
+ prev.end = Math.max(prev.end, s.end);
618
+ prev.confidence = Math.max(prev.confidence, s.confidence);
619
+ prev.kind = prev.kind ?? s.kind;
620
+ } else out.push(s);
621
+ }
622
+ return out;
623
+ }
624
+ function detectSpans(text, policy, tier1Spans = []) {
625
+ const t0 = scanTier0(text, policy).hits;
626
+ const mapped = [...t0, ...tier1Spans].map((s) => mapSpan(s, text)).filter((s) => s !== null);
627
+ return dedupe(mapped);
628
+ }
629
+ var PRODUCTION_CATEGORIES = ["SECRET", "PII", "FINANCIAL", "HEALTH", "INSURANCE", "CODE", "INJECTION"];
630
+
631
+ // src/shared/policy.ts
632
+ var DEFAULT_POLICY = {
633
+ disabledPatterns: [],
634
+ customPatterns: [],
635
+ // Concrete, disclosure-shaped phrasings: small NLI models spuriously entail
636
+ // abstract labels ("salary information") from any work-related sentence.
637
+ tier1Labels: {
638
+ "a secret api key or access token": 0.5,
639
+ "a password being shared": 0.5,
640
+ "a social security number": 0.55,
641
+ "a written-out credit card number": 0.6,
642
+ "private medical details about a person": 0.6,
643
+ "a specific salary amount or pay figure": 0.6,
644
+ "a person's home address": 0.6,
645
+ "a person's date of birth": 0.6,
646
+ "a passport, driver license, or national ID number": 0.6
647
+ },
648
+ nonVetoableLabels: [
649
+ "private medical details about a person",
650
+ "a passport, driver license, or national ID number",
651
+ "a person's home address",
652
+ "a person's date of birth"
653
+ ],
654
+ tier2Rules: [
655
+ "Credentials, API keys, tokens, or passwords must never be shared.",
656
+ "Personally identifiable information (SSN, DOB + full name, home address) must not be sent to external services.",
657
+ "Financial data (card numbers, bank accounts, payroll) must not be shared.",
658
+ "Internal project codenames, unreleased product details, and confidential strategy documents must not be pasted into public AI chatbots or webmail."
659
+ ],
660
+ contextProfiles: {
661
+ "chatgpt.com": "strict",
662
+ "chat.openai.com": "strict",
663
+ "claude.ai": "strict",
664
+ "gemini.google.com": "strict",
665
+ "mail.google.com": "strict"
666
+ },
667
+ defaultStrictness: "standard",
668
+ allowlistedOrigins: [],
669
+ thresholds: {
670
+ warn: 0.6,
671
+ block: 0.9,
672
+ gateTier1Low: 0.35,
673
+ gateTier1High: 0.9,
674
+ gateTier2Low: 0.55,
675
+ gateTier2High: 0.85,
676
+ submitSweepMinChars: 200
677
+ },
678
+ fusionWeights: { tier0: 0.4, tier1: 0.45, tier2: 0.15 },
679
+ ui: { maskFirst: true }
680
+ };
681
+ function resolvePolicy(managedJson) {
682
+ if (!managedJson) return DEFAULT_POLICY;
683
+ try {
684
+ const partial = JSON.parse(managedJson);
685
+ return {
686
+ ...DEFAULT_POLICY,
687
+ ...partial,
688
+ thresholds: { ...DEFAULT_POLICY.thresholds, ...partial.thresholds ?? {} },
689
+ fusionWeights: { ...DEFAULT_POLICY.fusionWeights, ...partial.fusionWeights ?? {} },
690
+ ui: { ...DEFAULT_POLICY.ui, ...partial.ui ?? {} }
691
+ };
692
+ } catch {
693
+ console.warn("[medusa] invalid managed policyJson; using defaults");
694
+ return DEFAULT_POLICY;
695
+ }
696
+ }
697
+
698
+ // src/transplant/entry.ts
699
+ var GLINER_LABELS = {
700
+ "api key or secret token": 0.5,
701
+ "password": 0.5,
702
+ "social security number": 0.55,
703
+ "credit card number": 0.6,
704
+ "bank account number": 0.55,
705
+ "medical condition or diagnosis": 0.6,
706
+ "medication": 0.6,
707
+ "home address": 0.6,
708
+ "date of birth": 0.6,
709
+ "passport or license number": 0.6,
710
+ "source code": 0.6,
711
+ "insurance policy or claim number": 0.6
712
+ };
713
+ export {
714
+ DEFAULT_POLICY,
715
+ GLINER_LABELS,
716
+ PRODUCTION_CATEGORIES,
717
+ detectSpans,
718
+ mapSpan,
719
+ resolvePolicy
720
+ };