@stacksjs/ts-cloud-core 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +321 -0
  3. package/package.json +31 -0
  4. package/src/advanced-features.test.ts +465 -0
  5. package/src/aws/cloudformation.ts +421 -0
  6. package/src/aws/cloudfront.ts +158 -0
  7. package/src/aws/credentials.test.ts +132 -0
  8. package/src/aws/credentials.ts +545 -0
  9. package/src/aws/index.ts +87 -0
  10. package/src/aws/s3.test.ts +188 -0
  11. package/src/aws/s3.ts +1088 -0
  12. package/src/aws/signature.test.ts +670 -0
  13. package/src/aws/signature.ts +1155 -0
  14. package/src/backup/disaster-recovery.test.ts +726 -0
  15. package/src/backup/disaster-recovery.ts +500 -0
  16. package/src/backup/index.ts +34 -0
  17. package/src/backup/manager.test.ts +498 -0
  18. package/src/backup/manager.ts +432 -0
  19. package/src/cicd/circleci.ts +430 -0
  20. package/src/cicd/github-actions.ts +424 -0
  21. package/src/cicd/gitlab-ci.ts +255 -0
  22. package/src/cicd/index.ts +8 -0
  23. package/src/cli/history.ts +396 -0
  24. package/src/cli/index.ts +10 -0
  25. package/src/cli/progress.ts +458 -0
  26. package/src/cli/repl.ts +454 -0
  27. package/src/cli/suggestions.ts +327 -0
  28. package/src/cli/table.test.ts +319 -0
  29. package/src/cli/table.ts +332 -0
  30. package/src/cloudformation/builder.test.ts +327 -0
  31. package/src/cloudformation/builder.ts +378 -0
  32. package/src/cloudformation/builders/api-gateway.ts +449 -0
  33. package/src/cloudformation/builders/cache.ts +334 -0
  34. package/src/cloudformation/builders/cdn.ts +278 -0
  35. package/src/cloudformation/builders/compute.ts +485 -0
  36. package/src/cloudformation/builders/database.ts +392 -0
  37. package/src/cloudformation/builders/functions.ts +343 -0
  38. package/src/cloudformation/builders/messaging.ts +140 -0
  39. package/src/cloudformation/builders/monitoring.ts +300 -0
  40. package/src/cloudformation/builders/network.ts +264 -0
  41. package/src/cloudformation/builders/queue.ts +147 -0
  42. package/src/cloudformation/builders/security.ts +399 -0
  43. package/src/cloudformation/builders/storage.ts +285 -0
  44. package/src/cloudformation/index.ts +30 -0
  45. package/src/cloudformation/types.ts +173 -0
  46. package/src/compliance/aws-config.ts +543 -0
  47. package/src/compliance/cloudtrail.ts +376 -0
  48. package/src/compliance/compliance.test.ts +423 -0
  49. package/src/compliance/guardduty.ts +446 -0
  50. package/src/compliance/index.ts +66 -0
  51. package/src/compliance/security-hub.ts +456 -0
  52. package/src/containers/build-optimization.ts +416 -0
  53. package/src/containers/containers.test.ts +508 -0
  54. package/src/containers/image-scanning.ts +360 -0
  55. package/src/containers/index.ts +9 -0
  56. package/src/containers/registry.ts +293 -0
  57. package/src/containers/service-mesh.ts +520 -0
  58. package/src/database/database.test.ts +762 -0
  59. package/src/database/index.ts +9 -0
  60. package/src/database/migrations.ts +444 -0
  61. package/src/database/performance.ts +528 -0
  62. package/src/database/replicas.ts +534 -0
  63. package/src/database/users.ts +494 -0
  64. package/src/dependency-graph.ts +143 -0
  65. package/src/deployment/ab-testing.ts +582 -0
  66. package/src/deployment/blue-green.ts +452 -0
  67. package/src/deployment/canary.ts +500 -0
  68. package/src/deployment/deployment.test.ts +526 -0
  69. package/src/deployment/index.ts +61 -0
  70. package/src/deployment/progressive.ts +62 -0
  71. package/src/dns/dns.test.ts +641 -0
  72. package/src/dns/dnssec.ts +315 -0
  73. package/src/dns/index.ts +8 -0
  74. package/src/dns/resolver.ts +496 -0
  75. package/src/dns/routing.ts +593 -0
  76. package/src/email/advanced/analytics.ts +445 -0
  77. package/src/email/advanced/index.ts +11 -0
  78. package/src/email/advanced/rules.ts +465 -0
  79. package/src/email/advanced/scheduling.ts +352 -0
  80. package/src/email/advanced/search.ts +412 -0
  81. package/src/email/advanced/shared-mailboxes.ts +404 -0
  82. package/src/email/advanced/templates.ts +455 -0
  83. package/src/email/advanced/threading.ts +281 -0
  84. package/src/email/analytics.ts +467 -0
  85. package/src/email/bounce-handling.ts +425 -0
  86. package/src/email/email.test.ts +431 -0
  87. package/src/email/handlers/__tests__/inbound.test.ts +38 -0
  88. package/src/email/handlers/__tests__/outbound.test.ts +37 -0
  89. package/src/email/handlers/converter.ts +227 -0
  90. package/src/email/handlers/feedback.ts +228 -0
  91. package/src/email/handlers/inbound.ts +169 -0
  92. package/src/email/handlers/outbound.ts +178 -0
  93. package/src/email/index.ts +15 -0
  94. package/src/email/reputation.ts +303 -0
  95. package/src/email/templates.ts +352 -0
  96. package/src/errors/index.test.ts +434 -0
  97. package/src/errors/index.ts +416 -0
  98. package/src/health-checks/index.ts +40 -0
  99. package/src/index.ts +360 -0
  100. package/src/intrinsic-functions.ts +118 -0
  101. package/src/lambda/concurrency.ts +330 -0
  102. package/src/lambda/destinations.ts +345 -0
  103. package/src/lambda/dlq.ts +425 -0
  104. package/src/lambda/index.ts +11 -0
  105. package/src/lambda/lambda.test.ts +840 -0
  106. package/src/lambda/layers.ts +263 -0
  107. package/src/lambda/versions.ts +376 -0
  108. package/src/lambda/vpc.ts +399 -0
  109. package/src/local/config.ts +114 -0
  110. package/src/local/index.ts +6 -0
  111. package/src/local/mock-aws.ts +351 -0
  112. package/src/modules/ai.ts +340 -0
  113. package/src/modules/api.ts +478 -0
  114. package/src/modules/auth.ts +805 -0
  115. package/src/modules/cache.ts +417 -0
  116. package/src/modules/cdn.ts +1062 -0
  117. package/src/modules/communication.ts +1094 -0
  118. package/src/modules/compute.ts +3348 -0
  119. package/src/modules/database.ts +554 -0
  120. package/src/modules/deployment.ts +1079 -0
  121. package/src/modules/dns.ts +337 -0
  122. package/src/modules/email.ts +1538 -0
  123. package/src/modules/filesystem.ts +515 -0
  124. package/src/modules/index.ts +32 -0
  125. package/src/modules/messaging.ts +486 -0
  126. package/src/modules/monitoring.ts +2086 -0
  127. package/src/modules/network.ts +664 -0
  128. package/src/modules/parameter-store.ts +325 -0
  129. package/src/modules/permissions.ts +1081 -0
  130. package/src/modules/phone.ts +494 -0
  131. package/src/modules/queue.ts +1260 -0
  132. package/src/modules/redirects.ts +464 -0
  133. package/src/modules/registry.ts +699 -0
  134. package/src/modules/search.ts +401 -0
  135. package/src/modules/secrets.ts +416 -0
  136. package/src/modules/security.ts +731 -0
  137. package/src/modules/sms.ts +389 -0
  138. package/src/modules/storage.ts +1120 -0
  139. package/src/modules/workflow.ts +680 -0
  140. package/src/multi-account/config.ts +521 -0
  141. package/src/multi-account/index.ts +7 -0
  142. package/src/multi-account/manager.ts +427 -0
  143. package/src/multi-region/cross-region.ts +410 -0
  144. package/src/multi-region/index.ts +8 -0
  145. package/src/multi-region/manager.ts +483 -0
  146. package/src/multi-region/regions.ts +435 -0
  147. package/src/network-security/index.ts +48 -0
  148. package/src/observability/index.ts +9 -0
  149. package/src/observability/logs.ts +522 -0
  150. package/src/observability/metrics.ts +460 -0
  151. package/src/observability/observability.test.ts +782 -0
  152. package/src/observability/synthetics.ts +568 -0
  153. package/src/observability/xray.ts +358 -0
  154. package/src/phone/advanced/analytics.ts +349 -0
  155. package/src/phone/advanced/callbacks.ts +428 -0
  156. package/src/phone/advanced/index.ts +8 -0
  157. package/src/phone/advanced/ivr-builder.ts +504 -0
  158. package/src/phone/advanced/recording.ts +310 -0
  159. package/src/phone/handlers/__tests__/incoming-call.test.ts +40 -0
  160. package/src/phone/handlers/incoming-call.ts +117 -0
  161. package/src/phone/handlers/missed-call.ts +116 -0
  162. package/src/phone/handlers/voicemail.ts +179 -0
  163. package/src/phone/index.ts +9 -0
  164. package/src/presets/api-backend.ts +134 -0
  165. package/src/presets/data-pipeline.ts +204 -0
  166. package/src/presets/extend.test.ts +295 -0
  167. package/src/presets/extend.ts +297 -0
  168. package/src/presets/fullstack-app.ts +144 -0
  169. package/src/presets/index.ts +27 -0
  170. package/src/presets/jamstack.ts +135 -0
  171. package/src/presets/microservices.ts +167 -0
  172. package/src/presets/ml-api.ts +208 -0
  173. package/src/presets/nodejs-server.ts +104 -0
  174. package/src/presets/nodejs-serverless.ts +114 -0
  175. package/src/presets/realtime-app.ts +184 -0
  176. package/src/presets/static-site.ts +64 -0
  177. package/src/presets/traditional-web-app.ts +339 -0
  178. package/src/presets/wordpress.ts +138 -0
  179. package/src/preview/github.test.ts +249 -0
  180. package/src/preview/github.ts +297 -0
  181. package/src/preview/index.ts +37 -0
  182. package/src/preview/manager.test.ts +440 -0
  183. package/src/preview/manager.ts +326 -0
  184. package/src/preview/notifications.test.ts +582 -0
  185. package/src/preview/notifications.ts +341 -0
  186. package/src/queue/batch-processing.ts +402 -0
  187. package/src/queue/dlq-monitoring.ts +402 -0
  188. package/src/queue/fifo.ts +342 -0
  189. package/src/queue/index.ts +9 -0
  190. package/src/queue/management.ts +428 -0
  191. package/src/queue/queue.test.ts +429 -0
  192. package/src/resource-mgmt/index.ts +39 -0
  193. package/src/resource-naming.ts +62 -0
  194. package/src/s3/index.ts +523 -0
  195. package/src/schema/cloud-config.schema.json +554 -0
  196. package/src/schema/index.ts +68 -0
  197. package/src/security/certificate-manager.ts +492 -0
  198. package/src/security/index.ts +9 -0
  199. package/src/security/scanning.ts +545 -0
  200. package/src/security/secrets-manager.ts +476 -0
  201. package/src/security/secrets-rotation.ts +456 -0
  202. package/src/security/security.test.ts +738 -0
  203. package/src/sms/advanced/ab-testing.ts +389 -0
  204. package/src/sms/advanced/analytics.ts +336 -0
  205. package/src/sms/advanced/campaigns.ts +523 -0
  206. package/src/sms/advanced/chatbot.ts +224 -0
  207. package/src/sms/advanced/index.ts +10 -0
  208. package/src/sms/advanced/link-tracking.ts +248 -0
  209. package/src/sms/advanced/mms.ts +308 -0
  210. package/src/sms/handlers/__tests__/send.test.ts +40 -0
  211. package/src/sms/handlers/delivery-status.ts +133 -0
  212. package/src/sms/handlers/receive.ts +162 -0
  213. package/src/sms/handlers/send.ts +174 -0
  214. package/src/sms/index.ts +9 -0
  215. package/src/stack-diff.ts +389 -0
  216. package/src/static-site/index.ts +85 -0
  217. package/src/template-builder.ts +110 -0
  218. package/src/template-validator.ts +574 -0
  219. package/src/utils/cache.ts +291 -0
  220. package/src/utils/diff.ts +269 -0
  221. package/src/utils/hash.ts +227 -0
  222. package/src/utils/index.ts +8 -0
  223. package/src/utils/parallel.ts +294 -0
  224. package/src/validators/credentials.test.ts +274 -0
  225. package/src/validators/credentials.ts +233 -0
  226. package/src/validators/quotas.test.ts +434 -0
  227. package/src/validators/quotas.ts +217 -0
  228. package/test/ai.test.ts +327 -0
  229. package/test/api.test.ts +511 -0
  230. package/test/auth.test.ts +632 -0
  231. package/test/cache.test.ts +406 -0
  232. package/test/cdn.test.ts +247 -0
  233. package/test/compute.test.ts +861 -0
  234. package/test/database.test.ts +523 -0
  235. package/test/deployment.test.ts +499 -0
  236. package/test/dns.test.ts +270 -0
  237. package/test/email.test.ts +439 -0
  238. package/test/filesystem.test.ts +382 -0
  239. package/test/integration.test.ts +350 -0
  240. package/test/messaging.test.ts +514 -0
  241. package/test/monitoring.test.ts +634 -0
  242. package/test/network.test.ts +425 -0
  243. package/test/permissions.test.ts +488 -0
  244. package/test/queue.test.ts +484 -0
  245. package/test/registry.test.ts +306 -0
  246. package/test/security.test.ts +462 -0
  247. package/test/storage.test.ts +463 -0
  248. package/test/template-validator.test.ts +559 -0
  249. package/test/workflow.test.ts +592 -0
  250. package/tsconfig.json +16 -0
  251. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,455 @@
1
+ /**
2
+ * Email Templates with Stacks Views
3
+ *
4
+ * Provides template management and rendering
5
+ */
6
+
7
+ export interface EmailTemplate {
8
+ id: string
9
+ name: string
10
+ subject: string
11
+ html: string
12
+ text?: string
13
+ variables: string[]
14
+ category?: string
15
+ createdAt: string
16
+ updatedAt: string
17
+ version: number
18
+ }
19
+
20
+ export interface TemplateRenderOptions {
21
+ data: Record<string, any>
22
+ locale?: string
23
+ timezone?: string
24
+ }
25
+
26
+ /**
27
+ * Email Templates Module
28
+ */
29
+ export class EmailTemplates {
30
+ /**
31
+ * Lambda code for template rendering
32
+ */
33
+ static TemplateRendererCode = `
34
+ const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
35
+
36
+ const s3 = new S3Client({});
37
+ const TEMPLATE_BUCKET = process.env.TEMPLATE_BUCKET;
38
+
39
+ exports.handler = async (event) => {
40
+ console.log('Template render request:', JSON.stringify(event, null, 2));
41
+
42
+ try {
43
+ const { templateId, data, locale, timezone } = JSON.parse(event.body || '{}');
44
+
45
+ if (!templateId) {
46
+ return {
47
+ statusCode: 400,
48
+ body: JSON.stringify({ error: 'Missing templateId' }),
49
+ };
50
+ }
51
+
52
+ // Get template
53
+ const templateKey = \`templates/\${templateId}.json\`;
54
+ const result = await s3.send(new GetObjectCommand({
55
+ Bucket: TEMPLATE_BUCKET,
56
+ Key: templateKey,
57
+ }));
58
+
59
+ const template = JSON.parse(await result.Body.transformToString());
60
+
61
+ // Render template
62
+ const rendered = {
63
+ subject: renderTemplate(template.subject, data),
64
+ html: renderTemplate(template.html, data),
65
+ text: template.text ? renderTemplate(template.text, data) : null,
66
+ };
67
+
68
+ return {
69
+ statusCode: 200,
70
+ headers: { 'Content-Type': 'application/json' },
71
+ body: JSON.stringify(rendered),
72
+ };
73
+ } catch (error) {
74
+ console.error('Error rendering template:', error);
75
+ return {
76
+ statusCode: error.name === 'NoSuchKey' ? 404 : 500,
77
+ body: JSON.stringify({ error: error.message }),
78
+ };
79
+ }
80
+ };
81
+
82
+ function renderTemplate(template, data) {
83
+ if (!template || !data) return template;
84
+
85
+ let result = template;
86
+
87
+ // Handle {{variable}} syntax
88
+ result = result.replace(/\\{\\{\\s*([\\w.]+)\\s*\\}\\}/g, (match, key) => {
89
+ const value = getNestedValue(data, key);
90
+ return value !== undefined ? String(value) : match;
91
+ });
92
+
93
+ // Handle {{#if condition}}...{{/if}} syntax
94
+ result = result.replace(/\\{\\{#if\\s+([\\w.]+)\\}\\}([\\s\\S]*?)\\{\\{\\/if\\}\\}/g, (match, key, content) => {
95
+ const value = getNestedValue(data, key);
96
+ return value ? content : '';
97
+ });
98
+
99
+ // Handle {{#unless condition}}...{{/unless}} syntax
100
+ result = result.replace(/\\{\\{#unless\\s+([\\w.]+)\\}\\}([\\s\\S]*?)\\{\\{\\/unless\\}\\}/g, (match, key, content) => {
101
+ const value = getNestedValue(data, key);
102
+ return !value ? content : '';
103
+ });
104
+
105
+ // Handle {{#each array}}...{{/each}} syntax
106
+ result = result.replace(/\\{\\{#each\\s+([\\w.]+)\\}\\}([\\s\\S]*?)\\{\\{\\/each\\}\\}/g, (match, key, content) => {
107
+ const array = getNestedValue(data, key);
108
+ if (!Array.isArray(array)) return '';
109
+ return array.map((item, index) => {
110
+ let itemContent = content;
111
+ itemContent = itemContent.replace(/\\{\\{this\\}\\}/g, String(item));
112
+ itemContent = itemContent.replace(/\\{\\{@index\\}\\}/g, String(index));
113
+ if (typeof item === 'object') {
114
+ Object.entries(item).forEach(([k, v]) => {
115
+ itemContent = itemContent.replace(new RegExp(\`\\\\{\\\\{\\\\s*\${k}\\\\s*\\\\}\\\\}\`, 'g'), String(v));
116
+ });
117
+ }
118
+ return itemContent;
119
+ }).join('');
120
+ });
121
+
122
+ return result;
123
+ }
124
+
125
+ function getNestedValue(obj, path) {
126
+ return path.split('.').reduce((current, key) => current?.[key], obj);
127
+ }
128
+ `
129
+
130
+ /**
131
+ * Built-in email templates
132
+ */
133
+ static BuiltInTemplates = {
134
+ welcome: {
135
+ id: 'welcome',
136
+ name: 'Welcome Email',
137
+ subject: 'Welcome to {{appName}}!',
138
+ html: `
139
+ <!DOCTYPE html>
140
+ <html>
141
+ <head>
142
+ <meta charset="utf-8">
143
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
144
+ <title>Welcome</title>
145
+ </head>
146
+ <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
147
+ <div style="text-align: center; margin-bottom: 30px;">
148
+ <h1 style="color: #2563eb;">Welcome to {{appName}}!</h1>
149
+ </div>
150
+
151
+ <p>Hi {{userName}},</p>
152
+
153
+ <p>Thank you for joining {{appName}}! We're excited to have you on board.</p>
154
+
155
+ {{#if verificationUrl}}
156
+ <div style="text-align: center; margin: 30px 0;">
157
+ <a href="{{verificationUrl}}" style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">Verify Your Email</a>
158
+ </div>
159
+ {{/if}}
160
+
161
+ <p>If you have any questions, feel free to reply to this email.</p>
162
+
163
+ <p>Best regards,<br>The {{appName}} Team</p>
164
+
165
+ <hr style="border: none; border-top: 1px solid #eee; margin: 30px 0;">
166
+ <p style="font-size: 12px; color: #666;">
167
+ You received this email because you signed up for {{appName}}.
168
+ </p>
169
+ </body>
170
+ </html>`,
171
+ text: `Welcome to {{appName}}!
172
+
173
+ Hi {{userName}},
174
+
175
+ Thank you for joining {{appName}}! We're excited to have you on board.
176
+
177
+ {{#if verificationUrl}}
178
+ Verify your email: {{verificationUrl}}
179
+ {{/if}}
180
+
181
+ If you have any questions, feel free to reply to this email.
182
+
183
+ Best regards,
184
+ The {{appName}} Team`,
185
+ variables: ['appName', 'userName', 'verificationUrl'] as const,
186
+ category: 'onboarding',
187
+ },
188
+
189
+ passwordReset: {
190
+ id: 'password-reset',
191
+ name: 'Password Reset',
192
+ subject: 'Reset your {{appName}} password',
193
+ html: `
194
+ <!DOCTYPE html>
195
+ <html>
196
+ <head>
197
+ <meta charset="utf-8">
198
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
199
+ </head>
200
+ <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
201
+ <h1 style="color: #2563eb;">Password Reset Request</h1>
202
+
203
+ <p>Hi {{userName}},</p>
204
+
205
+ <p>We received a request to reset your password. Click the button below to create a new password:</p>
206
+
207
+ <div style="text-align: center; margin: 30px 0;">
208
+ <a href="{{resetUrl}}" style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">Reset Password</a>
209
+ </div>
210
+
211
+ <p>This link will expire in {{expiresIn}}.</p>
212
+
213
+ <p>If you didn't request this, you can safely ignore this email.</p>
214
+
215
+ <p>Best regards,<br>The {{appName}} Team</p>
216
+ </body>
217
+ </html>`,
218
+ text: `Password Reset Request
219
+
220
+ Hi {{userName}},
221
+
222
+ We received a request to reset your password. Visit the link below to create a new password:
223
+
224
+ {{resetUrl}}
225
+
226
+ This link will expire in {{expiresIn}}.
227
+
228
+ If you didn't request this, you can safely ignore this email.
229
+
230
+ Best regards,
231
+ The {{appName}} Team`,
232
+ variables: ['appName', 'userName', 'resetUrl', 'expiresIn'] as const,
233
+ category: 'auth',
234
+ },
235
+
236
+ notification: {
237
+ id: 'notification',
238
+ name: 'Notification',
239
+ subject: '{{subject}}',
240
+ html: `
241
+ <!DOCTYPE html>
242
+ <html>
243
+ <head>
244
+ <meta charset="utf-8">
245
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
246
+ </head>
247
+ <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
248
+ <h1 style="color: #2563eb;">{{title}}</h1>
249
+
250
+ <p>{{message}}</p>
251
+
252
+ {{#if actionUrl}}
253
+ <div style="text-align: center; margin: 30px 0;">
254
+ <a href="{{actionUrl}}" style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">{{actionText}}</a>
255
+ </div>
256
+ {{/if}}
257
+
258
+ <p style="font-size: 12px; color: #666; margin-top: 30px;">
259
+ — {{appName}}
260
+ </p>
261
+ </body>
262
+ </html>`,
263
+ text: `{{title}}
264
+
265
+ {{message}}
266
+
267
+ {{#if actionUrl}}
268
+ {{actionText}}: {{actionUrl}}
269
+ {{/if}}
270
+
271
+ — {{appName}}`,
272
+ variables: ['subject', 'title', 'message', 'actionUrl', 'actionText', 'appName'] as const,
273
+ category: 'notification',
274
+ },
275
+
276
+ invoice: {
277
+ id: 'invoice',
278
+ name: 'Invoice',
279
+ subject: 'Invoice #{{invoiceNumber}} from {{appName}}',
280
+ html: `
281
+ <!DOCTYPE html>
282
+ <html>
283
+ <head>
284
+ <meta charset="utf-8">
285
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
286
+ </head>
287
+ <body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
288
+ <h1 style="color: #2563eb;">Invoice #{{invoiceNumber}}</h1>
289
+
290
+ <p>Hi {{customerName}},</p>
291
+
292
+ <p>Here's your invoice for {{period}}.</p>
293
+
294
+ <table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
295
+ <thead>
296
+ <tr style="background-color: #f3f4f6;">
297
+ <th style="padding: 10px; text-align: left; border-bottom: 1px solid #e5e7eb;">Item</th>
298
+ <th style="padding: 10px; text-align: right; border-bottom: 1px solid #e5e7eb;">Amount</th>
299
+ </tr>
300
+ </thead>
301
+ <tbody>
302
+ {{#each items}}
303
+ <tr>
304
+ <td style="padding: 10px; border-bottom: 1px solid #e5e7eb;">{{description}}</td>
305
+ <td style="padding: 10px; text-align: right; border-bottom: 1px solid #e5e7eb;">{{amount}}</td>
306
+ </tr>
307
+ {{/each}}
308
+ </tbody>
309
+ <tfoot>
310
+ <tr>
311
+ <td style="padding: 10px; font-weight: bold;">Total</td>
312
+ <td style="padding: 10px; text-align: right; font-weight: bold;">{{total}}</td>
313
+ </tr>
314
+ </tfoot>
315
+ </table>
316
+
317
+ {{#if paymentUrl}}
318
+ <div style="text-align: center; margin: 30px 0;">
319
+ <a href="{{paymentUrl}}" style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">Pay Now</a>
320
+ </div>
321
+ {{/if}}
322
+
323
+ <p>Thank you for your business!</p>
324
+
325
+ <p>Best regards,<br>The {{appName}} Team</p>
326
+ </body>
327
+ </html>`,
328
+ variables: ['invoiceNumber', 'customerName', 'period', 'items', 'total', 'paymentUrl', 'appName'] as const,
329
+ category: 'billing',
330
+ },
331
+ }
332
+
333
+ /**
334
+ * Create template storage bucket
335
+ */
336
+ static createTemplateBucket(config: { slug: string }): Record<string, any> {
337
+ return {
338
+ [`${config.slug}EmailTemplateBucket`]: {
339
+ Type: 'AWS::S3::Bucket',
340
+ Properties: {
341
+ BucketName: `${config.slug}-email-templates`,
342
+ VersioningConfiguration: {
343
+ Status: 'Enabled',
344
+ },
345
+ },
346
+ },
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Create template renderer Lambda
352
+ */
353
+ static createTemplateRendererLambda(config: {
354
+ slug: string
355
+ roleArn: string
356
+ templateBucket: string
357
+ }): Record<string, any> {
358
+ return {
359
+ [`${config.slug}TemplateRendererLambda`]: {
360
+ Type: 'AWS::Lambda::Function',
361
+ Properties: {
362
+ FunctionName: `${config.slug}-template-renderer`,
363
+ Runtime: 'nodejs20.x',
364
+ Handler: 'index.handler',
365
+ Role: config.roleArn,
366
+ Timeout: 30,
367
+ MemorySize: 256,
368
+ Code: {
369
+ ZipFile: EmailTemplates.TemplateRendererCode,
370
+ },
371
+ Environment: {
372
+ Variables: {
373
+ TEMPLATE_BUCKET: config.templateBucket,
374
+ },
375
+ },
376
+ },
377
+ },
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Render a template with data (SDK helper)
383
+ */
384
+ static render(template: string, data: Record<string, any>): string {
385
+ if (!template || !data) return template
386
+
387
+ let result = template
388
+
389
+ // Handle {{variable}} syntax
390
+ result = result.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (match, key) => {
391
+ const value = EmailTemplates.getNestedValue(data, key)
392
+ return value !== undefined ? String(value) : match
393
+ })
394
+
395
+ // Handle {{#if condition}}...{{/if}} syntax
396
+ result = result.replace(/\{\{#if\s+([\w.]+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (match, key, content) => {
397
+ const value = EmailTemplates.getNestedValue(data, key)
398
+ return value ? content : ''
399
+ })
400
+
401
+ // Handle {{#unless condition}}...{{/unless}} syntax
402
+ result = result.replace(/\{\{#unless\s+([\w.]+)\}\}([\s\S]*?)\{\{\/unless\}\}/g, (match, key, content) => {
403
+ const value = EmailTemplates.getNestedValue(data, key)
404
+ return !value ? content : ''
405
+ })
406
+
407
+ // Handle {{#each array}}...{{/each}} syntax
408
+ result = result.replace(/\{\{#each\s+([\w.]+)\}\}([\s\S]*?)\{\{\/each\}\}/g, (match, key, content) => {
409
+ const array = EmailTemplates.getNestedValue(data, key)
410
+ if (!Array.isArray(array)) return ''
411
+ return array.map((item, index) => {
412
+ let itemContent = content
413
+ itemContent = itemContent.replace(/\{\{this\}\}/g, String(item))
414
+ itemContent = itemContent.replace(/\{\{@index\}\}/g, String(index))
415
+ if (typeof item === 'object') {
416
+ Object.entries(item).forEach(([k, v]) => {
417
+ itemContent = itemContent.replace(new RegExp(`\\{\\{\\s*${k}\\s*\\}\\}`, 'g'), String(v))
418
+ })
419
+ }
420
+ return itemContent
421
+ }).join('')
422
+ })
423
+
424
+ return result
425
+ }
426
+
427
+ private static getNestedValue(obj: Record<string, any>, path: string): any {
428
+ return path.split('.').reduce((current, key) => current?.[key], obj)
429
+ }
430
+
431
+ /**
432
+ * Extract variables from template
433
+ */
434
+ static extractVariables(template: string): string[] {
435
+ const variables = new Set<string>()
436
+
437
+ // Match {{variable}}
438
+ const matches = template.matchAll(/\{\{\s*([\w.]+)\s*\}\}/g)
439
+ for (const match of matches) {
440
+ if (!match[1].startsWith('#') && !match[1].startsWith('/') && !match[1].startsWith('@')) {
441
+ variables.add(match[1].split('.')[0])
442
+ }
443
+ }
444
+
445
+ // Match {{#if variable}}
446
+ const ifMatches = template.matchAll(/\{\{#(?:if|unless|each)\s+([\w.]+)\}\}/g)
447
+ for (const match of ifMatches) {
448
+ variables.add(match[1].split('.')[0])
449
+ }
450
+
451
+ return Array.from(variables)
452
+ }
453
+ }
454
+
455
+ export default EmailTemplates