backend-manager 5.1.0 → 5.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/CLAUDE.md +87 -58
- package/docs/common-mistakes.md +11 -0
- package/docs/key-files.md +36 -0
- package/package.json +1 -1
- package/scripts/update-disposable-domains.js +1 -1
- package/src/manager/functions/core/actions/api/general/add-marketing-contact.js +5 -0
- package/src/manager/libraries/email/data/blocked-local-parts.json +55 -0
- package/src/manager/libraries/email/data/blocked-local-patterns.js +11 -0
- package/src/manager/libraries/email/data/corporate-domains.json +23 -0
- package/src/manager/libraries/{disposable-domains.json → email/data/disposable-domains.json} +1 -0
- package/src/manager/libraries/email/marketing/index.js +11 -0
- package/src/manager/libraries/email/validation.js +53 -38
- package/src/manager/routes/marketing/contact/post.js +5 -1
- package/test/helpers/email-validation.js +141 -3
- /package/src/manager/libraries/{custom-disposable-domains.json → email/data/custom-disposable-domains.json} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
14
14
|
- `Fixed` for any bug fixes.
|
|
15
15
|
- `Security` in case of vulnerabilities.
|
|
16
16
|
|
|
17
|
+
# [5.1.1] - 2026-05-13
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **Corporate / social-media domain blacklist** — new `corporate` check in `email/validation.js` blocks marketing-list adds from corporate social-media domains (meta.com, instagram.com, soundcloud.com, tiktok.com, x.com, reddit.com, linkedin.com, youtube.com, discord.com, telegram.org, signal.org, and more — 21 domains total). Added to `DEFAULT_CHECKS` so every existing caller of `validate()` picks it up automatically. New `isCorporate(emailOrDomain)` helper exported alongside `isDisposable()`.
|
|
22
|
+
- **Defense-in-depth guards** in `Marketing.add()` and `Marketing.sync()` — even when validation is bypassed (e.g. testing mode), corporate domains are blocked before any Beehiiv or SendGrid call.
|
|
23
|
+
- **13 new tests** in `test/helpers/email-validation.js` covering the `corporate` validate-check, the `isCorporate()` helper, case-insensitivity, edge cases, and check-ordering behavior. Suite: 44 pass, 0 fail, 2 skip (ZeroBounce-only).
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **Email data files reorganized** — all blacklists now live in `src/manager/libraries/email/data/` (one folder, next to their consumer), instead of being scattered one level up alongside unrelated libraries:
|
|
28
|
+
- `disposable-domains.json`, `custom-disposable-domains.json`, `corporate-domains.json` — domain blocklists
|
|
29
|
+
- `blocked-local-parts.json` — categorized local-part blocklist (`generic`, `system`, `junk`, `placeholder`), extracted from a hardcoded `Set` in `validation.js`
|
|
30
|
+
- `blocked-local-patterns.js` — regex patterns, extracted from `validation.js` (kept as JS so RegExp literals stay native)
|
|
31
|
+
- **`scripts/update-disposable-domains.js`** — prepare-step downloader now writes to `email/data/disposable-domains.json`.
|
|
32
|
+
|
|
17
33
|
# [5.1.0] - 2026-05-13
|
|
18
34
|
|
|
19
35
|
### Added
|
package/CLAUDE.md
CHANGED
|
@@ -2,24 +2,100 @@
|
|
|
2
2
|
|
|
3
3
|
> **Note for contributors and Claude:** This file is the architectural overview — identity, top-level conventions, and a map to deep references. The **meat** (per-subsystem APIs, behavior tables, recipes) lives in `docs/<topic>.md`. When extending or adding content, write it in the matching `docs/*.md` file and cross-link from here — do NOT inline it. If a topic doesn't have a doc yet, create one. Goal: keep this file under 250 lines.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Identity
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Backend Manager (BEM) is a comprehensive framework for building modern Firebase Cloud Functions backends. Sister project to Electron Manager (EM), Browser Extension Manager (BXM), and Ultimate Jekyll Manager (UJM). Provides a single `Manager.init(exports, {...})` bootstrap that wires built-in functions (`bm_api`, auth events, cron jobs), helper classes (Assistant, User, Analytics, Usage, Middleware, Settings, Utilities, Metadata), payment processor integrations (Stripe / PayPal), Firestore-trigger pipelines, marketing campaign automation, an MCP server, and a CLI for emulator/deploy/logs/auth/Firestore operations.
|
|
8
8
|
|
|
9
|
-
**This repository**
|
|
9
|
+
**This repository** is the BEM library itself. **Consumer projects** are Firebase projects that `require('backend-manager')` in their `functions/index.js`, with `backend-manager-config.json` + `service-account.json` alongside, plus optional `routes/`, `schemas/`, and `hooks/` directories for custom endpoints.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
- `functions/` directory with `index.js` that calls `Manager.init(exports, {...})`
|
|
13
|
-
- `backend-manager-config.json` configuration file
|
|
14
|
-
- `service-account.json` for Firebase credentials
|
|
15
|
-
- Optional `routes/` and `schemas/` directories for custom endpoints
|
|
11
|
+
## Recommended skills
|
|
16
12
|
|
|
17
|
-
|
|
13
|
+
Two Claude Code skills are tailored to this project. Both auto-trigger on relevant keywords and can be invoked manually with `/<skill-name>`:
|
|
14
|
+
|
|
15
|
+
- **`BEM:patterns`** — SSOT for Backend Manager routes, schemas, tests, Firebase functions, Firestore rules, usage tracking patterns. Auto-loads on BEM-specific keywords (`route`, `schema`, `endpoint`, `bm_api`, `Manager.init`, `npx mgr test`, `gcloud logs`, etc.) and when touching files in `functions/routes/`, `functions/schemas/`, `functions/index.js`, `test/`, `src/cli/commands/`.
|
|
16
|
+
- **`js:patterns`** — JavaScript/Node.js conventions: file structure, JSDoc, defensive coding (`?.` usage), template literals, `package.json` conventions. Auto-loads when creating new `.js` files or touching JS module structure.
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
### For Consuming Projects
|
|
21
|
+
|
|
22
|
+
1. `npm install backend-manager --save-dev` (inside `functions/`)
|
|
23
|
+
2. `npx mgr setup` — validates config, scaffolds defaults (CLAUDE.md, CHANGELOG.md, docs/, test/), provisions Firestore indexes
|
|
24
|
+
3. `npx mgr emulator` — start Firebase emulators (auth/firestore/functions/database/storage)
|
|
25
|
+
4. `npx mgr serve` — local serve with Stripe webhook forwarding (if `STRIPE_SECRET_KEY` is set)
|
|
26
|
+
5. `npx mgr test` — runs framework + project test suites against an emulator
|
|
27
|
+
6. `npx mgr deploy` — deploy Cloud Functions to Firebase
|
|
28
|
+
7. `npx mgr logs:read` / `npx mgr logs:tail` — Cloud Function logs from Google Cloud Logging
|
|
29
|
+
|
|
30
|
+
All `npx mgr <cmd>` aliases work: `npx bm <cmd>`, `npx bem <cmd>`, `npx backend-manager <cmd>`.
|
|
31
|
+
|
|
32
|
+
> **Important:** All `npx mgr ...` commands MUST be run from the consumer project's `functions/` subdirectory. The binary lives in `functions/node_modules/.bin/`.
|
|
33
|
+
|
|
34
|
+
### For Framework Development (This Repository)
|
|
35
|
+
|
|
36
|
+
1. `npm install` — install BEM's own deps
|
|
37
|
+
2. `npm run prepare` — build once: copies `src/` → `dist/` via prepare-package
|
|
38
|
+
3. `npm run prepare:watch` — watch mode
|
|
39
|
+
4. Test in a consumer project: from inside the consumer's `functions/` dir, run `npx mgr install local` (swaps BEM to the local repo via the `install` CLI). Reverse with `npx mgr install prod`.
|
|
40
|
+
|
|
41
|
+
## Architecture
|
|
18
42
|
|
|
19
43
|
BEM exposes a single `Manager` class that orchestrates everything: it initializes Firebase Admin, wires built-in functions (`bm_api`, auth events, cron), and hands out helper instances via factory methods. Supports **two deployment modes** — Firebase Functions (`projectType: 'firebase'`) or Custom Server (`projectType: 'custom'`). See [docs/architecture.md](docs/architecture.md) for the full overview of the Manager class, dual-mode support, and helper factory pattern.
|
|
20
44
|
|
|
21
45
|
For the directory layout of both the BEM library and consumer projects, see [docs/directory-structure.md](docs/directory-structure.md).
|
|
22
46
|
|
|
47
|
+
## CLI
|
|
48
|
+
|
|
49
|
+
`npx mgr <command>` (aliases `bm`, `bem`, `backend-manager`):
|
|
50
|
+
|
|
51
|
+
| Command | Description |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `setup` | Validate config, scaffold defaults (CLAUDE.md, CHANGELOG.md, docs/, test/), provision Firestore indexes |
|
|
54
|
+
| `emulator` | Start Firebase emulators (auth/firestore/functions/database/storage) |
|
|
55
|
+
| `serve` | Local Firebase serve (with auto Stripe webhook forwarding if keys set) |
|
|
56
|
+
| `watch` | Auto-reload functions on file change |
|
|
57
|
+
| `deploy` | Deploy Cloud Functions to Firebase |
|
|
58
|
+
| `test` | Run framework + project test suites against an emulator |
|
|
59
|
+
| `mcp` | Start the stdio MCP server (for Claude Code / Claude Desktop) |
|
|
60
|
+
| `firestore:get/set/query/delete` | Direct Firestore reads/writes from the terminal |
|
|
61
|
+
| `auth:get/list/delete/set-claims` | Manage Auth users from the terminal |
|
|
62
|
+
| `logs:read` / `logs:tail` | Cloud Function logs from Google Cloud Logging |
|
|
63
|
+
| `stripe` | Standalone Stripe CLI webhook forwarding |
|
|
64
|
+
| `indexes` | Sync required Firestore indexes into `firestore.indexes.json` |
|
|
65
|
+
| `firebase-init` | Run Firebase Admin SDK initialization helper |
|
|
66
|
+
| `clean` | Remove generated artifacts (logs, test outputs) |
|
|
67
|
+
| `version` | Print BEM version |
|
|
68
|
+
|
|
69
|
+
See [docs/cli-firestore-auth.md](docs/cli-firestore-auth.md) and [docs/cli-logs.md](docs/cli-logs.md) for full flag references.
|
|
70
|
+
|
|
71
|
+
## File Conventions
|
|
72
|
+
|
|
73
|
+
- **CommonJS** throughout. `prepare-package` copies `src/` → `dist/` 1:1 (no transforms).
|
|
74
|
+
- **`fs-jetpack`** over `fs` / `fs-extra` for file operations.
|
|
75
|
+
- One `module.exports = ...` per file.
|
|
76
|
+
- **Short-circuit early returns** rather than nested ifs.
|
|
77
|
+
- **Logical operators at the start of continuation lines** (`|| condB` on a new line, not `condA ||` trailing).
|
|
78
|
+
- **Firestore shorthand**: `admin.firestore().doc('users/abc123')` (path string) rather than `.collection('users').doc('abc123')`.
|
|
79
|
+
- **Template strings for requires**: `` require(`${functionsDir}/node_modules/backend-manager`) `` rather than string concat.
|
|
80
|
+
- **No backwards compatibility** unless explicitly requested.
|
|
81
|
+
- **Routes receive sanitized data by default** — see [docs/sanitization.md](docs/sanitization.md) for opt-out rules.
|
|
82
|
+
- **Match schema names to route names** — if route is `myEndpoint`, schema is `myEndpoint`.
|
|
83
|
+
- **Always use `assistant.respond()` for responses** — do NOT use `res.send()` directly.
|
|
84
|
+
- **Add Firestore composite indexes** for any compound query (`where` + `orderBy`, or multiple `where`s) to `src/cli/commands/setup-tests/helpers/required-indexes.js` (the SSOT). Without the index, queries crash with `FAILED_PRECONDITION` in production.
|
|
85
|
+
|
|
86
|
+
See [docs/code-patterns.md](docs/code-patterns.md) for code-pattern detail, [docs/common-mistakes.md](docs/common-mistakes.md) for the full anti-pattern checklist, and [docs/file-naming.md](docs/file-naming.md) for the naming table (routes / schemas / API commands / events / cron jobs / hooks).
|
|
87
|
+
|
|
88
|
+
## Doc-update parity
|
|
89
|
+
|
|
90
|
+
Whenever you make a behavioral change (new command, new flag, new pattern, removed feature), update:
|
|
91
|
+
|
|
92
|
+
1. **`README.md`** — user-facing summary
|
|
93
|
+
2. **`CLAUDE.md`** (this file) — architecture overview, one paragraph or cross-link
|
|
94
|
+
3. **`docs/<topic>.md`** — the meat. If a topic doesn't have a doc yet, create one.
|
|
95
|
+
4. **`CHANGELOG.md`** — if the project keeps one
|
|
96
|
+
|
|
97
|
+
Don't ship behavioral changes with stale docs. Validate first, then document — write docs that describe shipped reality, not intentions.
|
|
98
|
+
|
|
23
99
|
## Documentation
|
|
24
100
|
|
|
25
101
|
Deep references live in `docs/`. **Whenever you make a behavioral change, update both this overview AND the relevant `docs/*.md` deep reference.**
|
|
@@ -30,6 +106,8 @@ Deep references live in `docs/`. **Whenever you make a behavioral change, update
|
|
|
30
106
|
- [docs/directory-structure.md](docs/directory-structure.md) — BEM library + consumer project layouts
|
|
31
107
|
- [docs/code-patterns.md](docs/code-patterns.md) — short-circuit returns, logical operators on new lines, Firestore shorthand, template-string requires, fs-jetpack preference
|
|
32
108
|
- [docs/file-naming.md](docs/file-naming.md) — naming table for routes, schemas, API commands, events, cron jobs, hooks
|
|
109
|
+
- [docs/common-mistakes.md](docs/common-mistakes.md) — anti-pattern checklist (don't modify Manager internals, always await, increment-before-update, etc.)
|
|
110
|
+
- [docs/key-files.md](docs/key-files.md) — quick lookup for the most-touched files (Manager, helpers, auth events, cron, payment processors, CLI commands)
|
|
33
111
|
- [docs/environment-detection.md](docs/environment-detection.md) — `assistant.isDevelopment/isProduction/isTesting()`
|
|
34
112
|
- [docs/response-headers.md](docs/response-headers.md) — automatic `bm-properties` header
|
|
35
113
|
|
|
@@ -60,52 +138,3 @@ Deep references live in `docs/`. **Whenever you make a behavioral change, update
|
|
|
60
138
|
- [docs/testing.md](docs/testing.md) — running, filtering, log files, test types (standalone/suite/group), context object, assertions, auth levels
|
|
61
139
|
- [docs/cli-firestore-auth.md](docs/cli-firestore-auth.md) — `npx mgr firestore:*` and `auth:*` commands, shared flags, examples
|
|
62
140
|
- [docs/cli-logs.md](docs/cli-logs.md) — `npx mgr logs:read` / `logs:tail` with full flag reference and built-in Cloud Function names
|
|
63
|
-
|
|
64
|
-
## Common Mistakes to Avoid
|
|
65
|
-
|
|
66
|
-
1. **Don't modify Manager internals directly** — Use factory methods and public APIs
|
|
67
|
-
2. **Always use `assistant.respond()` for responses** — Don't use `res.send()` directly
|
|
68
|
-
3. **Match schema names to route names** — If route is `myEndpoint`, schema should be `myEndpoint`
|
|
69
|
-
4. **Always await async operations** — Don't forget `await` on Firestore operations
|
|
70
|
-
5. **Handle errors properly** — Use `assistant.errorify()` with appropriate status codes
|
|
71
|
-
6. **Don't call `respond()` multiple times** — Only one response per request
|
|
72
|
-
7. **Use short-circuit returns** — Return early from error conditions
|
|
73
|
-
8. **Increment usage before update** — Call `usage.increment()` then `usage.update()`
|
|
74
|
-
9. **Add Firestore composite indexes for new compound queries** — Any new Firestore query using multiple `.where()` clauses or `.where()` + `.orderBy()` requires a composite index. Add it to `src/cli/commands/setup-tests/helpers/required-indexes.js` (the SSOT). Consumer projects pick these up via `npx mgr setup`, which syncs them into `firestore.indexes.json`. Without the index, the query will crash with `FAILED_PRECONDITION` in production.
|
|
75
|
-
|
|
76
|
-
## Key Files Reference
|
|
77
|
-
|
|
78
|
-
| Purpose | File |
|
|
79
|
-
|---------|------|
|
|
80
|
-
| Main Manager class | `src/manager/index.js` |
|
|
81
|
-
| Request/response handling | `src/manager/helpers/assistant.js` |
|
|
82
|
-
| Middleware pipeline | `src/manager/helpers/middleware.js` |
|
|
83
|
-
| Schema validation | `src/manager/helpers/settings.js` |
|
|
84
|
-
| Rate limiting | `src/manager/helpers/usage.js` |
|
|
85
|
-
| User properties + schema | `src/manager/helpers/user.js` |
|
|
86
|
-
| Batch utilities | `src/manager/helpers/utilities.js` |
|
|
87
|
-
| Auth: before-create | `src/manager/events/auth/before-create.js` |
|
|
88
|
-
| Auth: before-signin | `src/manager/events/auth/before-signin.js` |
|
|
89
|
-
| Auth: on-create | `src/manager/events/auth/on-create.js` |
|
|
90
|
-
| Auth: on-delete | `src/manager/events/auth/on-delete.js` |
|
|
91
|
-
| Auth: shared utilities | `src/manager/events/auth/utils.js` |
|
|
92
|
-
| Cron runner | `src/manager/events/cron/runner.js` |
|
|
93
|
-
| Main API handler | `src/manager/functions/core/actions/api.js` |
|
|
94
|
-
| Config template | `templates/backend-manager-config.json` |
|
|
95
|
-
| CLI entry | `src/cli/index.js` |
|
|
96
|
-
| Stripe webhook forwarding | `src/cli/commands/stripe.js` |
|
|
97
|
-
| Firebase init helper (CLI) | `src/cli/commands/firebase-init.js` |
|
|
98
|
-
| Firestore CLI commands | `src/cli/commands/firestore.js` |
|
|
99
|
-
| Auth CLI commands | `src/cli/commands/auth.js` |
|
|
100
|
-
| Logs CLI commands | `src/cli/commands/logs.js` |
|
|
101
|
-
| Intent creation | `src/manager/routes/payments/intent/post.js` |
|
|
102
|
-
| Webhook ingestion | `src/manager/routes/payments/webhook/post.js` |
|
|
103
|
-
| Webhook processing (on-write) | `src/manager/events/firestore/payments-webhooks/on-write.js` |
|
|
104
|
-
| Payment analytics | `src/manager/events/firestore/payments-webhooks/analytics.js` |
|
|
105
|
-
| Transition detection | `src/manager/events/firestore/payments-webhooks/transitions/index.js` |
|
|
106
|
-
| Payment processor libraries | `src/manager/libraries/payment/processors/` |
|
|
107
|
-
| Stripe library | `src/manager/libraries/payment/processors/stripe.js` |
|
|
108
|
-
| PayPal library | `src/manager/libraries/payment/processors/paypal.js` |
|
|
109
|
-
| Order ID generator | `src/manager/libraries/payment/order-id.js` |
|
|
110
|
-
| Required Firestore indexes (SSOT) | `src/cli/commands/setup-tests/helpers/required-indexes.js` |
|
|
111
|
-
| Test accounts | `src/test/test-accounts.js` |
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Common Mistakes to Avoid
|
|
2
|
+
|
|
3
|
+
1. **Don't modify Manager internals directly** — Use factory methods and public APIs
|
|
4
|
+
2. **Always use `assistant.respond()` for responses** — Don't use `res.send()` directly
|
|
5
|
+
3. **Match schema names to route names** — If route is `myEndpoint`, schema should be `myEndpoint`
|
|
6
|
+
4. **Always await async operations** — Don't forget `await` on Firestore operations
|
|
7
|
+
5. **Handle errors properly** — Use `assistant.errorify()` with appropriate status codes
|
|
8
|
+
6. **Don't call `respond()` multiple times** — Only one response per request
|
|
9
|
+
7. **Use short-circuit returns** — Return early from error conditions
|
|
10
|
+
8. **Increment usage before update** — Call `usage.increment()` then `usage.update()`
|
|
11
|
+
9. **Add Firestore composite indexes for new compound queries** — Any new Firestore query using multiple `.where()` clauses or `.where()` + `.orderBy()` requires a composite index. Add it to `src/cli/commands/setup-tests/helpers/required-indexes.js` (the SSOT). Consumer projects pick these up via `npx mgr setup`, which syncs them into `firestore.indexes.json`. Without the index, the query will crash with `FAILED_PRECONDITION` in production.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Key Files Reference
|
|
2
|
+
|
|
3
|
+
| Purpose | File |
|
|
4
|
+
|---------|------|
|
|
5
|
+
| Main Manager class | `src/manager/index.js` |
|
|
6
|
+
| Request/response handling | `src/manager/helpers/assistant.js` |
|
|
7
|
+
| Middleware pipeline | `src/manager/helpers/middleware.js` |
|
|
8
|
+
| Schema validation | `src/manager/helpers/settings.js` |
|
|
9
|
+
| Rate limiting | `src/manager/helpers/usage.js` |
|
|
10
|
+
| User properties + schema | `src/manager/helpers/user.js` |
|
|
11
|
+
| Batch utilities | `src/manager/helpers/utilities.js` |
|
|
12
|
+
| Auth: before-create | `src/manager/events/auth/before-create.js` |
|
|
13
|
+
| Auth: before-signin | `src/manager/events/auth/before-signin.js` |
|
|
14
|
+
| Auth: on-create | `src/manager/events/auth/on-create.js` |
|
|
15
|
+
| Auth: on-delete | `src/manager/events/auth/on-delete.js` |
|
|
16
|
+
| Auth: shared utilities | `src/manager/events/auth/utils.js` |
|
|
17
|
+
| Cron runner | `src/manager/events/cron/runner.js` |
|
|
18
|
+
| Main API handler | `src/manager/functions/core/actions/api.js` |
|
|
19
|
+
| Config template | `templates/backend-manager-config.json` |
|
|
20
|
+
| CLI entry | `src/cli/index.js` |
|
|
21
|
+
| Stripe webhook forwarding | `src/cli/commands/stripe.js` |
|
|
22
|
+
| Firebase init helper (CLI) | `src/cli/commands/firebase-init.js` |
|
|
23
|
+
| Firestore CLI commands | `src/cli/commands/firestore.js` |
|
|
24
|
+
| Auth CLI commands | `src/cli/commands/auth.js` |
|
|
25
|
+
| Logs CLI commands | `src/cli/commands/logs.js` |
|
|
26
|
+
| Intent creation | `src/manager/routes/payments/intent/post.js` |
|
|
27
|
+
| Webhook ingestion | `src/manager/routes/payments/webhook/post.js` |
|
|
28
|
+
| Webhook processing (on-write) | `src/manager/events/firestore/payments-webhooks/on-write.js` |
|
|
29
|
+
| Payment analytics | `src/manager/events/firestore/payments-webhooks/analytics.js` |
|
|
30
|
+
| Transition detection | `src/manager/events/firestore/payments-webhooks/transitions/index.js` |
|
|
31
|
+
| Payment processor libraries | `src/manager/libraries/payment/processors/` |
|
|
32
|
+
| Stripe library | `src/manager/libraries/payment/processors/stripe.js` |
|
|
33
|
+
| PayPal library | `src/manager/libraries/payment/processors/paypal.js` |
|
|
34
|
+
| Order ID generator | `src/manager/libraries/payment/order-id.js` |
|
|
35
|
+
| Required Firestore indexes (SSOT) | `src/cli/commands/setup-tests/helpers/required-indexes.js` |
|
|
36
|
+
| Test accounts | `src/test/test-accounts.js` |
|
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@ const fs = require('fs');
|
|
|
13
13
|
const path = require('path');
|
|
14
14
|
|
|
15
15
|
const SOURCE_URL = 'https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/main/disposable_email_blocklist.conf';
|
|
16
|
-
const OUTPUT_PATH = path.join(__dirname, '..', 'src', 'manager', 'libraries', 'disposable-domains.json');
|
|
16
|
+
const OUTPUT_PATH = path.join(__dirname, '..', 'src', 'manager', 'libraries', 'email', 'data', 'disposable-domains.json');
|
|
17
17
|
|
|
18
18
|
async function main() {
|
|
19
19
|
console.log('Fetching disposable domain list...');
|
|
@@ -89,6 +89,11 @@ Module.prototype.main = function () {
|
|
|
89
89
|
return reject(assistant.errorify(`Disposable email domain not allowed: ${disposable.domain}`, { code: 400 }));
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
const corporate = validation.checks.corporate;
|
|
93
|
+
if (corporate && !corporate.valid) {
|
|
94
|
+
return reject(assistant.errorify(`Corporate/social-media domain not allowed: ${corporate.domain}`, { code: 400 }));
|
|
95
|
+
}
|
|
96
|
+
|
|
92
97
|
return reject(assistant.errorify('Email validation failed', { code: 400 }));
|
|
93
98
|
}
|
|
94
99
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"generic": [
|
|
3
|
+
"test",
|
|
4
|
+
"testing",
|
|
5
|
+
"tester",
|
|
6
|
+
"test1",
|
|
7
|
+
"test123",
|
|
8
|
+
"example",
|
|
9
|
+
"sample",
|
|
10
|
+
"demo",
|
|
11
|
+
"dummy",
|
|
12
|
+
"fake",
|
|
13
|
+
"temp"
|
|
14
|
+
],
|
|
15
|
+
"system": [
|
|
16
|
+
"noreply",
|
|
17
|
+
"no-reply",
|
|
18
|
+
"donotreply",
|
|
19
|
+
"do-not-reply",
|
|
20
|
+
"mailer-daemon",
|
|
21
|
+
"postmaster",
|
|
22
|
+
"webmaster",
|
|
23
|
+
"hostmaster",
|
|
24
|
+
"abuse",
|
|
25
|
+
"spam",
|
|
26
|
+
"root"
|
|
27
|
+
],
|
|
28
|
+
"junk": [
|
|
29
|
+
"asdf",
|
|
30
|
+
"qwerty",
|
|
31
|
+
"zxcv",
|
|
32
|
+
"asd",
|
|
33
|
+
"qwe",
|
|
34
|
+
"aaa",
|
|
35
|
+
"bbb",
|
|
36
|
+
"xxx",
|
|
37
|
+
"zzz",
|
|
38
|
+
"abc",
|
|
39
|
+
"abc123",
|
|
40
|
+
"abcdef"
|
|
41
|
+
],
|
|
42
|
+
"placeholder": [
|
|
43
|
+
"name",
|
|
44
|
+
"firstname",
|
|
45
|
+
"lastname",
|
|
46
|
+
"foo",
|
|
47
|
+
"bar",
|
|
48
|
+
"baz",
|
|
49
|
+
"foobar",
|
|
50
|
+
"null",
|
|
51
|
+
"undefined",
|
|
52
|
+
"none",
|
|
53
|
+
"anonymous"
|
|
54
|
+
]
|
|
55
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regex patterns that indicate junk local parts (checked after stripping +suffix).
|
|
3
|
+
* Kept as JS (not JSON) so patterns stay as native RegExp literals.
|
|
4
|
+
*/
|
|
5
|
+
module.exports = [
|
|
6
|
+
/^\d+$/, // All numeric: 123456
|
|
7
|
+
/^(.)\1{2,}$/, // Repeating single char: aaaa, xxxx
|
|
8
|
+
/^[a-z]{1,2}\d+$/, // Single letter + numbers: a123, x999
|
|
9
|
+
/^test[._-]/, // Starts with test separator: test.user, test_123
|
|
10
|
+
/^example[._-]/, // Starts with example separator: example.user, example_123
|
|
11
|
+
];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[
|
|
2
|
+
"meta.com",
|
|
3
|
+
"fb.com",
|
|
4
|
+
"facebook.com",
|
|
5
|
+
"instagram.com",
|
|
6
|
+
"whatsapp.com",
|
|
7
|
+
"threads.net",
|
|
8
|
+
"twitter.com",
|
|
9
|
+
"x.com",
|
|
10
|
+
"tiktok.com",
|
|
11
|
+
"bytedance.com",
|
|
12
|
+
"snap.com",
|
|
13
|
+
"snapchat.com",
|
|
14
|
+
"pinterest.com",
|
|
15
|
+
"reddit.com",
|
|
16
|
+
"linkedin.com",
|
|
17
|
+
"soundcloud.com",
|
|
18
|
+
"youtube.com",
|
|
19
|
+
"twitch.tv",
|
|
20
|
+
"discord.com",
|
|
21
|
+
"telegram.org",
|
|
22
|
+
"signal.org"
|
|
23
|
+
]
|
|
@@ -29,6 +29,7 @@ const md = new MarkdownIt({ html: true, breaks: true, linkify: true });
|
|
|
29
29
|
|
|
30
30
|
const { TEMPLATES, GROUPS, SENDERS } = require('../constants.js');
|
|
31
31
|
const { tagLinks } = require('../utm.js');
|
|
32
|
+
const { isCorporate } = require('../validation.js');
|
|
32
33
|
const sendgridProvider = require('../providers/sendgrid.js');
|
|
33
34
|
const beehiivProvider = require('../providers/beehiiv.js');
|
|
34
35
|
|
|
@@ -72,6 +73,11 @@ Marketing.prototype.add = async function (options) {
|
|
|
72
73
|
return {};
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
if (isCorporate(email)) {
|
|
77
|
+
assistant.warn(`Marketing.add(): Blocked corporate/social-media domain, skipping: ${email}`);
|
|
78
|
+
return { blocked: 'corporate', email };
|
|
79
|
+
}
|
|
80
|
+
|
|
75
81
|
if (assistant.isTesting() && !process.env.TEST_EXTENDED_MODE) {
|
|
76
82
|
assistant.log('Marketing.add(): Skipping providers (testing mode)');
|
|
77
83
|
return {};
|
|
@@ -151,6 +157,11 @@ Marketing.prototype.sync = async function (userDocOrUid) {
|
|
|
151
157
|
return {};
|
|
152
158
|
}
|
|
153
159
|
|
|
160
|
+
if (isCorporate(email)) {
|
|
161
|
+
assistant.warn(`Marketing.sync(): Blocked corporate/social-media domain, skipping: ${email}`);
|
|
162
|
+
return { blocked: 'corporate', email };
|
|
163
|
+
}
|
|
164
|
+
|
|
154
165
|
if (assistant.isTesting() && !process.env.TEST_EXTENDED_MODE) {
|
|
155
166
|
assistant.log('Marketing.sync(): Skipping providers (testing mode)');
|
|
156
167
|
return {};
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
* Available checks (run in this order):
|
|
5
5
|
* - format — basic email regex
|
|
6
6
|
* - disposable — checks against known disposable domain list
|
|
7
|
+
* - corporate — blocks corporate/social-media domains (meta.com, instagram.com, soundcloud.com, etc.)
|
|
7
8
|
* - localPart — blocks spam/junk local parts (test, noreply, all-numeric, etc.)
|
|
8
9
|
* - mailbox — verifies mailbox exists via API (costs money, requires ZEROBOUNCE_API_KEY)
|
|
9
10
|
*
|
|
10
11
|
* Usage:
|
|
11
|
-
* validate(email) // All free checks (format + disposable + localPart)
|
|
12
|
+
* validate(email) // All free checks (format + disposable + corporate + localPart)
|
|
12
13
|
* validate(email, { checks: ['format', 'disposable'] }) // Only format + disposable
|
|
13
14
|
* validate(email, { checks: ALL_CHECKS }) // Everything including mailbox
|
|
14
15
|
*
|
|
@@ -16,57 +17,41 @@
|
|
|
16
17
|
* - routes/marketing/contact/post.js
|
|
17
18
|
* - functions/core/actions/api/general/add-marketing-contact.js
|
|
18
19
|
* - routes/user/signup/post.js (disposable check only)
|
|
20
|
+
* - libraries/email/marketing/index.js (safety net before Beehiiv/SendGrid add/sync)
|
|
19
21
|
*/
|
|
20
22
|
const fetch = require('wonderful-fetch');
|
|
21
23
|
const path = require('path');
|
|
22
24
|
|
|
25
|
+
// All data lives in ./data/ — domains and local-part blocklists are co-located JSON files.
|
|
26
|
+
const DATA_DIR = path.join(__dirname, 'data');
|
|
27
|
+
|
|
23
28
|
// Load disposable domains: curated vendor list + custom additions
|
|
24
|
-
const DISPOSABLE_DOMAINS = require(path.join(
|
|
25
|
-
const CUSTOM_DISPOSABLE_DOMAINS = require(path.join(
|
|
29
|
+
const DISPOSABLE_DOMAINS = require(path.join(DATA_DIR, 'disposable-domains.json'));
|
|
30
|
+
const CUSTOM_DISPOSABLE_DOMAINS = require(path.join(DATA_DIR, 'custom-disposable-domains.json'));
|
|
26
31
|
const DISPOSABLE_SET = new Set([
|
|
27
32
|
...DISPOSABLE_DOMAINS.map(d => d.toLowerCase()),
|
|
28
33
|
...CUSTOM_DISPOSABLE_DOMAINS.map(d => d.toLowerCase()),
|
|
29
34
|
]);
|
|
30
35
|
|
|
31
|
-
//
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
'test', 'testing', 'tester', 'test1', 'test123',
|
|
35
|
-
'example', 'sample', 'demo', 'dummy', 'fake', 'temp',
|
|
36
|
-
// System/role addresses
|
|
37
|
-
'noreply', 'no-reply', 'donotreply', 'do-not-reply',
|
|
38
|
-
'mailer-daemon', 'postmaster', 'webmaster', 'hostmaster',
|
|
39
|
-
'abuse', 'spam', 'root',
|
|
40
|
-
// Keyboard walks / junk
|
|
41
|
-
'asdf', 'qwerty', 'zxcv', 'asd', 'qwe',
|
|
42
|
-
'aaa', 'bbb', 'xxx', 'zzz',
|
|
43
|
-
'abc', 'abc123', 'abcdef',
|
|
44
|
-
// Team
|
|
45
|
-
// 'user', 'email', 'mail', 'hello', 'info',
|
|
46
|
-
// 'admin', 'administrator', 'support',
|
|
47
|
-
// 'contact',
|
|
48
|
-
// Placeholder
|
|
49
|
-
'name', 'firstname', 'lastname',
|
|
50
|
-
'foo', 'bar', 'baz', 'foobar',
|
|
51
|
-
'null', 'undefined', 'none', 'anonymous',
|
|
52
|
-
]);
|
|
36
|
+
// Load corporate/social-media domains — real mailboxes we never want on marketing lists
|
|
37
|
+
const CORPORATE_DOMAINS = require(path.join(DATA_DIR, 'corporate-domains.json'));
|
|
38
|
+
const CORPORATE_SET = new Set(CORPORATE_DOMAINS.map(d => d.toLowerCase()));
|
|
53
39
|
|
|
54
|
-
//
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
];
|
|
40
|
+
// Load local-part blocklists from ./data/ (JSON for strings, JS for regex patterns)
|
|
41
|
+
const BLOCKED_LOCAL_PARTS_DATA = require(path.join(DATA_DIR, 'blocked-local-parts.json'));
|
|
42
|
+
const BLOCKED_LOCAL_PARTS = new Set(
|
|
43
|
+
Object.values(BLOCKED_LOCAL_PARTS_DATA).flat().map(p => p.toLowerCase())
|
|
44
|
+
);
|
|
45
|
+
const BLOCKED_LOCAL_PATTERNS = require(path.join(DATA_DIR, 'blocked-local-patterns.js'));
|
|
61
46
|
|
|
62
47
|
// Format regex
|
|
63
48
|
const EMAIL_FORMAT = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
64
49
|
|
|
65
50
|
// Default checks (all free checks — mailbox excluded because it costs money)
|
|
66
|
-
const DEFAULT_CHECKS = ['format', 'disposable', 'localPart'];
|
|
51
|
+
const DEFAULT_CHECKS = ['format', 'disposable', 'corporate', 'localPart'];
|
|
67
52
|
|
|
68
53
|
// All available checks
|
|
69
|
-
const ALL_CHECKS = ['format', 'disposable', 'localPart', 'mailbox'];
|
|
54
|
+
const ALL_CHECKS = ['format', 'disposable', 'corporate', 'localPart', 'mailbox'];
|
|
70
55
|
|
|
71
56
|
/**
|
|
72
57
|
* Validate an email address through selected checks.
|
|
@@ -76,7 +61,7 @@ const ALL_CHECKS = ['format', 'disposable', 'localPart', 'mailbox'];
|
|
|
76
61
|
* @param {string} email
|
|
77
62
|
* @param {object} [options]
|
|
78
63
|
* @param {Array<string>} [options.checks] - Which checks to run (default: DEFAULT_CHECKS)
|
|
79
|
-
* @returns {{ valid: boolean, checks: { format?: object, disposable?: object, localPart?: object, mailbox?: object } }}
|
|
64
|
+
* @returns {{ valid: boolean, checks: { format?: object, disposable?: object, corporate?: object, localPart?: object, mailbox?: object } }}
|
|
80
65
|
*/
|
|
81
66
|
async function validate(email, options = {}) {
|
|
82
67
|
const checks = new Set(options.checks || DEFAULT_CHECKS);
|
|
@@ -106,7 +91,18 @@ async function validate(email, options = {}) {
|
|
|
106
91
|
result.checks.disposable = { valid: true, blocked: false };
|
|
107
92
|
}
|
|
108
93
|
|
|
109
|
-
// 3.
|
|
94
|
+
// 3. Corporate / social-media domain (real mailbox, but never wanted on marketing lists)
|
|
95
|
+
if (checks.has('corporate') && domain) {
|
|
96
|
+
if (CORPORATE_SET.has(domain)) {
|
|
97
|
+
result.valid = false;
|
|
98
|
+
result.checks.corporate = { valid: false, blocked: true, domain, reason: 'Corporate/social-media domain' };
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
result.checks.corporate = { valid: true, blocked: false };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 4. Local part — strip +suffix before checking
|
|
110
106
|
if (checks.has('localPart') && rawLocalPart) {
|
|
111
107
|
const localPart = rawLocalPart.split('+')[0];
|
|
112
108
|
|
|
@@ -127,7 +123,7 @@ async function validate(email, options = {}) {
|
|
|
127
123
|
result.checks.localPart = { valid: true };
|
|
128
124
|
}
|
|
129
125
|
|
|
130
|
-
//
|
|
126
|
+
// 5. Mailbox verification (ZeroBounce)
|
|
131
127
|
if (checks.has('mailbox')) {
|
|
132
128
|
if (!process.env.ZEROBOUNCE_API_KEY) {
|
|
133
129
|
result.checks.mailbox = { valid: true, skipped: true, reason: 'No API key' };
|
|
@@ -190,4 +186,23 @@ function isDisposable(emailOrDomain) {
|
|
|
190
186
|
return DISPOSABLE_SET.has(domain.toLowerCase());
|
|
191
187
|
}
|
|
192
188
|
|
|
193
|
-
|
|
189
|
+
/**
|
|
190
|
+
* Quick check: is this email from a blocked corporate/social-media domain?
|
|
191
|
+
* Works with a full email address or just a domain.
|
|
192
|
+
*
|
|
193
|
+
* @param {string} emailOrDomain
|
|
194
|
+
* @returns {boolean}
|
|
195
|
+
*/
|
|
196
|
+
function isCorporate(emailOrDomain) {
|
|
197
|
+
if (!emailOrDomain) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const domain = emailOrDomain.includes('@')
|
|
202
|
+
? emailOrDomain.split('@')[1]
|
|
203
|
+
: emailOrDomain;
|
|
204
|
+
|
|
205
|
+
return CORPORATE_SET.has(domain.toLowerCase());
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = { validate, isDisposable, isCorporate, DEFAULT_CHECKS, ALL_CHECKS };
|
|
@@ -41,7 +41,7 @@ module.exports = async ({ assistant, Manager, settings, analytics }) => {
|
|
|
41
41
|
return assistant.respond({ success: true });
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
const { format, localPart, disposable } = validation.checks;
|
|
44
|
+
const { format, localPart, disposable, corporate } = validation.checks;
|
|
45
45
|
|
|
46
46
|
if (format && !format.valid) {
|
|
47
47
|
return assistant.respond('Invalid email format', { code: 400 });
|
|
@@ -55,6 +55,10 @@ module.exports = async ({ assistant, Manager, settings, analytics }) => {
|
|
|
55
55
|
return assistant.respond(`Disposable email domain not allowed: ${disposable.domain}`, { code: 400 });
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
if (corporate && !corporate.valid) {
|
|
59
|
+
return assistant.respond(`Corporate/social-media domain not allowed: ${corporate.domain}`, { code: 400 });
|
|
60
|
+
}
|
|
61
|
+
|
|
58
62
|
return assistant.respond('Email validation failed', { code: 400 });
|
|
59
63
|
}
|
|
60
64
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Format, local part, and disposable tests always run (free, regex-based).
|
|
6
6
|
* Mailbox verification tests require TEST_EXTENDED_MODE + ZEROBOUNCE_API_KEY.
|
|
7
7
|
*/
|
|
8
|
-
const { validate, isDisposable, DEFAULT_CHECKS, ALL_CHECKS } = require('../../src/manager/libraries/email/validation.js');
|
|
8
|
+
const { validate, isDisposable, isCorporate, DEFAULT_CHECKS, ALL_CHECKS } = require('../../src/manager/libraries/email/validation.js');
|
|
9
9
|
|
|
10
10
|
module.exports = {
|
|
11
11
|
description: 'Email validation',
|
|
@@ -287,6 +287,144 @@ module.exports = {
|
|
|
287
287
|
},
|
|
288
288
|
},
|
|
289
289
|
|
|
290
|
+
// --- Corporate / social-media domain checks ---
|
|
291
|
+
|
|
292
|
+
{
|
|
293
|
+
name: 'corporate-meta-blocked',
|
|
294
|
+
timeout: 5000,
|
|
295
|
+
|
|
296
|
+
async run({ assert }) {
|
|
297
|
+
const result = await validate('ian@meta.com');
|
|
298
|
+
|
|
299
|
+
assert.equal(result.valid, false, 'meta.com should be blocked');
|
|
300
|
+
assert.propertyEquals(result, 'checks.corporate.blocked', true, 'Should be flagged as blocked');
|
|
301
|
+
assert.propertyEquals(result, 'checks.corporate.domain', 'meta.com', 'Should include blocked domain');
|
|
302
|
+
assert.propertyEquals(result, 'checks.corporate.reason', 'Corporate/social-media domain', 'Should have human-readable reason');
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
{
|
|
307
|
+
name: 'corporate-instagram-blocked',
|
|
308
|
+
timeout: 5000,
|
|
309
|
+
|
|
310
|
+
async run({ assert }) {
|
|
311
|
+
const result = await validate('rachel.greene@instagram.com');
|
|
312
|
+
|
|
313
|
+
assert.equal(result.valid, false, 'instagram.com should be blocked');
|
|
314
|
+
assert.propertyEquals(result, 'checks.corporate.blocked', true, 'Should be flagged as blocked');
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
{
|
|
319
|
+
name: 'corporate-soundcloud-blocked',
|
|
320
|
+
timeout: 5000,
|
|
321
|
+
|
|
322
|
+
async run({ assert }) {
|
|
323
|
+
const result = await validate('user@soundcloud.com');
|
|
324
|
+
|
|
325
|
+
assert.equal(result.valid, false, 'soundcloud.com should be blocked');
|
|
326
|
+
assert.propertyEquals(result, 'checks.corporate.blocked', true, 'Should be flagged as blocked');
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
{
|
|
331
|
+
name: 'corporate-gmail-allowed',
|
|
332
|
+
timeout: 5000,
|
|
333
|
+
|
|
334
|
+
async run({ assert }) {
|
|
335
|
+
const result = await validate('rachel.greene@gmail.com');
|
|
336
|
+
|
|
337
|
+
assert.equal(result.valid, true, 'gmail.com should NOT be flagged as corporate');
|
|
338
|
+
assert.propertyEquals(result, 'checks.corporate.valid', true, 'Corporate check should pass');
|
|
339
|
+
assert.propertyEquals(result, 'checks.corporate.blocked', false, 'Should not be blocked');
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
{
|
|
344
|
+
name: 'corporate-runs-before-localpart',
|
|
345
|
+
timeout: 5000,
|
|
346
|
+
|
|
347
|
+
async run({ assert }) {
|
|
348
|
+
// "test@meta.com" would be blocked by BOTH corporate and localPart;
|
|
349
|
+
// corporate runs first, so we should see corporate (not localPart) in the result.
|
|
350
|
+
const result = await validate('test@meta.com');
|
|
351
|
+
|
|
352
|
+
assert.equal(result.valid, false, 'Should be blocked');
|
|
353
|
+
assert.propertyEquals(result, 'checks.corporate.blocked', true, 'Corporate should be the failure reason');
|
|
354
|
+
assert.equal(result.checks.localPart, undefined, 'localPart should not run after corporate fails');
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
{
|
|
359
|
+
name: 'corporate-can-be-skipped-via-checks-option',
|
|
360
|
+
timeout: 5000,
|
|
361
|
+
|
|
362
|
+
async run({ assert }) {
|
|
363
|
+
// Allow a caller to bypass the corporate check (e.g., during signup, where Meta employees are real users)
|
|
364
|
+
const result = await validate('ian@meta.com', { checks: ['format', 'disposable', 'localPart'] });
|
|
365
|
+
|
|
366
|
+
assert.equal(result.valid, true, 'Without corporate check, meta.com should pass');
|
|
367
|
+
assert.equal(result.checks.corporate, undefined, 'corporate should not run');
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
// --- isCorporate helper ---
|
|
372
|
+
|
|
373
|
+
{
|
|
374
|
+
name: 'isCorporate-social-domain-detected',
|
|
375
|
+
timeout: 5000,
|
|
376
|
+
|
|
377
|
+
async run({ assert }) {
|
|
378
|
+
assert.equal(isCorporate('user@meta.com'), true, 'meta.com should be corporate');
|
|
379
|
+
assert.equal(isCorporate('user@instagram.com'), true, 'instagram.com should be corporate');
|
|
380
|
+
assert.equal(isCorporate('user@soundcloud.com'), true, 'soundcloud.com should be corporate');
|
|
381
|
+
assert.equal(isCorporate('user@tiktok.com'), true, 'tiktok.com should be corporate');
|
|
382
|
+
assert.equal(isCorporate('user@linkedin.com'), true, 'linkedin.com should be corporate');
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
{
|
|
387
|
+
name: 'isCorporate-legitimate-domain-passes',
|
|
388
|
+
timeout: 5000,
|
|
389
|
+
|
|
390
|
+
async run({ assert }) {
|
|
391
|
+
assert.equal(isCorporate('user@gmail.com'), false, 'gmail.com should not be corporate');
|
|
392
|
+
assert.equal(isCorporate('user@somiibo.com'), false, 'Custom domain should not be corporate');
|
|
393
|
+
assert.equal(isCorporate('user@mailinator.com'), false, 'Disposable is a separate category');
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
{
|
|
398
|
+
name: 'isCorporate-accepts-domain-only',
|
|
399
|
+
timeout: 5000,
|
|
400
|
+
|
|
401
|
+
async run({ assert }) {
|
|
402
|
+
assert.equal(isCorporate('meta.com'), true, 'Should work with bare domain');
|
|
403
|
+
assert.equal(isCorporate('gmail.com'), false, 'Should work with bare domain');
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
{
|
|
408
|
+
name: 'isCorporate-handles-edge-cases',
|
|
409
|
+
timeout: 5000,
|
|
410
|
+
|
|
411
|
+
async run({ assert }) {
|
|
412
|
+
assert.equal(isCorporate(''), false, 'Empty string should return false');
|
|
413
|
+
assert.equal(isCorporate(null), false, 'Null should return false');
|
|
414
|
+
assert.equal(isCorporate(undefined), false, 'Undefined should return false');
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
|
|
418
|
+
{
|
|
419
|
+
name: 'isCorporate-case-insensitive',
|
|
420
|
+
timeout: 5000,
|
|
421
|
+
|
|
422
|
+
async run({ assert }) {
|
|
423
|
+
assert.equal(isCorporate('user@META.COM'), true, 'Should be case-insensitive');
|
|
424
|
+
assert.equal(isCorporate('USER@Instagram.Com'), true, 'Should be case-insensitive');
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
|
|
290
428
|
// --- isDisposable helper ---
|
|
291
429
|
|
|
292
430
|
{
|
|
@@ -389,8 +527,8 @@ module.exports = {
|
|
|
389
527
|
timeout: 5000,
|
|
390
528
|
|
|
391
529
|
async run({ assert }) {
|
|
392
|
-
assert.deepEqual(DEFAULT_CHECKS, ['format', 'disposable', 'localPart'], 'DEFAULT_CHECKS should be format + disposable + localPart');
|
|
393
|
-
assert.deepEqual(ALL_CHECKS, ['format', 'disposable', 'localPart', 'mailbox'], 'ALL_CHECKS should include mailbox');
|
|
530
|
+
assert.deepEqual(DEFAULT_CHECKS, ['format', 'disposable', 'corporate', 'localPart'], 'DEFAULT_CHECKS should be format + disposable + corporate + localPart');
|
|
531
|
+
assert.deepEqual(ALL_CHECKS, ['format', 'disposable', 'corporate', 'localPart', 'mailbox'], 'ALL_CHECKS should include mailbox');
|
|
394
532
|
},
|
|
395
533
|
},
|
|
396
534
|
|
|
File without changes
|