backend-manager 5.0.203 → 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 +72 -0
- package/CLAUDE.md +100 -1529
- package/TODO-CHARGEBLAST.md +32 -0
- package/TODO-email-auth.md +14 -0
- package/docs/admin-post-route.md +24 -0
- package/docs/ai-library.md +23 -0
- package/docs/architecture.md +31 -0
- package/docs/auth-hooks.md +74 -0
- package/docs/cli-firestore-auth.md +59 -0
- package/docs/cli-logs.md +67 -0
- package/docs/code-patterns.md +67 -0
- package/docs/common-mistakes.md +11 -0
- package/docs/common-operations.md +64 -0
- package/docs/directory-structure.md +119 -0
- package/docs/environment-detection.md +7 -0
- package/docs/file-naming.md +11 -0
- package/docs/key-files.md +36 -0
- package/docs/marketing-campaigns.md +244 -0
- package/docs/marketing-fields.md +25 -0
- package/docs/mcp.md +95 -0
- package/docs/payment-system.md +325 -0
- package/docs/response-headers.md +7 -0
- package/docs/routes.md +126 -0
- package/docs/sanitization.md +61 -0
- package/docs/schemas.md +39 -0
- package/docs/stripe-webhook-forwarding.md +18 -0
- package/docs/testing.md +129 -0
- package/docs/usage-rate-limiting.md +67 -0
- package/package.json +8 -4
- package/scripts/update-disposable-domains.js +1 -1
- package/src/defaults/CHANGELOG.md +15 -0
- package/src/defaults/CLAUDE.md +8 -4
- package/src/defaults/docs/README.md +17 -0
- package/src/defaults/test/README.md +33 -0
- package/src/manager/events/cron/daily/marketing-newsletter-generate.js +48 -8
- package/src/manager/functions/core/actions/api/admin/create-post.js +3 -27
- package/src/manager/functions/core/actions/api/general/add-marketing-contact.js +5 -0
- package/src/manager/helpers/utilities.js +21 -0
- package/src/manager/index.js +1 -1
- package/src/manager/libraries/ai/index.js +162 -0
- package/src/manager/libraries/ai/providers/anthropic.js +193 -0
- package/src/manager/libraries/ai/providers/claude-code.js +206 -0
- package/src/manager/libraries/ai/providers/openai.js +934 -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} +3 -0
- package/src/manager/libraries/email/generators/lib/filter.js +179 -0
- package/src/manager/libraries/email/generators/lib/image-host.js +231 -0
- package/src/manager/libraries/email/generators/lib/mjml-template.js +83 -0
- package/src/manager/libraries/email/generators/lib/structure.js +278 -0
- package/src/manager/libraries/email/generators/lib/svg-illustrator.js +184 -0
- package/src/manager/libraries/email/generators/lib/templates/classic-schema.js +63 -0
- package/src/manager/libraries/email/generators/lib/templates/clean.js +82 -0
- package/src/manager/libraries/email/generators/lib/templates/editorial-helpers.js +100 -0
- package/src/manager/libraries/email/generators/lib/templates/editorial.js +317 -0
- package/src/manager/libraries/email/generators/lib/templates/field-report-helpers.js +138 -0
- package/src/manager/libraries/email/generators/lib/templates/field-report.js +497 -0
- package/src/manager/libraries/email/generators/lib/templates/index.js +28 -0
- package/src/manager/libraries/email/generators/lib/templates/shared.js +534 -0
- package/src/manager/libraries/email/generators/newsletter.js +377 -95
- package/src/manager/libraries/email/marketing/index.js +16 -2
- package/src/manager/libraries/email/providers/beehiiv.js +7 -3
- package/src/manager/libraries/email/validation.js +53 -38
- package/src/manager/libraries/openai.js +13 -932
- package/src/manager/routes/admin/post/deduplicate-image-alts.js +52 -0
- package/src/manager/routes/admin/post/post.js +10 -17
- package/src/manager/routes/marketing/contact/post.js +5 -1
- package/templates/_.env +4 -0
- package/templates/_.gitignore +1 -0
- package/templates/backend-manager-config.json +48 -4
- package/test/helpers/email-validation.js +141 -3
- package/test/helpers/slugify.js +394 -0
- package/test/marketing/fixtures/clean.json +31 -0
- package/test/marketing/fixtures/editorial.json +31 -0
- package/test/marketing/fixtures/field-report.json +54 -0
- package/test/marketing/newsletter-generate.js +731 -0
- package/test/marketing/newsletter-templates.js +512 -0
- package/test/routes/admin/deduplicate-image-alts.js +190 -0
- /package/src/manager/libraries/{custom-disposable-domains.json → email/data/custom-disposable-domains.json} +0 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deduplicate alt-text across DIFFERENT image URLs.
|
|
3
|
+
*
|
|
4
|
+
* Image filenames in the admin/post route are derived from the markdown image's
|
|
5
|
+
* alt-text. Two images sharing alt-text would otherwise produce the same filename
|
|
6
|
+
* and overwrite each other on upload, causing the second image to "disappear"
|
|
7
|
+
* (both `@post/` references in the body resolve to the same file).
|
|
8
|
+
*
|
|
9
|
+
* Strategy: when a non-header image's alt collides with an earlier image's alt
|
|
10
|
+
* AND its URL is different, suffix the alt with ` (N)` (where N is the
|
|
11
|
+
* occurrence count). Same URL keeps its original alt — repeated embeds of the
|
|
12
|
+
* exact same image are not a collision and should resolve to the same file.
|
|
13
|
+
*
|
|
14
|
+
* @param {Array<{src: string, alt: string, header?: boolean}>} images — image
|
|
15
|
+
* entries extracted from the body (plus optional header). Mutated in place:
|
|
16
|
+
* `image.alt` is rewritten when a collision is detected.
|
|
17
|
+
* @param {string} body — markdown body string. Returned with any
|
|
18
|
+
* `` rewritten to `` for collisions.
|
|
19
|
+
* @returns {{images: Array, body: string}} mutated images array and rewritten body.
|
|
20
|
+
*/
|
|
21
|
+
module.exports = function deduplicateImageAlts(images, body) {
|
|
22
|
+
const seenAltByUrl = new Map();
|
|
23
|
+
const altCountByAlt = new Map();
|
|
24
|
+
let rewrittenBody = body;
|
|
25
|
+
|
|
26
|
+
for (const image of images) {
|
|
27
|
+
if (image.header) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const existingForUrl = seenAltByUrl.get(image.src);
|
|
32
|
+
if (existingForUrl) {
|
|
33
|
+
// Same URL appeared earlier — reuse its (possibly already-suffixed) alt.
|
|
34
|
+
// Repeated embeds of the same image should resolve to the same upload.
|
|
35
|
+
image.alt = existingForUrl;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const count = (altCountByAlt.get(image.alt) || 0) + 1;
|
|
40
|
+
altCountByAlt.set(image.alt, count);
|
|
41
|
+
|
|
42
|
+
if (count > 1) {
|
|
43
|
+
const newAlt = `${image.alt} (${count})`;
|
|
44
|
+
rewrittenBody = rewrittenBody.split(``).join(``);
|
|
45
|
+
image.alt = newAlt;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
seenAltByUrl.set(image.src, image.alt);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { images, body: rewrittenBody };
|
|
52
|
+
};
|
|
@@ -11,6 +11,8 @@ const path = require('path');
|
|
|
11
11
|
const { Octokit } = require('@octokit/rest');
|
|
12
12
|
const { get, set } = require('lodash');
|
|
13
13
|
|
|
14
|
+
const deduplicateImageAlts = require('./deduplicate-image-alts');
|
|
15
|
+
|
|
14
16
|
const POST_TEMPLATE = jetpack.read(`${__dirname}/templates/post.html`);
|
|
15
17
|
const IMAGE_PATH_SRC = `src/assets/images/blog/post-{id}/`;
|
|
16
18
|
const IMAGE_REGEX = /(?:!\[(.*?)\]\((.*?)\))/img;
|
|
@@ -63,14 +65,8 @@ module.exports = async ({ assistant, Manager, user, settings, analytics }) => {
|
|
|
63
65
|
return assistant.respond('Missing required parameter: body', { code: 400 });
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
// Fix URL
|
|
67
|
-
settings.url = settings.url
|
|
68
|
-
.replace(/blog\//ig, '')
|
|
69
|
-
.replace(/^\/|\/$/g, '')
|
|
70
|
-
.replace(/[^a-zA-Z0-9]/g, '-')
|
|
71
|
-
.replace(/-+/g, '-')
|
|
72
|
-
.replace(/^-+|-+$/g, '')
|
|
73
|
-
.toLowerCase();
|
|
68
|
+
// Fix URL — strip blog/ prefix then slugify (slugify handles slashes/special chars)
|
|
69
|
+
settings.url = Manager.Utilities().slugify(settings.url.replace(/blog\//ig, ''));
|
|
74
70
|
|
|
75
71
|
// Fix body
|
|
76
72
|
settings.body = settings.body
|
|
@@ -159,6 +155,11 @@ async function downloadImages(assistant, settings) {
|
|
|
159
155
|
header: true,
|
|
160
156
|
});
|
|
161
157
|
|
|
158
|
+
// Deduplicate alt-text across different image URLs (mutates images in place,
|
|
159
|
+
// returns rewritten body). See deduplicate-image-alts.js for full rationale.
|
|
160
|
+
const dedup = deduplicateImageAlts(images, settings.body);
|
|
161
|
+
settings.body = dedup.body;
|
|
162
|
+
|
|
162
163
|
assistant.log('downloadImages(): images', images);
|
|
163
164
|
|
|
164
165
|
if (!images.length) {
|
|
@@ -199,7 +200,7 @@ async function downloadImages(assistant, settings) {
|
|
|
199
200
|
// Helper: Download image
|
|
200
201
|
async function downloadImage(assistant, src, alt) {
|
|
201
202
|
const fetch = assistant.Manager.require('wonderful-fetch');
|
|
202
|
-
const hyphenated =
|
|
203
|
+
const hyphenated = assistant.Manager.Utilities().slugify(alt);
|
|
203
204
|
|
|
204
205
|
assistant.log(`downloadImage(): src=${src}, alt=${alt}, hyphenated=${hyphenated}`);
|
|
205
206
|
|
|
@@ -325,11 +326,3 @@ function formatClone(payload) {
|
|
|
325
326
|
return payload;
|
|
326
327
|
}
|
|
327
328
|
|
|
328
|
-
// Helper: Hyphenate string
|
|
329
|
-
function hyphenate(s) {
|
|
330
|
-
return s
|
|
331
|
-
.replace(/[^a-zA-Z0-9]/g, '-')
|
|
332
|
-
.replace(/-+/g, '-')
|
|
333
|
-
.replace(/^-|-$/g, '')
|
|
334
|
-
.toLowerCase();
|
|
335
|
-
}
|
|
@@ -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
|
|
package/templates/_.env
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
BACKEND_MANAGER_KEY=""
|
|
4
4
|
BACKEND_MANAGER_NAMESPACE=""
|
|
5
5
|
BACKEND_MANAGER_OPENAI_API_KEY=""
|
|
6
|
+
BACKEND_MANAGER_ANTHROPIC_API_KEY=""
|
|
6
7
|
|
|
7
8
|
# GitHub
|
|
8
9
|
GITHUB_TOKEN=""
|
|
@@ -10,6 +11,9 @@ GITHUB_TOKEN=""
|
|
|
10
11
|
# OpenAI
|
|
11
12
|
OPENAI_API_KEY=""
|
|
12
13
|
|
|
14
|
+
# Anthropic
|
|
15
|
+
ANTHROPIC_API_KEY=""
|
|
16
|
+
|
|
13
17
|
# Payment Processors
|
|
14
18
|
PAYPAL_CLIENT_SECRET=""
|
|
15
19
|
STRIPE_SECRET_KEY=""
|
package/templates/_.gitignore
CHANGED
|
@@ -7,6 +7,18 @@
|
|
|
7
7
|
contact: {
|
|
8
8
|
email: 'support@example.com',
|
|
9
9
|
},
|
|
10
|
+
// Physical postal address — required by CAN-SPAM in commercial email footers.
|
|
11
|
+
// Rendered in the newsletter footer + transactional email footers.
|
|
12
|
+
// Structured so the renderer can format it consistently across locales
|
|
13
|
+
// and break onto multiple lines if needed. line2/region/postalCode are optional.
|
|
14
|
+
address: {
|
|
15
|
+
line1: '123 Main St',
|
|
16
|
+
line2: 'Suite 100',
|
|
17
|
+
city: 'City',
|
|
18
|
+
region: 'ST',
|
|
19
|
+
postalCode: '12345',
|
|
20
|
+
country: 'United States',
|
|
21
|
+
},
|
|
10
22
|
images: {
|
|
11
23
|
wordmark: 'https://example.com/wordmark.png',
|
|
12
24
|
brandmark: 'https://example.com/wordmark.png',
|
|
@@ -17,6 +29,7 @@
|
|
|
17
29
|
user: 'username',
|
|
18
30
|
repo_website: 'https://github.com/username/backend-manager',
|
|
19
31
|
},
|
|
32
|
+
parent: '', // URL of the parent BEM instance (e.g., 'https://api.itwcreativeworks.com') — used by newsletter generator to pull sources
|
|
20
33
|
sentry: {
|
|
21
34
|
dsn: 'https://d965557418748jd749d837asf00552f@o777489.ingest.sentry.io/8789941',
|
|
22
35
|
},
|
|
@@ -129,14 +142,45 @@
|
|
|
129
142
|
beehiiv: {
|
|
130
143
|
enabled: false,
|
|
131
144
|
// publicationId: 'pub_xxxxx', // Set to skip fuzzy-match API call
|
|
145
|
+
// Content pipeline. Lives under the provider that publishes the result —
|
|
146
|
+
// Beehiiv for newsletters, eventually SendGrid for promo blasts. The
|
|
147
|
+
// shape is the same regardless of provider: sources, tone, template,
|
|
148
|
+
// theme, sponsorships. `beehiiv.enabled: false` above disables it all.
|
|
149
|
+
content: {
|
|
150
|
+
categories: [], // e.g., ['social-media', 'marketing'] — content categories to pull from parent server
|
|
151
|
+
// AI-customization fields below are all optional with sensible defaults
|
|
152
|
+
instructions: '', // free-form text passed to the AI ("focus on X", "avoid Y", brand voice notes)
|
|
153
|
+
tone: 'professional', // 'professional', 'casual', 'actionable', 'witty', etc. — passed to AI prompt
|
|
154
|
+
template: 'clean', // 'clean' | 'editorial' | 'field-report' — layout template (each owns its own content shape and aesthetic)
|
|
155
|
+
theme: {
|
|
156
|
+
primaryColor: '#5B5BFF', // accent color: buttons, links, brand text
|
|
157
|
+
secondaryColor: '#1E1E2A', // body text color
|
|
158
|
+
accentColor: '#F6F7FB', // page background outside the 600px body
|
|
159
|
+
font: 'Inter, system-ui, sans-serif',
|
|
160
|
+
},
|
|
161
|
+
// AI provider defaults are hard-coded in the library (openai for structure,
|
|
162
|
+
// anthropic for SVG — each chosen for what each model does best). Override
|
|
163
|
+
// per-run only via env: NEWSLETTER_PROVIDER_STRUCTURE / NEWSLETTER_PROVIDER_SVG.
|
|
164
|
+
// Brand-owned sponsorship/promo slots — rendered as "sponsored" blocks inline
|
|
165
|
+
// with the newsletter. Use for promoting your own products, affiliate offers,
|
|
166
|
+
// or paid placements. Each entry can be positioned at 'top', 'middle', or 'end'.
|
|
167
|
+
// Per-campaign overrides via settings.sponsorships on a marketing-campaigns doc.
|
|
168
|
+
sponsorships: [
|
|
169
|
+
// {
|
|
170
|
+
// label: 'Sponsored', // optional eyebrow label (defaults to "Sponsored")
|
|
171
|
+
// headline: 'Grow your audience faster', // bold headline
|
|
172
|
+
// body: 'Short pitch text…', // 1-2 sentence body
|
|
173
|
+
// url: 'https://somiibo.com/promo', // click destination
|
|
174
|
+
// image: 'https://...png', // optional image URL
|
|
175
|
+
// ctaLabel: 'Try Somiibo', // button text (default: "Learn more")
|
|
176
|
+
// position: 'middle', // 'top' | 'middle' | 'end' (default: 'middle')
|
|
177
|
+
// },
|
|
178
|
+
],
|
|
179
|
+
},
|
|
132
180
|
},
|
|
133
181
|
prune: {
|
|
134
182
|
enabled: true,
|
|
135
183
|
},
|
|
136
|
-
newsletter: {
|
|
137
|
-
enabled: false,
|
|
138
|
-
categories: [], // e.g., ['social-media', 'marketing'] — content categories to pull from parent server
|
|
139
|
-
},
|
|
140
184
|
},
|
|
141
185
|
firebaseConfig: {
|
|
142
186
|
apiKey: '123-456',
|
|
@@ -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
|
|