backend-manager 5.1.0 → 5.1.2

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 CHANGED
@@ -14,6 +14,31 @@ 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.2] - 2026-05-14
18
+
19
+ ### Changed
20
+
21
+ - **Broadened `test` / `example` local-part blocks** in `email/data/blocked-local-patterns.js`:
22
+ - `/^test[._-]/` → `/^test/` — now catches `testuser`, `test123abc`, etc., not only `test.user` / `test_123` / `test-foo`.
23
+ - Added `/^example/` — catches `example`, `exampleuser`, `example.user`, `examples`, etc.
24
+ - Both patterns are anchored to the start of the local part, so legitimate addresses that contain (but don't start with) those substrings are still allowed: `rachel.tester`, `contestant`, `exam` all pass.
25
+
26
+ # [5.1.1] - 2026-05-13
27
+
28
+ ### Added
29
+
30
+ - **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()`.
31
+ - **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.
32
+ - **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).
33
+
34
+ ### Changed
35
+
36
+ - **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:
37
+ - `disposable-domains.json`, `custom-disposable-domains.json`, `corporate-domains.json` — domain blocklists
38
+ - `blocked-local-parts.json` — categorized local-part blocklist (`generic`, `system`, `junk`, `placeholder`), extracted from a hardcoded `Set` in `validation.js`
39
+ - `blocked-local-patterns.js` — regex patterns, extracted from `validation.js` (kept as JS so RegExp literals stay native)
40
+ - **`scripts/update-disposable-domains.js`** — prepare-step downloader now writes to `email/data/disposable-domains.json`.
41
+
17
42
  # [5.1.0] - 2026-05-13
18
43
 
19
44
  ### Added
package/CLAUDE.md CHANGED
@@ -2,24 +2,98 @@
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
- ## Project Identity
5
+ ## Identity
6
6
 
7
- **Backend Manager (BEM)** is an NPM package that provides powerful backend features for Firebase Cloud Functions projects, including authentication, rate limiting, analytics, and more.
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** (`backend-manager`) is the BEM library itself. If you're working here, you're contributing to the library, not consuming it.
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
- **Consumer projects** are Firebase projects that `require('backend-manager')` in their `functions/index.js`. These have:
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
- ## Architecture (at a glance)
13
+ - **`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/`.
14
+ - **`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.
15
+
16
+ ## Quick Start
17
+
18
+ ### For Consuming Projects
19
+
20
+ 1. `npm install backend-manager --save-dev` (inside `functions/`)
21
+ 2. `npx mgr setup` — validates config, scaffolds defaults (CLAUDE.md, CHANGELOG.md, docs/, test/), provisions Firestore indexes
22
+ 3. `npx mgr emulator` — start Firebase emulators (auth/firestore/functions/database/storage)
23
+ 4. `npx mgr serve` — local serve with Stripe webhook forwarding (if `STRIPE_SECRET_KEY` is set)
24
+ 5. `npx mgr test` — runs framework + project test suites against an emulator
25
+ 6. `npx mgr deploy` — deploy Cloud Functions to Firebase
26
+ 7. `npx mgr logs:read` / `npx mgr logs:tail` — Cloud Function logs from Google Cloud Logging
27
+
28
+ All `npx mgr <cmd>` aliases work: `npx bm <cmd>`, `npx bem <cmd>`, `npx backend-manager <cmd>`.
29
+
30
+ > **Important:** All `npx mgr ...` commands MUST be run from the consumer project's `functions/` subdirectory. The binary lives in `functions/node_modules/.bin/`.
31
+
32
+ ### For Framework Development (This Repository)
33
+
34
+ 1. `npm install` — install BEM's own deps
35
+ 2. `npm run prepare` — build once: copies `src/` → `dist/` via prepare-package
36
+ 3. `npm run prepare:watch` — watch mode
37
+ 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`.
38
+
39
+ ## Architecture
18
40
 
19
41
  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
42
 
21
43
  For the directory layout of both the BEM library and consumer projects, see [docs/directory-structure.md](docs/directory-structure.md).
22
44
 
45
+ ## CLI
46
+
47
+ `npx mgr <command>` (aliases `bm`, `bem`, `backend-manager`):
48
+
49
+ | Command | Description |
50
+ |---|---|
51
+ | `setup` | Validate config, scaffold defaults (CLAUDE.md, CHANGELOG.md, docs/, test/), provision Firestore indexes |
52
+ | `emulator` | Start Firebase emulators (auth/firestore/functions/database/storage) |
53
+ | `serve` | Local Firebase serve (with auto Stripe webhook forwarding if keys set) |
54
+ | `watch` | Auto-reload functions on file change |
55
+ | `deploy` | Deploy Cloud Functions to Firebase |
56
+ | `test` | Run framework + project test suites against an emulator |
57
+ | `mcp` | Start the stdio MCP server (for Claude Code / Claude Desktop) |
58
+ | `firestore:get/set/query/delete` | Direct Firestore reads/writes from the terminal |
59
+ | `auth:get/list/delete/set-claims` | Manage Auth users from the terminal |
60
+ | `logs:read` / `logs:tail` | Cloud Function logs from Google Cloud Logging |
61
+ | `stripe` | Standalone Stripe CLI webhook forwarding |
62
+ | `indexes` | Sync required Firestore indexes into `firestore.indexes.json` |
63
+ | `firebase-init` | Run Firebase Admin SDK initialization helper |
64
+ | `clean` | Remove generated artifacts (logs, test outputs) |
65
+ | `version` | Print BEM version |
66
+
67
+ See [docs/cli-firestore-auth.md](docs/cli-firestore-auth.md) and [docs/cli-logs.md](docs/cli-logs.md) for full flag references.
68
+
69
+ ## File Conventions
70
+
71
+ - **CommonJS** throughout. `prepare-package` copies `src/` → `dist/` 1:1 (no transforms).
72
+ - **`fs-jetpack`** over `fs` / `fs-extra` for file operations.
73
+ - One `module.exports = ...` per file.
74
+ - **Short-circuit early returns** rather than nested ifs.
75
+ - **Logical operators at the start of continuation lines** (`|| condB` on a new line, not `condA ||` trailing).
76
+ - **Firestore shorthand**: `admin.firestore().doc('users/abc123')` (path string) rather than `.collection('users').doc('abc123')`.
77
+ - **Template strings for requires**: `` require(`${functionsDir}/node_modules/backend-manager`) `` rather than string concat.
78
+ - **No backwards compatibility** unless explicitly requested.
79
+ - **Routes receive sanitized data by default** — see [docs/sanitization.md](docs/sanitization.md) for opt-out rules.
80
+ - **Match schema names to route names** — if route is `myEndpoint`, schema is `myEndpoint`.
81
+ - **Always use `assistant.respond()` for responses** — do NOT use `res.send()` directly.
82
+ - **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.
83
+
84
+ 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).
85
+
86
+ ## Doc-update parity
87
+
88
+ Whenever you make a behavioral change (new command, new flag, new pattern, removed feature), update:
89
+
90
+ 1. **`README.md`** — user-facing summary
91
+ 2. **`CLAUDE.md`** (this file) — architecture overview, one paragraph or cross-link
92
+ 3. **`docs/<topic>.md`** — the meat. If a topic doesn't have a doc yet, create one.
93
+ 4. **`CHANGELOG.md`** — if the project keeps one
94
+
95
+ Don't ship behavioral changes with stale docs. Validate first, then document — write docs that describe shipped reality, not intentions.
96
+
23
97
  ## Documentation
24
98
 
25
99
  Deep references live in `docs/`. **Whenever you make a behavioral change, update both this overview AND the relevant `docs/*.md` deep reference.**
@@ -30,6 +104,8 @@ Deep references live in `docs/`. **Whenever you make a behavioral change, update
30
104
  - [docs/directory-structure.md](docs/directory-structure.md) — BEM library + consumer project layouts
31
105
  - [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
106
  - [docs/file-naming.md](docs/file-naming.md) — naming table for routes, schemas, API commands, events, cron jobs, hooks
107
+ - [docs/common-mistakes.md](docs/common-mistakes.md) — anti-pattern checklist (don't modify Manager internals, always await, increment-before-update, etc.)
108
+ - [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
109
  - [docs/environment-detection.md](docs/environment-detection.md) — `assistant.isDevelopment/isProduction/isTesting()`
34
110
  - [docs/response-headers.md](docs/response-headers.md) — automatic `bm-properties` header
35
111
 
@@ -60,52 +136,3 @@ Deep references live in `docs/`. **Whenever you make a behavioral change, update
60
136
  - [docs/testing.md](docs/testing.md) — running, filtering, log files, test types (standalone/suite/group), context object, assertions, auth levels
61
137
  - [docs/cli-firestore-auth.md](docs/cli-firestore-auth.md) — `npx mgr firestore:*` and `auth:*` commands, shared flags, examples
62
138
  - [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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "5.1.0",
3
+ "version": "5.1.2",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "bin": {
@@ -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: test, testuser, test123, test.user
10
+ /^example/, // Starts with example: example, exampleuser, example.user
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
+ ]
@@ -5268,6 +5268,7 @@
5268
5268
  "xepa.ru",
5269
5269
  "xfavaj.com",
5270
5270
  "xidealx.com",
5271
+ "xikemail.com",
5271
5272
  "ximenor.site",
5272
5273
  "xjoi.com",
5273
5274
  "xkx.me",
@@ -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(__dirname, '..', 'disposable-domains.json'));
25
- const CUSTOM_DISPOSABLE_DOMAINS = require(path.join(__dirname, '..', 'custom-disposable-domains.json'));
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
- // Spam/junk local parts exact matches (checked after stripping +suffix)
32
- const BLOCKED_LOCAL_PARTS = new Set([
33
- // Generic/test
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 domainsreal 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
- // Patterns that indicate junk local parts (checked after stripping +suffix)
55
- const BLOCKED_LOCAL_PATTERNS = [
56
- /^\d+$/, // All numeric: 123456
57
- /^(.)\1{2,}$/, // Repeating single char: aaaa, xxxx
58
- /^[a-z]{1,2}\d+$/, // Single letter + numbers: a123, x999
59
- /^test[._-]/, // Starts with test separator: test.user, test_123
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. Local part strip +suffix before checking
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
- // 4. Mailbox verification (ZeroBounce)
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
- module.exports = { validate, isDisposable, DEFAULT_CHECKS, ALL_CHECKS };
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