@digilogiclabs/platform-core 1.9.0 → 1.11.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.
@@ -171,7 +171,14 @@ function divider() {
171
171
  // src/email-templates/templates.ts
172
172
  function welcomeEmail(branding, data) {
173
173
  const safeName = escapeHtml(data.userName);
174
- const stepsHtml = data.steps.map((s, i) => stepBlock(i + 1, escapeHtml(s.title), escapeHtml(s.description), branding.accentColor ?? branding.primaryColor)).join("");
174
+ const stepsHtml = data.steps.map(
175
+ (s, i) => stepBlock(
176
+ i + 1,
177
+ escapeHtml(s.title),
178
+ escapeHtml(s.description),
179
+ branding.accentColor ?? branding.primaryColor
180
+ )
181
+ ).join("");
175
182
  const tipHtml = data.tip ? tipBlock(escapeHtml(data.tip)) : "";
176
183
  const body = `
177
184
  <p style="font-size:16px;margin-bottom:20px;">Hi ${safeName},</p>
@@ -217,7 +224,10 @@ function notificationEmail(branding, data) {
217
224
  const safeName = escapeHtml(data.recipientName);
218
225
  const safeHeadline = escapeHtml(data.headline);
219
226
  const safeMessage = escapeHtml(data.message);
220
- const detailHtml = data.detail ? calloutBlock(escapeHtml(data.detail), branding.accentColor ?? branding.primaryColor) : "";
227
+ const detailHtml = data.detail ? calloutBlock(
228
+ escapeHtml(data.detail),
229
+ branding.accentColor ?? branding.primaryColor
230
+ ) : "";
221
231
  const body = `
222
232
  <p style="font-size:16px;margin-bottom:20px;">Hi ${safeName},</p>
223
233
  <h2 style="margin:0 0 12px;font-size:18px;color:#111827;">${safeHeadline}</h2>
@@ -292,10 +302,7 @@ function transactionEmail(branding, data) {
292
302
  }
293
303
  function securityEmail(branding, data) {
294
304
  const safeName = escapeHtml(data.userName);
295
- const detailHtml = calloutBlock(
296
- escapeHtml(data.details),
297
- "#ef4444"
298
- );
305
+ const detailHtml = calloutBlock(escapeHtml(data.details), "#ef4444");
299
306
  const expiryHtml = data.expiryNote ? `<p style="font-size:13px;color:#9ca3af;margin-top:12px;text-align:center;">${escapeHtml(data.expiryNote)}</p>` : "";
300
307
  const body = `
301
308
  <p style="font-size:16px;margin-bottom:20px;">Hi ${safeName},</p>
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/email-templates/index.ts","../src/security.ts","../src/email-templates/layout.ts","../src/email-templates/templates.ts"],"sourcesContent":["/**\n * Shared email template module.\n *\n * Provides a branding-driven email layout system with pre-built templates\n * and composable HTML building blocks. Zero external dependencies —\n * only uses escapeHtml from platform-core's security module.\n *\n * @example\n * ```ts\n * import {\n * welcomeEmail,\n * digestEmail,\n * emailLayout,\n * calloutBlock,\n * } from \"@digilogiclabs/platform-core/email-templates\";\n *\n * const branding: EmailBranding = {\n * appName: \"MyApp\",\n * primaryColor: \"#667eea\",\n * fromEmail: \"noreply@myapp.com\",\n * baseUrl: \"https://myapp.com\",\n * };\n *\n * const { subject, html, text } = welcomeEmail(branding, {\n * userName: \"Alice\",\n * dashboardUrl: \"https://myapp.com/dashboard\",\n * steps: [\n * { title: \"Set up profile\", description: \"Add your photo and bio\" },\n * { title: \"Explore features\", description: \"Check out what's available\" },\n * ],\n * });\n * ```\n */\n\n// Types\nexport type {\n EmailBranding,\n EmailCTA,\n EmailStep,\n EmailStat,\n EmailTableRow,\n EmailLayoutOptions,\n EmailOutput,\n} from \"./types\";\n\n// Layout engine + building blocks\nexport {\n emailLayout,\n calloutBlock,\n stepBlock,\n statsBar,\n dataTable,\n tipBlock,\n sectionHeading,\n divider,\n escapeHtml,\n} from \"./layout\";\n\n// Pre-built templates\nexport {\n welcomeEmail,\n digestEmail,\n notificationEmail,\n moderationEmail,\n transactionEmail,\n securityEmail,\n} from \"./templates\";\n\n// Template data types\nexport type {\n WelcomeEmailData,\n DigestEmailData,\n NotificationEmailData,\n ModerationEmailData,\n ModerationAction,\n TransactionEmailData,\n SecurityEmailData,\n} from \"./templates\";\n","/**\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, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#039;\");\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(/&nbsp;/g, \" \")\n .replace(/&amp;/g, \"&\")\n .replace(/&lt;/g, \"<\")\n .replace(/&gt;/g, \">\")\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/&rarr;/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(\" &middot; \")\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) + \" &middot; \" : \"\"}\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":[]}
1
+ {"version":3,"sources":["../src/email-templates/index.ts","../src/security.ts","../src/email-templates/layout.ts","../src/email-templates/templates.ts"],"sourcesContent":["/**\n * Shared email template module.\n *\n * Provides a branding-driven email layout system with pre-built templates\n * and composable HTML building blocks. Zero external dependencies —\n * only uses escapeHtml from platform-core's security module.\n *\n * @example\n * ```ts\n * import {\n * welcomeEmail,\n * digestEmail,\n * emailLayout,\n * calloutBlock,\n * } from \"@digilogiclabs/platform-core/email-templates\";\n *\n * const branding: EmailBranding = {\n * appName: \"MyApp\",\n * primaryColor: \"#667eea\",\n * fromEmail: \"noreply@myapp.com\",\n * baseUrl: \"https://myapp.com\",\n * };\n *\n * const { subject, html, text } = welcomeEmail(branding, {\n * userName: \"Alice\",\n * dashboardUrl: \"https://myapp.com/dashboard\",\n * steps: [\n * { title: \"Set up profile\", description: \"Add your photo and bio\" },\n * { title: \"Explore features\", description: \"Check out what's available\" },\n * ],\n * });\n * ```\n */\n\n// Types\nexport type {\n EmailBranding,\n EmailCTA,\n EmailStep,\n EmailStat,\n EmailTableRow,\n EmailLayoutOptions,\n EmailOutput,\n} from \"./types\";\n\n// Layout engine + building blocks\nexport {\n emailLayout,\n calloutBlock,\n stepBlock,\n statsBar,\n dataTable,\n tipBlock,\n sectionHeading,\n divider,\n escapeHtml,\n} from \"./layout\";\n\n// Pre-built templates\nexport {\n welcomeEmail,\n digestEmail,\n notificationEmail,\n moderationEmail,\n transactionEmail,\n securityEmail,\n} from \"./templates\";\n\n// Template data types\nexport type {\n WelcomeEmailData,\n DigestEmailData,\n NotificationEmailData,\n ModerationEmailData,\n ModerationAction,\n TransactionEmailData,\n SecurityEmailData,\n} from \"./templates\";\n","/**\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, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#039;\");\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(/&nbsp;/g, \" \")\n .replace(/&amp;/g, \"&\")\n .replace(/&lt;/g, \"<\")\n .replace(/&gt;/g, \">\")\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/&rarr;/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(\" &middot; \")\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) + \" &middot; \" : \"\"}\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(headers: string[], rows: string[][]): 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 { EmailBranding, EmailOutput, EmailStep, EmailStat } 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) =>\n stepBlock(\n i + 1,\n escapeHtml(s.title),\n escapeHtml(s.description),\n branding.accentColor ?? branding.primaryColor,\n ),\n )\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(\n escapeHtml(data.detail),\n branding.accentColor ?? branding.primaryColor,\n )\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 =\n | \"warning\"\n | \"suspension\"\n | \"ban\"\n | \"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<\n ModerationAction,\n { icon: string; color: string; label: string }\n> = {\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 ${\n data.action === \"reinstatement\"\n ? \"Good news — your account has been reinstated.\"\n : `Your account has received a ${escapeHtml(data.action)}.`\n }\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(escapeHtml(data.details), \"#ef4444\");\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":[]}
@@ -131,7 +131,14 @@ function divider() {
131
131
  // src/email-templates/templates.ts
132
132
  function welcomeEmail(branding, data) {
133
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("");
134
+ const stepsHtml = data.steps.map(
135
+ (s, i) => stepBlock(
136
+ i + 1,
137
+ escapeHtml(s.title),
138
+ escapeHtml(s.description),
139
+ branding.accentColor ?? branding.primaryColor
140
+ )
141
+ ).join("");
135
142
  const tipHtml = data.tip ? tipBlock(escapeHtml(data.tip)) : "";
136
143
  const body = `
137
144
  <p style="font-size:16px;margin-bottom:20px;">Hi ${safeName},</p>
@@ -177,7 +184,10 @@ function notificationEmail(branding, data) {
177
184
  const safeName = escapeHtml(data.recipientName);
178
185
  const safeHeadline = escapeHtml(data.headline);
179
186
  const safeMessage = escapeHtml(data.message);
180
- const detailHtml = data.detail ? calloutBlock(escapeHtml(data.detail), branding.accentColor ?? branding.primaryColor) : "";
187
+ const detailHtml = data.detail ? calloutBlock(
188
+ escapeHtml(data.detail),
189
+ branding.accentColor ?? branding.primaryColor
190
+ ) : "";
181
191
  const body = `
182
192
  <p style="font-size:16px;margin-bottom:20px;">Hi ${safeName},</p>
183
193
  <h2 style="margin:0 0 12px;font-size:18px;color:#111827;">${safeHeadline}</h2>
@@ -252,10 +262,7 @@ function transactionEmail(branding, data) {
252
262
  }
253
263
  function securityEmail(branding, data) {
254
264
  const safeName = escapeHtml(data.userName);
255
- const detailHtml = calloutBlock(
256
- escapeHtml(data.details),
257
- "#ef4444"
258
- );
265
+ const detailHtml = calloutBlock(escapeHtml(data.details), "#ef4444");
259
266
  const expiryHtml = data.expiryNote ? `<p style="font-size:13px;color:#9ca3af;margin-top:12px;text-align:center;">${escapeHtml(data.expiryNote)}</p>` : "";
260
267
  const body = `
261
268
  <p style="font-size:16px;margin-bottom:20px;">Hi ${safeName},</p>
@@ -1 +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, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#039;\");\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(/&nbsp;/g, \" \")\n .replace(/&amp;/g, \"&\")\n .replace(/&lt;/g, \"<\")\n .replace(/&gt;/g, \">\")\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/&rarr;/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(\" &middot; \")\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) + \" &middot; \" : \"\"}\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":[]}
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, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#039;\");\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(/&nbsp;/g, \" \")\n .replace(/&amp;/g, \"&\")\n .replace(/&lt;/g, \"<\")\n .replace(/&gt;/g, \">\")\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/&rarr;/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(\" &middot; \")\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) + \" &middot; \" : \"\"}\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(headers: string[], rows: string[][]): 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 { EmailBranding, EmailOutput, EmailStep, EmailStat } 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) =>\n stepBlock(\n i + 1,\n escapeHtml(s.title),\n escapeHtml(s.description),\n branding.accentColor ?? branding.primaryColor,\n ),\n )\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(\n escapeHtml(data.detail),\n branding.accentColor ?? branding.primaryColor,\n )\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 =\n | \"warning\"\n | \"suspension\"\n | \"ban\"\n | \"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<\n ModerationAction,\n { icon: string; color: string; label: string }\n> = {\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 ${\n data.action === \"reinstatement\"\n ? \"Good news — your account has been reinstated.\"\n : `Your account has received a ${escapeHtml(data.action)}.`\n }\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(escapeHtml(data.details), \"#ef4444\");\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":[]}
@@ -1256,6 +1256,12 @@ declare const CommonRateLimits: {
1256
1256
  readonly windowSeconds: 3600;
1257
1257
  readonly blockDurationSeconds: 3600;
1258
1258
  };
1259
+ /** Beta code validation: 5/min with 5min block (prevents brute force guessing) */
1260
+ readonly betaValidation: {
1261
+ readonly limit: 5;
1262
+ readonly windowSeconds: 60;
1263
+ readonly blockDurationSeconds: 300;
1264
+ };
1259
1265
  };
1260
1266
  /**
1261
1267
  * In-memory rate limit store for testing and graceful degradation.
@@ -1256,6 +1256,12 @@ declare const CommonRateLimits: {
1256
1256
  readonly windowSeconds: 3600;
1257
1257
  readonly blockDurationSeconds: 3600;
1258
1258
  };
1259
+ /** Beta code validation: 5/min with 5min block (prevents brute force guessing) */
1260
+ readonly betaValidation: {
1261
+ readonly limit: 5;
1262
+ readonly windowSeconds: 60;
1263
+ readonly blockDurationSeconds: 300;
1264
+ };
1259
1265
  };
1260
1266
  /**
1261
1267
  * In-memory rate limit store for testing and graceful degradation.
package/dist/index.d.mts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { p as ILogger, q as IMetrics, I as IPlatform, P as PlatformHealthStatus, u as MetricsSummary, v as ICrypto, w as EncryptOptions, x as EncryptedField, D as DeterministicEncryptedField, K as KeyRotationResult, y as CryptoKeyMetadata, e as IDatabase, k as IQueryBuilder, Q as QueryResult, l as ICache, m as IStorage, U as UploadOptions, S as StorageFile, n as IEmail, t as EmailMessage, z as EmailResult, o as IQueue, s as JobOptions, J as Job, R as RepeatOptions, A as JobState, B as JobEventType, F as JobEventHandler, G as IAI, H as AIConfig, L as AIChatRequest, O as AIChatResponse, T as AIStreamChunk, V as AIStreamCallback, W as AICompletionRequest, X as AICompletionResponse, Y as AIEmbeddingRequest, Z as AIEmbeddingResponse, _ as AIModelConfig, $ as AIModelType, a0 as AIProvider, a1 as IRAG, a2 as RAGConfig, a3 as CreateCollectionOptions, a4 as RAGCollection, a5 as RAGDocument, a6 as IngestionOptions, a7 as BulkIngestionResult, a8 as IngestionResult, a9 as DocumentStatus, aa as RAGChunk, ab as RAGSearchQuery, ac as RAGSearchResponse, ad as RAGSearchResult, ae as ContextAssemblyConfig, af as AssembledContext, ag as RAGPipeline } from './ConsoleEmail-ubSVWgTa.mjs';
2
2
  export { aI as AIChatChoice, br as AIConfigSchema, aO as AIError, aN as AIErrorCode, aC as AIErrorMessages, aJ as AIFinishReason, aF as AIMessage, bk as AIProviderSchema, aE as AIRole, aM as AIRouterConfig, aH as AITool, aG as AIToolCall, aK as AIUsageInfo, am as BackoffOptions, by as BulkheadConfigSchema, bI as CacheConfig, bn as CacheConfigSchema, be as CacheProviderSchema, aS as ChunkingConfig, aQ as ChunkingPresets, aR as ChunkingStrategy, bw as CircuitBreakerConfigSchema, C as ConsoleEmail, h as ConsoleLogger, aX as CryptoAlgorithm, bP as CryptoConfig, bt as CryptoConfigSchema, aW as CryptoKeyStatus, bH as DatabaseConfig, bm as DatabaseConfigSchema, bd as DatabaseProviderSchema, b7 as EmailAddress, b8 as EmailAttachment, bK as EmailConfig, bp as EmailConfigSchema, bg as EmailProviderSchema, E as EnvSecrets, aw as GetSecretOptions, as as HistogramStats, ah as ICacheOptions, r as ISecrets, b1 as ISpan, b0 as ITracing, aj as JobContext, ak as JobEvent, ai as JobResult, ap as LogEntry, an as LogLevel, bj as LogLevelSchema, ao as LogMeta, aq as LoggerConfig, bA as LoggingConfigSchema, aD as MemoryAI, a as MemoryCache, M as MemoryDatabase, c as MemoryEmail, i as MemoryMetrics, d as MemoryQueue, aP as MemoryRAG, f as MemorySecrets, b as MemoryStorage, a_ as MemoryTracing, ar as MetricTags, bB as MetricsConfigSchema, bO as MiddlewareConfig, bE as MiddlewareConfigSchema, N as NoopLogger, j as NoopMetrics, a$ as NoopTracing, bN as ObservabilityConfig, bD as ObservabilityConfigSchema, bG as PlatformConfig, bF as PlatformConfigSchema, bL as QueueConfig, bq as QueueConfigSchema, bh as QueueProviderSchema, al as QueueStats, bs as RAGConfigSchema, aU as RAGFilter, aV as RAGPipelineStep, bl as RAGProviderSchema, bM as ResilienceConfig, bz as ResilienceConfigSchema, bv as RetryConfigSchema, ay as RotateSecretOptions, az as RotationResult, aL as RoutingStrategy, aT as SearchMode, au as Secret, av as SecretMetadata, bQ as SecurityConfig, bu as SecurityConfigSchema, ax as SetSecretOptions, b2 as SpanContext, bc as SpanEvent, b5 as SpanKind, b3 as SpanOptions, b4 as SpanStatus, bb as SpanStatusCode, bJ as StorageConfig, bo as StorageConfigSchema, bf as StorageProviderSchema, bx as TimeoutConfigSchema, at as TimingStats, b6 as TracingConfig, bC as TracingConfigSchema, bi as TracingProviderSchema, b9 as calculateBackoff, aA as createAIError, g as createPlatform, aY as createPlatformAsync, aZ as createScopedMetrics, ba as generateJobId, bU as getDefaultConfig, aB as isAIError, bR as loadConfig, bT as safeValidateConfig, bS as validateConfig } from './ConsoleEmail-ubSVWgTa.mjs';
3
3
  export { w as IMigrationDatabase, I as IMigrator, q as Migration, r as MigrationRecord, t as MigrationResult, u as MigrationStatus, M as Migrator, v as MigratorConfig, S as SQL, o as createBetaInvitesTable, n as createBetaSettingsTable, p as createBetaTestersTable, f as createDomainVerificationsTable, c as createMigration, b as createSsoOidcConfigsTable, m as createSsoSessionsTable, k as createTenantInvitationsTable, j as createTenantMembersTable, l as createTenantUsageTable, i as createTenantsTable, h as createVerifiedDomainsTable, d as defineMigration, e as enterpriseMigrations, g as generateVersion, a as getEnterpriseMigrations, s as sqlMigration } from './index-DzQ0Js5Z.mjs';
4
- import { aS as IBeta, aT as BetaConfig, aU as BetaSettings, aV as UpdateBetaSettingsOptions, aW as CreateBetaCodesOptions, aX as BetaInviteCode, aY as ListBetaCodesOptions, aZ as BetaValidationResult, a_ as BetaConsumeResult, a$ as BetaTester, b0 as BetaStats, b1 as BetaCodeUsageReport } from './env-DHPZR3Lv.mjs';
5
- export { al as AllowlistConfig, A as ApiError, d as ApiErrorCode, f as ApiErrorCodeType, h as ApiPaginatedResponse, Q as ApiSecurityConfig, V as ApiSecurityContext, g as ApiSuccessResponse, aD as AuditRequest, H as AuthCookiesConfig, N as AuthMethod, aL as BetaClientConfig, b6 as BetaCodeStatus, C as CommonApiErrors, ar as CommonRateLimits, ac as DateRangeInput, a6 as DateRangeSchema, ag as DeploymentStage, aa as EmailInput, $ as EmailSchema, E as EnvValidationConfig, p as EnvValidationResult, ai as FlagDefinition, aj as FlagDefinitions, ah as FlagValue, r as KEYCLOAK_DEFAULT_ROLES, F as KeycloakCallbacksConfig, K as KeycloakConfig, G as KeycloakJwtFields, q as KeycloakTokenSet, ae as LoginInput, a8 as LoginSchema, b2 as MemoryBeta, ay as OpsAuditActor, aA as OpsAuditEvent, aC as OpsAuditLoggerOptions, aB as OpsAuditRecord, az as OpsAuditResource, b7 as PG_ERROR_MAP, ab as PaginationInput, a5 as PaginationSchema, a0 as PasswordSchema, a3 as PersonNameSchema, a2 as PhoneSchema, aq as RateLimitCheckResult, b as RateLimitOptions, P as RateLimitPreset, a as RateLimitRule, R as RateLimitStore, J as RedirectCallbackConfig, ak as ResolvedFlags, O as RouteAuditConfig, ad as SearchQueryInput, a7 as SearchQuerySchema, U as SecuritySession, af as SignupInput, a9 as SignupSchema, a1 as SlugSchema, aF as StandardAuditActionType, aE as StandardAuditActions, S as StandardRateLimitPresets, T as TokenRefreshResult, _ as WrapperPresets, ao as buildAllowlist, I as buildAuthCookies, Z as buildErrorBody, M as buildKeycloakCallbacks, e as buildPagination, Y as buildRateLimitHeaders, aw as buildRateLimitResponseHeaders, L as buildRedirectCallback, y as buildTokenRefreshParams, n as checkEnvVars, at as checkRateLimit, c as classifyError, aR as clearStoredBetaCode, aJ as createAuditActor, aK as createAuditLogger, aM as createBetaClient, an as createFeatureFlags, as as createMemoryRateLimitStore, a4 as createSafeTextSchema, am as detectStage, aG as extractAuditIp, aI as extractAuditRequestId, aH as extractAuditUserAgent, X as extractClientIp, aN as fetchBetaSettings, b3 as generateBetaCode, b5 as generateBetaId, l as getBoolEnv, B as getEndSessionEndpoint, o as getEnvSummary, m as getIntEnv, k as getOptionalEnv, au as getRateLimitStatus, j as getRequiredEnv, aQ as getStoredBetaCode, z as getTokenEndpoint, w as hasAllRoles, u as hasAnyRole, t as hasRole, ap as isAllowlisted, i as isApiError, x as isTokenExpired, b4 as normalizeBetaCode, s as parseKeycloakRoles, D as refreshKeycloakToken, av as resetRateLimitForKey, ax as resolveIdentifier, W as resolveRateLimitIdentifier, aP as storeBetaCode, aO as validateBetaCode, v as validateEnvVars } from './env-DHPZR3Lv.mjs';
4
+ import { aS as IBeta, aT as BetaConfig, aU as BetaSettings, aV as UpdateBetaSettingsOptions, aW as CreateBetaCodesOptions, aX as BetaInviteCode, aY as ListBetaCodesOptions, aZ as BetaValidationResult, a_ as BetaConsumeResult, a$ as BetaTester, b0 as BetaStats, b1 as BetaCodeUsageReport } from './env-CYKVNpLl.mjs';
5
+ export { al as AllowlistConfig, A as ApiError, d as ApiErrorCode, f as ApiErrorCodeType, h as ApiPaginatedResponse, Q as ApiSecurityConfig, V as ApiSecurityContext, g as ApiSuccessResponse, aD as AuditRequest, H as AuthCookiesConfig, N as AuthMethod, aL as BetaClientConfig, b6 as BetaCodeStatus, C as CommonApiErrors, ar as CommonRateLimits, ac as DateRangeInput, a6 as DateRangeSchema, ag as DeploymentStage, aa as EmailInput, $ as EmailSchema, E as EnvValidationConfig, p as EnvValidationResult, ai as FlagDefinition, aj as FlagDefinitions, ah as FlagValue, r as KEYCLOAK_DEFAULT_ROLES, F as KeycloakCallbacksConfig, K as KeycloakConfig, G as KeycloakJwtFields, q as KeycloakTokenSet, ae as LoginInput, a8 as LoginSchema, b2 as MemoryBeta, ay as OpsAuditActor, aA as OpsAuditEvent, aC as OpsAuditLoggerOptions, aB as OpsAuditRecord, az as OpsAuditResource, b7 as PG_ERROR_MAP, ab as PaginationInput, a5 as PaginationSchema, a0 as PasswordSchema, a3 as PersonNameSchema, a2 as PhoneSchema, aq as RateLimitCheckResult, b as RateLimitOptions, P as RateLimitPreset, a as RateLimitRule, R as RateLimitStore, J as RedirectCallbackConfig, ak as ResolvedFlags, O as RouteAuditConfig, ad as SearchQueryInput, a7 as SearchQuerySchema, U as SecuritySession, af as SignupInput, a9 as SignupSchema, a1 as SlugSchema, aF as StandardAuditActionType, aE as StandardAuditActions, S as StandardRateLimitPresets, T as TokenRefreshResult, _ as WrapperPresets, ao as buildAllowlist, I as buildAuthCookies, Z as buildErrorBody, M as buildKeycloakCallbacks, e as buildPagination, Y as buildRateLimitHeaders, aw as buildRateLimitResponseHeaders, L as buildRedirectCallback, y as buildTokenRefreshParams, n as checkEnvVars, at as checkRateLimit, c as classifyError, aR as clearStoredBetaCode, aJ as createAuditActor, aK as createAuditLogger, aM as createBetaClient, an as createFeatureFlags, as as createMemoryRateLimitStore, a4 as createSafeTextSchema, am as detectStage, aG as extractAuditIp, aI as extractAuditRequestId, aH as extractAuditUserAgent, X as extractClientIp, aN as fetchBetaSettings, b3 as generateBetaCode, b5 as generateBetaId, l as getBoolEnv, B as getEndSessionEndpoint, o as getEnvSummary, m as getIntEnv, k as getOptionalEnv, au as getRateLimitStatus, j as getRequiredEnv, aQ as getStoredBetaCode, z as getTokenEndpoint, w as hasAllRoles, u as hasAnyRole, t as hasRole, ap as isAllowlisted, i as isApiError, x as isTokenExpired, b4 as normalizeBetaCode, s as parseKeycloakRoles, D as refreshKeycloakToken, av as resetRateLimitForKey, ax as resolveIdentifier, W as resolveRateLimitIdentifier, aP as storeBetaCode, aO as validateBetaCode, v as validateEnvVars } from './env-CYKVNpLl.mjs';
6
6
  export { H as HTML_TAG_PATTERN, i as URL_DOMAIN_PATTERN, U as URL_PROTOCOL_PATTERN, c as constantTimeEqual, b as containsHtml, a as containsUrls, f as defangUrl, e as escapeHtml, g as getCorrelationId, d as sanitizeApiError, h as sanitizeForEmail, s as stripHtml } from './security-BvLXaQkv.mjs';
7
7
  export { NextHeaderEntry, SecurityHeaderPresets, SecurityHeadersConfig, generateSecurityHeaders } from './security-headers.mjs';
8
8
  import { SupabaseClient } from '@supabase/supabase-js';
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { p as ILogger, q as IMetrics, I as IPlatform, P as PlatformHealthStatus, u as MetricsSummary, v as ICrypto, w as EncryptOptions, x as EncryptedField, D as DeterministicEncryptedField, K as KeyRotationResult, y as CryptoKeyMetadata, e as IDatabase, k as IQueryBuilder, Q as QueryResult, l as ICache, m as IStorage, U as UploadOptions, S as StorageFile, n as IEmail, t as EmailMessage, z as EmailResult, o as IQueue, s as JobOptions, J as Job, R as RepeatOptions, A as JobState, B as JobEventType, F as JobEventHandler, G as IAI, H as AIConfig, L as AIChatRequest, O as AIChatResponse, T as AIStreamChunk, V as AIStreamCallback, W as AICompletionRequest, X as AICompletionResponse, Y as AIEmbeddingRequest, Z as AIEmbeddingResponse, _ as AIModelConfig, $ as AIModelType, a0 as AIProvider, a1 as IRAG, a2 as RAGConfig, a3 as CreateCollectionOptions, a4 as RAGCollection, a5 as RAGDocument, a6 as IngestionOptions, a7 as BulkIngestionResult, a8 as IngestionResult, a9 as DocumentStatus, aa as RAGChunk, ab as RAGSearchQuery, ac as RAGSearchResponse, ad as RAGSearchResult, ae as ContextAssemblyConfig, af as AssembledContext, ag as RAGPipeline } from './ConsoleEmail-ubSVWgTa.js';
2
2
  export { aI as AIChatChoice, br as AIConfigSchema, aO as AIError, aN as AIErrorCode, aC as AIErrorMessages, aJ as AIFinishReason, aF as AIMessage, bk as AIProviderSchema, aE as AIRole, aM as AIRouterConfig, aH as AITool, aG as AIToolCall, aK as AIUsageInfo, am as BackoffOptions, by as BulkheadConfigSchema, bI as CacheConfig, bn as CacheConfigSchema, be as CacheProviderSchema, aS as ChunkingConfig, aQ as ChunkingPresets, aR as ChunkingStrategy, bw as CircuitBreakerConfigSchema, C as ConsoleEmail, h as ConsoleLogger, aX as CryptoAlgorithm, bP as CryptoConfig, bt as CryptoConfigSchema, aW as CryptoKeyStatus, bH as DatabaseConfig, bm as DatabaseConfigSchema, bd as DatabaseProviderSchema, b7 as EmailAddress, b8 as EmailAttachment, bK as EmailConfig, bp as EmailConfigSchema, bg as EmailProviderSchema, E as EnvSecrets, aw as GetSecretOptions, as as HistogramStats, ah as ICacheOptions, r as ISecrets, b1 as ISpan, b0 as ITracing, aj as JobContext, ak as JobEvent, ai as JobResult, ap as LogEntry, an as LogLevel, bj as LogLevelSchema, ao as LogMeta, aq as LoggerConfig, bA as LoggingConfigSchema, aD as MemoryAI, a as MemoryCache, M as MemoryDatabase, c as MemoryEmail, i as MemoryMetrics, d as MemoryQueue, aP as MemoryRAG, f as MemorySecrets, b as MemoryStorage, a_ as MemoryTracing, ar as MetricTags, bB as MetricsConfigSchema, bO as MiddlewareConfig, bE as MiddlewareConfigSchema, N as NoopLogger, j as NoopMetrics, a$ as NoopTracing, bN as ObservabilityConfig, bD as ObservabilityConfigSchema, bG as PlatformConfig, bF as PlatformConfigSchema, bL as QueueConfig, bq as QueueConfigSchema, bh as QueueProviderSchema, al as QueueStats, bs as RAGConfigSchema, aU as RAGFilter, aV as RAGPipelineStep, bl as RAGProviderSchema, bM as ResilienceConfig, bz as ResilienceConfigSchema, bv as RetryConfigSchema, ay as RotateSecretOptions, az as RotationResult, aL as RoutingStrategy, aT as SearchMode, au as Secret, av as SecretMetadata, bQ as SecurityConfig, bu as SecurityConfigSchema, ax as SetSecretOptions, b2 as SpanContext, bc as SpanEvent, b5 as SpanKind, b3 as SpanOptions, b4 as SpanStatus, bb as SpanStatusCode, bJ as StorageConfig, bo as StorageConfigSchema, bf as StorageProviderSchema, bx as TimeoutConfigSchema, at as TimingStats, b6 as TracingConfig, bC as TracingConfigSchema, bi as TracingProviderSchema, b9 as calculateBackoff, aA as createAIError, g as createPlatform, aY as createPlatformAsync, aZ as createScopedMetrics, ba as generateJobId, bU as getDefaultConfig, aB as isAIError, bR as loadConfig, bT as safeValidateConfig, bS as validateConfig } from './ConsoleEmail-ubSVWgTa.js';
3
3
  export { w as IMigrationDatabase, I as IMigrator, q as Migration, r as MigrationRecord, t as MigrationResult, u as MigrationStatus, M as Migrator, v as MigratorConfig, S as SQL, o as createBetaInvitesTable, n as createBetaSettingsTable, p as createBetaTestersTable, f as createDomainVerificationsTable, c as createMigration, b as createSsoOidcConfigsTable, m as createSsoSessionsTable, k as createTenantInvitationsTable, j as createTenantMembersTable, l as createTenantUsageTable, i as createTenantsTable, h as createVerifiedDomainsTable, d as defineMigration, e as enterpriseMigrations, g as generateVersion, a as getEnterpriseMigrations, s as sqlMigration } from './index-DzQ0Js5Z.js';
4
- import { aS as IBeta, aT as BetaConfig, aU as BetaSettings, aV as UpdateBetaSettingsOptions, aW as CreateBetaCodesOptions, aX as BetaInviteCode, aY as ListBetaCodesOptions, aZ as BetaValidationResult, a_ as BetaConsumeResult, a$ as BetaTester, b0 as BetaStats, b1 as BetaCodeUsageReport } from './env-DHPZR3Lv.js';
5
- export { al as AllowlistConfig, A as ApiError, d as ApiErrorCode, f as ApiErrorCodeType, h as ApiPaginatedResponse, Q as ApiSecurityConfig, V as ApiSecurityContext, g as ApiSuccessResponse, aD as AuditRequest, H as AuthCookiesConfig, N as AuthMethod, aL as BetaClientConfig, b6 as BetaCodeStatus, C as CommonApiErrors, ar as CommonRateLimits, ac as DateRangeInput, a6 as DateRangeSchema, ag as DeploymentStage, aa as EmailInput, $ as EmailSchema, E as EnvValidationConfig, p as EnvValidationResult, ai as FlagDefinition, aj as FlagDefinitions, ah as FlagValue, r as KEYCLOAK_DEFAULT_ROLES, F as KeycloakCallbacksConfig, K as KeycloakConfig, G as KeycloakJwtFields, q as KeycloakTokenSet, ae as LoginInput, a8 as LoginSchema, b2 as MemoryBeta, ay as OpsAuditActor, aA as OpsAuditEvent, aC as OpsAuditLoggerOptions, aB as OpsAuditRecord, az as OpsAuditResource, b7 as PG_ERROR_MAP, ab as PaginationInput, a5 as PaginationSchema, a0 as PasswordSchema, a3 as PersonNameSchema, a2 as PhoneSchema, aq as RateLimitCheckResult, b as RateLimitOptions, P as RateLimitPreset, a as RateLimitRule, R as RateLimitStore, J as RedirectCallbackConfig, ak as ResolvedFlags, O as RouteAuditConfig, ad as SearchQueryInput, a7 as SearchQuerySchema, U as SecuritySession, af as SignupInput, a9 as SignupSchema, a1 as SlugSchema, aF as StandardAuditActionType, aE as StandardAuditActions, S as StandardRateLimitPresets, T as TokenRefreshResult, _ as WrapperPresets, ao as buildAllowlist, I as buildAuthCookies, Z as buildErrorBody, M as buildKeycloakCallbacks, e as buildPagination, Y as buildRateLimitHeaders, aw as buildRateLimitResponseHeaders, L as buildRedirectCallback, y as buildTokenRefreshParams, n as checkEnvVars, at as checkRateLimit, c as classifyError, aR as clearStoredBetaCode, aJ as createAuditActor, aK as createAuditLogger, aM as createBetaClient, an as createFeatureFlags, as as createMemoryRateLimitStore, a4 as createSafeTextSchema, am as detectStage, aG as extractAuditIp, aI as extractAuditRequestId, aH as extractAuditUserAgent, X as extractClientIp, aN as fetchBetaSettings, b3 as generateBetaCode, b5 as generateBetaId, l as getBoolEnv, B as getEndSessionEndpoint, o as getEnvSummary, m as getIntEnv, k as getOptionalEnv, au as getRateLimitStatus, j as getRequiredEnv, aQ as getStoredBetaCode, z as getTokenEndpoint, w as hasAllRoles, u as hasAnyRole, t as hasRole, ap as isAllowlisted, i as isApiError, x as isTokenExpired, b4 as normalizeBetaCode, s as parseKeycloakRoles, D as refreshKeycloakToken, av as resetRateLimitForKey, ax as resolveIdentifier, W as resolveRateLimitIdentifier, aP as storeBetaCode, aO as validateBetaCode, v as validateEnvVars } from './env-DHPZR3Lv.js';
4
+ import { aS as IBeta, aT as BetaConfig, aU as BetaSettings, aV as UpdateBetaSettingsOptions, aW as CreateBetaCodesOptions, aX as BetaInviteCode, aY as ListBetaCodesOptions, aZ as BetaValidationResult, a_ as BetaConsumeResult, a$ as BetaTester, b0 as BetaStats, b1 as BetaCodeUsageReport } from './env-CYKVNpLl.js';
5
+ export { al as AllowlistConfig, A as ApiError, d as ApiErrorCode, f as ApiErrorCodeType, h as ApiPaginatedResponse, Q as ApiSecurityConfig, V as ApiSecurityContext, g as ApiSuccessResponse, aD as AuditRequest, H as AuthCookiesConfig, N as AuthMethod, aL as BetaClientConfig, b6 as BetaCodeStatus, C as CommonApiErrors, ar as CommonRateLimits, ac as DateRangeInput, a6 as DateRangeSchema, ag as DeploymentStage, aa as EmailInput, $ as EmailSchema, E as EnvValidationConfig, p as EnvValidationResult, ai as FlagDefinition, aj as FlagDefinitions, ah as FlagValue, r as KEYCLOAK_DEFAULT_ROLES, F as KeycloakCallbacksConfig, K as KeycloakConfig, G as KeycloakJwtFields, q as KeycloakTokenSet, ae as LoginInput, a8 as LoginSchema, b2 as MemoryBeta, ay as OpsAuditActor, aA as OpsAuditEvent, aC as OpsAuditLoggerOptions, aB as OpsAuditRecord, az as OpsAuditResource, b7 as PG_ERROR_MAP, ab as PaginationInput, a5 as PaginationSchema, a0 as PasswordSchema, a3 as PersonNameSchema, a2 as PhoneSchema, aq as RateLimitCheckResult, b as RateLimitOptions, P as RateLimitPreset, a as RateLimitRule, R as RateLimitStore, J as RedirectCallbackConfig, ak as ResolvedFlags, O as RouteAuditConfig, ad as SearchQueryInput, a7 as SearchQuerySchema, U as SecuritySession, af as SignupInput, a9 as SignupSchema, a1 as SlugSchema, aF as StandardAuditActionType, aE as StandardAuditActions, S as StandardRateLimitPresets, T as TokenRefreshResult, _ as WrapperPresets, ao as buildAllowlist, I as buildAuthCookies, Z as buildErrorBody, M as buildKeycloakCallbacks, e as buildPagination, Y as buildRateLimitHeaders, aw as buildRateLimitResponseHeaders, L as buildRedirectCallback, y as buildTokenRefreshParams, n as checkEnvVars, at as checkRateLimit, c as classifyError, aR as clearStoredBetaCode, aJ as createAuditActor, aK as createAuditLogger, aM as createBetaClient, an as createFeatureFlags, as as createMemoryRateLimitStore, a4 as createSafeTextSchema, am as detectStage, aG as extractAuditIp, aI as extractAuditRequestId, aH as extractAuditUserAgent, X as extractClientIp, aN as fetchBetaSettings, b3 as generateBetaCode, b5 as generateBetaId, l as getBoolEnv, B as getEndSessionEndpoint, o as getEnvSummary, m as getIntEnv, k as getOptionalEnv, au as getRateLimitStatus, j as getRequiredEnv, aQ as getStoredBetaCode, z as getTokenEndpoint, w as hasAllRoles, u as hasAnyRole, t as hasRole, ap as isAllowlisted, i as isApiError, x as isTokenExpired, b4 as normalizeBetaCode, s as parseKeycloakRoles, D as refreshKeycloakToken, av as resetRateLimitForKey, ax as resolveIdentifier, W as resolveRateLimitIdentifier, aP as storeBetaCode, aO as validateBetaCode, v as validateEnvVars } from './env-CYKVNpLl.js';
6
6
  export { H as HTML_TAG_PATTERN, i as URL_DOMAIN_PATTERN, U as URL_PROTOCOL_PATTERN, c as constantTimeEqual, b as containsHtml, a as containsUrls, f as defangUrl, e as escapeHtml, g as getCorrelationId, d as sanitizeApiError, h as sanitizeForEmail, s as stripHtml } from './security-BvLXaQkv.js';
7
7
  export { NextHeaderEntry, SecurityHeaderPresets, SecurityHeadersConfig, generateSecurityHeaders } from './security-headers.js';
8
8
  import { SupabaseClient } from '@supabase/supabase-js';
package/dist/index.js CHANGED
@@ -18885,6 +18885,12 @@ var CommonRateLimits = {
18885
18885
  limit: 10,
18886
18886
  windowSeconds: 3600,
18887
18887
  blockDurationSeconds: 3600
18888
+ },
18889
+ /** Beta code validation: 5/min with 5min block (prevents brute force guessing) */
18890
+ betaValidation: {
18891
+ limit: 5,
18892
+ windowSeconds: 60,
18893
+ blockDurationSeconds: 300
18888
18894
  }
18889
18895
  };
18890
18896
  function createMemoryRateLimitStore() {