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,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test: routes/admin/post/deduplicate-image-alts
|
|
3
|
+
* Unit tests for the alt-text dedup helper used by the admin/post route.
|
|
4
|
+
*
|
|
5
|
+
* Run: npx mgr test routes/admin/deduplicate-image-alts
|
|
6
|
+
*
|
|
7
|
+
* Contract:
|
|
8
|
+
* - Header images are never modified.
|
|
9
|
+
* - Two non-header images with the same alt AND different URLs:
|
|
10
|
+
* the second's alt is suffixed with " (2)" (and " (3)" for the third, etc.)
|
|
11
|
+
* and the body is rewritten to match.
|
|
12
|
+
* - Two non-header images with the same alt AND the same URL:
|
|
13
|
+
* the second reuses the first's (possibly already-suffixed) alt — no change.
|
|
14
|
+
* - Images with distinct alts are untouched.
|
|
15
|
+
*/
|
|
16
|
+
const deduplicateImageAlts = require('../../../src/manager/routes/admin/post/deduplicate-image-alts');
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
description: 'routes/admin/post/deduplicate-image-alts',
|
|
20
|
+
type: 'group',
|
|
21
|
+
|
|
22
|
+
tests: [
|
|
23
|
+
// ─── Happy path: no collisions ───
|
|
24
|
+
|
|
25
|
+
{
|
|
26
|
+
name: 'distinct-alts-are-untouched',
|
|
27
|
+
async run({ assert }) {
|
|
28
|
+
const images = [
|
|
29
|
+
{ src: 'https://a.com/1.jpg', alt: 'Cat', header: false },
|
|
30
|
+
{ src: 'https://a.com/2.jpg', alt: 'Dog', header: false },
|
|
31
|
+
];
|
|
32
|
+
const body = '\n\n';
|
|
33
|
+
const result = deduplicateImageAlts(images, body);
|
|
34
|
+
|
|
35
|
+
assert.equal(result.images[0].alt, 'Cat', 'First image alt unchanged');
|
|
36
|
+
assert.equal(result.images[1].alt, 'Dog', 'Second image alt unchanged');
|
|
37
|
+
assert.equal(result.body, body, 'Body is unchanged when no collisions');
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// ─── The bug: same alt, different URLs ───
|
|
42
|
+
|
|
43
|
+
{
|
|
44
|
+
name: 'same-alt-different-urls-suffixes-second',
|
|
45
|
+
async run({ assert }) {
|
|
46
|
+
const sharedAlt = '62f9b399-92c2-40c2-8ec4-e05b54077aaa';
|
|
47
|
+
const images = [
|
|
48
|
+
{ src: 'https://a.com/1.jpg', alt: sharedAlt, header: false },
|
|
49
|
+
{ src: 'https://a.com/2.jpg', alt: sharedAlt, header: false },
|
|
50
|
+
];
|
|
51
|
+
const body = `\n\nsome text\n\n`;
|
|
52
|
+
const result = deduplicateImageAlts(images, body);
|
|
53
|
+
|
|
54
|
+
assert.equal(result.images[0].alt, sharedAlt, 'First image keeps original alt');
|
|
55
|
+
assert.equal(result.images[1].alt, `${sharedAlt} (2)`, 'Second image alt is suffixed with " (2)"');
|
|
56
|
+
assert.ok(
|
|
57
|
+
result.body.includes(``),
|
|
58
|
+
'Body still contains the first image with original alt',
|
|
59
|
+
);
|
|
60
|
+
assert.ok(
|
|
61
|
+
result.body.includes(``),
|
|
62
|
+
'Body contains the second image with suffixed alt',
|
|
63
|
+
);
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
{
|
|
68
|
+
name: 'three-images-same-alt-different-urls-counts-up',
|
|
69
|
+
async run({ assert }) {
|
|
70
|
+
const sharedAlt = 'Logo';
|
|
71
|
+
const images = [
|
|
72
|
+
{ src: 'https://a.com/1.jpg', alt: sharedAlt, header: false },
|
|
73
|
+
{ src: 'https://a.com/2.jpg', alt: sharedAlt, header: false },
|
|
74
|
+
{ src: 'https://a.com/3.jpg', alt: sharedAlt, header: false },
|
|
75
|
+
];
|
|
76
|
+
const body = `\n\n`;
|
|
77
|
+
const result = deduplicateImageAlts(images, body);
|
|
78
|
+
|
|
79
|
+
assert.equal(result.images[0].alt, 'Logo');
|
|
80
|
+
assert.equal(result.images[1].alt, 'Logo (2)');
|
|
81
|
+
assert.equal(result.images[2].alt, 'Logo (3)');
|
|
82
|
+
assert.ok(result.body.includes(''));
|
|
83
|
+
assert.ok(result.body.includes(''));
|
|
84
|
+
assert.ok(result.body.includes(''));
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
// ─── Same alt, SAME URL: legitimate duplicate, no change ───
|
|
89
|
+
|
|
90
|
+
{
|
|
91
|
+
name: 'same-alt-same-url-is-not-a-collision',
|
|
92
|
+
async run({ assert }) {
|
|
93
|
+
const sharedAlt = 'Logo';
|
|
94
|
+
const sharedUrl = 'https://a.com/logo.jpg';
|
|
95
|
+
const images = [
|
|
96
|
+
{ src: sharedUrl, alt: sharedAlt, header: false },
|
|
97
|
+
{ src: sharedUrl, alt: sharedAlt, header: false },
|
|
98
|
+
];
|
|
99
|
+
const body = `\n\n`;
|
|
100
|
+
const result = deduplicateImageAlts(images, body);
|
|
101
|
+
|
|
102
|
+
assert.equal(result.images[0].alt, sharedAlt, 'First image alt unchanged');
|
|
103
|
+
assert.equal(result.images[1].alt, sharedAlt, 'Second image alt unchanged (same URL)');
|
|
104
|
+
assert.equal(result.body, body, 'Body unchanged when URLs match');
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
// ─── Header image is never touched ───
|
|
109
|
+
|
|
110
|
+
{
|
|
111
|
+
name: 'header-image-with-shared-alt-is-not-modified',
|
|
112
|
+
async run({ assert }) {
|
|
113
|
+
const sharedAlt = 'banner';
|
|
114
|
+
const images = [
|
|
115
|
+
{ src: 'https://a.com/header.jpg', alt: sharedAlt, header: true },
|
|
116
|
+
{ src: 'https://a.com/body.jpg', alt: sharedAlt, header: false },
|
|
117
|
+
];
|
|
118
|
+
const body = ``;
|
|
119
|
+
const result = deduplicateImageAlts(images, body);
|
|
120
|
+
|
|
121
|
+
assert.equal(result.images[0].alt, sharedAlt, 'Header image alt unchanged');
|
|
122
|
+
assert.equal(
|
|
123
|
+
result.images[1].alt,
|
|
124
|
+
sharedAlt,
|
|
125
|
+
'Body image alt unchanged because header is excluded from collision tracking',
|
|
126
|
+
);
|
|
127
|
+
assert.equal(result.body, body, 'Body unchanged');
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
// ─── Mixed scenarios ───
|
|
132
|
+
|
|
133
|
+
{
|
|
134
|
+
name: 'mixed-A-B-A-C-only-C-collides-with-A-once',
|
|
135
|
+
async run({ assert }) {
|
|
136
|
+
// Pattern: A, B, A (same URL as first A), C (different URL, same alt as A)
|
|
137
|
+
// Expected: A and A reuse, C gets " (2)" suffix because its alt collides with A.
|
|
138
|
+
const altA = 'Shared';
|
|
139
|
+
const altB = 'Other';
|
|
140
|
+
const urlA = 'https://a.com/a.jpg';
|
|
141
|
+
const urlB = 'https://a.com/b.jpg';
|
|
142
|
+
const urlC = 'https://a.com/c.jpg';
|
|
143
|
+
|
|
144
|
+
const images = [
|
|
145
|
+
{ src: urlA, alt: altA, header: false },
|
|
146
|
+
{ src: urlB, alt: altB, header: false },
|
|
147
|
+
{ src: urlA, alt: altA, header: false }, // Same URL as image 1 — legit duplicate
|
|
148
|
+
{ src: urlC, alt: altA, header: false }, // Different URL, same alt — collision
|
|
149
|
+
];
|
|
150
|
+
const body = `\n\n\n`;
|
|
151
|
+
const result = deduplicateImageAlts(images, body);
|
|
152
|
+
|
|
153
|
+
assert.equal(result.images[0].alt, altA, 'Image 1 unchanged');
|
|
154
|
+
assert.equal(result.images[1].alt, altB, 'Image 2 (different alt) unchanged');
|
|
155
|
+
assert.equal(result.images[2].alt, altA, 'Image 3 (same URL as 1) unchanged');
|
|
156
|
+
assert.equal(result.images[3].alt, `${altA} (2)`, 'Image 4 (different URL, same alt) gets " (2)"');
|
|
157
|
+
assert.ok(result.body.includes(``), 'Body has the C image with suffixed alt');
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
// ─── Edge cases ───
|
|
162
|
+
|
|
163
|
+
{
|
|
164
|
+
name: 'empty-images-array-returns-empty',
|
|
165
|
+
async run({ assert }) {
|
|
166
|
+
const result = deduplicateImageAlts([], 'some body');
|
|
167
|
+
assert.deepEqual(result.images, [], 'images is empty');
|
|
168
|
+
assert.equal(result.body, 'some body', 'body unchanged');
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
{
|
|
173
|
+
name: 'no-collisions-among-different-alts',
|
|
174
|
+
async run({ assert }) {
|
|
175
|
+
const images = [
|
|
176
|
+
{ src: 'https://a.com/1.jpg', alt: 'A', header: false },
|
|
177
|
+
{ src: 'https://a.com/2.jpg', alt: 'B', header: false },
|
|
178
|
+
{ src: 'https://a.com/3.jpg', alt: 'C', header: false },
|
|
179
|
+
];
|
|
180
|
+
const body = '\n\n';
|
|
181
|
+
const result = deduplicateImageAlts(images, body);
|
|
182
|
+
|
|
183
|
+
assert.equal(result.images[0].alt, 'A');
|
|
184
|
+
assert.equal(result.images[1].alt, 'B');
|
|
185
|
+
assert.equal(result.images[2].alt, 'C');
|
|
186
|
+
assert.equal(result.body, body, 'Body unchanged');
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
};
|
|
File without changes
|