@nuraly/lumenjs 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -282
- package/dist/auth/config.d.ts +23 -0
- package/dist/auth/config.js +115 -0
- package/dist/auth/guard.d.ts +12 -0
- package/dist/auth/guard.js +28 -0
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/index.js +1 -0
- package/dist/auth/middleware.d.ts +23 -0
- package/dist/auth/middleware.js +89 -0
- package/dist/auth/native-auth.d.ts +82 -0
- package/dist/auth/native-auth.js +340 -0
- package/dist/auth/oidc-client.d.ts +17 -0
- package/dist/auth/oidc-client.js +123 -0
- package/dist/auth/providers/google.d.ts +23 -0
- package/dist/auth/providers/google.js +25 -0
- package/dist/auth/providers/index.d.ts +2 -0
- package/dist/auth/providers/index.js +1 -0
- package/dist/auth/routes/login.d.ts +8 -0
- package/dist/auth/routes/login.js +121 -0
- package/dist/auth/routes/logout.d.ts +4 -0
- package/dist/auth/routes/logout.js +79 -0
- package/dist/auth/routes/oidc-callback.d.ts +3 -0
- package/dist/auth/routes/oidc-callback.js +70 -0
- package/dist/auth/routes/password.d.ts +5 -0
- package/dist/auth/routes/password.js +149 -0
- package/dist/auth/routes/signup.d.ts +3 -0
- package/dist/auth/routes/signup.js +81 -0
- package/dist/auth/routes/token.d.ts +4 -0
- package/dist/auth/routes/token.js +70 -0
- package/dist/auth/routes/totp.d.ts +22 -0
- package/dist/auth/routes/totp.js +232 -0
- package/dist/auth/routes/utils.d.ts +7 -0
- package/dist/auth/routes/utils.js +35 -0
- package/dist/auth/routes/verify.d.ts +3 -0
- package/dist/auth/routes/verify.js +26 -0
- package/dist/auth/routes.d.ts +8 -0
- package/dist/auth/routes.js +124 -0
- package/dist/auth/session.d.ts +8 -0
- package/dist/auth/session.js +54 -0
- package/dist/auth/token.d.ts +33 -0
- package/dist/auth/token.js +90 -0
- package/dist/auth/types.d.ts +156 -0
- package/dist/auth/types.js +2 -0
- package/dist/build/build-client.d.ts +15 -0
- package/dist/build/build-client.js +45 -0
- package/dist/build/build-prerender.d.ts +11 -0
- package/dist/build/build-prerender.js +159 -0
- package/dist/build/build-server.d.ts +18 -0
- package/dist/build/build-server.js +107 -0
- package/dist/build/build.js +60 -123
- package/dist/build/scan.d.ts +18 -0
- package/dist/build/scan.js +77 -6
- package/dist/build/serve-api.js +8 -2
- package/dist/build/serve-loaders.d.ts +4 -4
- package/dist/build/serve-loaders.js +26 -18
- package/dist/build/serve-ssr.js +38 -11
- package/dist/build/serve-static.js +3 -3
- package/dist/build/serve.js +341 -18
- package/dist/cli.js +37 -6
- package/dist/communication/encryption.d.ts +35 -0
- package/dist/communication/encryption.js +90 -0
- package/dist/communication/handlers/context.d.ts +27 -0
- package/dist/communication/handlers/context.js +1 -0
- package/dist/communication/handlers/conversation.d.ts +24 -0
- package/dist/communication/handlers/conversation.js +113 -0
- package/dist/communication/handlers/file-upload.d.ts +17 -0
- package/dist/communication/handlers/file-upload.js +62 -0
- package/dist/communication/handlers/messaging.d.ts +30 -0
- package/dist/communication/handlers/messaging.js +237 -0
- package/dist/communication/handlers/presence.d.ts +15 -0
- package/dist/communication/handlers/presence.js +76 -0
- package/dist/communication/handlers.d.ts +5 -0
- package/dist/communication/handlers.js +5 -0
- package/dist/communication/index.d.ts +9 -0
- package/dist/communication/index.js +7 -0
- package/dist/communication/link-preview.d.ts +18 -0
- package/dist/communication/link-preview.js +115 -0
- package/dist/communication/schema.d.ts +10 -0
- package/dist/communication/schema.js +101 -0
- package/dist/communication/server.d.ts +86 -0
- package/dist/communication/server.js +212 -0
- package/dist/communication/signaling.d.ts +43 -0
- package/dist/communication/signaling.js +271 -0
- package/dist/communication/store.d.ts +71 -0
- package/dist/communication/store.js +289 -0
- package/dist/communication/types.d.ts +454 -0
- package/dist/communication/types.js +1 -0
- package/dist/create.d.ts +1 -0
- package/dist/create.js +55 -0
- package/dist/db/auto-migrate.d.ts +3 -0
- package/dist/db/auto-migrate.js +100 -0
- package/dist/db/client.d.ts +3 -0
- package/dist/db/client.js +18 -0
- package/dist/db/index.d.ts +17 -13
- package/dist/db/index.js +205 -26
- package/dist/db/seed.d.ts +12 -0
- package/dist/db/seed.js +88 -0
- package/dist/db/table.d.ts +10 -0
- package/dist/db/table.js +12 -0
- package/dist/dev-server/config.d.ts +11 -0
- package/dist/dev-server/config.js +40 -20
- package/dist/dev-server/index-html.d.ts +4 -0
- package/dist/dev-server/index-html.js +21 -6
- package/dist/dev-server/nuralyui-aliases.d.ts +0 -4
- package/dist/dev-server/nuralyui-aliases.js +115 -94
- package/dist/dev-server/plugins/vite-plugin-api-routes.js +29 -5
- package/dist/dev-server/plugins/vite-plugin-auth.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-auth.js +223 -0
- package/dist/dev-server/plugins/vite-plugin-auto-define.d.ts +16 -0
- package/dist/dev-server/plugins/vite-plugin-auto-define.js +111 -0
- package/dist/dev-server/plugins/vite-plugin-communication.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-communication.js +205 -0
- package/dist/dev-server/plugins/vite-plugin-editor-api.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-editor-api.js +318 -0
- package/dist/dev-server/plugins/vite-plugin-i18n.js +69 -2
- package/dist/dev-server/plugins/vite-plugin-lit-dedup.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-lit-dedup.js +78 -34
- package/dist/dev-server/plugins/vite-plugin-lit-hmr.js +44 -2
- package/dist/dev-server/plugins/vite-plugin-llms.d.ts +2 -0
- package/dist/dev-server/plugins/vite-plugin-llms.js +92 -0
- package/dist/dev-server/plugins/vite-plugin-loaders.js +146 -13
- package/dist/dev-server/plugins/vite-plugin-routes.js +16 -5
- package/dist/dev-server/plugins/vite-plugin-socketio.d.ts +2 -0
- package/dist/dev-server/plugins/vite-plugin-socketio.js +51 -0
- package/dist/dev-server/plugins/vite-plugin-source-annotator.d.ts +2 -0
- package/dist/dev-server/plugins/vite-plugin-source-annotator.js +26 -3
- package/dist/dev-server/plugins/vite-plugin-storage.d.ts +10 -0
- package/dist/dev-server/plugins/vite-plugin-storage.js +126 -0
- package/dist/dev-server/plugins/vite-plugin-virtual-modules.js +140 -3
- package/dist/dev-server/server.js +242 -70
- package/dist/dev-server/ssr-render.d.ts +2 -1
- package/dist/dev-server/ssr-render.js +117 -50
- package/dist/editor/ai/backend.d.ts +20 -0
- package/dist/editor/ai/backend.js +113 -0
- package/dist/editor/ai/claude-code-client.d.ts +20 -0
- package/dist/editor/ai/claude-code-client.js +145 -0
- package/dist/editor/ai/deepseek-client.d.ts +7 -0
- package/dist/editor/ai/deepseek-client.js +113 -0
- package/dist/editor/ai/opencode-client.d.ts +14 -0
- package/dist/editor/ai/opencode-client.js +99 -0
- package/dist/editor/ai/snapshot-store.d.ts +22 -0
- package/dist/editor/ai/snapshot-store.js +35 -0
- package/dist/editor/ai/types.d.ts +30 -0
- package/dist/editor/ai/types.js +136 -0
- package/dist/editor/ai-chat-panel.d.ts +13 -0
- package/dist/editor/ai-chat-panel.js +613 -0
- package/dist/editor/ai-markdown.d.ts +10 -0
- package/dist/editor/ai-markdown.js +70 -0
- package/dist/editor/ai-project-panel.d.ts +11 -0
- package/dist/editor/ai-project-panel.js +332 -0
- package/dist/editor/ast-modification.d.ts +11 -0
- package/dist/editor/ast-modification.js +1 -0
- package/dist/editor/ast-service.d.ts +30 -0
- package/dist/editor/ast-service.js +180 -0
- package/dist/editor/css-rules.d.ts +54 -0
- package/dist/editor/css-rules.js +423 -0
- package/dist/editor/editor-api-client.d.ts +51 -0
- package/dist/editor/editor-api-client.js +162 -0
- package/dist/editor/editor-bridge.d.ts +1 -0
- package/dist/editor/editor-bridge.js +18 -8
- package/dist/editor/editor-toolbar.d.ts +14 -0
- package/dist/editor/editor-toolbar.js +115 -0
- package/dist/editor/file-editor.d.ts +9 -0
- package/dist/editor/file-editor.js +236 -0
- package/dist/editor/file-service.d.ts +16 -0
- package/dist/editor/file-service.js +52 -0
- package/dist/editor/i18n-key-gen.d.ts +1 -0
- package/dist/editor/i18n-key-gen.js +7 -0
- package/dist/editor/inline-text-edit.d.ts +5 -0
- package/dist/editor/inline-text-edit.js +173 -92
- package/dist/editor/overlay-events.d.ts +5 -0
- package/dist/editor/overlay-events.js +364 -0
- package/dist/editor/overlay-hmr.d.ts +2 -0
- package/dist/editor/overlay-hmr.js +76 -0
- package/dist/editor/overlay-selection.d.ts +29 -0
- package/dist/editor/overlay-selection.js +148 -0
- package/dist/editor/overlay-utils.d.ts +12 -0
- package/dist/editor/overlay-utils.js +59 -0
- package/dist/editor/properties-panel-persist.d.ts +14 -0
- package/dist/editor/properties-panel-persist.js +70 -0
- package/dist/editor/properties-panel-rows.d.ts +10 -0
- package/dist/editor/properties-panel-rows.js +349 -0
- package/dist/editor/properties-panel-styles.d.ts +4 -0
- package/dist/editor/properties-panel-styles.js +174 -0
- package/dist/editor/properties-panel.d.ts +4 -0
- package/dist/editor/properties-panel.js +148 -0
- package/dist/editor/property-registry.d.ts +16 -0
- package/dist/editor/property-registry.js +303 -0
- package/dist/editor/standalone-file-panel.d.ts +0 -0
- package/dist/editor/standalone-file-panel.js +1 -0
- package/dist/editor/standalone-overlay-dom.d.ts +0 -0
- package/dist/editor/standalone-overlay-dom.js +1 -0
- package/dist/editor/standalone-overlay-styles.d.ts +0 -0
- package/dist/editor/standalone-overlay-styles.js +1 -0
- package/dist/editor/standalone-overlay.d.ts +1 -0
- package/dist/editor/standalone-overlay.js +76 -0
- package/dist/editor/syntax-highlighter.d.ts +4 -0
- package/dist/editor/syntax-highlighter.js +81 -0
- package/dist/editor/text-toolbar.d.ts +11 -0
- package/dist/editor/text-toolbar.js +327 -0
- package/dist/editor/toolbar-styles.d.ts +4 -0
- package/dist/editor/toolbar-styles.js +198 -0
- package/dist/email/index.d.ts +32 -0
- package/dist/email/index.js +154 -0
- package/dist/email/providers/resend.d.ts +2 -0
- package/dist/email/providers/resend.js +24 -0
- package/dist/email/providers/sendgrid.d.ts +2 -0
- package/dist/email/providers/sendgrid.js +31 -0
- package/dist/email/providers/smtp.d.ts +13 -0
- package/dist/email/providers/smtp.js +125 -0
- package/dist/email/template-engine.d.ts +18 -0
- package/dist/email/template-engine.js +116 -0
- package/dist/email/templates/base.d.ts +9 -0
- package/dist/email/templates/base.js +65 -0
- package/dist/email/templates/password-reset.d.ts +5 -0
- package/dist/email/templates/password-reset.js +15 -0
- package/dist/email/templates/verify-email.d.ts +5 -0
- package/dist/email/templates/verify-email.js +15 -0
- package/dist/email/templates/welcome.d.ts +5 -0
- package/dist/email/templates/welcome.js +13 -0
- package/dist/email/types.d.ts +49 -0
- package/dist/email/types.js +1 -0
- package/dist/llms/generate.d.ts +46 -0
- package/dist/llms/generate.js +185 -0
- package/dist/permissions/guard.d.ts +28 -0
- package/dist/permissions/guard.js +30 -0
- package/dist/permissions/index.d.ts +6 -0
- package/dist/permissions/index.js +3 -0
- package/dist/permissions/service.d.ts +80 -0
- package/dist/permissions/service.js +210 -0
- package/dist/permissions/tables.d.ts +5 -0
- package/dist/permissions/tables.js +68 -0
- package/dist/permissions/types.d.ts +33 -0
- package/dist/permissions/types.js +1 -0
- package/dist/runtime/app-shell.d.ts +1 -1
- package/dist/runtime/app-shell.js +164 -0
- package/dist/runtime/auth.d.ts +10 -0
- package/dist/runtime/auth.js +30 -0
- package/dist/runtime/communication.d.ts +137 -0
- package/dist/runtime/communication.js +228 -0
- package/dist/runtime/error-boundary.d.ts +23 -0
- package/dist/runtime/error-boundary.js +120 -0
- package/dist/runtime/i18n.d.ts +6 -1
- package/dist/runtime/i18n.js +42 -21
- package/dist/runtime/island.d.ts +16 -0
- package/dist/runtime/island.js +80 -0
- package/dist/runtime/router-data.d.ts +3 -0
- package/dist/runtime/router-data.js +102 -17
- package/dist/runtime/router-hydration.js +34 -2
- package/dist/runtime/router.d.ts +19 -2
- package/dist/runtime/router.js +237 -43
- package/dist/runtime/socket-client.d.ts +2 -0
- package/dist/runtime/socket-client.js +30 -0
- package/dist/runtime/webrtc.d.ts +91 -0
- package/dist/runtime/webrtc.js +428 -0
- package/dist/shared/dom-shims.js +4 -2
- package/dist/shared/graceful-shutdown.d.ts +8 -0
- package/dist/shared/graceful-shutdown.js +36 -0
- package/dist/shared/health.d.ts +8 -0
- package/dist/shared/health.js +25 -0
- package/dist/shared/llms-txt.d.ts +31 -0
- package/dist/shared/llms-txt.js +85 -0
- package/dist/shared/logger.d.ts +32 -0
- package/dist/shared/logger.js +93 -0
- package/dist/shared/meta.d.ts +27 -0
- package/dist/shared/meta.js +71 -0
- package/dist/shared/middleware-runner.d.ts +9 -0
- package/dist/shared/middleware-runner.js +29 -0
- package/dist/shared/rate-limit.d.ts +18 -0
- package/dist/shared/rate-limit.js +71 -0
- package/dist/shared/request-id.d.ts +5 -0
- package/dist/shared/request-id.js +18 -0
- package/dist/shared/route-matching.js +16 -1
- package/dist/shared/security-headers.d.ts +18 -0
- package/dist/shared/security-headers.js +38 -0
- package/dist/shared/socket-io-setup.d.ts +11 -0
- package/dist/shared/socket-io-setup.js +51 -0
- package/dist/shared/types.d.ts +15 -0
- package/dist/shared/utils.d.ts +33 -7
- package/dist/shared/utils.js +164 -27
- package/dist/storage/adapters/local.d.ts +44 -0
- package/dist/storage/adapters/local.js +85 -0
- package/dist/storage/adapters/s3.d.ts +32 -0
- package/dist/storage/adapters/s3.js +119 -0
- package/dist/storage/adapters/types.d.ts +53 -0
- package/dist/storage/adapters/types.js +1 -0
- package/dist/storage/index.d.ts +76 -0
- package/dist/storage/index.js +83 -0
- package/package.json +45 -7
- package/templates/blog/api/posts.ts +4 -18
- package/templates/blog/data/migrations/001_init.sql +6 -5
- package/templates/blog/lumenjs.config.ts +3 -0
- package/templates/blog/package.json +14 -0
- package/templates/blog/pages/_layout.ts +25 -0
- package/templates/blog/pages/index.ts +48 -22
- package/templates/blog/pages/posts/[slug].ts +45 -20
- package/templates/blog/pages/tag/[tag].ts +44 -0
- package/templates/dashboard/api/stats.ts +8 -5
- package/templates/dashboard/lumenjs.config.ts +3 -0
- package/templates/dashboard/package.json +14 -0
- package/templates/dashboard/pages/_layout.ts +25 -0
- package/templates/dashboard/pages/index.ts +54 -23
- package/templates/dashboard/pages/settings/index.ts +29 -0
- package/templates/default/lumenjs.config.ts +3 -0
- package/templates/default/package.json +14 -0
- package/templates/default/pages/index.ts +24 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { renderVerifyEmail as builtInVerifyEmail } from './templates/verify-email.js';
|
|
4
|
+
import { renderPasswordReset as builtInPasswordReset } from './templates/password-reset.js';
|
|
5
|
+
import { renderWelcome as builtInWelcome } from './templates/welcome.js';
|
|
6
|
+
import { compileTemplate } from './template-engine.js';
|
|
7
|
+
export { renderVerifyEmail } from './templates/verify-email.js';
|
|
8
|
+
export { renderPasswordReset } from './templates/password-reset.js';
|
|
9
|
+
export { renderWelcome } from './templates/welcome.js';
|
|
10
|
+
export { renderTemplate, renderButton, escapeHtml } from './templates/base.js';
|
|
11
|
+
export { compileTemplate } from './template-engine.js';
|
|
12
|
+
const BUILT_IN_TEMPLATES = {
|
|
13
|
+
'verify-email': (d) => builtInVerifyEmail(d),
|
|
14
|
+
'password-reset': (d) => builtInPasswordReset(d),
|
|
15
|
+
'welcome': (d) => builtInWelcome({ ...d, loginUrl: d.loginUrl || d.url }),
|
|
16
|
+
};
|
|
17
|
+
let _projectDir = null;
|
|
18
|
+
/** Set the project directory for file-based template loading */
|
|
19
|
+
export function setEmailProjectDir(dir) {
|
|
20
|
+
_projectDir = dir;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Load an HTML template file from the project's `emails/` directory.
|
|
24
|
+
* Returns the raw HTML string or null if not found.
|
|
25
|
+
*/
|
|
26
|
+
function loadHtmlTemplate(name) {
|
|
27
|
+
if (!_projectDir)
|
|
28
|
+
return null;
|
|
29
|
+
const emailsDir = path.resolve(_projectDir, 'emails');
|
|
30
|
+
const filePath = path.resolve(emailsDir, `${name}.html`);
|
|
31
|
+
// Prevent path traversal outside the emails/ directory
|
|
32
|
+
if (!filePath.startsWith(emailsDir + path.sep) && filePath !== emailsDir)
|
|
33
|
+
return null;
|
|
34
|
+
try {
|
|
35
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Get a template renderer. Resolution order:
|
|
43
|
+
* 1. HTML file in emails/ directory (compiled with {{variable}} engine)
|
|
44
|
+
* 2. Custom function in config.templates
|
|
45
|
+
* 3. Built-in template
|
|
46
|
+
*/
|
|
47
|
+
export function getTemplate(config, name) {
|
|
48
|
+
// 1. File-based HTML template
|
|
49
|
+
const htmlFile = loadHtmlTemplate(name);
|
|
50
|
+
if (htmlFile) {
|
|
51
|
+
return (data) => compileTemplate(htmlFile, data);
|
|
52
|
+
}
|
|
53
|
+
// 2. Custom function
|
|
54
|
+
if (config.templates?.[name])
|
|
55
|
+
return config.templates[name];
|
|
56
|
+
// 3. Built-in
|
|
57
|
+
return BUILT_IN_TEMPLATES[name];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Render a named template with data. Returns HTML string or null if template not found.
|
|
61
|
+
*/
|
|
62
|
+
export function renderEmailTemplate(config, name, data) {
|
|
63
|
+
const renderer = getTemplate(config, name);
|
|
64
|
+
return renderer ? renderer(data) : null;
|
|
65
|
+
}
|
|
66
|
+
async function createProvider(config) {
|
|
67
|
+
switch (config.provider) {
|
|
68
|
+
case 'smtp': {
|
|
69
|
+
if (!config.smtp)
|
|
70
|
+
throw new Error('[LumenJS Email] smtp config required for SMTP provider');
|
|
71
|
+
const { createSmtpProvider } = await import('./providers/smtp.js');
|
|
72
|
+
return createSmtpProvider(config.smtp);
|
|
73
|
+
}
|
|
74
|
+
case 'resend': {
|
|
75
|
+
if (!config.resend?.apiKey)
|
|
76
|
+
throw new Error('[LumenJS Email] resend.apiKey required for Resend provider');
|
|
77
|
+
const { createResendProvider } = await import('./providers/resend.js');
|
|
78
|
+
return createResendProvider(config.resend.apiKey);
|
|
79
|
+
}
|
|
80
|
+
case 'sendgrid': {
|
|
81
|
+
if (!config.sendgrid?.apiKey)
|
|
82
|
+
throw new Error('[LumenJS Email] sendgrid.apiKey required for SendGrid provider');
|
|
83
|
+
const { createSendGridProvider } = await import('./providers/sendgrid.js');
|
|
84
|
+
return createSendGridProvider(config.sendgrid.apiKey);
|
|
85
|
+
}
|
|
86
|
+
default:
|
|
87
|
+
throw new Error(`[LumenJS Email] Unknown provider: ${config.provider}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** Strip HTML tags for plain text fallback */
|
|
91
|
+
function htmlToText(html) {
|
|
92
|
+
return html
|
|
93
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
94
|
+
.replace(/<\/p>/gi, '\n\n')
|
|
95
|
+
.replace(/<\/h[1-6]>/gi, '\n\n')
|
|
96
|
+
.replace(/<[^>]+>/g, '')
|
|
97
|
+
.replace(/ /g, ' ')
|
|
98
|
+
.replace(/&/g, '&')
|
|
99
|
+
.replace(/</g, '<')
|
|
100
|
+
.replace(/>/g, '>')
|
|
101
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
102
|
+
.trim();
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Send an email using the configured provider.
|
|
106
|
+
*/
|
|
107
|
+
export async function sendEmail(config, message) {
|
|
108
|
+
const provider = await createProvider(config);
|
|
109
|
+
await provider.send({
|
|
110
|
+
from: config.from,
|
|
111
|
+
to: message.to,
|
|
112
|
+
subject: message.subject,
|
|
113
|
+
html: message.html,
|
|
114
|
+
text: message.text || htmlToText(message.html),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Create a reusable email sender function.
|
|
119
|
+
*/
|
|
120
|
+
export function createEmailSender(config) {
|
|
121
|
+
let provider = null;
|
|
122
|
+
return async (message) => {
|
|
123
|
+
if (!provider)
|
|
124
|
+
provider = await createProvider(config);
|
|
125
|
+
await provider.send({
|
|
126
|
+
from: config.from,
|
|
127
|
+
to: message.to,
|
|
128
|
+
subject: message.subject,
|
|
129
|
+
html: message.html,
|
|
130
|
+
text: message.text || htmlToText(message.html),
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Load email config from lumenjs.email.ts.
|
|
136
|
+
*/
|
|
137
|
+
export async function loadEmailConfig(projectDir, ssrLoadModule) {
|
|
138
|
+
try {
|
|
139
|
+
let mod;
|
|
140
|
+
if (ssrLoadModule) {
|
|
141
|
+
mod = await ssrLoadModule(path.join(projectDir, 'lumenjs.email.ts'));
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
mod = await import(path.join(projectDir, 'lumenjs.email.ts'));
|
|
145
|
+
}
|
|
146
|
+
const config = mod.default || mod;
|
|
147
|
+
if (!config?.provider || !config?.from)
|
|
148
|
+
return null;
|
|
149
|
+
return config;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function createResendProvider(apiKey) {
|
|
2
|
+
return {
|
|
3
|
+
async send(message) {
|
|
4
|
+
const res = await fetch('https://api.resend.com/emails', {
|
|
5
|
+
method: 'POST',
|
|
6
|
+
headers: {
|
|
7
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
8
|
+
'Content-Type': 'application/json',
|
|
9
|
+
},
|
|
10
|
+
body: JSON.stringify({
|
|
11
|
+
from: message.from,
|
|
12
|
+
to: [message.to],
|
|
13
|
+
subject: message.subject,
|
|
14
|
+
html: message.html,
|
|
15
|
+
text: message.text,
|
|
16
|
+
}),
|
|
17
|
+
});
|
|
18
|
+
if (!res.ok) {
|
|
19
|
+
const err = await res.text();
|
|
20
|
+
throw new Error(`Resend API error (${res.status}): ${err}`);
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function createSendGridProvider(apiKey) {
|
|
2
|
+
return {
|
|
3
|
+
async send(message) {
|
|
4
|
+
const res = await fetch('https://api.sendgrid.com/v3/mail/send', {
|
|
5
|
+
method: 'POST',
|
|
6
|
+
headers: {
|
|
7
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
8
|
+
'Content-Type': 'application/json',
|
|
9
|
+
},
|
|
10
|
+
body: JSON.stringify({
|
|
11
|
+
personalizations: [{ to: [{ email: message.to }] }],
|
|
12
|
+
from: (() => {
|
|
13
|
+
const hasAngleBrackets = /<.+>/.test(message.from);
|
|
14
|
+
const email = hasAngleBrackets ? message.from.replace(/.*<(.+)>/, '$1') : message.from;
|
|
15
|
+
const name = hasAngleBrackets ? message.from.replace(/<.+>/, '').trim() || undefined : undefined;
|
|
16
|
+
return { email, name };
|
|
17
|
+
})(),
|
|
18
|
+
subject: message.subject,
|
|
19
|
+
content: [
|
|
20
|
+
...(message.text ? [{ type: 'text/plain', value: message.text }] : []),
|
|
21
|
+
{ type: 'text/html', value: message.html },
|
|
22
|
+
],
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
if (!res.ok) {
|
|
26
|
+
const err = await res.text();
|
|
27
|
+
throw new Error(`SendGrid API error (${res.status}): ${err}`);
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { EmailProvider } from '../types.js';
|
|
2
|
+
interface SmtpConfig {
|
|
3
|
+
host: string;
|
|
4
|
+
port: number;
|
|
5
|
+
secure?: boolean;
|
|
6
|
+
rejectUnauthorized?: boolean;
|
|
7
|
+
auth: {
|
|
8
|
+
user: string;
|
|
9
|
+
pass: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export declare function createSmtpProvider(config: SmtpConfig): EmailProvider;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
import tls from 'node:tls';
|
|
3
|
+
function readLine(socket) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
let data = '';
|
|
6
|
+
const timer = setTimeout(() => {
|
|
7
|
+
socket.removeListener('data', onData);
|
|
8
|
+
reject(new Error('SMTP timeout'));
|
|
9
|
+
}, 30000);
|
|
10
|
+
const onData = (chunk) => {
|
|
11
|
+
data += chunk.toString();
|
|
12
|
+
const lines = data.split('\r\n');
|
|
13
|
+
// Find the last complete line (ignore trailing empty from split)
|
|
14
|
+
const complete = lines.slice(0, -1);
|
|
15
|
+
if (complete.length > 0) {
|
|
16
|
+
const lastLine = complete[complete.length - 1];
|
|
17
|
+
// Final SMTP response line has a space after the 3-digit code, not '-'
|
|
18
|
+
if (/^\d{3} /.test(lastLine) || !/^\d{3}[-]/.test(lastLine)) {
|
|
19
|
+
socket.removeListener('data', onData);
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
resolve(complete.join('\r\n'));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
socket.on('data', onData);
|
|
26
|
+
socket.once('error', (err) => {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
reject(err);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function writeLine(socket, line) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
socket.write(line + '\r\n', () => {
|
|
35
|
+
readLine(socket).then(resolve).catch(reject);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** Strip CR/LF to prevent SMTP header/command injection. */
|
|
40
|
+
function sanitizeHeaderValue(value) {
|
|
41
|
+
return value.replace(/[\r\n]/g, '');
|
|
42
|
+
}
|
|
43
|
+
export function createSmtpProvider(config) {
|
|
44
|
+
return {
|
|
45
|
+
async send(message) {
|
|
46
|
+
const fromRaw = sanitizeHeaderValue(message.from);
|
|
47
|
+
const fromEmail = fromRaw.replace(/.*<(.+)>/, '$1').trim() || fromRaw;
|
|
48
|
+
let socket;
|
|
49
|
+
if (config.secure) {
|
|
50
|
+
socket = tls.connect({ host: config.host, port: config.port, rejectUnauthorized: config.rejectUnauthorized !== false });
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
socket = net.createConnection({ host: config.host, port: config.port });
|
|
54
|
+
}
|
|
55
|
+
await new Promise((resolve, reject) => {
|
|
56
|
+
socket.once('connect', resolve);
|
|
57
|
+
socket.once('secureConnect', resolve);
|
|
58
|
+
socket.once('error', reject);
|
|
59
|
+
});
|
|
60
|
+
try {
|
|
61
|
+
await readLine(socket); // greeting
|
|
62
|
+
const ehloRes = await writeLine(socket, `EHLO localhost`);
|
|
63
|
+
// STARTTLS if not already secure
|
|
64
|
+
if (!config.secure && ehloRes.includes('STARTTLS')) {
|
|
65
|
+
await writeLine(socket, 'STARTTLS');
|
|
66
|
+
socket = tls.connect({ socket, host: config.host, rejectUnauthorized: config.rejectUnauthorized !== false });
|
|
67
|
+
await new Promise((resolve) => socket.once('secureConnect', resolve));
|
|
68
|
+
await writeLine(socket, 'EHLO localhost');
|
|
69
|
+
}
|
|
70
|
+
// AUTH LOGIN
|
|
71
|
+
const authRes = await writeLine(socket, 'AUTH LOGIN');
|
|
72
|
+
if (authRes.startsWith('334')) {
|
|
73
|
+
const userRes = await writeLine(socket, Buffer.from(config.auth.user).toString('base64'));
|
|
74
|
+
if (userRes.startsWith('334')) {
|
|
75
|
+
const passRes = await writeLine(socket, Buffer.from(config.auth.pass).toString('base64'));
|
|
76
|
+
if (!passRes.startsWith('235'))
|
|
77
|
+
throw new Error(`SMTP auth failed: ${passRes}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const mailRes = await writeLine(socket, `MAIL FROM:<${fromEmail}>`);
|
|
81
|
+
if (!mailRes.startsWith('250'))
|
|
82
|
+
throw new Error(`MAIL FROM failed: ${mailRes}`);
|
|
83
|
+
const safeTo = sanitizeHeaderValue(message.to);
|
|
84
|
+
const rcptRes = await writeLine(socket, `RCPT TO:<${safeTo}>`);
|
|
85
|
+
if (!rcptRes.startsWith('250'))
|
|
86
|
+
throw new Error(`RCPT TO failed: ${rcptRes}`);
|
|
87
|
+
const dataRes = await writeLine(socket, 'DATA');
|
|
88
|
+
if (!dataRes.startsWith('354'))
|
|
89
|
+
throw new Error(`DATA failed: ${dataRes}`);
|
|
90
|
+
const boundary = `----=_Part_${Date.now()}`;
|
|
91
|
+
const textBody = (message.text || message.html.replace(/<[^>]+>/g, '')).replace(/^\./gm, '..');
|
|
92
|
+
const htmlBody = message.html.replace(/^\./gm, '..');
|
|
93
|
+
const safeSubject = sanitizeHeaderValue(message.subject);
|
|
94
|
+
const emailData = [
|
|
95
|
+
`From: ${fromRaw}`,
|
|
96
|
+
`To: ${safeTo}`,
|
|
97
|
+
`Subject: ${safeSubject}`,
|
|
98
|
+
`MIME-Version: 1.0`,
|
|
99
|
+
`Content-Type: multipart/alternative; boundary="${boundary}"`,
|
|
100
|
+
`Date: ${new Date().toUTCString()}`,
|
|
101
|
+
'',
|
|
102
|
+
`--${boundary}`,
|
|
103
|
+
`Content-Type: text/plain; charset=utf-8`,
|
|
104
|
+
'',
|
|
105
|
+
textBody,
|
|
106
|
+
'',
|
|
107
|
+
`--${boundary}`,
|
|
108
|
+
`Content-Type: text/html; charset=utf-8`,
|
|
109
|
+
'',
|
|
110
|
+
htmlBody,
|
|
111
|
+
'',
|
|
112
|
+
`--${boundary}--`,
|
|
113
|
+
'.',
|
|
114
|
+
].join('\r\n');
|
|
115
|
+
const sendRes = await writeLine(socket, emailData);
|
|
116
|
+
if (!sendRes.startsWith('250'))
|
|
117
|
+
throw new Error(`Send failed: ${sendRes}`);
|
|
118
|
+
await writeLine(socket, 'QUIT');
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
socket.destroy();
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { TemplateData } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Compile an HTML template string with Handlebars-like syntax.
|
|
4
|
+
*
|
|
5
|
+
* Variables:
|
|
6
|
+
* {{variable}} — HTML-escaped
|
|
7
|
+
* {{obj.prop}} — dotted path access
|
|
8
|
+
* {{{variable}}} — raw/unescaped
|
|
9
|
+
*
|
|
10
|
+
* Blocks:
|
|
11
|
+
* {{#if variable}}...{{/if}} — conditional (truthy check)
|
|
12
|
+
* {{#each items}}...{{/each}} — loop over array. Inside: {{name}}, {{@index}}
|
|
13
|
+
*
|
|
14
|
+
* Helpers:
|
|
15
|
+
* {{#button url="..." text="..."}} — renders CTA button
|
|
16
|
+
* {{#layout}}...{{/layout}} — wraps content in base email layout
|
|
17
|
+
*/
|
|
18
|
+
export declare function compileTemplate(html: string, data: TemplateData): string;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { renderTemplate, renderButton } from './templates/base.js';
|
|
2
|
+
/** Escape HTML entities */
|
|
3
|
+
function escapeHtml(str) {
|
|
4
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
5
|
+
}
|
|
6
|
+
/** Resolve a dotted path like "user.name" from an object */
|
|
7
|
+
function resolve(obj, path) {
|
|
8
|
+
let current = obj;
|
|
9
|
+
for (const part of path.split('.')) {
|
|
10
|
+
if (current == null || typeof current !== 'object')
|
|
11
|
+
return undefined;
|
|
12
|
+
current = current[part];
|
|
13
|
+
}
|
|
14
|
+
return current;
|
|
15
|
+
}
|
|
16
|
+
/** Interpolate {{var}} or {{obj.prop}} inside a string */
|
|
17
|
+
function interpolate(str, data, escape) {
|
|
18
|
+
return str.replace(/\{\{(\w[\w.]*)\}\}/g, (_m, path) => {
|
|
19
|
+
const value = resolve(data, path);
|
|
20
|
+
const s = value != null ? String(value) : '';
|
|
21
|
+
return escape ? escapeHtml(s) : s;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Compile an HTML template string with Handlebars-like syntax.
|
|
26
|
+
*
|
|
27
|
+
* Variables:
|
|
28
|
+
* {{variable}} — HTML-escaped
|
|
29
|
+
* {{obj.prop}} — dotted path access
|
|
30
|
+
* {{{variable}}} — raw/unescaped
|
|
31
|
+
*
|
|
32
|
+
* Blocks:
|
|
33
|
+
* {{#if variable}}...{{/if}} — conditional (truthy check)
|
|
34
|
+
* {{#each items}}...{{/each}} — loop over array. Inside: {{name}}, {{@index}}
|
|
35
|
+
*
|
|
36
|
+
* Helpers:
|
|
37
|
+
* {{#button url="..." text="..."}} — renders CTA button
|
|
38
|
+
* {{#layout}}...{{/layout}} — wraps content in base email layout
|
|
39
|
+
*/
|
|
40
|
+
export function compileTemplate(html, data) {
|
|
41
|
+
let result = html;
|
|
42
|
+
// 1. {{#layout}}...{{/layout}}
|
|
43
|
+
const layoutMatch = result.match(/\{\{#layout\}\}([\s\S]*?)\{\{\/layout\}\}/);
|
|
44
|
+
let useLayout = false;
|
|
45
|
+
if (layoutMatch) {
|
|
46
|
+
result = layoutMatch[1];
|
|
47
|
+
useLayout = true;
|
|
48
|
+
}
|
|
49
|
+
// 2. {{#each items}}...{{/each}} (supports nesting)
|
|
50
|
+
result = processEach(result, data);
|
|
51
|
+
// 3. {{#if variable}}...{{/if}} (no nesting for simplicity)
|
|
52
|
+
result = result.replace(/\{\{#if\s+([\w.]+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (_match, path, content) => {
|
|
53
|
+
const value = resolve(data, path);
|
|
54
|
+
return value ? content : '';
|
|
55
|
+
});
|
|
56
|
+
// 4. {{#button url="..." text="..."}}
|
|
57
|
+
result = result.replace(/\{\{#button\s+url="([^"]*?)"\s+text="([^"]*?)"\s*\}\}/g, (_match, url, text) => {
|
|
58
|
+
return renderButton(interpolate(text, data, false), interpolate(url, data, false));
|
|
59
|
+
});
|
|
60
|
+
// 5. {{{variable}}} — raw
|
|
61
|
+
result = result.replace(/\{\{\{([\w.]+)\}\}\}/g, (_match, path) => {
|
|
62
|
+
const value = resolve(data, path);
|
|
63
|
+
return value != null ? String(value) : '';
|
|
64
|
+
});
|
|
65
|
+
// 6. {{variable}} — escaped
|
|
66
|
+
result = result.replace(/\{\{([\w.]+)\}\}/g, (_match, path) => {
|
|
67
|
+
// Skip @index (already replaced in each loop)
|
|
68
|
+
if (path.startsWith('@'))
|
|
69
|
+
return '';
|
|
70
|
+
const value = resolve(data, path);
|
|
71
|
+
return value != null ? escapeHtml(String(value)) : '';
|
|
72
|
+
});
|
|
73
|
+
// 7. Wrap in layout
|
|
74
|
+
if (useLayout) {
|
|
75
|
+
result = renderTemplate(data.appName, result);
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
/** Process {{#each items}}...{{/each}} blocks, resolving item variables */
|
|
80
|
+
function processEach(html, data) {
|
|
81
|
+
return html.replace(/\{\{#each\s+([\w.]+)\}\}([\s\S]*?)\{\{\/each\}\}/g, (_match, path, body) => {
|
|
82
|
+
const items = resolve(data, path);
|
|
83
|
+
if (!Array.isArray(items))
|
|
84
|
+
return '';
|
|
85
|
+
return items.map((item, index) => {
|
|
86
|
+
let row = body;
|
|
87
|
+
// Replace {{@index}}
|
|
88
|
+
row = row.replace(/\{\{@index\}\}/g, String(index));
|
|
89
|
+
// If item is an object, replace {{prop}} with item.prop
|
|
90
|
+
if (item && typeof item === 'object') {
|
|
91
|
+
// Process nested {{#if}} within the loop context
|
|
92
|
+
row = row.replace(/\{\{#if\s+([\w.]+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (_m, p, content) => {
|
|
93
|
+
const value = resolve(item, p) ?? resolve(data, p);
|
|
94
|
+
return value ? content : '';
|
|
95
|
+
});
|
|
96
|
+
// Replace {{{prop}}} raw
|
|
97
|
+
row = row.replace(/\{\{\{([\w.]+)\}\}\}/g, (_m, p) => {
|
|
98
|
+
const v = resolve(item, p) ?? resolve(data, p);
|
|
99
|
+
return v != null ? String(v) : '';
|
|
100
|
+
});
|
|
101
|
+
// Replace {{prop}} escaped
|
|
102
|
+
row = row.replace(/\{\{([\w.]+)\}\}/g, (_m, p) => {
|
|
103
|
+
if (p.startsWith('@'))
|
|
104
|
+
return '';
|
|
105
|
+
const v = resolve(item, p) ?? resolve(data, p);
|
|
106
|
+
return v != null ? escapeHtml(String(v)) : '';
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
// Primitive item — replace {{.}} or {{this}}
|
|
111
|
+
row = row.replace(/\{\{\.?\}\}|\{\{this\}\}/g, escapeHtml(String(item)));
|
|
112
|
+
}
|
|
113
|
+
return row;
|
|
114
|
+
}).join('');
|
|
115
|
+
});
|
|
116
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Escape special HTML characters to prevent injection. */
|
|
2
|
+
export declare function escapeHtml(s: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* Shared HTML email layout wrapper.
|
|
5
|
+
* Table-based for maximum email client compatibility (Outlook, Gmail, Apple Mail).
|
|
6
|
+
*/
|
|
7
|
+
export declare function renderTemplate(appName: string, content: string, footerText?: string): string;
|
|
8
|
+
/** Render a purple CTA button */
|
|
9
|
+
export declare function renderButton(text: string, url: string): string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Escape special HTML characters to prevent injection. */
|
|
2
|
+
export function escapeHtml(s) {
|
|
3
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Shared HTML email layout wrapper.
|
|
7
|
+
* Table-based for maximum email client compatibility (Outlook, Gmail, Apple Mail).
|
|
8
|
+
*/
|
|
9
|
+
export function renderTemplate(appName, content, footerText) {
|
|
10
|
+
const safeAppName = escapeHtml(appName);
|
|
11
|
+
const safeFooter = footerText ? escapeHtml(footerText) : `This email was sent by ${safeAppName}. If you didn't expect this, you can safely ignore it.`;
|
|
12
|
+
return `<!DOCTYPE html>
|
|
13
|
+
<html lang="en">
|
|
14
|
+
<head>
|
|
15
|
+
<meta charset="utf-8">
|
|
16
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
17
|
+
<title>${safeAppName}</title>
|
|
18
|
+
</head>
|
|
19
|
+
<body style="margin:0; padding:0; background-color:#f7f9f9; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;">
|
|
20
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f7f9f9;">
|
|
21
|
+
<tr>
|
|
22
|
+
<td align="center" style="padding:40px 20px;">
|
|
23
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:520px; background-color:#ffffff; border-radius:12px; overflow:hidden;">
|
|
24
|
+
<!-- Header -->
|
|
25
|
+
<tr>
|
|
26
|
+
<td style="padding:32px 40px 0; text-align:center;">
|
|
27
|
+
<div style="display:inline-block; width:40px; height:40px; background-color:#0f1419; color:#ffffff; border-radius:8px; font-size:20px; font-weight:900; line-height:40px; text-align:center;">N</div>
|
|
28
|
+
<div style="font-size:14px; font-weight:700; color:#0f1419; margin-top:8px;">${safeAppName}</div>
|
|
29
|
+
</td>
|
|
30
|
+
</tr>
|
|
31
|
+
<!-- Content -->
|
|
32
|
+
<tr>
|
|
33
|
+
<td style="padding:24px 40px 40px;">
|
|
34
|
+
${content}
|
|
35
|
+
</td>
|
|
36
|
+
</tr>
|
|
37
|
+
</table>
|
|
38
|
+
<!-- Footer -->
|
|
39
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:520px;">
|
|
40
|
+
<tr>
|
|
41
|
+
<td style="padding:24px 40px; text-align:center; font-size:12px; color:#536471; line-height:1.5;">
|
|
42
|
+
${safeFooter}
|
|
43
|
+
</td>
|
|
44
|
+
</tr>
|
|
45
|
+
</table>
|
|
46
|
+
</td>
|
|
47
|
+
</tr>
|
|
48
|
+
</table>
|
|
49
|
+
</body>
|
|
50
|
+
</html>`;
|
|
51
|
+
}
|
|
52
|
+
/** Render a purple CTA button */
|
|
53
|
+
export function renderButton(text, url) {
|
|
54
|
+
const safeText = escapeHtml(text);
|
|
55
|
+
const safeUrl = escapeHtml(url);
|
|
56
|
+
return `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:24px 0;">
|
|
57
|
+
<tr>
|
|
58
|
+
<td align="center">
|
|
59
|
+
<a href="${safeUrl}" target="_blank" style="display:inline-block; padding:14px 32px; background-color:#7c3aed; color:#ffffff; font-size:16px; font-weight:700; text-decoration:none; border-radius:9999px;">
|
|
60
|
+
${safeText}
|
|
61
|
+
</a>
|
|
62
|
+
</td>
|
|
63
|
+
</tr>
|
|
64
|
+
</table>`;
|
|
65
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { renderTemplate, renderButton, escapeHtml } from './base.js';
|
|
2
|
+
export function renderPasswordReset(opts) {
|
|
3
|
+
const greeting = opts.userName ? `Hi ${escapeHtml(opts.userName)},` : 'Hi,';
|
|
4
|
+
const safeAppName = escapeHtml(opts.appName);
|
|
5
|
+
const safeUrl = escapeHtml(opts.url);
|
|
6
|
+
const content = `
|
|
7
|
+
<h1 style="font-size:22px; font-weight:800; color:#0f1419; margin:0 0 16px; line-height:1.3;">Reset your password</h1>
|
|
8
|
+
<p style="font-size:15px; color:#536471; line-height:1.6; margin:0 0 8px;">${greeting}</p>
|
|
9
|
+
<p style="font-size:15px; color:#536471; line-height:1.6; margin:0 0 4px;">We received a request to reset your password for your ${safeAppName} account. Click the button below to choose a new password.</p>
|
|
10
|
+
${renderButton('Reset password', opts.url)}
|
|
11
|
+
<p style="font-size:13px; color:#8b98a5; line-height:1.5; margin:0;">This link expires in 1 hour. If you didn't request a password reset, you can safely ignore this email.</p>
|
|
12
|
+
<p style="font-size:12px; color:#8b98a5; line-height:1.5; margin:16px 0 0; word-break:break-all;">Or copy this link: ${safeUrl}</p>
|
|
13
|
+
`;
|
|
14
|
+
return renderTemplate(opts.appName, content);
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { renderTemplate, renderButton, escapeHtml } from './base.js';
|
|
2
|
+
export function renderVerifyEmail(opts) {
|
|
3
|
+
const greeting = opts.userName ? `Hi ${escapeHtml(opts.userName)},` : 'Hi,';
|
|
4
|
+
const safeAppName = escapeHtml(opts.appName);
|
|
5
|
+
const safeUrl = escapeHtml(opts.url);
|
|
6
|
+
const content = `
|
|
7
|
+
<h1 style="font-size:22px; font-weight:800; color:#0f1419; margin:0 0 16px; line-height:1.3;">Verify your email address</h1>
|
|
8
|
+
<p style="font-size:15px; color:#536471; line-height:1.6; margin:0 0 8px;">${greeting}</p>
|
|
9
|
+
<p style="font-size:15px; color:#536471; line-height:1.6; margin:0 0 4px;">Thanks for signing up for ${safeAppName}. Please verify your email address by clicking the button below.</p>
|
|
10
|
+
${renderButton('Verify email', opts.url)}
|
|
11
|
+
<p style="font-size:13px; color:#8b98a5; line-height:1.5; margin:0;">If you didn't create an account, you can safely ignore this email.</p>
|
|
12
|
+
<p style="font-size:12px; color:#8b98a5; line-height:1.5; margin:16px 0 0; word-break:break-all;">Or copy this link: ${safeUrl}</p>
|
|
13
|
+
`;
|
|
14
|
+
return renderTemplate(opts.appName, content);
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { renderTemplate, renderButton, escapeHtml } from './base.js';
|
|
2
|
+
export function renderWelcome(opts) {
|
|
3
|
+
const greeting = opts.userName ? `Hi ${escapeHtml(opts.userName)},` : 'Hi,';
|
|
4
|
+
const safeAppName = escapeHtml(opts.appName);
|
|
5
|
+
const content = `
|
|
6
|
+
<h1 style="font-size:22px; font-weight:800; color:#0f1419; margin:0 0 16px; line-height:1.3;">Welcome to ${safeAppName}!</h1>
|
|
7
|
+
<p style="font-size:15px; color:#536471; line-height:1.6; margin:0 0 8px;">${greeting}</p>
|
|
8
|
+
<p style="font-size:15px; color:#536471; line-height:1.6; margin:0 0 4px;">Your email has been verified and your account is ready. You can now sign in and start exploring.</p>
|
|
9
|
+
${renderButton('Sign in', opts.loginUrl)}
|
|
10
|
+
<p style="font-size:13px; color:#8b98a5; line-height:1.5; margin:0;">Welcome aboard. We're glad you're here.</p>
|
|
11
|
+
`;
|
|
12
|
+
return renderTemplate(opts.appName, content);
|
|
13
|
+
}
|