@marslanmustafa/input-shield 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 @@
1
+ {"version":3,"sources":["../src/validators/presets.ts"],"names":[],"mappings":";;;AAsBO,SAAS,iBAAiB,IAAA,EAAgC;AAC/D,EAAA,OAAO,eAAA,GACJ,KAAA,CAAM,UAAU,EAChB,GAAA,CAAI,CAAC,CAAA,CACL,GAAA,CAAI,EAAE,CAAA,CACN,aAAY,CACZ,WAAA,CAAY,EAAE,WAAA,EAAa,QAAA,EAAU,CAAA,CACrC,YAAA,EAAa,CACb,QAAA,CAAS,IAAI,CAAA;AAClB;AAMO,SAAS,iBAAA,CAAkB,IAAA,EAAc,SAAA,GAAY,MAAA,EAA0B;AACpF,EAAA,OAAO,eAAA,EAAgB,CACpB,KAAA,CAAM,SAAS,CAAA,CACf,IAAI,CAAC,CAAA,CACL,GAAA,CAAI,GAAG,CAAA,CACP,WAAA,GACA,WAAA,CAAY,EAAE,WAAA,EAAa,QAAA,EAAU,CAAA,CACrC,QAAO,CACP,YAAA,EAAa,CACb,QAAA,CAAS,IAAI,CAAA;AAClB;AAOO,SAAS,YAAY,IAAA,EAAgC;AAC1D,EAAA,OAAO,eAAA,GACJ,KAAA,CAAM,KAAK,EACX,GAAA,CAAI,EAAE,CAAA,CACN,GAAA,CAAI,GAAG,CAAA,CACP,aAAY,CACZ,MAAA,GACA,WAAA,CAAY,EAAE,aAAa,OAAA,EAAS,CAAA,CACpC,QAAA,CAAS,IAAI,CAAA;AAClB;AAOO,SAAS,gBAAA,CAAiB,IAAA,EAAc,SAAA,GAAY,SAAA,EAA6B;AACtF,EAAA,OAAO,iBAAgB,CACpB,KAAA,CAAM,SAAS,CAAA,CACf,IAAI,CAAC,CAAA,CACL,GAAA,CAAI,GAAI,EACR,WAAA,EAAY,CACZ,MAAA,EAAO,CACP,SAAS,IAAI,CAAA;AAClB;AAOO,SAAS,oBAAoB,IAAA,EAAgC;AAClE,EAAA,OAAO,eAAA,EAAgB,CACpB,KAAA,CAAM,cAAc,EACpB,GAAA,CAAI,CAAC,CAAA,CACL,GAAA,CAAI,GAAG,CAAA,CACP,MAAA,EAAO,CACP,SAAS,IAAI,CAAA;AAClB","file":"chunk-WACGX73I.mjs","sourcesContent":["/**\n * presets.ts\n *\n * Ready-to-use validators for the most common field types.\n * Zero config — import and call.\n *\n * All presets are pre-configured InputShieldValidator instances.\n * You can also use them as a starting point and extend with .custom():\n *\n * import { usernameValidator } from 'input-shield/presets';\n * // They're factories, so each call gives a fresh instance:\n * const myValidator = usernameValidator().custom(t => t === 'admin', 'custom', 'reserved name');\n */\n\nimport { createValidator } from './builder.js';\nimport type { ValidationResult } from '../types.js';\n\n/**\n * Username / display name.\n * 3–30 chars, no profanity, strict gibberish detection, no spam.\n * Repeated words allowed (e.g. \"John John\" as a nickname).\n */\nexport function validateUsername(text: string): ValidationResult {\n return createValidator()\n .field('Username')\n .min(3)\n .max(30)\n .noProfanity()\n .noGibberish({ sensitivity: 'strict' })\n .noLowQuality()\n .validate(text);\n}\n\n/**\n * Short text / name fields (product name, company name, form title).\n * 2–100 chars, no profanity, normal gibberish, no spam.\n */\nexport function validateShortText(text: string, fieldName = 'Name'): ValidationResult {\n return createValidator()\n .field(fieldName)\n .min(2)\n .max(100)\n .noProfanity()\n .noGibberish({ sensitivity: 'normal' })\n .noSpam()\n .noLowQuality()\n .validate(text);\n}\n\n/**\n * Bio / description / about me.\n * 10–300 chars, no profanity, no spam, loose gibberish (allows natural language).\n * Repeated words NOT flagged (natural in prose).\n */\nexport function validateBio(text: string): ValidationResult {\n return createValidator()\n .field('Bio')\n .min(10)\n .max(300)\n .noProfanity()\n .noSpam()\n .noGibberish({ sensitivity: 'loose' })\n .validate(text);\n}\n\n/**\n * Long-form text (comment, review, feedback).\n * 5–2000 chars, no profanity, no spam. No gibberish check\n * (long text can contain intentional fragments, code, etc.)\n */\nexport function validateLongText(text: string, fieldName = 'Message'): ValidationResult {\n return createValidator()\n .field(fieldName)\n .min(5)\n .max(2000)\n .noProfanity()\n .noSpam()\n .validate(text);\n}\n\n/**\n * Search query input.\n * 1–200 chars, no spam URLs (but allows short/fragmentary text).\n * Does NOT flag gibberish (code snippets, SKUs, part numbers are valid queries).\n */\nexport function validateSearchQuery(text: string): ValidationResult {\n return createValidator()\n .field('Search query')\n .min(1)\n .max(200)\n .noSpam()\n .validate(text);\n}\n"]}
package/dist/email.cjs ADDED
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ var chunk67CBN3U4_cjs = require('./chunk-67CBN3U4.cjs');
4
+
5
+ // src/email.ts
6
+ function stripHtml(html) {
7
+ const styleUrls = [];
8
+ const urlPattern = /url\(["']?(https?:\/\/[^"')]+)["']?\)/gi;
9
+ let urlMatch;
10
+ while ((urlMatch = urlPattern.exec(html)) !== null) {
11
+ styleUrls.push(urlMatch[1]);
12
+ }
13
+ return [
14
+ html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, " ").replace(/<script[^>]*>[\s\S]*?<\/script>/gi, " ").replace(/<a[^>]+href=["']([^"']+)["'][^>]*>/gi, " $1 ").replace(/<\/(p|div|li|td|th|h[1-6]|blockquote)>/gi, " ").replace(/<br\s*\/?>/gi, " ").replace(/<[^>]+>/g, "").replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code))).replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/&nbsp;/gi, " ").replace(/&amp;/gi, "&").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&quot;/gi, '"').replace(/&apos;/gi, "'"),
15
+ // Append extracted style URLs so spam detector sees them
16
+ ...styleUrls
17
+ ].join(" ").replace(/\s+/g, " ").trim();
18
+ }
19
+ function validateMailContent(mail, validator) {
20
+ const v = validator ?? chunk67CBN3U4_cjs.createValidator().field("Email content").min(1).max(5e4).noProfanity().noSpam();
21
+ const fields = [];
22
+ if (mail.subject) {
23
+ fields.push({ key: "subject", value: mail.subject });
24
+ }
25
+ if (mail.text) {
26
+ fields.push({ key: "text", value: mail.text });
27
+ }
28
+ if (mail.html) {
29
+ fields.push({ key: "html", value: stripHtml(mail.html) });
30
+ }
31
+ for (const { key, value } of fields) {
32
+ const result = v.validate(value);
33
+ if (!result.isValid) {
34
+ return {
35
+ isValid: false,
36
+ field: key,
37
+ reason: result.reason,
38
+ message: result.message
39
+ };
40
+ }
41
+ }
42
+ return { isValid: true };
43
+ }
44
+
45
+ exports.stripHtml = stripHtml;
46
+ exports.validateMailContent = validateMailContent;
47
+ //# sourceMappingURL=email.cjs.map
48
+ //# sourceMappingURL=email.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/email.ts"],"names":["createValidator"],"mappings":";;;;;AAkCO,SAAS,UAAU,IAAA,EAAsB;AAI9C,EAAA,MAAM,YAAsB,EAAC;AAC7B,EAAA,MAAM,UAAA,GAAa,yCAAA;AACnB,EAAA,IAAI,QAAA;AACJ,EAAA,OAAA,CAAQ,QAAA,GAAW,UAAA,CAAW,IAAA,CAAK,IAAI,OAAO,IAAA,EAAM;AAClD,IAAA,SAAA,CAAU,IAAA,CAAK,QAAA,CAAS,CAAC,CAAC,CAAA;AAAA,EAC5B;AAEA,EAAA,OAAO;AAAA,IACL,IAAA,CAEG,OAAA,CAAQ,iCAAA,EAAmC,GAAG,EAE9C,OAAA,CAAQ,mCAAA,EAAqC,GAAG,CAAA,CAEhD,QAAQ,sCAAA,EAAwC,MAAM,CAAA,CAGtD,OAAA,CAAQ,4CAA4C,GAAG,CAAA,CACvD,OAAA,CAAQ,cAAA,EAAgB,GAAG,CAAA,CAE3B,OAAA,CAAQ,UAAA,EAAY,EAAE,CAAA,CAGtB,OAAA,CAAQ,WAAA,EAAa,CAAC,GAAG,IAAA,KAAS,MAAA,CAAO,YAAA,CAAa,MAAA,CAAO,IAAI,CAAC,CAAC,CAAA,CACnE,OAAA,CAAQ,mBAAA,EAAqB,CAAC,CAAA,EAAG,GAAA,KAAQ,OAAO,YAAA,CAAa,QAAA,CAAS,GAAA,EAAK,EAAE,CAAC,CAAC,CAAA,CAE/E,OAAA,CAAQ,UAAA,EAAY,GAAG,CAAA,CACvB,OAAA,CAAQ,SAAA,EAAW,GAAG,CAAA,CACtB,OAAA,CAAQ,QAAA,EAAU,GAAG,EACrB,OAAA,CAAQ,QAAA,EAAU,GAAG,CAAA,CACrB,QAAQ,UAAA,EAAY,GAAG,CAAA,CACvB,OAAA,CAAQ,YAAY,GAAG,CAAA;AAAA;AAAA,IAE1B,GAAG;AAAA,GACL,CACG,KAAK,GAAG,CAAA,CACR,QAAQ,MAAA,EAAQ,GAAG,EACnB,IAAA,EAAK;AACV;AAoCO,SAAS,mBAAA,CACd,MACA,SAAA,EACsB;AAEtB,EAAA,MAAM,CAAA,GAAI,SAAA,IAAaA,iCAAA,EAAgB,CACpC,MAAM,eAAe,CAAA,CACrB,GAAA,CAAI,CAAC,EACL,GAAA,CAAI,GAAK,CAAA,CACT,WAAA,GACA,MAAA,EAAO;AAEV,EAAA,MAAM,SAAqE,EAAC;AAE5E,EAAA,IAAI,KAAK,OAAA,EAAS;AAChB,IAAA,MAAA,CAAO,KAAK,EAAE,GAAA,EAAK,WAAW,KAAA,EAAO,IAAA,CAAK,SAAS,CAAA;AAAA,EACrD;AACA,EAAA,IAAI,KAAK,IAAA,EAAM;AACb,IAAA,MAAA,CAAO,KAAK,EAAE,GAAA,EAAK,QAAQ,KAAA,EAAO,IAAA,CAAK,MAAM,CAAA;AAAA,EAC/C;AACA,EAAA,IAAI,KAAK,IAAA,EAAM;AAEb,IAAA,MAAA,CAAO,IAAA,CAAK,EAAE,GAAA,EAAK,MAAA,EAAQ,OAAO,SAAA,CAAU,IAAA,CAAK,IAAI,CAAA,EAAG,CAAA;AAAA,EAC1D;AAEA,EAAA,KAAA,MAAW,EAAE,GAAA,EAAK,KAAA,EAAM,IAAK,MAAA,EAAQ;AACnC,IAAA,MAAM,MAAA,GAAS,CAAA,CAAE,QAAA,CAAS,KAAK,CAAA;AAC/B,IAAA,IAAI,CAAC,OAAO,OAAA,EAAS;AACnB,MAAA,OAAO;AAAA,QACL,OAAA,EAAS,KAAA;AAAA,QACT,KAAA,EAAO,GAAA;AAAA,QACP,QAAQ,MAAA,CAAO,MAAA;AAAA,QACf,SAAS,MAAA,CAAO;AAAA,OAClB;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO,EAAE,SAAS,IAAA,EAAK;AACzB","file":"email.cjs","sourcesContent":["/**\n * email.ts (exported as '@marslanmustafa/input-shield/email')\n *\n * Email-specific validation utilities.\n *\n * WHY THIS EXISTS:\n * Nodemailer (and every other mail library) has zero content validation.\n * Email bodies are almost always HTML — raw input-shield validators\n * running on HTML strings will miss profanity/spam hidden inside tags,\n * encoded as HTML entities, or buried in href attributes.\n *\n * WHAT THIS PROVIDES:\n * - stripHtml() → clean HTML → plain text for validation\n * - validateMailContent() → validate a full Nodemailer mail options object\n */\n\nimport { createValidator, InputShieldValidator } from './validators/builder.js';\n\n// ─── HTML Stripper ────────────────────────────────────────────────────────────\n\n/**\n * Strip HTML and decode entities — producing clean plain text for validation.\n *\n * Handles the following attack vectors:\n * - Tags wrapping profanity: <b>f*ck</b> → \"f*ck\"\n * - Split-tag evasion: <s>f</s><s>uck</s> → \"f uck\" → skeleton → \"fuck\"\n * - Decimal entities: &#102;&#117;&#99;&#107; → \"fuck\"\n * - Hex entities: &#x66;&#x75;&#x63;&#x6B; → \"fuck\"\n * - Spam URLs in href: <a href=\"https://spam.com\">click</a> → includes URL\n * - Hidden CSS/scripts: <style> and <script> blocks removed entirely\n *\n * @param html - Raw HTML string (e.g. Nodemailer `html` field)\n * @returns Plain text safe to pass to any input-shield validator\n */\nexport function stripHtml(html: string): string {\n // Step 0: Extract ALL url() values from inline styles FIRST, before tags are stripped.\n // This catches: style=\"background:url(https://tracker.spam.com/pixel)\"\n // If we strip tags first, the URL inside the style attribute is lost forever.\n const styleUrls: string[] = [];\n const urlPattern = /url\\([\"']?(https?:\\/\\/[^\"')]+)[\"']?\\)/gi;\n let urlMatch: RegExpExecArray | null;\n while ((urlMatch = urlPattern.exec(html)) !== null) {\n styleUrls.push(urlMatch[1]);\n }\n\n return [\n html\n // Remove <style> blocks entirely — CSS can visually hide text\n .replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, ' ')\n // Remove <script> blocks entirely\n .replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, ' ')\n // Extract href values — link text may be clean but href is spam\n .replace(/<a[^>]+href=[\"']([^\"']+)[\"'][^>]*>/gi, ' $1 ')\n // Preserve word boundaries at block-level closing tags\n // Without this: <p>free</p><p>money</p> → \"freemoney\" (breaks \\b)\n .replace(/<\\/(p|div|li|td|th|h[1-6]|blockquote)>/gi, ' ')\n .replace(/<br\\s*\\/?>/gi, ' ')\n // Strip all remaining tags\n .replace(/<[^>]+>/g, '')\n // Decode numeric entities BEFORE named (order matters)\n // &#102; → \"f\", &#x66; → \"f\"\n .replace(/&#(\\d+);/g, (_, code) => String.fromCharCode(Number(code)))\n .replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16)))\n // Decode named entities\n .replace(/&nbsp;/gi, ' ')\n .replace(/&amp;/gi, '&')\n .replace(/&lt;/gi, '<')\n .replace(/&gt;/gi, '>')\n .replace(/&quot;/gi, '\"')\n .replace(/&apos;/gi, \"'\"),\n // Append extracted style URLs so spam detector sees them\n ...styleUrls,\n ]\n .join(' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\n// ─── Mail content validator ───────────────────────────────────────────────────\n\nexport interface MailContent {\n /** Email subject line */\n subject?: string;\n /** Plain text body */\n text?: string;\n /** HTML body — will be stripped before validation */\n html?: string;\n}\n\nexport type MailValidationResult =\n | { isValid: true }\n | { isValid: false; field: 'subject' | 'text' | 'html'; reason: string; message: string };\n\n/**\n * Validate a Nodemailer mail options object before passing to sendMail().\n *\n * Strips HTML from the `html` field automatically.\n * Validates subject, text, and html fields independently.\n *\n * @param mail - The mail content to validate (subset of Nodemailer's MailOptions)\n * @param validator - Optional custom validator. Defaults to noProfanity + noSpam.\n *\n * @example\n * const result = validateMailContent({\n * subject: 'Hello',\n * html: '<p>Your order is confirmed.</p>',\n * });\n * if (!result.isValid) {\n * throw new Error(result.message); // never reaches sendMail()\n * }\n * await transporter.sendMail({ ...mailOptions });\n */\nexport function validateMailContent(\n mail: MailContent,\n validator?: InputShieldValidator\n): MailValidationResult {\n // Default validator: profanity + spam, generous length limits for email\n const v = validator ?? createValidator()\n .field('Email content')\n .min(1)\n .max(50000)\n .noProfanity()\n .noSpam();\n\n const fields: Array<{ key: 'subject' | 'text' | 'html'; value: string }> = [];\n\n if (mail.subject) {\n fields.push({ key: 'subject', value: mail.subject });\n }\n if (mail.text) {\n fields.push({ key: 'text', value: mail.text });\n }\n if (mail.html) {\n // Strip HTML before validating — this is the critical step\n fields.push({ key: 'html', value: stripHtml(mail.html) });\n }\n\n for (const { key, value } of fields) {\n const result = v.validate(value);\n if (!result.isValid) {\n return {\n isValid: false,\n field: key,\n reason: result.reason,\n message: result.message,\n };\n }\n }\n\n return { isValid: true };\n}"]}
@@ -0,0 +1,71 @@
1
+ import { I as InputShieldValidator } from './builder-C10YQjbV.cjs';
2
+
3
+ /**
4
+ * email.ts (exported as '@marslanmustafa/input-shield/email')
5
+ *
6
+ * Email-specific validation utilities.
7
+ *
8
+ * WHY THIS EXISTS:
9
+ * Nodemailer (and every other mail library) has zero content validation.
10
+ * Email bodies are almost always HTML — raw input-shield validators
11
+ * running on HTML strings will miss profanity/spam hidden inside tags,
12
+ * encoded as HTML entities, or buried in href attributes.
13
+ *
14
+ * WHAT THIS PROVIDES:
15
+ * - stripHtml() → clean HTML → plain text for validation
16
+ * - validateMailContent() → validate a full Nodemailer mail options object
17
+ */
18
+
19
+ /**
20
+ * Strip HTML and decode entities — producing clean plain text for validation.
21
+ *
22
+ * Handles the following attack vectors:
23
+ * - Tags wrapping profanity: <b>f*ck</b> → "f*ck"
24
+ * - Split-tag evasion: <s>f</s><s>uck</s> → "f uck" → skeleton → "fuck"
25
+ * - Decimal entities: &#102;&#117;&#99;&#107; → "fuck"
26
+ * - Hex entities: &#x66;&#x75;&#x63;&#x6B; → "fuck"
27
+ * - Spam URLs in href: <a href="https://spam.com">click</a> → includes URL
28
+ * - Hidden CSS/scripts: <style> and <script> blocks removed entirely
29
+ *
30
+ * @param html - Raw HTML string (e.g. Nodemailer `html` field)
31
+ * @returns Plain text safe to pass to any input-shield validator
32
+ */
33
+ declare function stripHtml(html: string): string;
34
+ interface MailContent {
35
+ /** Email subject line */
36
+ subject?: string;
37
+ /** Plain text body */
38
+ text?: string;
39
+ /** HTML body — will be stripped before validation */
40
+ html?: string;
41
+ }
42
+ type MailValidationResult = {
43
+ isValid: true;
44
+ } | {
45
+ isValid: false;
46
+ field: 'subject' | 'text' | 'html';
47
+ reason: string;
48
+ message: string;
49
+ };
50
+ /**
51
+ * Validate a Nodemailer mail options object before passing to sendMail().
52
+ *
53
+ * Strips HTML from the `html` field automatically.
54
+ * Validates subject, text, and html fields independently.
55
+ *
56
+ * @param mail - The mail content to validate (subset of Nodemailer's MailOptions)
57
+ * @param validator - Optional custom validator. Defaults to noProfanity + noSpam.
58
+ *
59
+ * @example
60
+ * const result = validateMailContent({
61
+ * subject: 'Hello',
62
+ * html: '<p>Your order is confirmed.</p>',
63
+ * });
64
+ * if (!result.isValid) {
65
+ * throw new Error(result.message); // never reaches sendMail()
66
+ * }
67
+ * await transporter.sendMail({ ...mailOptions });
68
+ */
69
+ declare function validateMailContent(mail: MailContent, validator?: InputShieldValidator): MailValidationResult;
70
+
71
+ export { type MailContent, type MailValidationResult, stripHtml, validateMailContent };
@@ -0,0 +1,71 @@
1
+ import { I as InputShieldValidator } from './builder-C10YQjbV.js';
2
+
3
+ /**
4
+ * email.ts (exported as '@marslanmustafa/input-shield/email')
5
+ *
6
+ * Email-specific validation utilities.
7
+ *
8
+ * WHY THIS EXISTS:
9
+ * Nodemailer (and every other mail library) has zero content validation.
10
+ * Email bodies are almost always HTML — raw input-shield validators
11
+ * running on HTML strings will miss profanity/spam hidden inside tags,
12
+ * encoded as HTML entities, or buried in href attributes.
13
+ *
14
+ * WHAT THIS PROVIDES:
15
+ * - stripHtml() → clean HTML → plain text for validation
16
+ * - validateMailContent() → validate a full Nodemailer mail options object
17
+ */
18
+
19
+ /**
20
+ * Strip HTML and decode entities — producing clean plain text for validation.
21
+ *
22
+ * Handles the following attack vectors:
23
+ * - Tags wrapping profanity: <b>f*ck</b> → "f*ck"
24
+ * - Split-tag evasion: <s>f</s><s>uck</s> → "f uck" → skeleton → "fuck"
25
+ * - Decimal entities: &#102;&#117;&#99;&#107; → "fuck"
26
+ * - Hex entities: &#x66;&#x75;&#x63;&#x6B; → "fuck"
27
+ * - Spam URLs in href: <a href="https://spam.com">click</a> → includes URL
28
+ * - Hidden CSS/scripts: <style> and <script> blocks removed entirely
29
+ *
30
+ * @param html - Raw HTML string (e.g. Nodemailer `html` field)
31
+ * @returns Plain text safe to pass to any input-shield validator
32
+ */
33
+ declare function stripHtml(html: string): string;
34
+ interface MailContent {
35
+ /** Email subject line */
36
+ subject?: string;
37
+ /** Plain text body */
38
+ text?: string;
39
+ /** HTML body — will be stripped before validation */
40
+ html?: string;
41
+ }
42
+ type MailValidationResult = {
43
+ isValid: true;
44
+ } | {
45
+ isValid: false;
46
+ field: 'subject' | 'text' | 'html';
47
+ reason: string;
48
+ message: string;
49
+ };
50
+ /**
51
+ * Validate a Nodemailer mail options object before passing to sendMail().
52
+ *
53
+ * Strips HTML from the `html` field automatically.
54
+ * Validates subject, text, and html fields independently.
55
+ *
56
+ * @param mail - The mail content to validate (subset of Nodemailer's MailOptions)
57
+ * @param validator - Optional custom validator. Defaults to noProfanity + noSpam.
58
+ *
59
+ * @example
60
+ * const result = validateMailContent({
61
+ * subject: 'Hello',
62
+ * html: '<p>Your order is confirmed.</p>',
63
+ * });
64
+ * if (!result.isValid) {
65
+ * throw new Error(result.message); // never reaches sendMail()
66
+ * }
67
+ * await transporter.sendMail({ ...mailOptions });
68
+ */
69
+ declare function validateMailContent(mail: MailContent, validator?: InputShieldValidator): MailValidationResult;
70
+
71
+ export { type MailContent, type MailValidationResult, stripHtml, validateMailContent };
package/dist/email.mjs ADDED
@@ -0,0 +1,45 @@
1
+ import { createValidator } from './chunk-ADGSP522.mjs';
2
+
3
+ // src/email.ts
4
+ function stripHtml(html) {
5
+ const styleUrls = [];
6
+ const urlPattern = /url\(["']?(https?:\/\/[^"')]+)["']?\)/gi;
7
+ let urlMatch;
8
+ while ((urlMatch = urlPattern.exec(html)) !== null) {
9
+ styleUrls.push(urlMatch[1]);
10
+ }
11
+ return [
12
+ html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, " ").replace(/<script[^>]*>[\s\S]*?<\/script>/gi, " ").replace(/<a[^>]+href=["']([^"']+)["'][^>]*>/gi, " $1 ").replace(/<\/(p|div|li|td|th|h[1-6]|blockquote)>/gi, " ").replace(/<br\s*\/?>/gi, " ").replace(/<[^>]+>/g, "").replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code))).replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/&nbsp;/gi, " ").replace(/&amp;/gi, "&").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&quot;/gi, '"').replace(/&apos;/gi, "'"),
13
+ // Append extracted style URLs so spam detector sees them
14
+ ...styleUrls
15
+ ].join(" ").replace(/\s+/g, " ").trim();
16
+ }
17
+ function validateMailContent(mail, validator) {
18
+ const v = validator ?? createValidator().field("Email content").min(1).max(5e4).noProfanity().noSpam();
19
+ const fields = [];
20
+ if (mail.subject) {
21
+ fields.push({ key: "subject", value: mail.subject });
22
+ }
23
+ if (mail.text) {
24
+ fields.push({ key: "text", value: mail.text });
25
+ }
26
+ if (mail.html) {
27
+ fields.push({ key: "html", value: stripHtml(mail.html) });
28
+ }
29
+ for (const { key, value } of fields) {
30
+ const result = v.validate(value);
31
+ if (!result.isValid) {
32
+ return {
33
+ isValid: false,
34
+ field: key,
35
+ reason: result.reason,
36
+ message: result.message
37
+ };
38
+ }
39
+ }
40
+ return { isValid: true };
41
+ }
42
+
43
+ export { stripHtml, validateMailContent };
44
+ //# sourceMappingURL=email.mjs.map
45
+ //# sourceMappingURL=email.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/email.ts"],"names":[],"mappings":";;;AAkCO,SAAS,UAAU,IAAA,EAAsB;AAI9C,EAAA,MAAM,YAAsB,EAAC;AAC7B,EAAA,MAAM,UAAA,GAAa,yCAAA;AACnB,EAAA,IAAI,QAAA;AACJ,EAAA,OAAA,CAAQ,QAAA,GAAW,UAAA,CAAW,IAAA,CAAK,IAAI,OAAO,IAAA,EAAM;AAClD,IAAA,SAAA,CAAU,IAAA,CAAK,QAAA,CAAS,CAAC,CAAC,CAAA;AAAA,EAC5B;AAEA,EAAA,OAAO;AAAA,IACL,IAAA,CAEG,OAAA,CAAQ,iCAAA,EAAmC,GAAG,EAE9C,OAAA,CAAQ,mCAAA,EAAqC,GAAG,CAAA,CAEhD,QAAQ,sCAAA,EAAwC,MAAM,CAAA,CAGtD,OAAA,CAAQ,4CAA4C,GAAG,CAAA,CACvD,OAAA,CAAQ,cAAA,EAAgB,GAAG,CAAA,CAE3B,OAAA,CAAQ,UAAA,EAAY,EAAE,CAAA,CAGtB,OAAA,CAAQ,WAAA,EAAa,CAAC,GAAG,IAAA,KAAS,MAAA,CAAO,YAAA,CAAa,MAAA,CAAO,IAAI,CAAC,CAAC,CAAA,CACnE,OAAA,CAAQ,mBAAA,EAAqB,CAAC,CAAA,EAAG,GAAA,KAAQ,OAAO,YAAA,CAAa,QAAA,CAAS,GAAA,EAAK,EAAE,CAAC,CAAC,CAAA,CAE/E,OAAA,CAAQ,UAAA,EAAY,GAAG,CAAA,CACvB,OAAA,CAAQ,SAAA,EAAW,GAAG,CAAA,CACtB,OAAA,CAAQ,QAAA,EAAU,GAAG,EACrB,OAAA,CAAQ,QAAA,EAAU,GAAG,CAAA,CACrB,QAAQ,UAAA,EAAY,GAAG,CAAA,CACvB,OAAA,CAAQ,YAAY,GAAG,CAAA;AAAA;AAAA,IAE1B,GAAG;AAAA,GACL,CACG,KAAK,GAAG,CAAA,CACR,QAAQ,MAAA,EAAQ,GAAG,EACnB,IAAA,EAAK;AACV;AAoCO,SAAS,mBAAA,CACd,MACA,SAAA,EACsB;AAEtB,EAAA,MAAM,CAAA,GAAI,SAAA,IAAa,eAAA,EAAgB,CACpC,MAAM,eAAe,CAAA,CACrB,GAAA,CAAI,CAAC,EACL,GAAA,CAAI,GAAK,CAAA,CACT,WAAA,GACA,MAAA,EAAO;AAEV,EAAA,MAAM,SAAqE,EAAC;AAE5E,EAAA,IAAI,KAAK,OAAA,EAAS;AAChB,IAAA,MAAA,CAAO,KAAK,EAAE,GAAA,EAAK,WAAW,KAAA,EAAO,IAAA,CAAK,SAAS,CAAA;AAAA,EACrD;AACA,EAAA,IAAI,KAAK,IAAA,EAAM;AACb,IAAA,MAAA,CAAO,KAAK,EAAE,GAAA,EAAK,QAAQ,KAAA,EAAO,IAAA,CAAK,MAAM,CAAA;AAAA,EAC/C;AACA,EAAA,IAAI,KAAK,IAAA,EAAM;AAEb,IAAA,MAAA,CAAO,IAAA,CAAK,EAAE,GAAA,EAAK,MAAA,EAAQ,OAAO,SAAA,CAAU,IAAA,CAAK,IAAI,CAAA,EAAG,CAAA;AAAA,EAC1D;AAEA,EAAA,KAAA,MAAW,EAAE,GAAA,EAAK,KAAA,EAAM,IAAK,MAAA,EAAQ;AACnC,IAAA,MAAM,MAAA,GAAS,CAAA,CAAE,QAAA,CAAS,KAAK,CAAA;AAC/B,IAAA,IAAI,CAAC,OAAO,OAAA,EAAS;AACnB,MAAA,OAAO;AAAA,QACL,OAAA,EAAS,KAAA;AAAA,QACT,KAAA,EAAO,GAAA;AAAA,QACP,QAAQ,MAAA,CAAO,MAAA;AAAA,QACf,SAAS,MAAA,CAAO;AAAA,OAClB;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO,EAAE,SAAS,IAAA,EAAK;AACzB","file":"email.mjs","sourcesContent":["/**\n * email.ts (exported as '@marslanmustafa/input-shield/email')\n *\n * Email-specific validation utilities.\n *\n * WHY THIS EXISTS:\n * Nodemailer (and every other mail library) has zero content validation.\n * Email bodies are almost always HTML — raw input-shield validators\n * running on HTML strings will miss profanity/spam hidden inside tags,\n * encoded as HTML entities, or buried in href attributes.\n *\n * WHAT THIS PROVIDES:\n * - stripHtml() → clean HTML → plain text for validation\n * - validateMailContent() → validate a full Nodemailer mail options object\n */\n\nimport { createValidator, InputShieldValidator } from './validators/builder.js';\n\n// ─── HTML Stripper ────────────────────────────────────────────────────────────\n\n/**\n * Strip HTML and decode entities — producing clean plain text for validation.\n *\n * Handles the following attack vectors:\n * - Tags wrapping profanity: <b>f*ck</b> → \"f*ck\"\n * - Split-tag evasion: <s>f</s><s>uck</s> → \"f uck\" → skeleton → \"fuck\"\n * - Decimal entities: &#102;&#117;&#99;&#107; → \"fuck\"\n * - Hex entities: &#x66;&#x75;&#x63;&#x6B; → \"fuck\"\n * - Spam URLs in href: <a href=\"https://spam.com\">click</a> → includes URL\n * - Hidden CSS/scripts: <style> and <script> blocks removed entirely\n *\n * @param html - Raw HTML string (e.g. Nodemailer `html` field)\n * @returns Plain text safe to pass to any input-shield validator\n */\nexport function stripHtml(html: string): string {\n // Step 0: Extract ALL url() values from inline styles FIRST, before tags are stripped.\n // This catches: style=\"background:url(https://tracker.spam.com/pixel)\"\n // If we strip tags first, the URL inside the style attribute is lost forever.\n const styleUrls: string[] = [];\n const urlPattern = /url\\([\"']?(https?:\\/\\/[^\"')]+)[\"']?\\)/gi;\n let urlMatch: RegExpExecArray | null;\n while ((urlMatch = urlPattern.exec(html)) !== null) {\n styleUrls.push(urlMatch[1]);\n }\n\n return [\n html\n // Remove <style> blocks entirely — CSS can visually hide text\n .replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, ' ')\n // Remove <script> blocks entirely\n .replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, ' ')\n // Extract href values — link text may be clean but href is spam\n .replace(/<a[^>]+href=[\"']([^\"']+)[\"'][^>]*>/gi, ' $1 ')\n // Preserve word boundaries at block-level closing tags\n // Without this: <p>free</p><p>money</p> → \"freemoney\" (breaks \\b)\n .replace(/<\\/(p|div|li|td|th|h[1-6]|blockquote)>/gi, ' ')\n .replace(/<br\\s*\\/?>/gi, ' ')\n // Strip all remaining tags\n .replace(/<[^>]+>/g, '')\n // Decode numeric entities BEFORE named (order matters)\n // &#102; → \"f\", &#x66; → \"f\"\n .replace(/&#(\\d+);/g, (_, code) => String.fromCharCode(Number(code)))\n .replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16)))\n // Decode named entities\n .replace(/&nbsp;/gi, ' ')\n .replace(/&amp;/gi, '&')\n .replace(/&lt;/gi, '<')\n .replace(/&gt;/gi, '>')\n .replace(/&quot;/gi, '\"')\n .replace(/&apos;/gi, \"'\"),\n // Append extracted style URLs so spam detector sees them\n ...styleUrls,\n ]\n .join(' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\n// ─── Mail content validator ───────────────────────────────────────────────────\n\nexport interface MailContent {\n /** Email subject line */\n subject?: string;\n /** Plain text body */\n text?: string;\n /** HTML body — will be stripped before validation */\n html?: string;\n}\n\nexport type MailValidationResult =\n | { isValid: true }\n | { isValid: false; field: 'subject' | 'text' | 'html'; reason: string; message: string };\n\n/**\n * Validate a Nodemailer mail options object before passing to sendMail().\n *\n * Strips HTML from the `html` field automatically.\n * Validates subject, text, and html fields independently.\n *\n * @param mail - The mail content to validate (subset of Nodemailer's MailOptions)\n * @param validator - Optional custom validator. Defaults to noProfanity + noSpam.\n *\n * @example\n * const result = validateMailContent({\n * subject: 'Hello',\n * html: '<p>Your order is confirmed.</p>',\n * });\n * if (!result.isValid) {\n * throw new Error(result.message); // never reaches sendMail()\n * }\n * await transporter.sendMail({ ...mailOptions });\n */\nexport function validateMailContent(\n mail: MailContent,\n validator?: InputShieldValidator\n): MailValidationResult {\n // Default validator: profanity + spam, generous length limits for email\n const v = validator ?? createValidator()\n .field('Email content')\n .min(1)\n .max(50000)\n .noProfanity()\n .noSpam();\n\n const fields: Array<{ key: 'subject' | 'text' | 'html'; value: string }> = [];\n\n if (mail.subject) {\n fields.push({ key: 'subject', value: mail.subject });\n }\n if (mail.text) {\n fields.push({ key: 'text', value: mail.text });\n }\n if (mail.html) {\n // Strip HTML before validating — this is the critical step\n fields.push({ key: 'html', value: stripHtml(mail.html) });\n }\n\n for (const { key, value } of fields) {\n const result = v.validate(value);\n if (!result.isValid) {\n return {\n isValid: false,\n field: key,\n reason: result.reason,\n message: result.message,\n };\n }\n }\n\n return { isValid: true };\n}"]}
package/dist/index.cjs ADDED
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ var chunkKVXEPETW_cjs = require('./chunk-KVXEPETW.cjs');
4
+ var chunk67CBN3U4_cjs = require('./chunk-67CBN3U4.cjs');
5
+
6
+
7
+
8
+ Object.defineProperty(exports, "validateBio", {
9
+ enumerable: true,
10
+ get: function () { return chunkKVXEPETW_cjs.validateBio; }
11
+ });
12
+ Object.defineProperty(exports, "validateLongText", {
13
+ enumerable: true,
14
+ get: function () { return chunkKVXEPETW_cjs.validateLongText; }
15
+ });
16
+ Object.defineProperty(exports, "validateSearchQuery", {
17
+ enumerable: true,
18
+ get: function () { return chunkKVXEPETW_cjs.validateSearchQuery; }
19
+ });
20
+ Object.defineProperty(exports, "validateShortText", {
21
+ enumerable: true,
22
+ get: function () { return chunkKVXEPETW_cjs.validateShortText; }
23
+ });
24
+ Object.defineProperty(exports, "validateUsername", {
25
+ enumerable: true,
26
+ get: function () { return chunkKVXEPETW_cjs.validateUsername; }
27
+ });
28
+ Object.defineProperty(exports, "InputShieldValidator", {
29
+ enumerable: true,
30
+ get: function () { return chunk67CBN3U4_cjs.InputShieldValidator; }
31
+ });
32
+ Object.defineProperty(exports, "containsProfanity", {
33
+ enumerable: true,
34
+ get: function () { return chunk67CBN3U4_cjs.containsProfanity; }
35
+ });
36
+ Object.defineProperty(exports, "containsSpam", {
37
+ enumerable: true,
38
+ get: function () { return chunk67CBN3U4_cjs.containsSpam; }
39
+ });
40
+ Object.defineProperty(exports, "createValidator", {
41
+ enumerable: true,
42
+ get: function () { return chunk67CBN3U4_cjs.createValidator; }
43
+ });
44
+ Object.defineProperty(exports, "getMatchedProfanityPattern", {
45
+ enumerable: true,
46
+ get: function () { return chunk67CBN3U4_cjs.getMatchedProfanityPattern; }
47
+ });
48
+ Object.defineProperty(exports, "hasExcessiveSymbols", {
49
+ enumerable: true,
50
+ get: function () { return chunk67CBN3U4_cjs.hasExcessiveSymbols; }
51
+ });
52
+ Object.defineProperty(exports, "hasLowAlphabetRatio", {
53
+ enumerable: true,
54
+ get: function () { return chunk67CBN3U4_cjs.hasLowAlphabetRatio; }
55
+ });
56
+ Object.defineProperty(exports, "hasRepeatedContentWords", {
57
+ enumerable: true,
58
+ get: function () { return chunk67CBN3U4_cjs.hasRepeatedContentWords; }
59
+ });
60
+ Object.defineProperty(exports, "hasRepeatingChars", {
61
+ enumerable: true,
62
+ get: function () { return chunk67CBN3U4_cjs.hasRepeatingChars; }
63
+ });
64
+ Object.defineProperty(exports, "isGibberish", {
65
+ enumerable: true,
66
+ get: function () { return chunk67CBN3U4_cjs.isGibberish; }
67
+ });
68
+ Object.defineProperty(exports, "isLowEffortExact", {
69
+ enumerable: true,
70
+ get: function () { return chunk67CBN3U4_cjs.isLowEffortExact; }
71
+ });
72
+ Object.defineProperty(exports, "toSkeleton", {
73
+ enumerable: true,
74
+ get: function () { return chunk67CBN3U4_cjs.toSkeleton; }
75
+ });
76
+ Object.defineProperty(exports, "toStructural", {
77
+ enumerable: true,
78
+ get: function () { return chunk67CBN3U4_cjs.toStructural; }
79
+ });
80
+ //# sourceMappingURL=index.cjs.map
81
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.cjs"}
@@ -0,0 +1,217 @@
1
+ import { V as ValidationResult, G as GibberishSensitivity } from './builder-C10YQjbV.cjs';
2
+ export { F as FailReason, I as InputShieldValidator, a as ValidationOptions, c as createValidator } from './builder-C10YQjbV.cjs';
3
+
4
+ /**
5
+ * presets.ts
6
+ *
7
+ * Ready-to-use validators for the most common field types.
8
+ * Zero config — import and call.
9
+ *
10
+ * All presets are pre-configured InputShieldValidator instances.
11
+ * You can also use them as a starting point and extend with .custom():
12
+ *
13
+ * import { usernameValidator } from 'input-shield/presets';
14
+ * // They're factories, so each call gives a fresh instance:
15
+ * const myValidator = usernameValidator().custom(t => t === 'admin', 'custom', 'reserved name');
16
+ */
17
+
18
+ /**
19
+ * Username / display name.
20
+ * 3–30 chars, no profanity, strict gibberish detection, no spam.
21
+ * Repeated words allowed (e.g. "John John" as a nickname).
22
+ */
23
+ declare function validateUsername(text: string): ValidationResult;
24
+ /**
25
+ * Short text / name fields (product name, company name, form title).
26
+ * 2–100 chars, no profanity, normal gibberish, no spam.
27
+ */
28
+ declare function validateShortText(text: string, fieldName?: string): ValidationResult;
29
+ /**
30
+ * Bio / description / about me.
31
+ * 10–300 chars, no profanity, no spam, loose gibberish (allows natural language).
32
+ * Repeated words NOT flagged (natural in prose).
33
+ */
34
+ declare function validateBio(text: string): ValidationResult;
35
+ /**
36
+ * Long-form text (comment, review, feedback).
37
+ * 5–2000 chars, no profanity, no spam. No gibberish check
38
+ * (long text can contain intentional fragments, code, etc.)
39
+ */
40
+ declare function validateLongText(text: string, fieldName?: string): ValidationResult;
41
+ /**
42
+ * Search query input.
43
+ * 1–200 chars, no spam URLs (but allows short/fragmentary text).
44
+ * Does NOT flag gibberish (code snippets, SKUs, part numbers are valid queries).
45
+ */
46
+ declare function validateSearchQuery(text: string): ValidationResult;
47
+
48
+ /**
49
+ * normalize.ts
50
+ *
51
+ * THREE-STAGE normalization pipeline.
52
+ * This is THE most important file in the package.
53
+ *
54
+ * Stage 1 — Unicode NFKC:
55
+ * Collapses compatibility variants before anything else.
56
+ * "A" (fullwidth) → "A", "fi" (ligature) → "fi", "𝐅" (math bold) → "F"
57
+ * This also handles bidirectional control characters and zero-width spaces.
58
+ *
59
+ * Stage 2 — Homoglyph map:
60
+ * Catches Cyrillic/Greek/Armenian lookalikes that survive NFKC.
61
+ * "о" (U+043E Cyrillic) → "o", "а" (U+0430 Cyrillic) → "a"
62
+ * This is the CVE-2025-27611 class of bypass — NFKC alone doesn't fix it.
63
+ *
64
+ * Stage 3 — Leet-speak map:
65
+ * Classic ASCII substitutions: "3" → "e", "@" → "a", "$" → "s", etc.
66
+ * Runs LAST so homoglyphs don't interfere with leet detection.
67
+ *
68
+ * Result: "P.0.r.n" → "porn", "ſhit" → "shit", "аss" (Cyrillic а) → "ass"
69
+ *
70
+ * IMPORTANT: This output is ONLY for pattern matching — never display it.
71
+ * Always return error messages that reference the original input.
72
+ */
73
+ /**
74
+ * Produce a "skeleton" string for pattern matching ONLY.
75
+ *
76
+ * Pipeline:
77
+ * raw → NFKC → stripSeparators → homoglyph → lowercase → leet → strip non-alpha → collapse spaces
78
+ *
79
+ * @param t - Raw input string
80
+ * @returns Normalized string safe for regex pattern matching
81
+ */
82
+ declare function toSkeleton(t: string): string;
83
+ /**
84
+ * Lightweight normalization for structural checks (length, symbol ratio).
85
+ * Does NOT apply leet/homoglyph maps — just trims and normalizes whitespace.
86
+ * Preserves symbols so hasExcessiveSymbols() works correctly.
87
+ */
88
+ declare function toStructural(t: string): string;
89
+
90
+ /**
91
+ * profanity.ts
92
+ *
93
+ * Profanity detection. All patterns run against the skeleton (toSkeleton()),
94
+ * meaning they automatically handle:
95
+ * - Leet-speak: "f4ck", "@ss", "sh!t"
96
+ * - Homoglyphs: "fuсk" (Cyrillic с), "аss" (Cyrillic а)
97
+ * - Separator dots: "f.u.c.k", "s-h-i-t"
98
+ * - Fullwidth: "fuck"
99
+ * - Repeated chars: "fuuuuck", "shhhhit"
100
+ *
101
+ * Pattern design:
102
+ * - Use \b word boundaries so "classic" doesn't match "ass"
103
+ * - Use + quantifiers to catch character stretching ("fuuuuck")
104
+ * - Cover plurals and -er/-ing forms (shits, bitch, bitching)
105
+ */
106
+ /**
107
+ * Returns true if the skeleton of `text` matches any profanity pattern.
108
+ * Always check skeleton — never raw — so bypasses don't work.
109
+ */
110
+ declare function containsProfanity(text: string): boolean;
111
+ /**
112
+ * Returns the specific pattern that matched, or null.
113
+ * Useful for debug logging (never expose to users directly).
114
+ */
115
+ declare function getMatchedProfanityPattern(text: string): RegExp | null;
116
+
117
+ /**
118
+ * spam.ts
119
+ *
120
+ * Spam detection.
121
+ *
122
+ * CRITICAL DESIGN DECISION:
123
+ * URL and domain patterns MUST run on the raw (or NFKC-only) text.
124
+ * If you normalize first (stripping dots, slashes, colons), URLs become
125
+ * undetectable. "https://spam.com" → after skeleton → "httpsspamcom" which
126
+ * no URL regex can match.
127
+ *
128
+ * So: keyword spam runs on skeleton (catches leet evasion),
129
+ * URL/domain spam runs on NFKC-normalized raw text only.
130
+ */
131
+ /**
132
+ * Returns true if text contains spam keywords (checked on skeleton)
133
+ * or URLs/domains (checked on NFKC-only normalized text).
134
+ */
135
+ declare function containsSpam(text: string): boolean;
136
+
137
+ /**
138
+ * gibberish.ts
139
+ *
140
+ * Gibberish / keyboard-mash detection with a configurable sensitivity scale.
141
+ *
142
+ * Why sensitivity levels?
143
+ * "Strict" mode is needed for display names & usernames.
144
+ * "Loose" mode prevents false positives on:
145
+ * - Polish names: "Krzysztof", "Szczepański"
146
+ * - Technical strings: "kubectl", "nginx", "src"
147
+ * - Abbreviations: "HVAC", "VLSI"
148
+ * - Short legitimate words like "nth", "gym", "lynx"
149
+ *
150
+ * Heuristics used (layered by sensitivity):
151
+ * LOOSE: 7+ consonants in a row (obvious keyboard mash only)
152
+ * NORMAL: 6+ consonants in a row OR vowel ratio < 10% on words ≥ 8 chars
153
+ * STRICT: 5+ consonants in a row OR vowel ratio < 15% on words ≥ 6 chars
154
+ * OR no vowels at all on words ≥ 4 chars
155
+ *
156
+ * All checks run on the skeleton so leet/homoglyphs are already resolved.
157
+ */
158
+
159
+ /**
160
+ * Returns true if any word in the text looks like gibberish.
161
+ *
162
+ * Filters out very short words (< 4 chars) before applying heuristics
163
+ * to prevent false positives on "by", "mr", "st", "nth", etc.
164
+ */
165
+ declare function isGibberish(text: string, sensitivity?: GibberishSensitivity): boolean;
166
+ /**
167
+ * Returns true if the text contains 5+ of the same character consecutively.
168
+ * e.g. "aaaaaaa", "!!!!!!", "heeeeey" (5 e's)
169
+ * Runs on skeleton so leet chars are already resolved.
170
+ */
171
+ declare function hasRepeatingChars(text: string): boolean;
172
+
173
+ /**
174
+ * structure.ts
175
+ *
176
+ * Structural quality checks. These run on the NFKC-normalized original text
177
+ * (not the full skeleton), because they're measuring the *shape* of the input
178
+ * (symbol density, letter presence) — not its semantic content.
179
+ *
180
+ * Running these on the skeleton would give wrong results because the skeleton
181
+ * strips ALL symbols, making everything look clean structurally.
182
+ */
183
+ /**
184
+ * Returns true if more than 40% of characters are symbols
185
+ * (not letters, digits, or whitespace).
186
+ *
187
+ * Short strings (< 5 chars) are excluded — too noisy to judge.
188
+ * e.g. "!!??@@##" → 100% symbols → flagged
189
+ * "Hello!!" → 22% symbols → pass
190
+ */
191
+ declare function hasExcessiveSymbols(text: string): boolean;
192
+ /**
193
+ * Returns true if fewer than 20% of characters are letters AND
194
+ * there are fewer than 3 total letter characters.
195
+ *
196
+ * This catches strings like "123 456", "--- ---", "42", "!2!"
197
+ * but allows legitimate short inputs like "QA", "IT", "Go".
198
+ *
199
+ * Both conditions must be true to avoid false positives.
200
+ */
201
+ declare function hasLowAlphabetRatio(text: string): boolean;
202
+ /**
203
+ * Detects repeated *content* words (length > 3).
204
+ * Stop words ("the", "and", "for") are excluded to avoid false positives
205
+ * in natural English sentences.
206
+ *
207
+ * Flags only when 2+ distinct content words appear more than once.
208
+ * "the cat sat on the mat" → 0 repeated content words → pass
209
+ * "cat cat cat dog dog" → "cat" repeated, "dog" repeated → flag
210
+ */
211
+ declare function hasRepeatedContentWords(text: string): boolean;
212
+ /**
213
+ * Returns true if the skeleton of `text` exactly matches a known low-effort phrase.
214
+ */
215
+ declare function isLowEffortExact(skeletonText: string): boolean;
216
+
217
+ export { GibberishSensitivity, ValidationResult, containsProfanity, containsSpam, getMatchedProfanityPattern, hasExcessiveSymbols, hasLowAlphabetRatio, hasRepeatedContentWords, hasRepeatingChars, isGibberish, isLowEffortExact, toSkeleton, toStructural, validateBio, validateLongText, validateSearchQuery, validateShortText, validateUsername };