@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.
Files changed (306) hide show
  1. package/README.md +62 -282
  2. package/dist/auth/config.d.ts +23 -0
  3. package/dist/auth/config.js +115 -0
  4. package/dist/auth/guard.d.ts +12 -0
  5. package/dist/auth/guard.js +28 -0
  6. package/dist/auth/index.d.ts +3 -0
  7. package/dist/auth/index.js +1 -0
  8. package/dist/auth/middleware.d.ts +23 -0
  9. package/dist/auth/middleware.js +89 -0
  10. package/dist/auth/native-auth.d.ts +82 -0
  11. package/dist/auth/native-auth.js +340 -0
  12. package/dist/auth/oidc-client.d.ts +17 -0
  13. package/dist/auth/oidc-client.js +123 -0
  14. package/dist/auth/providers/google.d.ts +23 -0
  15. package/dist/auth/providers/google.js +25 -0
  16. package/dist/auth/providers/index.d.ts +2 -0
  17. package/dist/auth/providers/index.js +1 -0
  18. package/dist/auth/routes/login.d.ts +8 -0
  19. package/dist/auth/routes/login.js +121 -0
  20. package/dist/auth/routes/logout.d.ts +4 -0
  21. package/dist/auth/routes/logout.js +79 -0
  22. package/dist/auth/routes/oidc-callback.d.ts +3 -0
  23. package/dist/auth/routes/oidc-callback.js +70 -0
  24. package/dist/auth/routes/password.d.ts +5 -0
  25. package/dist/auth/routes/password.js +149 -0
  26. package/dist/auth/routes/signup.d.ts +3 -0
  27. package/dist/auth/routes/signup.js +81 -0
  28. package/dist/auth/routes/token.d.ts +4 -0
  29. package/dist/auth/routes/token.js +70 -0
  30. package/dist/auth/routes/totp.d.ts +22 -0
  31. package/dist/auth/routes/totp.js +232 -0
  32. package/dist/auth/routes/utils.d.ts +7 -0
  33. package/dist/auth/routes/utils.js +35 -0
  34. package/dist/auth/routes/verify.d.ts +3 -0
  35. package/dist/auth/routes/verify.js +26 -0
  36. package/dist/auth/routes.d.ts +8 -0
  37. package/dist/auth/routes.js +124 -0
  38. package/dist/auth/session.d.ts +8 -0
  39. package/dist/auth/session.js +54 -0
  40. package/dist/auth/token.d.ts +33 -0
  41. package/dist/auth/token.js +90 -0
  42. package/dist/auth/types.d.ts +156 -0
  43. package/dist/auth/types.js +2 -0
  44. package/dist/build/build-client.d.ts +15 -0
  45. package/dist/build/build-client.js +45 -0
  46. package/dist/build/build-prerender.d.ts +11 -0
  47. package/dist/build/build-prerender.js +159 -0
  48. package/dist/build/build-server.d.ts +18 -0
  49. package/dist/build/build-server.js +107 -0
  50. package/dist/build/build.js +60 -123
  51. package/dist/build/scan.d.ts +18 -0
  52. package/dist/build/scan.js +77 -6
  53. package/dist/build/serve-api.js +8 -2
  54. package/dist/build/serve-loaders.d.ts +4 -4
  55. package/dist/build/serve-loaders.js +26 -18
  56. package/dist/build/serve-ssr.js +38 -11
  57. package/dist/build/serve-static.js +3 -3
  58. package/dist/build/serve.js +341 -18
  59. package/dist/cli.js +37 -6
  60. package/dist/communication/encryption.d.ts +35 -0
  61. package/dist/communication/encryption.js +90 -0
  62. package/dist/communication/handlers/context.d.ts +27 -0
  63. package/dist/communication/handlers/context.js +1 -0
  64. package/dist/communication/handlers/conversation.d.ts +24 -0
  65. package/dist/communication/handlers/conversation.js +113 -0
  66. package/dist/communication/handlers/file-upload.d.ts +17 -0
  67. package/dist/communication/handlers/file-upload.js +62 -0
  68. package/dist/communication/handlers/messaging.d.ts +30 -0
  69. package/dist/communication/handlers/messaging.js +237 -0
  70. package/dist/communication/handlers/presence.d.ts +15 -0
  71. package/dist/communication/handlers/presence.js +76 -0
  72. package/dist/communication/handlers.d.ts +5 -0
  73. package/dist/communication/handlers.js +5 -0
  74. package/dist/communication/index.d.ts +9 -0
  75. package/dist/communication/index.js +7 -0
  76. package/dist/communication/link-preview.d.ts +18 -0
  77. package/dist/communication/link-preview.js +115 -0
  78. package/dist/communication/schema.d.ts +10 -0
  79. package/dist/communication/schema.js +101 -0
  80. package/dist/communication/server.d.ts +86 -0
  81. package/dist/communication/server.js +212 -0
  82. package/dist/communication/signaling.d.ts +43 -0
  83. package/dist/communication/signaling.js +271 -0
  84. package/dist/communication/store.d.ts +71 -0
  85. package/dist/communication/store.js +289 -0
  86. package/dist/communication/types.d.ts +454 -0
  87. package/dist/communication/types.js +1 -0
  88. package/dist/create.d.ts +1 -0
  89. package/dist/create.js +55 -0
  90. package/dist/db/auto-migrate.d.ts +3 -0
  91. package/dist/db/auto-migrate.js +100 -0
  92. package/dist/db/client.d.ts +3 -0
  93. package/dist/db/client.js +18 -0
  94. package/dist/db/index.d.ts +17 -13
  95. package/dist/db/index.js +205 -26
  96. package/dist/db/seed.d.ts +12 -0
  97. package/dist/db/seed.js +88 -0
  98. package/dist/db/table.d.ts +10 -0
  99. package/dist/db/table.js +12 -0
  100. package/dist/dev-server/config.d.ts +11 -0
  101. package/dist/dev-server/config.js +40 -20
  102. package/dist/dev-server/index-html.d.ts +4 -0
  103. package/dist/dev-server/index-html.js +21 -6
  104. package/dist/dev-server/nuralyui-aliases.d.ts +0 -4
  105. package/dist/dev-server/nuralyui-aliases.js +115 -94
  106. package/dist/dev-server/plugins/vite-plugin-api-routes.js +29 -5
  107. package/dist/dev-server/plugins/vite-plugin-auth.d.ts +6 -0
  108. package/dist/dev-server/plugins/vite-plugin-auth.js +223 -0
  109. package/dist/dev-server/plugins/vite-plugin-auto-define.d.ts +16 -0
  110. package/dist/dev-server/plugins/vite-plugin-auto-define.js +111 -0
  111. package/dist/dev-server/plugins/vite-plugin-communication.d.ts +6 -0
  112. package/dist/dev-server/plugins/vite-plugin-communication.js +205 -0
  113. package/dist/dev-server/plugins/vite-plugin-editor-api.d.ts +6 -0
  114. package/dist/dev-server/plugins/vite-plugin-editor-api.js +318 -0
  115. package/dist/dev-server/plugins/vite-plugin-i18n.js +69 -2
  116. package/dist/dev-server/plugins/vite-plugin-lit-dedup.d.ts +6 -0
  117. package/dist/dev-server/plugins/vite-plugin-lit-dedup.js +78 -34
  118. package/dist/dev-server/plugins/vite-plugin-lit-hmr.js +44 -2
  119. package/dist/dev-server/plugins/vite-plugin-llms.d.ts +2 -0
  120. package/dist/dev-server/plugins/vite-plugin-llms.js +92 -0
  121. package/dist/dev-server/plugins/vite-plugin-loaders.js +146 -13
  122. package/dist/dev-server/plugins/vite-plugin-routes.js +16 -5
  123. package/dist/dev-server/plugins/vite-plugin-socketio.d.ts +2 -0
  124. package/dist/dev-server/plugins/vite-plugin-socketio.js +51 -0
  125. package/dist/dev-server/plugins/vite-plugin-source-annotator.d.ts +2 -0
  126. package/dist/dev-server/plugins/vite-plugin-source-annotator.js +26 -3
  127. package/dist/dev-server/plugins/vite-plugin-storage.d.ts +10 -0
  128. package/dist/dev-server/plugins/vite-plugin-storage.js +126 -0
  129. package/dist/dev-server/plugins/vite-plugin-virtual-modules.js +140 -3
  130. package/dist/dev-server/server.js +242 -70
  131. package/dist/dev-server/ssr-render.d.ts +2 -1
  132. package/dist/dev-server/ssr-render.js +117 -50
  133. package/dist/editor/ai/backend.d.ts +20 -0
  134. package/dist/editor/ai/backend.js +113 -0
  135. package/dist/editor/ai/claude-code-client.d.ts +20 -0
  136. package/dist/editor/ai/claude-code-client.js +145 -0
  137. package/dist/editor/ai/deepseek-client.d.ts +7 -0
  138. package/dist/editor/ai/deepseek-client.js +113 -0
  139. package/dist/editor/ai/opencode-client.d.ts +14 -0
  140. package/dist/editor/ai/opencode-client.js +99 -0
  141. package/dist/editor/ai/snapshot-store.d.ts +22 -0
  142. package/dist/editor/ai/snapshot-store.js +35 -0
  143. package/dist/editor/ai/types.d.ts +30 -0
  144. package/dist/editor/ai/types.js +136 -0
  145. package/dist/editor/ai-chat-panel.d.ts +13 -0
  146. package/dist/editor/ai-chat-panel.js +613 -0
  147. package/dist/editor/ai-markdown.d.ts +10 -0
  148. package/dist/editor/ai-markdown.js +70 -0
  149. package/dist/editor/ai-project-panel.d.ts +11 -0
  150. package/dist/editor/ai-project-panel.js +332 -0
  151. package/dist/editor/ast-modification.d.ts +11 -0
  152. package/dist/editor/ast-modification.js +1 -0
  153. package/dist/editor/ast-service.d.ts +30 -0
  154. package/dist/editor/ast-service.js +180 -0
  155. package/dist/editor/css-rules.d.ts +54 -0
  156. package/dist/editor/css-rules.js +423 -0
  157. package/dist/editor/editor-api-client.d.ts +51 -0
  158. package/dist/editor/editor-api-client.js +162 -0
  159. package/dist/editor/editor-bridge.d.ts +1 -0
  160. package/dist/editor/editor-bridge.js +18 -8
  161. package/dist/editor/editor-toolbar.d.ts +14 -0
  162. package/dist/editor/editor-toolbar.js +115 -0
  163. package/dist/editor/file-editor.d.ts +9 -0
  164. package/dist/editor/file-editor.js +236 -0
  165. package/dist/editor/file-service.d.ts +16 -0
  166. package/dist/editor/file-service.js +52 -0
  167. package/dist/editor/i18n-key-gen.d.ts +1 -0
  168. package/dist/editor/i18n-key-gen.js +7 -0
  169. package/dist/editor/inline-text-edit.d.ts +5 -0
  170. package/dist/editor/inline-text-edit.js +173 -92
  171. package/dist/editor/overlay-events.d.ts +5 -0
  172. package/dist/editor/overlay-events.js +364 -0
  173. package/dist/editor/overlay-hmr.d.ts +2 -0
  174. package/dist/editor/overlay-hmr.js +76 -0
  175. package/dist/editor/overlay-selection.d.ts +29 -0
  176. package/dist/editor/overlay-selection.js +148 -0
  177. package/dist/editor/overlay-utils.d.ts +12 -0
  178. package/dist/editor/overlay-utils.js +59 -0
  179. package/dist/editor/properties-panel-persist.d.ts +14 -0
  180. package/dist/editor/properties-panel-persist.js +70 -0
  181. package/dist/editor/properties-panel-rows.d.ts +10 -0
  182. package/dist/editor/properties-panel-rows.js +349 -0
  183. package/dist/editor/properties-panel-styles.d.ts +4 -0
  184. package/dist/editor/properties-panel-styles.js +174 -0
  185. package/dist/editor/properties-panel.d.ts +4 -0
  186. package/dist/editor/properties-panel.js +148 -0
  187. package/dist/editor/property-registry.d.ts +16 -0
  188. package/dist/editor/property-registry.js +303 -0
  189. package/dist/editor/standalone-file-panel.d.ts +0 -0
  190. package/dist/editor/standalone-file-panel.js +1 -0
  191. package/dist/editor/standalone-overlay-dom.d.ts +0 -0
  192. package/dist/editor/standalone-overlay-dom.js +1 -0
  193. package/dist/editor/standalone-overlay-styles.d.ts +0 -0
  194. package/dist/editor/standalone-overlay-styles.js +1 -0
  195. package/dist/editor/standalone-overlay.d.ts +1 -0
  196. package/dist/editor/standalone-overlay.js +76 -0
  197. package/dist/editor/syntax-highlighter.d.ts +4 -0
  198. package/dist/editor/syntax-highlighter.js +81 -0
  199. package/dist/editor/text-toolbar.d.ts +11 -0
  200. package/dist/editor/text-toolbar.js +327 -0
  201. package/dist/editor/toolbar-styles.d.ts +4 -0
  202. package/dist/editor/toolbar-styles.js +198 -0
  203. package/dist/email/index.d.ts +32 -0
  204. package/dist/email/index.js +154 -0
  205. package/dist/email/providers/resend.d.ts +2 -0
  206. package/dist/email/providers/resend.js +24 -0
  207. package/dist/email/providers/sendgrid.d.ts +2 -0
  208. package/dist/email/providers/sendgrid.js +31 -0
  209. package/dist/email/providers/smtp.d.ts +13 -0
  210. package/dist/email/providers/smtp.js +125 -0
  211. package/dist/email/template-engine.d.ts +18 -0
  212. package/dist/email/template-engine.js +116 -0
  213. package/dist/email/templates/base.d.ts +9 -0
  214. package/dist/email/templates/base.js +65 -0
  215. package/dist/email/templates/password-reset.d.ts +5 -0
  216. package/dist/email/templates/password-reset.js +15 -0
  217. package/dist/email/templates/verify-email.d.ts +5 -0
  218. package/dist/email/templates/verify-email.js +15 -0
  219. package/dist/email/templates/welcome.d.ts +5 -0
  220. package/dist/email/templates/welcome.js +13 -0
  221. package/dist/email/types.d.ts +49 -0
  222. package/dist/email/types.js +1 -0
  223. package/dist/llms/generate.d.ts +46 -0
  224. package/dist/llms/generate.js +185 -0
  225. package/dist/permissions/guard.d.ts +28 -0
  226. package/dist/permissions/guard.js +30 -0
  227. package/dist/permissions/index.d.ts +6 -0
  228. package/dist/permissions/index.js +3 -0
  229. package/dist/permissions/service.d.ts +80 -0
  230. package/dist/permissions/service.js +210 -0
  231. package/dist/permissions/tables.d.ts +5 -0
  232. package/dist/permissions/tables.js +68 -0
  233. package/dist/permissions/types.d.ts +33 -0
  234. package/dist/permissions/types.js +1 -0
  235. package/dist/runtime/app-shell.d.ts +1 -1
  236. package/dist/runtime/app-shell.js +164 -0
  237. package/dist/runtime/auth.d.ts +10 -0
  238. package/dist/runtime/auth.js +30 -0
  239. package/dist/runtime/communication.d.ts +137 -0
  240. package/dist/runtime/communication.js +228 -0
  241. package/dist/runtime/error-boundary.d.ts +23 -0
  242. package/dist/runtime/error-boundary.js +120 -0
  243. package/dist/runtime/i18n.d.ts +6 -1
  244. package/dist/runtime/i18n.js +42 -21
  245. package/dist/runtime/island.d.ts +16 -0
  246. package/dist/runtime/island.js +80 -0
  247. package/dist/runtime/router-data.d.ts +3 -0
  248. package/dist/runtime/router-data.js +102 -17
  249. package/dist/runtime/router-hydration.js +34 -2
  250. package/dist/runtime/router.d.ts +19 -2
  251. package/dist/runtime/router.js +237 -43
  252. package/dist/runtime/socket-client.d.ts +2 -0
  253. package/dist/runtime/socket-client.js +30 -0
  254. package/dist/runtime/webrtc.d.ts +91 -0
  255. package/dist/runtime/webrtc.js +428 -0
  256. package/dist/shared/dom-shims.js +4 -2
  257. package/dist/shared/graceful-shutdown.d.ts +8 -0
  258. package/dist/shared/graceful-shutdown.js +36 -0
  259. package/dist/shared/health.d.ts +8 -0
  260. package/dist/shared/health.js +25 -0
  261. package/dist/shared/llms-txt.d.ts +31 -0
  262. package/dist/shared/llms-txt.js +85 -0
  263. package/dist/shared/logger.d.ts +32 -0
  264. package/dist/shared/logger.js +93 -0
  265. package/dist/shared/meta.d.ts +27 -0
  266. package/dist/shared/meta.js +71 -0
  267. package/dist/shared/middleware-runner.d.ts +9 -0
  268. package/dist/shared/middleware-runner.js +29 -0
  269. package/dist/shared/rate-limit.d.ts +18 -0
  270. package/dist/shared/rate-limit.js +71 -0
  271. package/dist/shared/request-id.d.ts +5 -0
  272. package/dist/shared/request-id.js +18 -0
  273. package/dist/shared/route-matching.js +16 -1
  274. package/dist/shared/security-headers.d.ts +18 -0
  275. package/dist/shared/security-headers.js +38 -0
  276. package/dist/shared/socket-io-setup.d.ts +11 -0
  277. package/dist/shared/socket-io-setup.js +51 -0
  278. package/dist/shared/types.d.ts +15 -0
  279. package/dist/shared/utils.d.ts +33 -7
  280. package/dist/shared/utils.js +164 -27
  281. package/dist/storage/adapters/local.d.ts +44 -0
  282. package/dist/storage/adapters/local.js +85 -0
  283. package/dist/storage/adapters/s3.d.ts +32 -0
  284. package/dist/storage/adapters/s3.js +119 -0
  285. package/dist/storage/adapters/types.d.ts +53 -0
  286. package/dist/storage/adapters/types.js +1 -0
  287. package/dist/storage/index.d.ts +76 -0
  288. package/dist/storage/index.js +83 -0
  289. package/package.json +45 -7
  290. package/templates/blog/api/posts.ts +4 -18
  291. package/templates/blog/data/migrations/001_init.sql +6 -5
  292. package/templates/blog/lumenjs.config.ts +3 -0
  293. package/templates/blog/package.json +14 -0
  294. package/templates/blog/pages/_layout.ts +25 -0
  295. package/templates/blog/pages/index.ts +48 -22
  296. package/templates/blog/pages/posts/[slug].ts +45 -20
  297. package/templates/blog/pages/tag/[tag].ts +44 -0
  298. package/templates/dashboard/api/stats.ts +8 -5
  299. package/templates/dashboard/lumenjs.config.ts +3 -0
  300. package/templates/dashboard/package.json +14 -0
  301. package/templates/dashboard/pages/_layout.ts +25 -0
  302. package/templates/dashboard/pages/index.ts +54 -23
  303. package/templates/dashboard/pages/settings/index.ts +29 -0
  304. package/templates/default/lumenjs.config.ts +3 -0
  305. package/templates/default/package.json +14 -0
  306. 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(/&nbsp;/g, ' ')
98
+ .replace(/&amp;/g, '&')
99
+ .replace(/&lt;/g, '<')
100
+ .replace(/&gt;/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,2 @@
1
+ import type { EmailProvider } from '../types.js';
2
+ export declare function createResendProvider(apiKey: string): EmailProvider;
@@ -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,2 @@
1
+ import type { EmailProvider } from '../types.js';
2
+ export declare function createSendGridProvider(apiKey: string): EmailProvider;
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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,5 @@
1
+ export declare function renderPasswordReset(opts: {
2
+ appName: string;
3
+ url: string;
4
+ userName?: string;
5
+ }): string;
@@ -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,5 @@
1
+ export declare function renderVerifyEmail(opts: {
2
+ appName: string;
3
+ url: string;
4
+ userName?: string;
5
+ }): string;
@@ -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,5 @@
1
+ export declare function renderWelcome(opts: {
2
+ appName: string;
3
+ userName?: string;
4
+ loginUrl: string;
5
+ }): string;
@@ -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
+ }