@digilogiclabs/platform-core 1.7.0 → 1.9.0
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/dist/auth.d.mts +3 -2
- package/dist/auth.d.ts +3 -2
- package/dist/auth.js +119 -0
- package/dist/auth.js.map +1 -1
- package/dist/auth.mjs +113 -0
- package/dist/auth.mjs.map +1 -1
- package/dist/email-templates.d.mts +210 -0
- package/dist/email-templates.d.ts +210 -0
- package/dist/email-templates.js +338 -0
- package/dist/email-templates.js.map +1 -0
- package/dist/email-templates.mjs +297 -0
- package/dist/email-templates.mjs.map +1 -0
- package/dist/{env-DerQ7Da-.d.mts → env-DHPZR3Lv.d.mts} +345 -74
- package/dist/{env-DerQ7Da-.d.ts → env-DHPZR3Lv.d.ts} +345 -74
- package/dist/{index-CepDdu7h.d.mts → index-DzQ0Js5Z.d.mts} +13 -1
- package/dist/{index-CepDdu7h.d.ts → index-DzQ0Js5Z.d.ts} +13 -1
- package/dist/index.d.mts +99 -3
- package/dist/index.d.ts +99 -3
- package/dist/index.js +974 -14
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +960 -14
- package/dist/index.mjs.map +1 -1
- package/dist/migrations/index.d.mts +1 -1
- package/dist/migrations/index.d.ts +1 -1
- package/dist/migrations/index.js +72 -1
- package/dist/migrations/index.js.map +1 -1
- package/dist/migrations/index.mjs +72 -1
- package/dist/migrations/index.mjs.map +1 -1
- package/dist/security-BvLXaQkv.d.mts +88 -0
- package/dist/security-BvLXaQkv.d.ts +88 -0
- package/package.json +6 -1
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// src/security.ts
|
|
2
|
+
function escapeHtml(str) {
|
|
3
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// src/email-templates/layout.ts
|
|
7
|
+
var FONT_STACK = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif";
|
|
8
|
+
function darkenHex(hex, percent) {
|
|
9
|
+
const num = parseInt(hex.replace("#", ""), 16);
|
|
10
|
+
const r = Math.max(0, (num >> 16) - Math.round(2.55 * percent));
|
|
11
|
+
const g = Math.max(0, (num >> 8 & 255) - Math.round(2.55 * percent));
|
|
12
|
+
const b = Math.max(0, (num & 255) - Math.round(2.55 * percent));
|
|
13
|
+
return `#${(r << 16 | g << 8 | b).toString(16).padStart(6, "0")}`;
|
|
14
|
+
}
|
|
15
|
+
function htmlToText(html) {
|
|
16
|
+
return html.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n\n").replace(/<\/div>/gi, "\n").replace(/<\/tr>/gi, "\n").replace(/<\/li>/gi, "\n").replace(/<li[^>]*>/gi, "- ").replace(/<a[^>]*href="([^"]*)"[^>]*>([^<]*)<\/a>/gi, "$2 ($1)").replace(/<[^>]+>/g, "").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/→/g, "\u2192").replace(/\n{3,}/g, "\n\n").trim();
|
|
17
|
+
}
|
|
18
|
+
function emailLayout(branding, options) {
|
|
19
|
+
const gradientFrom = branding.gradientFrom ?? branding.primaryColor;
|
|
20
|
+
const gradientTo = branding.gradientTo ?? darkenHex(branding.primaryColor, 15);
|
|
21
|
+
const accentColor = branding.accentColor ?? branding.primaryColor;
|
|
22
|
+
const preheaderHtml = options.preheader ? `<div style="display:none;font-size:1px;color:#f3f4f6;line-height:1px;max-height:0;max-width:0;opacity:0;overflow:hidden;">${escapeHtml(options.preheader)}</div>` : "";
|
|
23
|
+
const iconHtml = options.icon ? `${options.icon} ` : "";
|
|
24
|
+
const subtitleHtml = options.subtitle ? `<p style="margin:4px 0 0;color:rgba(255,255,255,0.85);font-size:14px;">${escapeHtml(options.subtitle)}</p>` : "";
|
|
25
|
+
const ctaHtml = options.cta ? `
|
|
26
|
+
<tr>
|
|
27
|
+
<td style="padding:0 32px 24px;text-align:center;">
|
|
28
|
+
<a href="${escapeHtml(options.cta.url)}"
|
|
29
|
+
style="display:inline-block;padding:12px 28px;background:${accentColor};color:#ffffff;text-decoration:none;border-radius:6px;font-weight:600;font-size:14px;">
|
|
30
|
+
${escapeHtml(options.cta.text)}
|
|
31
|
+
</a>
|
|
32
|
+
</td>
|
|
33
|
+
</tr>` : "";
|
|
34
|
+
const footerLinksHtml = options.footerLinks && options.footerLinks.length > 0 ? options.footerLinks.map(
|
|
35
|
+
(link) => `<a href="${escapeHtml(link.url)}" style="color:${accentColor};text-decoration:none;">${escapeHtml(link.label)}</a>`
|
|
36
|
+
).join(" · ") : "";
|
|
37
|
+
const preferencesHtml = branding.preferencesUrl ? `<p style="margin:4px 0 0;font-size:12px;color:#9ca3af;">
|
|
38
|
+
<a href="${escapeHtml(branding.preferencesUrl)}" style="color:${accentColor};text-decoration:none;">Update email preferences</a>
|
|
39
|
+
</p>` : "";
|
|
40
|
+
const html = `<!DOCTYPE html>
|
|
41
|
+
<html lang="en">
|
|
42
|
+
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>${escapeHtml(options.subject)}</title></head>
|
|
43
|
+
<body style="margin:0;padding:0;background:#f3f4f6;font-family:${FONT_STACK};">
|
|
44
|
+
${preheaderHtml}
|
|
45
|
+
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f3f4f6;padding:24px 0;">
|
|
46
|
+
<tr><td align="center">
|
|
47
|
+
<table width="600" cellpadding="0" cellspacing="0" style="background:#ffffff;border-radius:8px;overflow:hidden;max-width:600px;">
|
|
48
|
+
<!-- Header -->
|
|
49
|
+
<tr>
|
|
50
|
+
<td style="background:linear-gradient(135deg,${gradientFrom},${gradientTo});padding:28px 32px;text-align:center;">
|
|
51
|
+
<h1 style="margin:0;color:#ffffff;font-size:22px;font-weight:700;">${iconHtml}${escapeHtml(branding.appName)}</h1>
|
|
52
|
+
${subtitleHtml}
|
|
53
|
+
</td>
|
|
54
|
+
</tr>
|
|
55
|
+
|
|
56
|
+
<!-- Body -->
|
|
57
|
+
<tr>
|
|
58
|
+
<td style="padding:28px 32px;color:#374151;font-size:15px;line-height:1.6;">
|
|
59
|
+
${options.body}
|
|
60
|
+
</td>
|
|
61
|
+
</tr>
|
|
62
|
+
|
|
63
|
+
${ctaHtml}
|
|
64
|
+
|
|
65
|
+
<!-- Footer -->
|
|
66
|
+
<tr>
|
|
67
|
+
<td style="padding:16px 32px;background:#f9fafb;text-align:center;border-top:1px solid #e5e7eb;">
|
|
68
|
+
${footerLinksHtml ? `<p style="margin:0 0 4px;font-size:12px;color:#9ca3af;">${footerLinksHtml}</p>` : ""}
|
|
69
|
+
<p style="margin:0;font-size:12px;color:#9ca3af;">
|
|
70
|
+
${branding.footerText ? escapeHtml(branding.footerText) + " · " : ""}
|
|
71
|
+
<a href="${escapeHtml(branding.baseUrl)}" style="color:${accentColor};text-decoration:none;">${escapeHtml(branding.appName)}</a>
|
|
72
|
+
</p>
|
|
73
|
+
${preferencesHtml}
|
|
74
|
+
</td>
|
|
75
|
+
</tr>
|
|
76
|
+
</table>
|
|
77
|
+
</td></tr>
|
|
78
|
+
</table>
|
|
79
|
+
</body>
|
|
80
|
+
</html>`;
|
|
81
|
+
const text = `${options.icon ? options.icon + " " : ""}${options.subject}
|
|
82
|
+
${"=".repeat(40)}
|
|
83
|
+
|
|
84
|
+
${htmlToText(options.body)}${options.cta ? `
|
|
85
|
+
|
|
86
|
+
${options.cta.text}: ${options.cta.url}` : ""}
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
${branding.footerText ? branding.footerText + "\n" : ""}${branding.appName} - ${branding.baseUrl}${branding.preferencesUrl ? "\nEmail preferences: " + branding.preferencesUrl : ""}`;
|
|
90
|
+
return { subject: options.subject, html, text };
|
|
91
|
+
}
|
|
92
|
+
function calloutBlock(content, color = "#3b82f6") {
|
|
93
|
+
return `<div style="background:#f9fafb;padding:16px 20px;border-radius:8px;border-left:4px solid ${color};margin:16px 0;">${content}</div>`;
|
|
94
|
+
}
|
|
95
|
+
function stepBlock(number, title, description, color = "#667eea") {
|
|
96
|
+
return `<div style="background:#f3f4f6;padding:16px;border-radius:8px;margin-bottom:12px;border-left:4px solid ${color};">
|
|
97
|
+
<p style="margin:0;font-weight:600;color:#111827;">${number}. ${title}</p>
|
|
98
|
+
<p style="margin:8px 0 0;font-size:14px;color:#6b7280;">${description}</p>
|
|
99
|
+
</div>`;
|
|
100
|
+
}
|
|
101
|
+
function statsBar(stats) {
|
|
102
|
+
const cells = stats.map(
|
|
103
|
+
(s) => `<span style="display:inline-block;margin:0 16px;font-size:14px;color:#374151;"><strong>${escapeHtml(String(s.value))}</strong> ${escapeHtml(s.label)}</span>`
|
|
104
|
+
).join("");
|
|
105
|
+
return `<div style="padding:12px 0;text-align:center;background:#f0fdf4;border-radius:6px;margin-bottom:16px;">${cells}</div>`;
|
|
106
|
+
}
|
|
107
|
+
function dataTable(headers, rows) {
|
|
108
|
+
const headerCells = headers.map(
|
|
109
|
+
(h) => `<th style="padding:8px 12px;text-align:left;font-size:13px;color:#374151;border-bottom:1px solid #e5e7eb;">${escapeHtml(h)}</th>`
|
|
110
|
+
).join("");
|
|
111
|
+
const bodyRows = rows.map(
|
|
112
|
+
(row) => `<tr>${row.map((cell) => `<td style="padding:8px 12px;border-bottom:1px solid #e5e7eb;font-size:14px;color:#374151;">${cell}</td>`).join("")}</tr>`
|
|
113
|
+
).join("");
|
|
114
|
+
return `<table width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:6px;overflow:hidden;">
|
|
115
|
+
<tr style="background:#f9fafb;">${headerCells}</tr>
|
|
116
|
+
${bodyRows}
|
|
117
|
+
</table>`;
|
|
118
|
+
}
|
|
119
|
+
function tipBlock(content, icon = "\u{1F4A1}") {
|
|
120
|
+
return `<div style="background:#eff6ff;border:2px solid #3b82f6;padding:16px 20px;border-radius:8px;margin:16px 0;">
|
|
121
|
+
<p style="margin:0;color:#1e40af;font-size:14px;"><strong>${icon} Pro Tip:</strong> ${content}</p>
|
|
122
|
+
</div>`;
|
|
123
|
+
}
|
|
124
|
+
function sectionHeading(text) {
|
|
125
|
+
return `<h2 style="margin:24px 0 12px;font-size:16px;color:#111827;">${escapeHtml(text)}</h2>`;
|
|
126
|
+
}
|
|
127
|
+
function divider() {
|
|
128
|
+
return `<hr style="border:none;border-top:1px solid #e5e7eb;margin:24px 0;">`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/email-templates/templates.ts
|
|
132
|
+
function welcomeEmail(branding, data) {
|
|
133
|
+
const safeName = escapeHtml(data.userName);
|
|
134
|
+
const stepsHtml = data.steps.map((s, i) => stepBlock(i + 1, escapeHtml(s.title), escapeHtml(s.description), branding.accentColor ?? branding.primaryColor)).join("");
|
|
135
|
+
const tipHtml = data.tip ? tipBlock(escapeHtml(data.tip)) : "";
|
|
136
|
+
const body = `
|
|
137
|
+
<p style="font-size:16px;margin-bottom:20px;">Hi ${safeName},</p>
|
|
138
|
+
<p style="font-size:15px;margin-bottom:20px;">
|
|
139
|
+
Welcome aboard! Your account is all set up and ready to go.
|
|
140
|
+
</p>
|
|
141
|
+
${sectionHeading("Get Started")}
|
|
142
|
+
${stepsHtml}
|
|
143
|
+
${tipHtml}
|
|
144
|
+
`;
|
|
145
|
+
return emailLayout(branding, {
|
|
146
|
+
subject: `Welcome to ${branding.appName}!`,
|
|
147
|
+
preheader: `Your ${branding.appName} account is ready`,
|
|
148
|
+
icon: "\u{1F389}",
|
|
149
|
+
subtitle: "Welcome aboard",
|
|
150
|
+
body,
|
|
151
|
+
cta: { text: "Go to Dashboard", url: data.dashboardUrl },
|
|
152
|
+
footerLinks: branding.supportEmail ? [{ label: "Get Help", url: `mailto:${branding.supportEmail}` }] : void 0
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function digestEmail(branding, data) {
|
|
156
|
+
const safeName = escapeHtml(data.recipientName);
|
|
157
|
+
const safePeriod = escapeHtml(data.period);
|
|
158
|
+
const body = `
|
|
159
|
+
<p style="font-size:16px;margin-bottom:20px;">Hi ${safeName},</p>
|
|
160
|
+
<p style="font-size:15px;margin-bottom:16px;">
|
|
161
|
+
Here's your ${safePeriod.toLowerCase()} summary:
|
|
162
|
+
</p>
|
|
163
|
+
${statsBar(data.stats)}
|
|
164
|
+
${data.contentHtml}
|
|
165
|
+
${data.subscriptionNote ? `<p style="margin-top:20px;font-size:13px;color:#9ca3af;">${escapeHtml(data.subscriptionNote)}</p>` : ""}
|
|
166
|
+
`;
|
|
167
|
+
return emailLayout(branding, {
|
|
168
|
+
subject: `${branding.appName} \u2014 ${data.period} Summary`,
|
|
169
|
+
preheader: `Your ${data.period.toLowerCase()} activity on ${branding.appName}`,
|
|
170
|
+
icon: "\u{1F4CA}",
|
|
171
|
+
subtitle: safePeriod,
|
|
172
|
+
body,
|
|
173
|
+
cta: data.reportUrl ? { text: "View Full Report", url: data.reportUrl } : void 0
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
function notificationEmail(branding, data) {
|
|
177
|
+
const safeName = escapeHtml(data.recipientName);
|
|
178
|
+
const safeHeadline = escapeHtml(data.headline);
|
|
179
|
+
const safeMessage = escapeHtml(data.message);
|
|
180
|
+
const detailHtml = data.detail ? calloutBlock(escapeHtml(data.detail), branding.accentColor ?? branding.primaryColor) : "";
|
|
181
|
+
const body = `
|
|
182
|
+
<p style="font-size:16px;margin-bottom:20px;">Hi ${safeName},</p>
|
|
183
|
+
<h2 style="margin:0 0 12px;font-size:18px;color:#111827;">${safeHeadline}</h2>
|
|
184
|
+
<p style="font-size:15px;margin-bottom:16px;">${safeMessage}</p>
|
|
185
|
+
${detailHtml}
|
|
186
|
+
`;
|
|
187
|
+
return emailLayout(branding, {
|
|
188
|
+
subject: `${branding.appName}: ${data.headline}`,
|
|
189
|
+
preheader: data.message.slice(0, 100),
|
|
190
|
+
body,
|
|
191
|
+
cta: data.actionUrl ? { text: data.actionText ?? "View Details", url: data.actionUrl } : void 0
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
var MODERATION_CONFIG = {
|
|
195
|
+
warning: { icon: "\u26A0\uFE0F", color: "#f59e0b", label: "Account Warning" },
|
|
196
|
+
suspension: { icon: "\u{1F512}", color: "#ef4444", label: "Account Suspended" },
|
|
197
|
+
ban: { icon: "\u{1F6AB}", color: "#dc2626", label: "Account Banned" },
|
|
198
|
+
reinstatement: { icon: "\u2705", color: "#10b981", label: "Account Reinstated" }
|
|
199
|
+
};
|
|
200
|
+
function moderationEmail(branding, data) {
|
|
201
|
+
const safeName = escapeHtml(data.userName);
|
|
202
|
+
const config = MODERATION_CONFIG[data.action];
|
|
203
|
+
const reasonHtml = calloutBlock(
|
|
204
|
+
`<strong>Reason:</strong> ${escapeHtml(data.reason)}`,
|
|
205
|
+
config.color
|
|
206
|
+
);
|
|
207
|
+
const nextStepsHtml = data.nextSteps ? `<p style="font-size:15px;margin:16px 0;">${escapeHtml(data.nextSteps)}</p>` : "";
|
|
208
|
+
const body = `
|
|
209
|
+
<p style="font-size:16px;margin-bottom:20px;">Hi ${safeName},</p>
|
|
210
|
+
<p style="font-size:15px;margin-bottom:16px;">
|
|
211
|
+
${data.action === "reinstatement" ? "Good news \u2014 your account has been reinstated." : `Your account has received a ${escapeHtml(data.action)}.`}
|
|
212
|
+
</p>
|
|
213
|
+
${reasonHtml}
|
|
214
|
+
${nextStepsHtml}
|
|
215
|
+
`;
|
|
216
|
+
return emailLayout(branding, {
|
|
217
|
+
subject: `${branding.appName}: ${config.label}`,
|
|
218
|
+
preheader: config.label,
|
|
219
|
+
icon: config.icon,
|
|
220
|
+
subtitle: config.label,
|
|
221
|
+
body,
|
|
222
|
+
cta: data.appealUrl ? { text: "Appeal or Contact Support", url: data.appealUrl } : void 0
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
function transactionEmail(branding, data) {
|
|
226
|
+
const safeName = escapeHtml(data.recipientName);
|
|
227
|
+
const safeHeadline = escapeHtml(data.headline);
|
|
228
|
+
const rows = data.lineItems.map(([label, value]) => [
|
|
229
|
+
escapeHtml(label),
|
|
230
|
+
escapeHtml(value)
|
|
231
|
+
]);
|
|
232
|
+
if (data.total) {
|
|
233
|
+
rows.push([
|
|
234
|
+
`<strong>Total</strong>`,
|
|
235
|
+
`<strong>${escapeHtml(data.total)}</strong>`
|
|
236
|
+
]);
|
|
237
|
+
}
|
|
238
|
+
const messageHtml = data.message ? `<p style="font-size:15px;margin-bottom:16px;">${escapeHtml(data.message)}</p>` : "";
|
|
239
|
+
const body = `
|
|
240
|
+
<p style="font-size:16px;margin-bottom:20px;">Hi ${safeName},</p>
|
|
241
|
+
<h2 style="margin:0 0 12px;font-size:18px;color:#111827;">${safeHeadline}</h2>
|
|
242
|
+
${messageHtml}
|
|
243
|
+
${dataTable(["Item", "Amount"], rows)}
|
|
244
|
+
`;
|
|
245
|
+
return emailLayout(branding, {
|
|
246
|
+
subject: `${branding.appName}: ${data.headline}`,
|
|
247
|
+
preheader: data.headline,
|
|
248
|
+
icon: "\u{1F9FE}",
|
|
249
|
+
body,
|
|
250
|
+
cta: data.receiptUrl ? { text: "View Receipt", url: data.receiptUrl } : void 0
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
function securityEmail(branding, data) {
|
|
254
|
+
const safeName = escapeHtml(data.userName);
|
|
255
|
+
const detailHtml = calloutBlock(
|
|
256
|
+
escapeHtml(data.details),
|
|
257
|
+
"#ef4444"
|
|
258
|
+
);
|
|
259
|
+
const expiryHtml = data.expiryNote ? `<p style="font-size:13px;color:#9ca3af;margin-top:12px;text-align:center;">${escapeHtml(data.expiryNote)}</p>` : "";
|
|
260
|
+
const body = `
|
|
261
|
+
<p style="font-size:16px;margin-bottom:20px;">Hi ${safeName},</p>
|
|
262
|
+
<p style="font-size:15px;margin-bottom:16px;">
|
|
263
|
+
${escapeHtml(data.event)}
|
|
264
|
+
</p>
|
|
265
|
+
${detailHtml}
|
|
266
|
+
${expiryHtml}
|
|
267
|
+
<p style="font-size:14px;color:#6b7280;margin-top:16px;">
|
|
268
|
+
If you didn't initiate this action, please contact support immediately.
|
|
269
|
+
</p>
|
|
270
|
+
`;
|
|
271
|
+
return emailLayout(branding, {
|
|
272
|
+
subject: `${branding.appName}: ${data.event}`,
|
|
273
|
+
preheader: data.event,
|
|
274
|
+
icon: "\u{1F510}",
|
|
275
|
+
subtitle: "Security Alert",
|
|
276
|
+
body,
|
|
277
|
+
cta: data.actionUrl ? { text: data.actionText ?? "Take Action", url: data.actionUrl } : void 0
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
export {
|
|
281
|
+
calloutBlock,
|
|
282
|
+
dataTable,
|
|
283
|
+
digestEmail,
|
|
284
|
+
divider,
|
|
285
|
+
emailLayout,
|
|
286
|
+
escapeHtml,
|
|
287
|
+
moderationEmail,
|
|
288
|
+
notificationEmail,
|
|
289
|
+
sectionHeading,
|
|
290
|
+
securityEmail,
|
|
291
|
+
statsBar,
|
|
292
|
+
stepBlock,
|
|
293
|
+
tipBlock,
|
|
294
|
+
transactionEmail,
|
|
295
|
+
welcomeEmail
|
|
296
|
+
};
|
|
297
|
+
//# sourceMappingURL=email-templates.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/security.ts","../src/email-templates/layout.ts","../src/email-templates/templates.ts"],"sourcesContent":["/**\n * Security Utilities\n *\n * HTML escaping, input detection, sanitization helpers,\n * timing-safe comparison, error sanitization, and request\n * correlation for safe, consistent security across all apps.\n */\n\nimport { timingSafeEqual } from \"crypto\";\n\n/** Regex for protocol-prefixed URLs */\nexport const URL_PROTOCOL_PATTERN = /(https?:\\/\\/|ftp:\\/\\/|www\\.)\\S+/i;\n\n/** Regex for bare domain names with common TLDs */\nexport const URL_DOMAIN_PATTERN =\n /\\b[\\w.-]+\\.(com|net|org|io|co|dev|app|xyz|info|biz|me|us|uk|edu|gov)\\b/i;\n\n/** Regex for HTML tags */\nexport const HTML_TAG_PATTERN = /<[^>]*>/;\n\n/**\n * Escape HTML special characters to prevent injection.\n * Use when inserting user content into HTML email templates or rendered HTML.\n */\nexport function escapeHtml(str: string): string {\n return str\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\n/** Check if a string contains protocol-prefixed URLs or bare domains */\nexport function containsUrls(str: string): boolean {\n return URL_PROTOCOL_PATTERN.test(str) || URL_DOMAIN_PATTERN.test(str);\n}\n\n/** Check if a string contains HTML tags */\nexport function containsHtml(str: string): boolean {\n return HTML_TAG_PATTERN.test(str);\n}\n\n/** Strip all HTML tags from a string */\nexport function stripHtml(str: string): string {\n return str.replace(/<[^>]*>/g, \"\");\n}\n\n/**\n * Defang URLs to prevent auto-linking in email clients.\n * Converts https://evil.com → hxxps://evil[.]com\n */\nexport function defangUrl(str: string): string {\n return str\n .replace(/https:\\/\\//gi, \"hxxps://\")\n .replace(/http:\\/\\//gi, \"hxxp://\")\n .replace(/ftp:\\/\\//gi, \"fxp://\")\n .replace(\n /\\.(com|net|org|io|co|dev|app|xyz|info|biz|me|us|uk|edu|gov)\\b/gi,\n \"[$1]\",\n );\n}\n\n/**\n * Sanitize user content for safe insertion into HTML email templates.\n * Escapes HTML entities AND defangs any URLs that slipped through validation.\n */\nexport function sanitizeForEmail(str: string): string {\n return escapeHtml(str);\n}\n\n// ═══════════════════════════════════════════════════════════════\n// API SECURITY UTILITIES\n// ═══════════════════════════════════════════════════════════════\n\n/**\n * Constant-time string comparison to prevent timing side-channel attacks.\n * Use for comparing secrets, tokens, API keys, HMAC signatures, etc.\n *\n * Returns false (not throws) for length mismatches — still constant-time\n * relative to the shorter string to avoid leaking length info.\n *\n * @example\n * ```typescript\n * if (!constantTimeEqual(providedToken, expectedSecret)) {\n * return { status: 401, error: 'Invalid token' }\n * }\n * ```\n */\nexport function constantTimeEqual(a: string, b: string): boolean {\n try {\n const aBuf = Buffer.from(a, \"utf-8\");\n const bBuf = Buffer.from(b, \"utf-8\");\n if (aBuf.length !== bBuf.length) return false;\n return timingSafeEqual(aBuf, bBuf);\n } catch {\n return false;\n }\n}\n\n/**\n * Sanitize an error for client-facing API responses.\n *\n * - 4xx errors: returns the actual message (client needs to know what went wrong)\n * - 5xx errors: returns a generic message (never leak internals to clients)\n * - Development mode: optionally includes stack trace for debugging\n *\n * @example\n * ```typescript\n * catch (error) {\n * const { message, code } = sanitizeApiError(error, 500)\n * return Response.json({ error: message, code }, { status: 500 })\n * }\n * ```\n */\nexport function sanitizeApiError(\n error: unknown,\n statusCode: number,\n isDevelopment = false,\n): { message: string; code?: string; stack?: string } {\n // Client errors — safe to expose the message\n if (statusCode >= 400 && statusCode < 500) {\n const message =\n error instanceof Error ? error.message : String(error || \"Bad request\");\n return { message };\n }\n\n // Server errors — generic message to clients, real error in logs only\n const result: { message: string; code: string; stack?: string } = {\n message: \"An internal error occurred. Please try again later.\",\n code: \"INTERNAL_ERROR\",\n };\n\n if (isDevelopment && error instanceof Error) {\n result.stack = error.stack;\n }\n\n return result;\n}\n\n/**\n * Extract a correlation/request ID from standard headers, or generate one.\n *\n * Checks (in order): X-Request-ID, X-Correlation-ID, then falls back to\n * crypto.randomUUID(). Works with any headers-like object (plain object,\n * Headers API, or a getter function).\n *\n * @example\n * ```typescript\n * // With Next.js request\n * const id = getCorrelationId((name) => request.headers.get(name))\n *\n * // With plain object\n * const id = getCorrelationId({ 'x-request-id': 'abc-123' })\n * ```\n */\nexport function getCorrelationId(\n headers:\n | Record<string, string | string[] | undefined>\n | ((name: string) => string | null | undefined),\n): string {\n const get =\n typeof headers === \"function\"\n ? headers\n : (name: string) => {\n const val = headers[name] ?? headers[name.toLowerCase()];\n return Array.isArray(val) ? val[0] : val;\n };\n\n return (\n get(\"x-request-id\") ||\n get(\"X-Request-ID\") ||\n get(\"x-correlation-id\") ||\n get(\"X-Correlation-ID\") ||\n crypto.randomUUID()\n );\n}\n","/**\n * Base email layout builder.\n *\n * Generates a fully responsive, email-client-compatible HTML email\n * using only inline styles and table-based layout.\n *\n * Every user-facing string in the layout is pre-escaped. Template functions\n * that accept user input MUST escape those values before passing them to body.\n */\nimport type { EmailBranding, EmailLayoutOptions, EmailOutput } from \"./types\";\nimport { escapeHtml } from \"../security\";\n\nconst FONT_STACK =\n \"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif\";\n\n/**\n * Darken a hex color by a percentage (0-100).\n * Used to auto-generate gradientTo from primaryColor.\n */\nfunction darkenHex(hex: string, percent: number): string {\n const num = parseInt(hex.replace(\"#\", \"\"), 16);\n const r = Math.max(0, (num >> 16) - Math.round(2.55 * percent));\n const g = Math.max(0, ((num >> 8) & 0x00ff) - Math.round(2.55 * percent));\n const b = Math.max(0, (num & 0x0000ff) - Math.round(2.55 * percent));\n return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, \"0\")}`;\n}\n\n/**\n * Strip HTML tags for plain-text fallback.\n */\nfunction htmlToText(html: string): string {\n return html\n .replace(/<br\\s*\\/?>/gi, \"\\n\")\n .replace(/<\\/p>/gi, \"\\n\\n\")\n .replace(/<\\/div>/gi, \"\\n\")\n .replace(/<\\/tr>/gi, \"\\n\")\n .replace(/<\\/li>/gi, \"\\n\")\n .replace(/<li[^>]*>/gi, \"- \")\n .replace(/<a[^>]*href=\"([^\"]*)\"[^>]*>([^<]*)<\\/a>/gi, \"$2 ($1)\")\n .replace(/<[^>]+>/g, \"\")\n .replace(/ /g, \" \")\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/→/g, \"→\")\n .replace(/\\n{3,}/g, \"\\n\\n\")\n .trim();\n}\n\n/**\n * Build a complete HTML email from branding + layout options.\n *\n * Returns `{ subject, html, text }` ready to pass to any IEmail adapter.\n */\nexport function emailLayout(\n branding: EmailBranding,\n options: EmailLayoutOptions\n): EmailOutput {\n const gradientFrom = branding.gradientFrom ?? branding.primaryColor;\n const gradientTo =\n branding.gradientTo ?? darkenHex(branding.primaryColor, 15);\n const accentColor = branding.accentColor ?? branding.primaryColor;\n\n const preheaderHtml = options.preheader\n ? `<div style=\"display:none;font-size:1px;color:#f3f4f6;line-height:1px;max-height:0;max-width:0;opacity:0;overflow:hidden;\">${escapeHtml(options.preheader)}</div>`\n : \"\";\n\n const iconHtml = options.icon ? `${options.icon} ` : \"\";\n\n const subtitleHtml = options.subtitle\n ? `<p style=\"margin:4px 0 0;color:rgba(255,255,255,0.85);font-size:14px;\">${escapeHtml(options.subtitle)}</p>`\n : \"\";\n\n const ctaHtml = options.cta\n ? `\n <tr>\n <td style=\"padding:0 32px 24px;text-align:center;\">\n <a href=\"${escapeHtml(options.cta.url)}\"\n style=\"display:inline-block;padding:12px 28px;background:${accentColor};color:#ffffff;text-decoration:none;border-radius:6px;font-weight:600;font-size:14px;\">\n ${escapeHtml(options.cta.text)}\n </a>\n </td>\n </tr>`\n : \"\";\n\n const footerLinksHtml =\n options.footerLinks && options.footerLinks.length > 0\n ? options.footerLinks\n .map(\n (link) =>\n `<a href=\"${escapeHtml(link.url)}\" style=\"color:${accentColor};text-decoration:none;\">${escapeHtml(link.label)}</a>`\n )\n .join(\" · \")\n : \"\";\n\n const preferencesHtml = branding.preferencesUrl\n ? `<p style=\"margin:4px 0 0;font-size:12px;color:#9ca3af;\">\n <a href=\"${escapeHtml(branding.preferencesUrl)}\" style=\"color:${accentColor};text-decoration:none;\">Update email preferences</a>\n </p>`\n : \"\";\n\n const html = `<!DOCTYPE html>\n<html lang=\"en\">\n<head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\"><title>${escapeHtml(options.subject)}</title></head>\n<body style=\"margin:0;padding:0;background:#f3f4f6;font-family:${FONT_STACK};\">\n ${preheaderHtml}\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#f3f4f6;padding:24px 0;\">\n <tr><td align=\"center\">\n <table width=\"600\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#ffffff;border-radius:8px;overflow:hidden;max-width:600px;\">\n <!-- Header -->\n <tr>\n <td style=\"background:linear-gradient(135deg,${gradientFrom},${gradientTo});padding:28px 32px;text-align:center;\">\n <h1 style=\"margin:0;color:#ffffff;font-size:22px;font-weight:700;\">${iconHtml}${escapeHtml(branding.appName)}</h1>\n ${subtitleHtml}\n </td>\n </tr>\n\n <!-- Body -->\n <tr>\n <td style=\"padding:28px 32px;color:#374151;font-size:15px;line-height:1.6;\">\n ${options.body}\n </td>\n </tr>\n\n ${ctaHtml}\n\n <!-- Footer -->\n <tr>\n <td style=\"padding:16px 32px;background:#f9fafb;text-align:center;border-top:1px solid #e5e7eb;\">\n ${footerLinksHtml ? `<p style=\"margin:0 0 4px;font-size:12px;color:#9ca3af;\">${footerLinksHtml}</p>` : \"\"}\n <p style=\"margin:0;font-size:12px;color:#9ca3af;\">\n ${branding.footerText ? escapeHtml(branding.footerText) + \" · \" : \"\"}\n <a href=\"${escapeHtml(branding.baseUrl)}\" style=\"color:${accentColor};text-decoration:none;\">${escapeHtml(branding.appName)}</a>\n </p>\n ${preferencesHtml}\n </td>\n </tr>\n </table>\n </td></tr>\n </table>\n</body>\n</html>`;\n\n const text = `${options.icon ? options.icon + \" \" : \"\"}${options.subject}\\n${\"=\".repeat(40)}\\n\\n${htmlToText(options.body)}${options.cta ? `\\n\\n${options.cta.text}: ${options.cta.url}` : \"\"}\\n\\n---\\n${branding.footerText ? branding.footerText + \"\\n\" : \"\"}${branding.appName} - ${branding.baseUrl}${branding.preferencesUrl ? \"\\nEmail preferences: \" + branding.preferencesUrl : \"\"}`;\n\n return { subject: options.subject, html, text };\n}\n\n// ─── HTML Building Blocks ────────────────────────────────────────────────────\n// These helpers generate safe HTML fragments for use inside template body content.\n// All user-provided values MUST be escaped before calling these.\n\n/** Highlighted callout box with colored left border. */\nexport function calloutBlock(\n content: string,\n color: string = \"#3b82f6\"\n): string {\n return `<div style=\"background:#f9fafb;padding:16px 20px;border-radius:8px;border-left:4px solid ${color};margin:16px 0;\">${content}</div>`;\n}\n\n/** Numbered step card (for onboarding sequences). */\nexport function stepBlock(\n number: number,\n title: string,\n description: string,\n color: string = \"#667eea\"\n): string {\n return `<div style=\"background:#f3f4f6;padding:16px;border-radius:8px;margin-bottom:12px;border-left:4px solid ${color};\">\n <p style=\"margin:0;font-weight:600;color:#111827;\">${number}. ${title}</p>\n <p style=\"margin:8px 0 0;font-size:14px;color:#6b7280;\">${description}</p>\n </div>`;\n}\n\n/** Stats bar — row of metric cards. */\nexport function statsBar(\n stats: { label: string; value: string | number }[]\n): string {\n const cells = stats\n .map(\n (s) =>\n `<span style=\"display:inline-block;margin:0 16px;font-size:14px;color:#374151;\"><strong>${escapeHtml(String(s.value))}</strong> ${escapeHtml(s.label)}</span>`\n )\n .join(\"\");\n return `<div style=\"padding:12px 0;text-align:center;background:#f0fdf4;border-radius:6px;margin-bottom:16px;\">${cells}</div>`;\n}\n\n/** Simple data table with header row. */\nexport function dataTable(\n headers: string[],\n rows: string[][]\n): string {\n const headerCells = headers\n .map(\n (h) =>\n `<th style=\"padding:8px 12px;text-align:left;font-size:13px;color:#374151;border-bottom:1px solid #e5e7eb;\">${escapeHtml(h)}</th>`\n )\n .join(\"\");\n\n const bodyRows = rows\n .map(\n (row) =>\n `<tr>${row.map((cell) => `<td style=\"padding:8px 12px;border-bottom:1px solid #e5e7eb;font-size:14px;color:#374151;\">${cell}</td>`).join(\"\")}</tr>`\n )\n .join(\"\");\n\n return `<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"border:1px solid #e5e7eb;border-radius:6px;overflow:hidden;\">\n <tr style=\"background:#f9fafb;\">${headerCells}</tr>\n ${bodyRows}\n </table>`;\n}\n\n/** Info/tip box with icon. */\nexport function tipBlock(content: string, icon: string = \"💡\"): string {\n return `<div style=\"background:#eff6ff;border:2px solid #3b82f6;padding:16px 20px;border-radius:8px;margin:16px 0;\">\n <p style=\"margin:0;color:#1e40af;font-size:14px;\"><strong>${icon} Pro Tip:</strong> ${content}</p>\n </div>`;\n}\n\n/** Centered heading within email body. */\nexport function sectionHeading(text: string): string {\n return `<h2 style=\"margin:24px 0 12px;font-size:16px;color:#111827;\">${escapeHtml(text)}</h2>`;\n}\n\n/** Horizontal rule divider. */\nexport function divider(): string {\n return `<hr style=\"border:none;border-top:1px solid #e5e7eb;margin:24px 0;\">`;\n}\n\n/** Re-export escapeHtml for template authors. */\nexport { escapeHtml } from \"../security\";\n","/**\n * Pre-built email templates.\n *\n * Each template accepts branding + template-specific data, composes the\n * layout + building blocks, and returns { subject, html, text }.\n *\n * Apps call these directly — no need to touch layout internals.\n * All user-provided strings are escaped internally.\n */\nimport type {\n EmailBranding,\n EmailOutput,\n EmailStep,\n EmailStat,\n} from \"./types\";\nimport {\n emailLayout,\n calloutBlock,\n stepBlock,\n statsBar,\n dataTable,\n tipBlock,\n sectionHeading,\n divider,\n escapeHtml,\n} from \"./layout\";\n\n// ─── Welcome / Onboarding ────────────────────────────────────────────────────\n\nexport interface WelcomeEmailData {\n /** User's display name */\n userName: string;\n /** URL to their dashboard or profile */\n dashboardUrl: string;\n /** Onboarding steps (typically 3) */\n steps: EmailStep[];\n /** Optional pro-tip text */\n tip?: string;\n}\n\nexport function welcomeEmail(\n branding: EmailBranding,\n data: WelcomeEmailData\n): EmailOutput {\n const safeName = escapeHtml(data.userName);\n\n const stepsHtml = data.steps\n .map((s, i) => stepBlock(i + 1, escapeHtml(s.title), escapeHtml(s.description), branding.accentColor ?? branding.primaryColor))\n .join(\"\");\n\n const tipHtml = data.tip ? tipBlock(escapeHtml(data.tip)) : \"\";\n\n const body = `\n <p style=\"font-size:16px;margin-bottom:20px;\">Hi ${safeName},</p>\n <p style=\"font-size:15px;margin-bottom:20px;\">\n Welcome aboard! Your account is all set up and ready to go.\n </p>\n ${sectionHeading(\"Get Started\")}\n ${stepsHtml}\n ${tipHtml}\n `;\n\n return emailLayout(branding, {\n subject: `Welcome to ${branding.appName}!`,\n preheader: `Your ${branding.appName} account is ready`,\n icon: \"🎉\",\n subtitle: \"Welcome aboard\",\n body,\n cta: { text: \"Go to Dashboard\", url: data.dashboardUrl },\n footerLinks: branding.supportEmail\n ? [{ label: \"Get Help\", url: `mailto:${branding.supportEmail}` }]\n : undefined,\n });\n}\n\n// ─── Digest / Summary ────────────────────────────────────────────────────────\n\nexport interface DigestEmailData {\n /** Recipient display name */\n recipientName: string;\n /** Period label (e.g. \"This Week\", \"March 2026\") */\n period: string;\n /** Summary stats shown in the stats bar */\n stats: EmailStat[];\n /** Main body HTML (app-specific content — must be pre-escaped) */\n contentHtml: string;\n /** Optional: URL to view full report */\n reportUrl?: string;\n /** Optional: why they're receiving this */\n subscriptionNote?: string;\n}\n\nexport function digestEmail(\n branding: EmailBranding,\n data: DigestEmailData\n): EmailOutput {\n const safeName = escapeHtml(data.recipientName);\n const safePeriod = escapeHtml(data.period);\n\n const body = `\n <p style=\"font-size:16px;margin-bottom:20px;\">Hi ${safeName},</p>\n <p style=\"font-size:15px;margin-bottom:16px;\">\n Here's your ${safePeriod.toLowerCase()} summary:\n </p>\n ${statsBar(data.stats)}\n ${data.contentHtml}\n ${data.subscriptionNote ? `<p style=\"margin-top:20px;font-size:13px;color:#9ca3af;\">${escapeHtml(data.subscriptionNote)}</p>` : \"\"}\n `;\n\n return emailLayout(branding, {\n subject: `${branding.appName} — ${data.period} Summary`,\n preheader: `Your ${data.period.toLowerCase()} activity on ${branding.appName}`,\n icon: \"📊\",\n subtitle: safePeriod,\n body,\n cta: data.reportUrl\n ? { text: \"View Full Report\", url: data.reportUrl }\n : undefined,\n });\n}\n\n// ─── Notification ────────────────────────────────────────────────────────────\n\nexport interface NotificationEmailData {\n /** Recipient display name */\n recipientName: string;\n /** Notification headline */\n headline: string;\n /** Main message body (HTML-safe — will be escaped) */\n message: string;\n /** Optional highlighted detail block */\n detail?: string;\n /** Optional CTA */\n actionUrl?: string;\n /** Optional CTA label (defaults to \"View Details\") */\n actionText?: string;\n}\n\nexport function notificationEmail(\n branding: EmailBranding,\n data: NotificationEmailData\n): EmailOutput {\n const safeName = escapeHtml(data.recipientName);\n const safeHeadline = escapeHtml(data.headline);\n const safeMessage = escapeHtml(data.message);\n\n const detailHtml = data.detail\n ? calloutBlock(escapeHtml(data.detail), branding.accentColor ?? branding.primaryColor)\n : \"\";\n\n const body = `\n <p style=\"font-size:16px;margin-bottom:20px;\">Hi ${safeName},</p>\n <h2 style=\"margin:0 0 12px;font-size:18px;color:#111827;\">${safeHeadline}</h2>\n <p style=\"font-size:15px;margin-bottom:16px;\">${safeMessage}</p>\n ${detailHtml}\n `;\n\n return emailLayout(branding, {\n subject: `${branding.appName}: ${data.headline}`,\n preheader: data.message.slice(0, 100),\n body,\n cta: data.actionUrl\n ? { text: data.actionText ?? \"View Details\", url: data.actionUrl }\n : undefined,\n });\n}\n\n// ─── Moderation ──────────────────────────────────────────────────────────────\n\nexport type ModerationAction = \"warning\" | \"suspension\" | \"ban\" | \"reinstatement\";\n\nexport interface ModerationEmailData {\n /** User display name */\n userName: string;\n /** Type of moderation action */\n action: ModerationAction;\n /** Reason for the action */\n reason: string;\n /** What the user should do next (optional) */\n nextSteps?: string;\n /** Appeal/support URL (optional) */\n appealUrl?: string;\n}\n\nconst MODERATION_CONFIG: Record<ModerationAction, { icon: string; color: string; label: string }> = {\n warning: { icon: \"⚠️\", color: \"#f59e0b\", label: \"Account Warning\" },\n suspension: { icon: \"🔒\", color: \"#ef4444\", label: \"Account Suspended\" },\n ban: { icon: \"🚫\", color: \"#dc2626\", label: \"Account Banned\" },\n reinstatement: { icon: \"✅\", color: \"#10b981\", label: \"Account Reinstated\" },\n};\n\nexport function moderationEmail(\n branding: EmailBranding,\n data: ModerationEmailData\n): EmailOutput {\n const safeName = escapeHtml(data.userName);\n const config = MODERATION_CONFIG[data.action];\n\n const reasonHtml = calloutBlock(\n `<strong>Reason:</strong> ${escapeHtml(data.reason)}`,\n config.color\n );\n\n const nextStepsHtml = data.nextSteps\n ? `<p style=\"font-size:15px;margin:16px 0;\">${escapeHtml(data.nextSteps)}</p>`\n : \"\";\n\n const body = `\n <p style=\"font-size:16px;margin-bottom:20px;\">Hi ${safeName},</p>\n <p style=\"font-size:15px;margin-bottom:16px;\">\n ${data.action === \"reinstatement\"\n ? \"Good news — your account has been reinstated.\"\n : `Your account has received a ${escapeHtml(data.action)}.`}\n </p>\n ${reasonHtml}\n ${nextStepsHtml}\n `;\n\n return emailLayout(branding, {\n subject: `${branding.appName}: ${config.label}`,\n preheader: config.label,\n icon: config.icon,\n subtitle: config.label,\n body,\n cta: data.appealUrl\n ? { text: \"Appeal or Contact Support\", url: data.appealUrl }\n : undefined,\n });\n}\n\n// ─── Transaction / Receipt ───────────────────────────────────────────────────\n\nexport interface TransactionEmailData {\n /** Recipient display name */\n recipientName: string;\n /** Transaction headline (e.g. \"Payment Received\", \"Order Confirmed\") */\n headline: string;\n /** Line items: [label, value] pairs */\n lineItems: [string, string][];\n /** Total amount string (e.g. \"$25.00\") */\n total?: string;\n /** Optional message above the table */\n message?: string;\n /** Optional receipt/order URL */\n receiptUrl?: string;\n}\n\nexport function transactionEmail(\n branding: EmailBranding,\n data: TransactionEmailData\n): EmailOutput {\n const safeName = escapeHtml(data.recipientName);\n const safeHeadline = escapeHtml(data.headline);\n\n const rows = data.lineItems.map(([label, value]) => [\n escapeHtml(label),\n escapeHtml(value),\n ]);\n\n if (data.total) {\n rows.push([\n `<strong>Total</strong>`,\n `<strong>${escapeHtml(data.total)}</strong>`,\n ]);\n }\n\n const messageHtml = data.message\n ? `<p style=\"font-size:15px;margin-bottom:16px;\">${escapeHtml(data.message)}</p>`\n : \"\";\n\n const body = `\n <p style=\"font-size:16px;margin-bottom:20px;\">Hi ${safeName},</p>\n <h2 style=\"margin:0 0 12px;font-size:18px;color:#111827;\">${safeHeadline}</h2>\n ${messageHtml}\n ${dataTable([\"Item\", \"Amount\"], rows)}\n `;\n\n return emailLayout(branding, {\n subject: `${branding.appName}: ${data.headline}`,\n preheader: data.headline,\n icon: \"🧾\",\n body,\n cta: data.receiptUrl\n ? { text: \"View Receipt\", url: data.receiptUrl }\n : undefined,\n });\n}\n\n// ─── Security Alert ──────────────────────────────────────────────────────────\n\nexport interface SecurityEmailData {\n /** User display name */\n userName: string;\n /** What happened (e.g. \"Password Changed\", \"New Login Detected\") */\n event: string;\n /** Details about the event */\n details: string;\n /** Optional action URL (e.g. reset password link) */\n actionUrl?: string;\n /** Optional action label */\n actionText?: string;\n /** Expiry note (e.g. \"This link expires in 1 hour\") */\n expiryNote?: string;\n}\n\nexport function securityEmail(\n branding: EmailBranding,\n data: SecurityEmailData\n): EmailOutput {\n const safeName = escapeHtml(data.userName);\n\n const detailHtml = calloutBlock(\n escapeHtml(data.details),\n \"#ef4444\"\n );\n\n const expiryHtml = data.expiryNote\n ? `<p style=\"font-size:13px;color:#9ca3af;margin-top:12px;text-align:center;\">${escapeHtml(data.expiryNote)}</p>`\n : \"\";\n\n const body = `\n <p style=\"font-size:16px;margin-bottom:20px;\">Hi ${safeName},</p>\n <p style=\"font-size:15px;margin-bottom:16px;\">\n ${escapeHtml(data.event)}\n </p>\n ${detailHtml}\n ${expiryHtml}\n <p style=\"font-size:14px;color:#6b7280;margin-top:16px;\">\n If you didn't initiate this action, please contact support immediately.\n </p>\n `;\n\n return emailLayout(branding, {\n subject: `${branding.appName}: ${data.event}`,\n preheader: data.event,\n icon: \"🔐\",\n subtitle: \"Security Alert\",\n body,\n cta: data.actionUrl\n ? { text: data.actionText ?? \"Take Action\", url: data.actionUrl }\n : undefined,\n });\n}\n"],"mappings":"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","names":[]}
|