@codelegend/email-validator 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ declare namespace emailValidator {
2
+ interface Options {
3
+ /** Exact exceptions overriding bundled and custom blocks. */
4
+ allowedDomains?: Iterable<string>;
5
+ blockedDomains?: Iterable<string>;
6
+ /** Apply blocking rules to subdomains. Default true. */
7
+ blockSubdomains?: boolean;
8
+ /** Use bundled disposable domains. Default true. */
9
+ useDefaultDomains?: boolean;
10
+ }
11
+ interface Accepted { valid: true; code: 'EMAIL_ACCEPTED'; message: string; email: string; domain: string; }
12
+ interface Rejected { valid: false; code: 'EMAIL_REQUIRED' | 'INVALID_EMAIL' | 'BLOCKED_DOMAIN' | 'DISPOSABLE_EMAIL'; message: string; email?: string; domain?: string; matchedDomain?: string; }
13
+ type Result = Accepted | Rejected;
14
+ interface MiddlewareRequest { body?: { email?: unknown; [key: string]: unknown }; }
15
+ interface MiddlewareResponse { status(code: number): MiddlewareResponse; json(body: Rejected): unknown; }
16
+ interface Validator {
17
+ validateEmail(input: unknown): Result;
18
+ validateMany(inputs: readonly unknown[]): Result[];
19
+ isValid(input: unknown): boolean;
20
+ middleware(req: MiddlewareRequest, res: MiddlewareResponse, next: () => void): unknown;
21
+ }
22
+ function createEmailValidator(options?: Options): Validator;
23
+ function validateEmail(input: unknown): Result;
24
+ function isValidEmail(input: unknown): boolean;
25
+ }
26
+ export = emailValidator;
package/index.js ADDED
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+ const { domainToASCII } = require('node:url');
3
+ const bundledDomains = new Set(require('./disposable-domains.json'));
4
+
5
+ function normalizeDomain(value) {
6
+ if (typeof value !== 'string') return '';
7
+ const raw = value.trim().toLowerCase();
8
+ // IDNA conversion must not silently discard URL paths, ports, or other syntax.
9
+ if (!raw || /[\s/@:#?\\%\[\]]/.test(raw)) return '';
10
+ let domain;
11
+ try { domain = domainToASCII(raw); } catch { return ''; }
12
+ const labels = domain.split('.');
13
+ if (domain.length > 253 || labels.length < 2 || /^\d+$/.test(labels.at(-1))) return '';
14
+ return labels.every(label => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)) ? domain : '';
15
+ }
16
+
17
+ function domainSet(values, name) {
18
+ if (values == null || typeof values === 'string' || typeof values[Symbol.iterator] !== 'function') throw new TypeError(`${name} must be an iterable of domain strings, such as an array or Set.`);
19
+ const domains = new Set();
20
+ for (const value of values) {
21
+ const domain = normalizeDomain(value);
22
+ if (!domain) throw new TypeError(`Invalid domain in ${name}: ${String(value)}`);
23
+ domains.add(domain);
24
+ }
25
+ return domains;
26
+ }
27
+
28
+ /** Compile independent domain sets once and reuse across requests. */
29
+ function createEmailValidator(options = {}) {
30
+ if (!options || typeof options !== 'object' || Array.isArray(options)) throw new TypeError('Options must be an object.');
31
+ const { allowedDomains = [], blockedDomains = [], blockSubdomains = true, useDefaultDomains = true } = options;
32
+ if (typeof blockSubdomains !== 'boolean' || typeof useDefaultDomains !== 'boolean') throw new TypeError('blockSubdomains and useDefaultDomains must be booleans.');
33
+ const allowed = domainSet(allowedDomains, 'allowedDomains');
34
+ const blocked = domainSet(blockedDomains, 'blockedDomains');
35
+
36
+ function match(domain, domains) {
37
+ if (domains.has(domain)) return domain;
38
+ if (!blockSubdomains) return null;
39
+ const labels = domain.split('.');
40
+ for (let i = 1; i < labels.length - 1; i++) {
41
+ const parent = labels.slice(i).join('.');
42
+ if (domains.has(parent)) return parent;
43
+ }
44
+ return null;
45
+ }
46
+
47
+ function validateEmail(input) {
48
+ const reject = (code, message, extra = {}) => ({ valid: false, code, message, ...extra });
49
+ if (typeof input !== 'string' || !input.trim()) return reject('EMAIL_REQUIRED', 'Email is required');
50
+ const parts = input.trim().split('@');
51
+ if (parts.length !== 2) return reject('INVALID_EMAIL', 'Invalid email format');
52
+ const [localPart, rawDomain] = parts;
53
+ const domain = rawDomain === rawDomain.trim() ? normalizeDomain(rawDomain) : '';
54
+ const validLocal = /^[A-Za-z0-9!#$%&'*+/=?^_`{|}~.-]+$/.test(localPart) && !localPart.startsWith('.') && !localPart.endsWith('.') && !localPart.includes('..');
55
+ if (!validLocal || !domain || localPart.length > 64 || localPart.length + domain.length + 1 > 254) return reject('INVALID_EMAIL', 'Invalid email format');
56
+ const email = `${localPart}@${domain}`;
57
+ // Exact allowlist exceptions override custom and bundled blocks after syntax checks.
58
+ if (allowed.has(domain)) return { valid: true, code: 'EMAIL_ACCEPTED', message: 'Email accepted', email, domain };
59
+ const customMatch = match(domain, blocked);
60
+ if (customMatch) return reject('BLOCKED_DOMAIN', 'Email domain is blocked', { email, domain, matchedDomain: customMatch });
61
+ const disposableMatch = useDefaultDomains && match(domain, bundledDomains);
62
+ if (disposableMatch) return reject('DISPOSABLE_EMAIL', 'Temporary or disposable emails are not allowed', { email, domain, matchedDomain: disposableMatch });
63
+ return { valid: true, code: 'EMAIL_ACCEPTED', message: 'Email accepted', email, domain };
64
+ }
65
+
66
+ function validateMany(inputs) {
67
+ if (!Array.isArray(inputs)) throw new TypeError('validateMany expects an array.');
68
+ return inputs.map(validateEmail);
69
+ }
70
+
71
+ function middleware(req, res, next) {
72
+ const result = validateEmail(req.body?.email);
73
+ if (!result.valid) return res.status(400).json(result);
74
+ req.body.email = result.email;
75
+ return next();
76
+ }
77
+
78
+ return Object.freeze({ validateEmail, validateMany, isValid: input => validateEmail(input).valid, middleware });
79
+ }
80
+
81
+ const defaultValidator = createEmailValidator();
82
+ module.exports = { createEmailValidator, validateEmail: defaultValidator.validateEmail, isValidEmail: defaultValidator.isValid };
package/package.json CHANGED
@@ -1,6 +1,30 @@
1
1
  {
2
2
  "name": "@codelegend/email-validator",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.1",
4
+ "description": "Reusable email validation and disposable domain blocking",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "type": "commonjs",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./index.d.ts",
11
+ "default": "./index.js"
12
+ },
13
+ "./disposable-domains.json": "./disposable-domains.json"
14
+ },
15
+ "files": [
16
+ "index.js",
17
+ "index.d.ts",
18
+ "disposable-domains.json"
19
+ ],
20
+ "license": "MIT",
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "keywords": [
25
+ "email",
26
+ "validator",
27
+ "disposable",
28
+ "express"
29
+ ]
30
+ }