llm-entropy-filter 1.1.0 → 1.2.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.
- package/CHANGELOG.md +155 -51
- package/LICENSE +93 -93
- package/README.md +297 -352
- package/dist/index.cjs +104 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +31 -1
- package/dist/index.d.ts +31 -1
- package/dist/index.js +104 -70
- package/dist/index.js.map +1 -1
- package/integrations/express.mjs +117 -117
- package/integrations/fastify.mjs +106 -106
- package/integrations/langchain.mjs +98 -98
- package/integrations/vercel-ai-sdk.mjs +44 -44
- package/package.json +99 -59
- package/rulesets/default.js +8 -0
- package/rulesets/default.json +77 -73
- package/rulesets/public-api.js +7 -0
- package/rulesets/public-api.json +179 -27
- package/rulesets/schema +24 -24
- package/rulesets/strict.js +7 -0
- package/rulesets/strict.json +173 -25
- package/rulesets/support.js +7 -0
- package/rulesets/support.json +22 -22
package/dist/index.js
CHANGED
|
@@ -11,18 +11,48 @@ function analyzeEntropy(text) {
|
|
|
11
11
|
const t = raw.toLowerCase();
|
|
12
12
|
const flags = [];
|
|
13
13
|
let score = 0;
|
|
14
|
-
if (/\b(ahora|ya|urgente
|
|
14
|
+
if (/\b(ahora|ya|urgente|urgencia|hoy|inmediato|inmediatamente|últim[oa]s?|solo\s+hoy|ap[uú]rate|rápido|de\s+inmediato)\b/.test(
|
|
15
|
+
t
|
|
16
|
+
)) {
|
|
15
17
|
flags.push("urgency");
|
|
16
18
|
score += 0.2;
|
|
17
19
|
}
|
|
18
|
-
if (/\b(compra|oferta|promo|descuento|gratis|clic|click
|
|
20
|
+
if (/\b(compra|oferta|promo|promoci[oó]n|descuento|rebaja|gratis|free|premio|prize|winner|gana|claim|reward|clic|click)\b/.test(
|
|
21
|
+
t
|
|
22
|
+
)) {
|
|
19
23
|
flags.push("spam_sales");
|
|
20
24
|
score += 0.25;
|
|
21
25
|
}
|
|
26
|
+
const wantsSendVerb = /\b(envi(a|á)me|env[ií]ame|m(a|á)ndame|p(a|á)same|dame|compart(e|a|as)|reenv[ií]a(me)?)\b/.test(
|
|
27
|
+
t
|
|
28
|
+
);
|
|
29
|
+
const mentionsCode = /\b(c[oó]digo|codigo|otp|2fa|token|pin|clave)\b/.test(t);
|
|
30
|
+
const mentionsVerify = /\b(verificaci[oó]n|verificar|confirmar|validar)\b/.test(t);
|
|
31
|
+
const mentionsAccount = /\b(cuenta|account)\b/.test(t);
|
|
32
|
+
const mentionsSms = /\b(sms|por\s+sms)\b/.test(t);
|
|
33
|
+
if (wantsSendVerb && mentionsCode && (mentionsVerify || mentionsAccount || mentionsSms)) {
|
|
34
|
+
flags.push("phishing_2fa_code");
|
|
35
|
+
score += 0.55;
|
|
36
|
+
}
|
|
37
|
+
if (/\bverify\b/.test(t) && /\baccount\b/.test(t) && /\bclick\b/.test(t) && /\b(closed|close|suspend|suspended|disable|disabled|locked)\b/.test(t)) {
|
|
38
|
+
flags.push("phishing_verify_threat_en");
|
|
39
|
+
score += 0.35;
|
|
40
|
+
}
|
|
41
|
+
if (/\b(te\s+deposito|te\s+dep[oó]sito|te\s+transfiero|te\s+transferir[eé]|transferencia|dep[oó]sito)\b/.test(
|
|
42
|
+
t
|
|
43
|
+
) && /\b(tarjeta|cuenta|clabe|iban|swift|n[uú]mero\s+de\s+tarjeta|numero\s+de\s+tarjeta)\b/.test(t)) {
|
|
44
|
+
flags.push("fraud_payment_request");
|
|
45
|
+
score += 0.35;
|
|
46
|
+
}
|
|
47
|
+
if (/\b(gana(r)?\s+dinero|ingresos|dinero\s+extra)\b/.test(t) && /\b(desde\s+casa|en\s+casa|home)\b/.test(t) && /\b(sin\s+esfuerzo|f[aá]cil|r[aá]pido|easy|fast)\b/.test(t)) {
|
|
48
|
+
flags.push("scam_wfh");
|
|
49
|
+
score += 0.3;
|
|
50
|
+
}
|
|
22
51
|
const moneyHits = countRegex(raw, /\$+/g);
|
|
23
|
-
|
|
52
|
+
const pctHits = countRegex(raw, /%/g);
|
|
53
|
+
if (moneyHits > 0 || pctHits > 0 || /\b(usd|mxn|eur)\b/i.test(raw)) {
|
|
24
54
|
flags.push("money_signal");
|
|
25
|
-
score += Math.min(0.
|
|
55
|
+
score += Math.min(0.25, moneyHits * 0.05 + pctHits * 0.05 + 0.1);
|
|
26
56
|
}
|
|
27
57
|
const exclam = countRegex(raw, /!/g);
|
|
28
58
|
const capsRatio = (() => {
|
|
@@ -35,39 +65,24 @@ function analyzeEntropy(text) {
|
|
|
35
65
|
flags.push("shouting");
|
|
36
66
|
score += 0.2;
|
|
37
67
|
}
|
|
38
|
-
if (/\b(si
|
|
68
|
+
if (/\b(si\s+de\s+verdad|si\s+me\s+quisieras|es\s+tu\s+culpa|no\s+tienes\s+opci[oó]n|me\s+debes|hazlo\s+o\s+si\s+no|si\s+no\s+lo\s+haces)\b/.test(
|
|
69
|
+
t
|
|
70
|
+
)) {
|
|
39
71
|
flags.push("emotional_manipulation");
|
|
40
72
|
score += 0.35;
|
|
41
73
|
}
|
|
42
|
-
if (/\b(todos
|
|
74
|
+
if (/\b(todos\s+lo\s+saben|lo\s+esconden|la\s+verdad\s+oculta|ellos\s+no\s+quieren|simulaci[oó]n)\b/.test(
|
|
75
|
+
t
|
|
76
|
+
)) {
|
|
43
77
|
flags.push("conspiracy_vague");
|
|
44
78
|
score += 0.2;
|
|
45
79
|
}
|
|
46
|
-
if (/\b(
|
|
80
|
+
if (/\b(es\s+obvio|todo\s+mundo\s+sabe|se\s+sabe|est[aá]\s+claro|la\s+cultura\s+lo\s+prueba)\b/.test(
|
|
81
|
+
t
|
|
82
|
+
)) {
|
|
47
83
|
flags.push("weak_evidence");
|
|
48
84
|
score += 0.2;
|
|
49
85
|
}
|
|
50
|
-
if (/\b(ellos|la élite|los de arriba)\b/.test(t) && /\b(esconden|ocultan|tapan)\b/.test(t)) {
|
|
51
|
-
flags.push("hidden_actor");
|
|
52
|
-
score += 0.15;
|
|
53
|
-
}
|
|
54
|
-
if (/\b(física cuántica|cuantica|cuántico|quantum)\b/.test(t)) {
|
|
55
|
-
flags.push("pseudo_science_quantum");
|
|
56
|
-
score += 0.2;
|
|
57
|
-
}
|
|
58
|
-
const manifestHits = countRegex(t, /\b(manifestar|manifestación|decretar|decreto|vibración|vibracion|frecuencia|energía|energia|ley de la atracción|universo me lo dará)\b/g) + countRegex(t, /\b(realine(a|ar)\b.*\bátom|\bátom|\batomos\b)/g);
|
|
59
|
-
if (manifestHits > 0) {
|
|
60
|
-
flags.push("magic_manifesting");
|
|
61
|
-
score += Math.min(0.35, 0.15 + manifestHits * 0.06);
|
|
62
|
-
}
|
|
63
|
-
if (/\b(no hay una verdad objetiva|no existe la verdad objetiva|tu verdad|mi verdad|la verdad es relativa|lo que importa es lo que sientes)\b/.test(t)) {
|
|
64
|
-
flags.push("truth_relativism");
|
|
65
|
-
score += 0.35;
|
|
66
|
-
}
|
|
67
|
-
if (/\b(deben|debe)\b.*\b(obligatoriamente|por lo tanto|por ende)\b/.test(t) || /\b(la materia)\b.*\b(se subordina|obedece)\b/.test(t)) {
|
|
68
|
-
flags.push("broken_causality");
|
|
69
|
-
score += 0.2;
|
|
70
|
-
}
|
|
71
86
|
score = clamp01(score);
|
|
72
87
|
return { score, flags };
|
|
73
88
|
}
|
|
@@ -158,39 +173,56 @@ function mergeFlags(base, extra) {
|
|
|
158
173
|
}
|
|
159
174
|
return out;
|
|
160
175
|
}
|
|
161
|
-
function englishSpamBooster(
|
|
162
|
-
const t = (
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
{ re: /\btax refund\b|\bunpaid\b|\brefund\b|\bchargeback\b/g, score: 0.1, flag: "spam_kw_refund" }
|
|
176
|
+
function englishSpamBooster(text) {
|
|
177
|
+
const t = (text || "").toLowerCase();
|
|
178
|
+
const hits = [];
|
|
179
|
+
const kw = [
|
|
180
|
+
["free", "spam_kw_free"],
|
|
181
|
+
["winner", "spam_kw_winner"],
|
|
182
|
+
["claim", "spam_kw_claim"],
|
|
183
|
+
["click", "spam_kw_click"],
|
|
184
|
+
["verify", "spam_kw_verify"],
|
|
185
|
+
["prize", "spam_kw_prize"],
|
|
186
|
+
["urgent", "spam_kw_urgency_en"],
|
|
187
|
+
["limited time", "spam_kw_urgency_en"],
|
|
188
|
+
["today only", "spam_kw_urgency_en"]
|
|
175
189
|
];
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
let hitCount = 0;
|
|
179
|
-
for (const p of patterns) {
|
|
180
|
-
const m = t.match(p.re);
|
|
181
|
-
if (m && m.length > 0) {
|
|
182
|
-
hitCount += m.length;
|
|
183
|
-
addScore += p.score;
|
|
184
|
-
addFlags.push(p.flag);
|
|
185
|
-
}
|
|
190
|
+
for (const [s, flag] of kw) {
|
|
191
|
+
if (t.includes(s)) hits.push(flag);
|
|
186
192
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
193
|
+
if (hits.length === 0) return { hitCount: 0, addScore: 0, addFlags: [] };
|
|
194
|
+
const addScore = Math.min(0.25, hits.length * 0.05);
|
|
195
|
+
const addFlags = mergeFlags(["spam_keywords_en"], hits);
|
|
196
|
+
return { hitCount: hits.length, addScore, addFlags };
|
|
197
|
+
}
|
|
198
|
+
function decideAction(params) {
|
|
199
|
+
const {
|
|
200
|
+
score,
|
|
201
|
+
warnT,
|
|
202
|
+
blockT,
|
|
203
|
+
strongSpam,
|
|
204
|
+
intention = "",
|
|
205
|
+
flags,
|
|
206
|
+
policy = {}
|
|
207
|
+
} = params;
|
|
208
|
+
const blockIntentions = new Set(policy.block_intentions ?? []);
|
|
209
|
+
const warnIntentions = new Set(policy.warn_intentions ?? []);
|
|
210
|
+
const blockFlags = new Set(policy.block_flags ?? []);
|
|
211
|
+
const warnFlags = new Set(policy.warn_flags ?? []);
|
|
212
|
+
if (blockIntentions.has(intention)) return "BLOCK";
|
|
213
|
+
if (flags.some((f) => blockFlags.has(f))) return "BLOCK";
|
|
214
|
+
if (warnIntentions.has(intention)) return "WARN";
|
|
215
|
+
if (flags.some((f) => warnFlags.has(f))) return "WARN";
|
|
216
|
+
const strongSpamBlock = policy.strong_spam_block ?? true;
|
|
217
|
+
if (strongSpamBlock && strongSpam) return "BLOCK";
|
|
218
|
+
if (score >= blockT) return "BLOCK";
|
|
219
|
+
if (score >= warnT) return "WARN";
|
|
220
|
+
return "ALLOW";
|
|
190
221
|
}
|
|
191
222
|
function gateLLM(text, config = {}) {
|
|
192
|
-
const
|
|
193
|
-
const
|
|
223
|
+
const ruleset = config.ruleset;
|
|
224
|
+
const warnT = ruleset?.thresholds?.warn ?? config.warnThreshold ?? 0.25;
|
|
225
|
+
const blockT = ruleset?.thresholds?.block ?? config.blockThreshold ?? 0.6;
|
|
194
226
|
const r = runEntropyFilter(text);
|
|
195
227
|
let score = r.entropy_analysis.score;
|
|
196
228
|
let flags = [...r.entropy_analysis.flags];
|
|
@@ -199,20 +231,22 @@ function gateLLM(text, config = {}) {
|
|
|
199
231
|
score = clamp013(score + booster.addScore);
|
|
200
232
|
flags = mergeFlags(flags, booster.addFlags);
|
|
201
233
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
confidence = Math.max(confidence, 0.75);
|
|
208
|
-
rationale = (rationale ? rationale + " " : "") + "Detect\xE9 keywords t\xEDpicas de spam/phishing en ingl\xE9s.";
|
|
209
|
-
}
|
|
210
|
-
const hasSpam = flags.includes("spam_sales") || flags.includes("spam_keywords_en");
|
|
211
|
-
const hasMoneySignals = flags.includes("money_signal") || flags.includes("spam_kw_prize") || flags.includes("spam_kw_loan");
|
|
234
|
+
const intention = r.intention_evaluation.intention || "unknown";
|
|
235
|
+
const confidence = r.intention_evaluation.confidence ?? 0;
|
|
236
|
+
const rationale = r.intention_evaluation.rationale ?? "";
|
|
237
|
+
const hasSpam = flags.includes("spam_sales");
|
|
238
|
+
const hasMoneySignals = flags.includes("money_signal") || flags.includes("money_signal_high");
|
|
212
239
|
const strongSpam = hasSpam && hasMoneySignals;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
240
|
+
const policy = ruleset?.policy ?? {};
|
|
241
|
+
const action = decideAction({
|
|
242
|
+
score,
|
|
243
|
+
warnT,
|
|
244
|
+
blockT,
|
|
245
|
+
strongSpam,
|
|
246
|
+
intention,
|
|
247
|
+
flags,
|
|
248
|
+
policy
|
|
249
|
+
});
|
|
216
250
|
return {
|
|
217
251
|
action,
|
|
218
252
|
entropy_score: score,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/entropy.ts","../src/intention.ts","../src/wrapper.ts","../src/middleware.ts","../src/gate.ts"],"sourcesContent":["// src/entropy.ts\nexport type EntropyResult = {\n score: number; // 0..1\n flags: string[];\n};\n\nfunction clamp01(x: number) {\n return Math.max(0, Math.min(1, x));\n}\n\nfunction countRegex(text: string, re: RegExp) {\n const m = text.match(re);\n return m ? m.length : 0;\n}\n\nexport function analyzeEntropy(text: string): EntropyResult {\n const raw = text || \"\";\n const t = raw.toLowerCase();\n const flags: string[] = [];\n let score = 0;\n\n // 1) Urgencia / presión\n if (/\\b(ahora|ya|urgente|última|hoy|inmediato)\\b/.test(t)) {\n flags.push(\"urgency\");\n score += 0.20;\n }\n\n // 2) Spam / venta agresiva\n if (/\\b(compra|oferta|promo|descuento|gratis|clic|click|off)\\b/.test(t)) {\n flags.push(\"spam_sales\");\n score += 0.25;\n }\n\n // 3) Señales $$$ / símbolos\n const moneyHits = countRegex(raw, /\\$+/g);\n if (moneyHits > 0) {\n flags.push(\"money_signal\");\n score += Math.min(0.20, moneyHits * 0.05);\n }\n\n // 4) Exceso de signos / gritos\n const exclam = countRegex(raw, /!/g);\n const capsRatio = (() => {\n const letters = raw.match(/[A-Za-zÁÉÍÓÚÜÑáéíóúüñ]/g) || [];\n if (letters.length === 0) return 0;\n const caps = (raw.match(/[A-ZÁÉÍÓÚÜÑ]/g) || []).length;\n return caps / letters.length;\n })();\n\n if (exclam >= 3 || capsRatio >= 0.35) {\n flags.push(\"shouting\");\n score += 0.20;\n }\n\n // 5) Manipulación / culpa / coerción\n if (/\\b(si de verdad|si me quisieras|es tu culpa|no tienes opción|me debes)\\b/.test(t)) {\n flags.push(\"emotional_manipulation\");\n score += 0.35;\n }\n\n // 6) Conspiración vaga / “todos lo saben”\n if (/\\b(todos lo saben|lo esconden|la verdad oculta|ellos no quieren|simulación)\\b/.test(t)) {\n flags.push(\"conspiracy_vague\");\n score += 0.20;\n }\n\n // 6.5) “Prueba vaga” / apelación a cultura como evidencia\n if (/\\b(la cultura lo prueba|es obvio|todo mundo sabe|se sabe|está claro)\\b/.test(t)) {\n flags.push(\"weak_evidence\");\n score += 0.20;\n }\n\n // 6.6) Totalización + agente oculto (“ellos”)\n if (/\\b(ellos|la élite|los de arriba)\\b/.test(t) && /\\b(esconden|ocultan|tapan)\\b/.test(t)) {\n flags.push(\"hidden_actor\");\n score += 0.15;\n }\n\n // ------------------------------------------------------------\n // NUEVO: Entropía por pseudo-ciencia / pensamiento mágico / relativismo\n // ------------------------------------------------------------\n\n // 7) Pseudo-ciencia \"cuántica\" usada como licencia mágica\n if (/\\b(física cuántica|cuantica|cuántico|quantum)\\b/.test(t)) {\n flags.push(\"pseudo_science_quantum\");\n score += 0.20;\n }\n\n // 8) Manifestación mágica / decretos / vibración / energía como causalidad\n const manifestHits =\n countRegex(t, /\\b(manifestar|manifestación|decretar|decreto|vibración|vibracion|frecuencia|energía|energia|ley de la atracción|universo me lo dará)\\b/g) +\n countRegex(t, /\\b(realine(a|ar)\\b.*\\bátom|\\bátom|\\batomos\\b)/g);\n\n if (manifestHits > 0) {\n flags.push(\"magic_manifesting\");\n score += Math.min(0.35, 0.15 + manifestHits * 0.06);\n }\n\n // 9) Relativismo de la verdad / negación explícita de verdad objetiva\n if (/\\b(no hay una verdad objetiva|no existe la verdad objetiva|tu verdad|mi verdad|la verdad es relativa|lo que importa es lo que sientes)\\b/.test(t)) {\n flags.push(\"truth_relativism\");\n score += 0.35;\n }\n\n // 10) Causalidad rota / obligación metafísica (\"debe\" porque lo deseo)\n if (/\\b(deben|debe)\\b.*\\b(obligatoriamente|por lo tanto|por ende)\\b/.test(t) || /\\b(la materia)\\b.*\\b(se subordina|obedece)\\b/.test(t)) {\n flags.push(\"broken_causality\");\n score += 0.20;\n }\n\n // Normaliza: score final 0..1\n score = clamp01(score);\n\n return { score, flags };\n}\n","// src/intention.ts\nimport type { IntentionEvaluation, IntentionType } from \"./types\";\n\nfunction clamp01(x: number) {\n return Math.max(0, Math.min(1, x));\n}\n\nexport function evaluateIntention(text: string): IntentionEvaluation {\n const raw = text || \"\";\n const t = raw.toLowerCase();\n\n // Heurísticas rápidas (MVP)\n const isHelp =\n /\\b(ayuda|ayúdame|explica|resume|resumir|cómo|como|puedes|podrías|por favor)\\b/.test(\n t\n );\n\n const isSpam =\n /\\b(compra|oferta|promo|descuento|gratis|haz clic|click|off|90%|% off)\\b/.test(\n t\n ) || (raw.match(/\\$+/g) || []).length > 0;\n\n const isManip =\n /\\b(si de verdad|si me quisieras|es tu culpa|no tienes opción|me debes)\\b/.test(\n t\n );\n\n const isConsp =\n /\\b(simulación|todos lo saben|lo esconden|verdad oculta|ellos no quieren)\\b/.test(\n t\n );\n\n // NUEVO: pseudo-ciencia / pensamiento mágico / relativismo (desinformación)\n const isMisinformation =\n /\\b(física cuántica|cuantica|cuántica|cuántico|quantum)\\b/.test(t) ||\n /\\b(manifestar|manifestación|decretar|decreto|vibración|vibracion|frecuencia|energía|energia|ley de la atracción)\\b/.test(t) ||\n /\\b(no hay una verdad objetiva|no existe la verdad objetiva|tu verdad|mi verdad|la verdad es relativa)\\b/.test(t) ||\n /\\b(la materia)\\b.*\\b(se subordina|obedece)\\b/.test(t);\n\n let intention: IntentionType = \"unknown\";\n let confidence = 0.0;\n let rationale = \"\";\n\n if (isSpam) {\n intention = \"marketing_spam\";\n confidence = 0.85;\n rationale = \"Detecté señales de venta agresiva/urgencia/dinero.\";\n } else if (isManip) {\n intention = \"manipulation\";\n confidence = 0.85;\n rationale = \"Detecté coerción/culpa/chantaje emocional.\";\n } else if (isConsp) {\n intention = \"conspiracy\";\n confidence = 0.75;\n rationale = \"Detecté marco conspirativo vago ('lo esconden', 'todos lo saben').\";\n } else if (isMisinformation) {\n intention = \"misinformation\";\n confidence = 0.85;\n rationale =\n \"Detecté patrón de pseudo-ciencia/pensamiento mágico/relativismo de la verdad (alta probabilidad de desinformación).\";\n } else if (isHelp) {\n intention = \"request_help\";\n confidence = 0.7;\n rationale = \"Parece una petición legítima de ayuda/explicación.\";\n }\n\n return { intention, confidence: clamp01(confidence), rationale };\n}\n","// src/wrapper.ts\nimport { analyzeEntropy } from \"./entropy\";\nimport { evaluateIntention } from \"./intention\";\nimport type { FilterResult, IntentionEvaluation } from \"./types\";\n\nexport function runEntropyFilter(text: string): FilterResult {\n const entropy_analysis = analyzeEntropy(text);\n let intention_evaluation: IntentionEvaluation = evaluateIntention(text);\n\n // Corrección: si el texto trae señales fuertes de entropía epistemológica,\n // no lo clasifiques como \"request_help\" solo por ser pregunta larga.\n const hardFlags = new Set(entropy_analysis.flags);\n const epistemicEntropy =\n hardFlags.has(\"truth_relativism\") ||\n hardFlags.has(\"magic_manifesting\") ||\n hardFlags.has(\"pseudo_science_quantum\") ||\n hardFlags.has(\"broken_causality\");\n\n if (epistemicEntropy) {\n intention_evaluation = {\n intention: \"misinformation\",\n confidence: Math.max(intention_evaluation.confidence ?? 0.7, 0.8),\n rationale:\n \"Detecté patrón de pseudo-ciencia/pensamiento mágico/relativismo de la verdad; alta probabilidad de desinformación o argumento sin anclaje causal.\"\n };\n }\n\n return { entropy_analysis, intention_evaluation };\n}\n","// src/middleware.ts\nimport { runEntropyFilter } from \"./wrapper\";\nimport type { FilterResult } from \"./types\";\n\n/**\n * Middleware agnóstico (NO depende de express types).\n * Compatible con Express / Next API routes / cualquier framework estilo req-res-next.\n *\n * Espera: req.body.text (string)\n * Escribe: req.entropy = FilterResult\n */\nexport function entropyMiddleware(req: any, _res: any, next: any) {\n const text = req?.body?.text;\n\n const result: FilterResult =\n typeof text === \"string\" ? runEntropyFilter(text) : runEntropyFilter(\"\");\n\n req.entropy = result;\n next?.();\n}\n\n/**\n * Tipo auxiliar opcional (sin forzar dependencias).\n * Útil si quieres tipar tu req en tu app.\n */\nexport type EntropyAugmentedRequest = {\n body?: { text?: unknown };\n entropy?: FilterResult;\n};\n","// src/gate.ts\nimport { runEntropyFilter } from \"./wrapper\";\n\nexport type GateAction = \"ALLOW\" | \"WARN\" | \"BLOCK\";\n\nexport type GateResult = {\n action: GateAction;\n entropy_score: number;\n flags: string[];\n intention: string;\n confidence: number;\n rationale: string;\n};\n\nexport type GateConfig = {\n warnThreshold?: number; // default: 0.25\n blockThreshold?: number; // default: 0.60\n};\n\n/** Clamp 0..1 */\nfunction clamp01(x: number) {\n return Math.max(0, Math.min(1, x));\n}\n\n/** Add unique flags preserving order */\nfunction mergeFlags(base: string[], extra: string[]) {\n const set = new Set(base);\n const out = [...base];\n for (const f of extra) {\n if (!set.has(f)) {\n set.add(f);\n out.push(f);\n }\n }\n return out;\n}\n\n/**\n * Detección de spam/phishing en inglés por keywords.\n * Se implementa como “booster” de score + flags (sin tocar entropy.ts).\n */\nfunction englishSpamBooster(rawText: string): {\n addScore: number;\n addFlags: string[];\n hitCount: number;\n} {\n const t = (rawText || \"\").toLowerCase();\n\n // Patrones típicos (spam / phishing / promos agresivas)\n const patterns: Array<{ re: RegExp; score: number; flag: string }> = [\n { re: /\\bfree\\b/g, score: 0.08, flag: \"spam_kw_free\" },\n { re: /\\bwinner\\b|\\bwon\\b|\\bcongratulations\\b/g, score: 0.10, flag: \"spam_kw_winner\" },\n { re: /\\bclaim\\b|\\bredeem\\b/g, score: 0.08, flag: \"spam_kw_claim\" },\n { re: /\\bclick\\b|\\bclick here\\b|\\bopen link\\b|\\btap here\\b/g, score: 0.10, flag: \"spam_kw_click\" },\n { re: /\\blimited time\\b|\\bact now\\b|\\bfinal notice\\b|\\bbefore midnight\\b/g, score: 0.10, flag: \"spam_kw_urgency_en\" },\n { re: /\\bverify\\b|\\bconfirm\\b|\\baccount\\b.*\\b(suspended|locked)\\b/g, score: 0.12, flag: \"spam_kw_verify\" },\n { re: /\\bprize\\b|\\bgift card\\b|\\bgiftcard\\b|\\bvoucher\\b|\\biphone\\b|\\bsurvey\\b/g, score: 0.10, flag: \"spam_kw_prize\" },\n { re: /\\bloan\\b|\\bpre-?approved\\b|\\bno credit check\\b/g, score: 0.12, flag: \"spam_kw_loan\" },\n { re: /\\bcrypto\\b|\\bairdrop\\b|\\bwallet\\b|\\bseed phrase\\b/g, score: 0.10, flag: \"spam_kw_crypto\" },\n { re: /\\bdelivery failed\\b|\\breschedule\\b|\\bpackage\\b|\\bcourier\\b/g, score: 0.10, flag: \"spam_kw_delivery\" },\n { re: /\\btax refund\\b|\\bunpaid\\b|\\brefund\\b|\\bchargeback\\b/g, score: 0.10, flag: \"spam_kw_refund\" },\n ];\n\n let addScore = 0;\n const addFlags: string[] = [];\n let hitCount = 0;\n\n for (const p of patterns) {\n const m = t.match(p.re);\n if (m && m.length > 0) {\n hitCount += m.length;\n addScore += p.score; // suma “por patrón”, no por ocurrencia (controlado)\n addFlags.push(p.flag);\n }\n }\n\n // cap para no sobre-castigar\n addScore = Math.min(0.35, addScore);\n\n if (hitCount > 0) addFlags.push(\"spam_keywords_en\");\n\n return { addScore, addFlags, hitCount };\n}\n\n/**\n * Gate principal (producto): deterministic + barato.\n */\nexport function gateLLM(text: string, config: GateConfig = {}): GateResult {\n const warnT = config.warnThreshold ?? 0.25;\n const blockT = config.blockThreshold ?? 0.6;\n\n const r = runEntropyFilter(text);\n\n // base\n let score = r.entropy_analysis.score;\n let flags = [...r.entropy_analysis.flags];\n\n // booster EN\n const booster = englishSpamBooster(text);\n if (booster.hitCount > 0) {\n score = clamp01(score + booster.addScore);\n flags = mergeFlags(flags, booster.addFlags);\n }\n\n // intención base (intention.ts)\n let intention = r.intention_evaluation.intention || \"unknown\";\n let confidence = r.intention_evaluation.confidence ?? 0;\n let rationale = r.intention_evaluation.rationale ?? \"\";\n\n // si pegó booster EN y quedó unknown → marketing_spam\n if (booster.hitCount > 0 && intention === \"unknown\") {\n intention = \"marketing_spam\";\n confidence = Math.max(confidence, 0.75);\n rationale = (rationale ? rationale + \" \" : \"\") + \"Detecté keywords típicas de spam/phishing en inglés.\";\n }\n\n // reglas fuertes (para BLOCK “vendible”)\n const hasSpam =\n flags.includes(\"spam_sales\") ||\n flags.includes(\"spam_keywords_en\");\n\n const hasMoneySignals =\n flags.includes(\"money_signal\") ||\n flags.includes(\"spam_kw_prize\") ||\n flags.includes(\"spam_kw_loan\");\n\n const strongSpam = hasSpam && hasMoneySignals;\n\n let action: GateAction = \"ALLOW\";\n if (score > blockT || strongSpam) action = \"BLOCK\";\n else if (score >= warnT) action = \"WARN\";\n\n return {\n action,\n entropy_score: score,\n flags,\n intention,\n confidence: clamp01(confidence),\n rationale,\n };\n}\n\n// Alias “bonito” para tu server: gate(text)\nexport const gate = gateLLM;\n"],"mappings":"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","names":["clamp01","clamp01"]}
|
|
1
|
+
{"version":3,"sources":["../src/entropy.ts","../src/intention.ts","../src/wrapper.ts","../src/middleware.ts","../src/gate.ts"],"sourcesContent":["// src/entropy.ts\r\nexport type EntropyResult = {\r\n score: number; // 0..1\r\n flags: string[];\r\n};\r\n\r\nfunction clamp01(x: number) {\r\n return Math.max(0, Math.min(1, x));\r\n}\r\n\r\nfunction countRegex(text: string, re: RegExp) {\r\n const m = text.match(re);\r\n return m ? m.length : 0;\r\n}\r\n\r\n/**\r\n * Entropy = señales lingüísticas + lógicas (determinístico, barato).\r\n * Nota: aquí SOLO generamos banderas y score. La acción (ALLOW/WARN/BLOCK)\r\n * se decide en gate.ts según thresholds/policy del ruleset.\r\n */\r\nexport function analyzeEntropy(text: string): EntropyResult {\r\n const raw = text || \"\";\r\n const t = raw.toLowerCase();\r\n const flags: string[] = [];\r\n let score = 0;\r\n\r\n // ------------------------------------------------------------\r\n // 1) Urgencia / presión temporal\r\n // ------------------------------------------------------------\r\n if (\r\n /\\b(ahora|ya|urgente|urgencia|hoy|inmediato|inmediatamente|últim[oa]s?|solo\\s+hoy|ap[uú]rate|rápido|de\\s+inmediato)\\b/.test(\r\n t\r\n )\r\n ) {\r\n flags.push(\"urgency\");\r\n score += 0.2;\r\n }\r\n\r\n // ------------------------------------------------------------\r\n // 2) Spam / venta agresiva (ES/EN básico)\r\n // ------------------------------------------------------------\r\n if (\r\n /\\b(compra|oferta|promo|promoci[oó]n|descuento|rebaja|gratis|free|premio|prize|winner|gana|claim|reward|clic|click)\\b/.test(\r\n t\r\n )\r\n ) {\r\n flags.push(\"spam_sales\");\r\n score += 0.25;\r\n }\r\n\r\n // ------------------------------------------------------------\r\n // 3) Phishing / Fraude / Scam (señales de alto riesgo)\r\n // ------------------------------------------------------------\r\n\r\n // 3A) Phishing: pedir código/OTP/token de verificación (ES/EN)\r\n // Ej: \"Envíame tu código de verificación para confirmar tu cuenta.\"\r\n const wantsSendVerb =\r\n /\\b(envi(a|á)me|env[ií]ame|m(a|á)ndame|p(a|á)same|dame|compart(e|a|as)|reenv[ií]a(me)?)\\b/.test(\r\n t\r\n );\r\n\r\n const mentionsCode =\r\n /\\b(c[oó]digo|codigo|otp|2fa|token|pin|clave)\\b/.test(t);\r\n\r\n const mentionsVerify =\r\n /\\b(verificaci[oó]n|verificar|confirmar|validar)\\b/.test(t);\r\n\r\n const mentionsAccount = /\\b(cuenta|account)\\b/.test(t);\r\n\r\n const mentionsSms = /\\b(sms|por\\s+sms)\\b/.test(t);\r\n\r\n // Regla: verbo de “enviar/pasar” + (código/otp/token/pin) + (verificación/cuenta/sms)\r\n if (wantsSendVerb && mentionsCode && (mentionsVerify || mentionsAccount || mentionsSms)) {\r\n flags.push(\"phishing_2fa_code\");\r\n score += 0.55; // fuerte: debe quedar mínimo en WARN con casi cualquier preset\r\n }\r\n\r\n // 3B) Phishing EN: “verify account” + “click” + amenaza de cierre\r\n if (\r\n /\\bverify\\b/.test(t) &&\r\n /\\baccount\\b/.test(t) &&\r\n /\\bclick\\b/.test(t) &&\r\n /\\b(closed|close|suspend|suspended|disable|disabled|locked)\\b/.test(t)\r\n ) {\r\n flags.push(\"phishing_verify_threat_en\");\r\n score += 0.35;\r\n }\r\n\r\n // 3C) Fraude: “te deposito / transfiero” + “tarjeta/cuenta/clabe”\r\n if (\r\n /\\b(te\\s+deposito|te\\s+dep[oó]sito|te\\s+transfiero|te\\s+transferir[eé]|transferencia|dep[oó]sito)\\b/.test(\r\n t\r\n ) &&\r\n /\\b(tarjeta|cuenta|clabe|iban|swift|n[uú]mero\\s+de\\s+tarjeta|numero\\s+de\\s+tarjeta)\\b/.test(t)\r\n ) {\r\n flags.push(\"fraud_payment_request\");\r\n score += 0.35;\r\n }\r\n\r\n // 3D) Scam: “gana dinero desde casa / sin esfuerzo”\r\n if (\r\n /\\b(gana(r)?\\s+dinero|ingresos|dinero\\s+extra)\\b/.test(t) &&\r\n /\\b(desde\\s+casa|en\\s+casa|home)\\b/.test(t) &&\r\n /\\b(sin\\s+esfuerzo|f[aá]cil|r[aá]pido|easy|fast)\\b/.test(t)\r\n ) {\r\n flags.push(\"scam_wfh\");\r\n score += 0.3;\r\n }\r\n\r\n // ------------------------------------------------------------\r\n // 4) Señales de dinero ($$$, %, monedas)\r\n // ------------------------------------------------------------\r\n const moneyHits = countRegex(raw, /\\$+/g);\r\n const pctHits = countRegex(raw, /%/g);\r\n if (moneyHits > 0 || pctHits > 0 || /\\b(usd|mxn|eur)\\b/i.test(raw)) {\r\n flags.push(\"money_signal\");\r\n score += Math.min(0.25, moneyHits * 0.05 + pctHits * 0.05 + 0.1);\r\n }\r\n\r\n // ------------------------------------------------------------\r\n // 5) Exceso de signos / gritos (señal de baja calidad o manipulación)\r\n // ------------------------------------------------------------\r\n const exclam = countRegex(raw, /!/g);\r\n const capsRatio = (() => {\r\n const letters = raw.match(/[A-Za-zÁÉÍÓÚÜÑáéíóúüñ]/g) || [];\r\n if (letters.length === 0) return 0;\r\n const caps = (raw.match(/[A-ZÁÉÍÓÚÜÑ]/g) || []).length;\r\n return caps / letters.length;\r\n })();\r\n\r\n if (exclam >= 3 || capsRatio >= 0.35) {\r\n flags.push(\"shouting\");\r\n score += 0.2;\r\n }\r\n\r\n // ------------------------------------------------------------\r\n // 6) Coerción / culpa / chantaje emocional\r\n // ------------------------------------------------------------\r\n if (\r\n /\\b(si\\s+de\\s+verdad|si\\s+me\\s+quisieras|es\\s+tu\\s+culpa|no\\s+tienes\\s+opci[oó]n|me\\s+debes|hazlo\\s+o\\s+si\\s+no|si\\s+no\\s+lo\\s+haces)\\b/.test(\r\n t\r\n )\r\n ) {\r\n flags.push(\"emotional_manipulation\");\r\n score += 0.35;\r\n }\r\n\r\n // ------------------------------------------------------------\r\n // 7) Conspiración vaga / evidencia débil (baja confiabilidad)\r\n // ------------------------------------------------------------\r\n if (\r\n /\\b(todos\\s+lo\\s+saben|lo\\s+esconden|la\\s+verdad\\s+oculta|ellos\\s+no\\s+quieren|simulaci[oó]n)\\b/.test(\r\n t\r\n )\r\n ) {\r\n flags.push(\"conspiracy_vague\");\r\n score += 0.2;\r\n }\r\n\r\n if (\r\n /\\b(es\\s+obvio|todo\\s+mundo\\s+sabe|se\\s+sabe|est[aá]\\s+claro|la\\s+cultura\\s+lo\\s+prueba)\\b/.test(\r\n t\r\n )\r\n ) {\r\n flags.push(\"weak_evidence\");\r\n score += 0.2;\r\n }\r\n\r\n score = clamp01(score);\r\n return { score, flags };\r\n}\r\n","// src/intention.ts\r\nimport type { IntentionEvaluation, IntentionType } from \"./types\";\r\n\r\nfunction clamp01(x: number) {\r\n return Math.max(0, Math.min(1, x));\r\n}\r\n\r\nexport function evaluateIntention(text: string): IntentionEvaluation {\r\n const raw = text || \"\";\r\n const t = raw.toLowerCase();\r\n\r\n // Heurísticas rápidas (MVP)\r\n const isHelp =\r\n /\\b(ayuda|ayúdame|explica|resume|resumir|cómo|como|puedes|podrías|por favor)\\b/.test(\r\n t\r\n );\r\n\r\n const isSpam =\r\n /\\b(compra|oferta|promo|descuento|gratis|haz clic|click|off|90%|% off)\\b/.test(\r\n t\r\n ) || (raw.match(/\\$+/g) || []).length > 0;\r\n\r\n const isManip =\r\n /\\b(si de verdad|si me quisieras|es tu culpa|no tienes opción|me debes)\\b/.test(\r\n t\r\n );\r\n\r\n const isConsp =\r\n /\\b(simulación|todos lo saben|lo esconden|verdad oculta|ellos no quieren)\\b/.test(\r\n t\r\n );\r\n\r\n // NUEVO: pseudo-ciencia / pensamiento mágico / relativismo (desinformación)\r\n const isMisinformation =\r\n /\\b(física cuántica|cuantica|cuántica|cuántico|quantum)\\b/.test(t) ||\r\n /\\b(manifestar|manifestación|decretar|decreto|vibración|vibracion|frecuencia|energía|energia|ley de la atracción)\\b/.test(t) ||\r\n /\\b(no hay una verdad objetiva|no existe la verdad objetiva|tu verdad|mi verdad|la verdad es relativa)\\b/.test(t) ||\r\n /\\b(la materia)\\b.*\\b(se subordina|obedece)\\b/.test(t);\r\n\r\n let intention: IntentionType = \"unknown\";\r\n let confidence = 0.0;\r\n let rationale = \"\";\r\n\r\n if (isSpam) {\r\n intention = \"marketing_spam\";\r\n confidence = 0.85;\r\n rationale = \"Detecté señales de venta agresiva/urgencia/dinero.\";\r\n } else if (isManip) {\r\n intention = \"manipulation\";\r\n confidence = 0.85;\r\n rationale = \"Detecté coerción/culpa/chantaje emocional.\";\r\n } else if (isConsp) {\r\n intention = \"conspiracy\";\r\n confidence = 0.75;\r\n rationale = \"Detecté marco conspirativo vago ('lo esconden', 'todos lo saben').\";\r\n } else if (isMisinformation) {\r\n intention = \"misinformation\";\r\n confidence = 0.85;\r\n rationale =\r\n \"Detecté patrón de pseudo-ciencia/pensamiento mágico/relativismo de la verdad (alta probabilidad de desinformación).\";\r\n } else if (isHelp) {\r\n intention = \"request_help\";\r\n confidence = 0.7;\r\n rationale = \"Parece una petición legítima de ayuda/explicación.\";\r\n }\r\n\r\n return { intention, confidence: clamp01(confidence), rationale };\r\n}\r\n","// src/wrapper.ts\r\nimport { analyzeEntropy } from \"./entropy\";\r\nimport { evaluateIntention } from \"./intention\";\r\nimport type { FilterResult, IntentionEvaluation } from \"./types\";\r\n\r\nexport function runEntropyFilter(text: string): FilterResult {\r\n const entropy_analysis = analyzeEntropy(text);\r\n let intention_evaluation: IntentionEvaluation = evaluateIntention(text);\r\n\r\n // Corrección: si el texto trae señales fuertes de entropía epistemológica,\r\n // no lo clasifiques como \"request_help\" solo por ser pregunta larga.\r\n const hardFlags = new Set(entropy_analysis.flags);\r\n const epistemicEntropy =\r\n hardFlags.has(\"truth_relativism\") ||\r\n hardFlags.has(\"magic_manifesting\") ||\r\n hardFlags.has(\"pseudo_science_quantum\") ||\r\n hardFlags.has(\"broken_causality\");\r\n\r\n if (epistemicEntropy) {\r\n intention_evaluation = {\r\n intention: \"misinformation\",\r\n confidence: Math.max(intention_evaluation.confidence ?? 0.7, 0.8),\r\n rationale:\r\n \"Detecté patrón de pseudo-ciencia/pensamiento mágico/relativismo de la verdad; alta probabilidad de desinformación o argumento sin anclaje causal.\"\r\n };\r\n }\r\n\r\n return { entropy_analysis, intention_evaluation };\r\n}\r\n","// src/middleware.ts\r\nimport { runEntropyFilter } from \"./wrapper\";\r\nimport type { FilterResult } from \"./types\";\r\n\r\n/**\r\n * Middleware agnóstico (NO depende de express types).\r\n * Compatible con Express / Next API routes / cualquier framework estilo req-res-next.\r\n *\r\n * Espera: req.body.text (string)\r\n * Escribe: req.entropy = FilterResult\r\n */\r\nexport function entropyMiddleware(req: any, _res: any, next: any) {\r\n const text = req?.body?.text;\r\n\r\n const result: FilterResult =\r\n typeof text === \"string\" ? runEntropyFilter(text) : runEntropyFilter(\"\");\r\n\r\n req.entropy = result;\r\n next?.();\r\n}\r\n\r\n/**\r\n * Tipo auxiliar opcional (sin forzar dependencias).\r\n * Útil si quieres tipar tu req en tu app.\r\n */\r\nexport type EntropyAugmentedRequest = {\r\n body?: { text?: unknown };\r\n entropy?: FilterResult;\r\n};\r\n","// src/gate.ts\r\nimport { runEntropyFilter } from \"./wrapper\";\r\n\r\nexport type GateAction = \"ALLOW\" | \"WARN\" | \"BLOCK\";\r\n\r\nexport type GateResult = {\r\n action: GateAction;\r\n entropy_score: number;\r\n flags: string[];\r\n intention: string;\r\n confidence: number;\r\n rationale: string;\r\n};\r\n\r\nexport type RulesetThresholds = {\r\n warn?: number;\r\n block?: number;\r\n};\r\n\r\nexport type RulesetPolicy = {\r\n /** if true, allow strongSpam to force BLOCK */\r\n strong_spam_block?: boolean;\r\n\r\n /** optional overrides */\r\n block_intentions?: string[];\r\n warn_intentions?: string[];\r\n\r\n /** flag overrides */\r\n block_flags?: string[];\r\n warn_flags?: string[];\r\n};\r\n\r\nexport type RulesetConfig = {\r\n name?: string;\r\n version?: number;\r\n description?: string;\r\n thresholds?: RulesetThresholds;\r\n policy?: RulesetPolicy;\r\n // normalization is applied in wrapper/ruleset loader; kept here for completeness\r\n normalization?: Record<string, unknown>;\r\n};\r\n\r\nexport type GateConfig = {\r\n /** Per-call threshold overrides (fallback if ruleset.thresholds not provided) */\r\n warnThreshold?: number; // default: 0.25\r\n blockThreshold?: number; // default: 0.60\r\n\r\n /** Optional ruleset (default/strict/public-api) */\r\n ruleset?: RulesetConfig;\r\n};\r\n\r\n/** Clamp 0..1 */\r\nfunction clamp01(x: number) {\r\n return Math.max(0, Math.min(1, x));\r\n}\r\n\r\n/** Add unique flags preserving order */\r\nfunction mergeFlags(base: string[], extra: string[]) {\r\n const set = new Set(base);\r\n const out = [...base];\r\n for (const f of extra) {\r\n if (!set.has(f)) {\r\n set.add(f);\r\n out.push(f);\r\n }\r\n }\r\n return out;\r\n}\r\n\r\n/**\r\n * Very small EN spam booster (keyword-ish).\r\n * Keep it deterministic and cheap. Tune in src/entropy.ts for real weights/patterns.\r\n */\r\nfunction englishSpamBooster(text: string): {\r\n hitCount: number;\r\n addScore: number;\r\n addFlags: string[];\r\n} {\r\n const t = (text || \"\").toLowerCase();\r\n const hits: string[] = [];\r\n\r\n const kw = [\r\n [\"free\", \"spam_kw_free\"],\r\n [\"winner\", \"spam_kw_winner\"],\r\n [\"claim\", \"spam_kw_claim\"],\r\n [\"click\", \"spam_kw_click\"],\r\n [\"verify\", \"spam_kw_verify\"],\r\n [\"prize\", \"spam_kw_prize\"],\r\n [\"urgent\", \"spam_kw_urgency_en\"],\r\n [\"limited time\", \"spam_kw_urgency_en\"],\r\n [\"today only\", \"spam_kw_urgency_en\"],\r\n ] as const;\r\n\r\n for (const [s, flag] of kw) {\r\n if (t.includes(s)) hits.push(flag);\r\n }\r\n\r\n if (hits.length === 0) return { hitCount: 0, addScore: 0, addFlags: [] };\r\n\r\n // conservative bump: 0.05 per hit, capped\r\n const addScore = Math.min(0.25, hits.length * 0.05);\r\n const addFlags = mergeFlags([\"spam_keywords_en\"], hits);\r\n return { hitCount: hits.length, addScore, addFlags };\r\n}\r\n\r\nfunction decideAction(params: {\r\n score: number;\r\n warnT: number;\r\n blockT: number;\r\n strongSpam: boolean;\r\n intention?: string;\r\n flags: string[];\r\n policy?: RulesetPolicy;\r\n}): GateAction {\r\n const {\r\n score,\r\n warnT,\r\n blockT,\r\n strongSpam,\r\n intention = \"\",\r\n flags,\r\n policy = {},\r\n } = params;\r\n\r\n const blockIntentions = new Set(policy.block_intentions ?? []);\r\n const warnIntentions = new Set(policy.warn_intentions ?? []);\r\n const blockFlags = new Set(policy.block_flags ?? []);\r\n const warnFlags = new Set(policy.warn_flags ?? []);\r\n\r\n // 1) explicit overrides (if provided)\r\n if (blockIntentions.has(intention)) return \"BLOCK\";\r\n if (flags.some((f) => blockFlags.has(f))) return \"BLOCK\";\r\n\r\n if (warnIntentions.has(intention)) return \"WARN\";\r\n if (flags.some((f) => warnFlags.has(f))) return \"WARN\";\r\n\r\n // 2) strong spam override (configurable)\r\n const strongSpamBlock = policy.strong_spam_block ?? true;\r\n if (strongSpamBlock && strongSpam) return \"BLOCK\";\r\n\r\n // 3) thresholds\r\n if (score >= blockT) return \"BLOCK\";\r\n if (score >= warnT) return \"WARN\";\r\n return \"ALLOW\";\r\n}\r\n\r\n/**\r\n * Gate principal (producto): deterministic + barato.\r\n */\r\nexport function gateLLM(text: string, config: GateConfig = {}): GateResult {\r\n const ruleset = config.ruleset;\r\n\r\n // thresholds: ruleset > config > defaults\r\n const warnT = ruleset?.thresholds?.warn ?? config.warnThreshold ?? 0.25;\r\n const blockT = ruleset?.thresholds?.block ?? config.blockThreshold ?? 0.6;\r\n\r\n const r = runEntropyFilter(text);\r\n\r\n // base\r\n let score = r.entropy_analysis.score;\r\n let flags = [...r.entropy_analysis.flags];\r\n\r\n // booster EN\r\n const booster = englishSpamBooster(text);\r\n if (booster.hitCount > 0) {\r\n score = clamp01(score + booster.addScore);\r\n flags = mergeFlags(flags, booster.addFlags);\r\n }\r\n\r\n // intención base (intention.ts)\r\n const intention = r.intention_evaluation.intention || \"unknown\";\r\n const confidence = r.intention_evaluation.confidence ?? 0;\r\n const rationale = r.intention_evaluation.rationale ?? \"\";\r\n\r\n // heuristic: \"strong spam\" only when BOTH spam_sales + money_signal are present\r\n // (tune this in entropy.ts; here we only consume flags)\r\n const hasSpam = flags.includes(\"spam_sales\");\r\n const hasMoneySignals =\r\n flags.includes(\"money_signal\") || flags.includes(\"money_signal_high\");\r\n const strongSpam = hasSpam && hasMoneySignals;\r\n\r\n const policy: RulesetPolicy = ruleset?.policy ?? {};\r\n\r\n const action: GateAction = decideAction({\r\n score,\r\n warnT,\r\n blockT,\r\n strongSpam,\r\n intention,\r\n flags,\r\n policy,\r\n });\r\n\r\n return {\r\n action,\r\n entropy_score: score,\r\n flags,\r\n intention,\r\n confidence: clamp01(confidence),\r\n rationale,\r\n };\r\n}\r\n\r\n// Alias “bonito” para tu server: gate(text)\r\nexport const gate = gateLLM;\r\n"],"mappings":"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","names":["clamp01","clamp01"]}
|
package/integrations/express.mjs
CHANGED
|
@@ -1,117 +1,117 @@
|
|
|
1
|
-
// integrations/express.mjs
|
|
2
|
-
import { gate } from "llm-entropy-filter";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Create an Express middleware that runs `gate()` before LLM calls.
|
|
6
|
-
*
|
|
7
|
-
* Design goals:
|
|
8
|
-
* - Zero behavior changes to core `gate()`
|
|
9
|
-
* - Drop-in for public chat endpoints
|
|
10
|
-
* - Deterministic: no external calls
|
|
11
|
-
*
|
|
12
|
-
* @param {object} [opts]
|
|
13
|
-
* @param {string} [opts.bodyField="text"] - Field name in req.body that contains user text.
|
|
14
|
-
* @param {string} [opts.queryField] - Optional query param fallback (e.g., ?text=...).
|
|
15
|
-
* @param {boolean} [opts.attachResult=true] - Attach result to req.entropyGate.
|
|
16
|
-
* @param {boolean} [opts.blockOn="BLOCK"] - Block when action matches this string ("BLOCK") or array of actions.
|
|
17
|
-
* @param {number} [opts.blockStatus=400] - HTTP status when blocked.
|
|
18
|
-
* @param {object|function} [opts.blockResponse] - Custom JSON response or function(req, res, result) => any
|
|
19
|
-
* @param {boolean} [opts.warnHeader=true] - If WARN, add response headers with gate metadata.
|
|
20
|
-
* @param {boolean} [opts.alwaysHeader=false] - If true, add headers for all actions.
|
|
21
|
-
* @param {function} [opts.onResult] - Hook: (req, result) => void
|
|
22
|
-
* @param {function} [opts.getText] - Hook: (req) => string (overrides bodyField/queryField)
|
|
23
|
-
*/
|
|
24
|
-
export function entropyGateMiddleware(opts = {}) {
|
|
25
|
-
const {
|
|
26
|
-
bodyField = "text",
|
|
27
|
-
queryField,
|
|
28
|
-
attachResult = true,
|
|
29
|
-
blockOn = "BLOCK",
|
|
30
|
-
blockStatus = 400,
|
|
31
|
-
blockResponse,
|
|
32
|
-
warnHeader = true,
|
|
33
|
-
alwaysHeader = false,
|
|
34
|
-
onResult,
|
|
35
|
-
getText,
|
|
36
|
-
} = opts;
|
|
37
|
-
|
|
38
|
-
const blockSet = Array.isArray(blockOn) ? new Set(blockOn) : new Set([blockOn]);
|
|
39
|
-
|
|
40
|
-
return function entropyGate(req, res, next) {
|
|
41
|
-
try {
|
|
42
|
-
// 1) Extract text
|
|
43
|
-
let text = "";
|
|
44
|
-
if (typeof getText === "function") {
|
|
45
|
-
text = String(getText(req) ?? "");
|
|
46
|
-
} else {
|
|
47
|
-
const bodyVal = req?.body?.[bodyField];
|
|
48
|
-
const queryVal = queryField ? req?.query?.[queryField] : undefined;
|
|
49
|
-
text = String(bodyVal ?? queryVal ?? "");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// 2) Run deterministic gate
|
|
53
|
-
const result = gate(text);
|
|
54
|
-
|
|
55
|
-
// 3) Attach result for downstream routing/logging
|
|
56
|
-
if (attachResult) {
|
|
57
|
-
// Convention: req.entropyGate
|
|
58
|
-
req.entropyGate = result;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Optional hook
|
|
62
|
-
if (typeof onResult === "function") {
|
|
63
|
-
onResult(req, result);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// 4) Telemetry headers (optional)
|
|
67
|
-
const shouldHeader = alwaysHeader || (warnHeader && result?.action === "WARN");
|
|
68
|
-
if (shouldHeader) {
|
|
69
|
-
// Keep headers small and stable
|
|
70
|
-
res.setHeader("x-entropy-action", String(result?.action ?? ""));
|
|
71
|
-
res.setHeader("x-entropy-score", String(result?.entropy_score ?? ""));
|
|
72
|
-
res.setHeader("x-entropy-intention", String(result?.intention ?? ""));
|
|
73
|
-
// Flags can be large; keep compact
|
|
74
|
-
if (Array.isArray(result?.flags)) {
|
|
75
|
-
res.setHeader("x-entropy-flags", result.flags.slice(0, 10).join(","));
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// 5) Block if configured
|
|
80
|
-
if (blockSet.has(result?.action)) {
|
|
81
|
-
res.status(blockStatus);
|
|
82
|
-
|
|
83
|
-
if (typeof blockResponse === "function") {
|
|
84
|
-
return res.json(blockResponse(req, res, result));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (blockResponse && typeof blockResponse === "object") {
|
|
88
|
-
return res.json(blockResponse);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Default response: transparent + actionable
|
|
92
|
-
return res.json({
|
|
93
|
-
ok: false,
|
|
94
|
-
blocked: true,
|
|
95
|
-
gate: result,
|
|
96
|
-
message:
|
|
97
|
-
"Request blocked by llm-entropy-filter (high-entropy / low-signal input).",
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Otherwise proceed
|
|
102
|
-
return next();
|
|
103
|
-
} catch (err) {
|
|
104
|
-
// Fail-open by default: do not block requests if the gate errors.
|
|
105
|
-
// You can change this behavior by wrapping with your own error handler.
|
|
106
|
-
return next(err);
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Small helper for routing:
|
|
113
|
-
* If you prefer to run gate manually inside route handlers.
|
|
114
|
-
*/
|
|
115
|
-
export function runEntropyGate(text) {
|
|
116
|
-
return gate(String(text ?? ""));
|
|
117
|
-
}
|
|
1
|
+
// integrations/express.mjs
|
|
2
|
+
import { gate } from "llm-entropy-filter";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create an Express middleware that runs `gate()` before LLM calls.
|
|
6
|
+
*
|
|
7
|
+
* Design goals:
|
|
8
|
+
* - Zero behavior changes to core `gate()`
|
|
9
|
+
* - Drop-in for public chat endpoints
|
|
10
|
+
* - Deterministic: no external calls
|
|
11
|
+
*
|
|
12
|
+
* @param {object} [opts]
|
|
13
|
+
* @param {string} [opts.bodyField="text"] - Field name in req.body that contains user text.
|
|
14
|
+
* @param {string} [opts.queryField] - Optional query param fallback (e.g., ?text=...).
|
|
15
|
+
* @param {boolean} [opts.attachResult=true] - Attach result to req.entropyGate.
|
|
16
|
+
* @param {boolean} [opts.blockOn="BLOCK"] - Block when action matches this string ("BLOCK") or array of actions.
|
|
17
|
+
* @param {number} [opts.blockStatus=400] - HTTP status when blocked.
|
|
18
|
+
* @param {object|function} [opts.blockResponse] - Custom JSON response or function(req, res, result) => any
|
|
19
|
+
* @param {boolean} [opts.warnHeader=true] - If WARN, add response headers with gate metadata.
|
|
20
|
+
* @param {boolean} [opts.alwaysHeader=false] - If true, add headers for all actions.
|
|
21
|
+
* @param {function} [opts.onResult] - Hook: (req, result) => void
|
|
22
|
+
* @param {function} [opts.getText] - Hook: (req) => string (overrides bodyField/queryField)
|
|
23
|
+
*/
|
|
24
|
+
export function entropyGateMiddleware(opts = {}) {
|
|
25
|
+
const {
|
|
26
|
+
bodyField = "text",
|
|
27
|
+
queryField,
|
|
28
|
+
attachResult = true,
|
|
29
|
+
blockOn = "BLOCK",
|
|
30
|
+
blockStatus = 400,
|
|
31
|
+
blockResponse,
|
|
32
|
+
warnHeader = true,
|
|
33
|
+
alwaysHeader = false,
|
|
34
|
+
onResult,
|
|
35
|
+
getText,
|
|
36
|
+
} = opts;
|
|
37
|
+
|
|
38
|
+
const blockSet = Array.isArray(blockOn) ? new Set(blockOn) : new Set([blockOn]);
|
|
39
|
+
|
|
40
|
+
return function entropyGate(req, res, next) {
|
|
41
|
+
try {
|
|
42
|
+
// 1) Extract text
|
|
43
|
+
let text = "";
|
|
44
|
+
if (typeof getText === "function") {
|
|
45
|
+
text = String(getText(req) ?? "");
|
|
46
|
+
} else {
|
|
47
|
+
const bodyVal = req?.body?.[bodyField];
|
|
48
|
+
const queryVal = queryField ? req?.query?.[queryField] : undefined;
|
|
49
|
+
text = String(bodyVal ?? queryVal ?? "");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 2) Run deterministic gate
|
|
53
|
+
const result = gate(text);
|
|
54
|
+
|
|
55
|
+
// 3) Attach result for downstream routing/logging
|
|
56
|
+
if (attachResult) {
|
|
57
|
+
// Convention: req.entropyGate
|
|
58
|
+
req.entropyGate = result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Optional hook
|
|
62
|
+
if (typeof onResult === "function") {
|
|
63
|
+
onResult(req, result);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 4) Telemetry headers (optional)
|
|
67
|
+
const shouldHeader = alwaysHeader || (warnHeader && result?.action === "WARN");
|
|
68
|
+
if (shouldHeader) {
|
|
69
|
+
// Keep headers small and stable
|
|
70
|
+
res.setHeader("x-entropy-action", String(result?.action ?? ""));
|
|
71
|
+
res.setHeader("x-entropy-score", String(result?.entropy_score ?? ""));
|
|
72
|
+
res.setHeader("x-entropy-intention", String(result?.intention ?? ""));
|
|
73
|
+
// Flags can be large; keep compact
|
|
74
|
+
if (Array.isArray(result?.flags)) {
|
|
75
|
+
res.setHeader("x-entropy-flags", result.flags.slice(0, 10).join(","));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 5) Block if configured
|
|
80
|
+
if (blockSet.has(result?.action)) {
|
|
81
|
+
res.status(blockStatus);
|
|
82
|
+
|
|
83
|
+
if (typeof blockResponse === "function") {
|
|
84
|
+
return res.json(blockResponse(req, res, result));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (blockResponse && typeof blockResponse === "object") {
|
|
88
|
+
return res.json(blockResponse);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Default response: transparent + actionable
|
|
92
|
+
return res.json({
|
|
93
|
+
ok: false,
|
|
94
|
+
blocked: true,
|
|
95
|
+
gate: result,
|
|
96
|
+
message:
|
|
97
|
+
"Request blocked by llm-entropy-filter (high-entropy / low-signal input).",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Otherwise proceed
|
|
102
|
+
return next();
|
|
103
|
+
} catch (err) {
|
|
104
|
+
// Fail-open by default: do not block requests if the gate errors.
|
|
105
|
+
// You can change this behavior by wrapping with your own error handler.
|
|
106
|
+
return next(err);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Small helper for routing:
|
|
113
|
+
* If you prefer to run gate manually inside route handlers.
|
|
114
|
+
*/
|
|
115
|
+
export function runEntropyGate(text) {
|
|
116
|
+
return gate(String(text ?? ""));
|
|
117
|
+
}
|