digital-workers 2.1.3 → 2.3.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 (183) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +2 -0
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +33 -21
  5. package/dist/actions.js.map +1 -1
  6. package/dist/agent-comms.d.ts.map +1 -1
  7. package/dist/agent-comms.js +36 -25
  8. package/dist/agent-comms.js.map +1 -1
  9. package/dist/approve.d.ts +40 -8
  10. package/dist/approve.d.ts.map +1 -1
  11. package/dist/approve.js +86 -20
  12. package/dist/approve.js.map +1 -1
  13. package/dist/ask.d.ts +38 -7
  14. package/dist/ask.d.ts.map +1 -1
  15. package/dist/ask.js +85 -25
  16. package/dist/ask.js.map +1 -1
  17. package/dist/browse.d.ts +223 -0
  18. package/dist/browse.d.ts.map +1 -0
  19. package/dist/browse.js +392 -0
  20. package/dist/browse.js.map +1 -0
  21. package/dist/capability-tiers.js +3 -3
  22. package/dist/capability-tiers.js.map +1 -1
  23. package/dist/cascade-context.d.ts +28 -28
  24. package/dist/client.d.ts +162 -0
  25. package/dist/client.d.ts.map +1 -0
  26. package/dist/client.js +64 -0
  27. package/dist/client.js.map +1 -0
  28. package/dist/decide.d.ts +42 -6
  29. package/dist/decide.d.ts.map +1 -1
  30. package/dist/decide.js +54 -11
  31. package/dist/decide.js.map +1 -1
  32. package/dist/do.d.ts +36 -7
  33. package/dist/do.d.ts.map +1 -1
  34. package/dist/do.js +82 -39
  35. package/dist/do.js.map +1 -1
  36. package/dist/error-escalation.d.ts.map +1 -1
  37. package/dist/error-escalation.js +38 -38
  38. package/dist/error-escalation.js.map +1 -1
  39. package/dist/generate.d.ts +48 -7
  40. package/dist/generate.d.ts.map +1 -1
  41. package/dist/generate.js +49 -8
  42. package/dist/generate.js.map +1 -1
  43. package/dist/goals.d.ts +10 -9
  44. package/dist/goals.d.ts.map +1 -1
  45. package/dist/goals.js +30 -24
  46. package/dist/goals.js.map +1 -1
  47. package/dist/image.d.ts +189 -0
  48. package/dist/image.d.ts.map +1 -0
  49. package/dist/image.js +528 -0
  50. package/dist/image.js.map +1 -0
  51. package/dist/index.d.ts +49 -2
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +58 -2
  54. package/dist/index.js.map +1 -1
  55. package/dist/is.d.ts +45 -10
  56. package/dist/is.d.ts.map +1 -1
  57. package/dist/is.js +56 -21
  58. package/dist/is.js.map +1 -1
  59. package/dist/kpis.d.ts +24 -15
  60. package/dist/kpis.d.ts.map +1 -1
  61. package/dist/kpis.js +16 -14
  62. package/dist/kpis.js.map +1 -1
  63. package/dist/load-balancing.d.ts.map +1 -1
  64. package/dist/load-balancing.js +124 -38
  65. package/dist/load-balancing.js.map +1 -1
  66. package/dist/logger.d.ts +76 -0
  67. package/dist/logger.d.ts.map +1 -0
  68. package/dist/logger.js +39 -0
  69. package/dist/logger.js.map +1 -0
  70. package/dist/notify.d.ts +38 -9
  71. package/dist/notify.d.ts.map +1 -1
  72. package/dist/notify.js +72 -17
  73. package/dist/notify.js.map +1 -1
  74. package/dist/role.d.ts +5 -4
  75. package/dist/role.d.ts.map +1 -1
  76. package/dist/role.js +13 -10
  77. package/dist/role.js.map +1 -1
  78. package/dist/runtime.d.ts +310 -0
  79. package/dist/runtime.d.ts.map +1 -0
  80. package/dist/runtime.js +510 -0
  81. package/dist/runtime.js.map +1 -0
  82. package/dist/team.d.ts +11 -6
  83. package/dist/team.d.ts.map +1 -1
  84. package/dist/team.js +22 -15
  85. package/dist/team.js.map +1 -1
  86. package/dist/transports/email.d.ts +318 -0
  87. package/dist/transports/email.d.ts.map +1 -0
  88. package/dist/transports/email.js +779 -0
  89. package/dist/transports/email.js.map +1 -0
  90. package/dist/transports/slack.d.ts +515 -0
  91. package/dist/transports/slack.d.ts.map +1 -0
  92. package/dist/transports/slack.js +844 -0
  93. package/dist/transports/slack.js.map +1 -0
  94. package/dist/transports.d.ts.map +1 -1
  95. package/dist/transports.js +44 -25
  96. package/dist/transports.js.map +1 -1
  97. package/dist/types.d.ts +141 -19
  98. package/dist/types.d.ts.map +1 -1
  99. package/dist/types.js +5 -0
  100. package/dist/types.js.map +1 -1
  101. package/dist/utils/id.d.ts +19 -0
  102. package/dist/utils/id.d.ts.map +1 -0
  103. package/dist/utils/id.js +21 -0
  104. package/dist/utils/id.js.map +1 -0
  105. package/dist/video.d.ts +203 -0
  106. package/dist/video.d.ts.map +1 -0
  107. package/dist/video.js +528 -0
  108. package/dist/video.js.map +1 -0
  109. package/dist/worker.d.ts +343 -0
  110. package/dist/worker.d.ts.map +1 -0
  111. package/dist/worker.js +698 -0
  112. package/dist/worker.js.map +1 -0
  113. package/package.json +32 -14
  114. package/src/actions.ts +39 -30
  115. package/src/agent-comms.ts +54 -92
  116. package/src/approve.ts +91 -20
  117. package/src/ask.ts +99 -25
  118. package/src/browse.ts +627 -0
  119. package/src/capability-tiers.ts +5 -5
  120. package/src/client.ts +221 -0
  121. package/src/decide.ts +81 -35
  122. package/src/do.ts +98 -52
  123. package/src/error-escalation.ts +55 -67
  124. package/src/generate.ts +52 -18
  125. package/src/goals.ts +36 -27
  126. package/src/image.ts +816 -0
  127. package/src/index.ts +187 -2
  128. package/src/is.ts +59 -25
  129. package/src/kpis.ts +41 -36
  130. package/src/load-balancing.ts +132 -46
  131. package/src/logger.ts +93 -0
  132. package/src/notify.ts +78 -17
  133. package/src/role.ts +30 -20
  134. package/src/runtime.ts +796 -0
  135. package/src/team.ts +24 -19
  136. package/src/transports/email.ts +1160 -0
  137. package/src/transports/slack.ts +1320 -0
  138. package/src/transports.ts +58 -43
  139. package/src/types.ts +174 -46
  140. package/src/utils/id.ts +21 -0
  141. package/src/video.ts +906 -0
  142. package/src/worker.ts +1007 -0
  143. package/test/approve.test.ts +305 -0
  144. package/test/ask.test.ts +274 -0
  145. package/test/browse.test.ts +361 -0
  146. package/test/decide.test.ts +252 -0
  147. package/test/do.test.ts +144 -0
  148. package/test/error-logging.test.ts +357 -0
  149. package/test/generate.test.ts +319 -0
  150. package/test/image.test.ts +398 -0
  151. package/test/is.test.ts +287 -0
  152. package/test/load-balancing-safety.test.ts +404 -0
  153. package/test/notify.test.ts +434 -0
  154. package/test/primitives.test.ts +320 -0
  155. package/test/runtime-integration.test.ts +892 -0
  156. package/test/transports/crypto.test.ts +230 -0
  157. package/test/transports/email.test.ts +866 -0
  158. package/test/transports/id-generation.test.ts +91 -0
  159. package/test/transports/slack.test.ts +760 -0
  160. package/test/type-safety.test.ts +834 -0
  161. package/test/types.test.ts +60 -2
  162. package/test/video.test.ts +530 -0
  163. package/test/worker.test.ts +1433 -0
  164. package/tsconfig.json +4 -1
  165. package/vitest.config.ts +42 -0
  166. package/wrangler.jsonc +36 -0
  167. package/.turbo/turbo-build.log +0 -4
  168. package/LICENSE +0 -21
  169. package/src/actions.js +0 -436
  170. package/src/approve.js +0 -234
  171. package/src/ask.js +0 -226
  172. package/src/decide.js +0 -244
  173. package/src/do.js +0 -227
  174. package/src/generate.js +0 -298
  175. package/src/goals.js +0 -205
  176. package/src/index.js +0 -68
  177. package/src/is.js +0 -317
  178. package/src/kpis.js +0 -270
  179. package/src/notify.js +0 -219
  180. package/src/role.js +0 -110
  181. package/src/team.js +0 -130
  182. package/src/transports.js +0 -357
  183. package/src/types.js +0 -71
@@ -0,0 +1,779 @@
1
+ /**
2
+ * Email Transport Adapter for digital-workers
3
+ *
4
+ * Implements the transport interface for sending notifications, approval requests,
5
+ * and handling email replies. Designed primarily for Resend but with a
6
+ * provider-agnostic interface.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ import { registerTransport } from '../transports.js';
11
+ import { generateRequestId } from '../utils/id.js';
12
+ // =============================================================================
13
+ // Resend Provider Implementation
14
+ // =============================================================================
15
+ /**
16
+ * Resend email provider
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * const resend = createResendProvider({ apiKey: 'your-api-key' })
21
+ * const transport = new EmailTransport({ provider: resend })
22
+ * ```
23
+ */
24
+ export function createResendProvider(config) {
25
+ const apiUrl = config.apiUrl || 'https://api.resend.com';
26
+ return {
27
+ name: 'resend',
28
+ async send(message) {
29
+ try {
30
+ const response = await fetch(`${apiUrl}/emails`, {
31
+ method: 'POST',
32
+ headers: {
33
+ Authorization: `Bearer ${config.apiKey}`,
34
+ 'Content-Type': 'application/json',
35
+ },
36
+ body: JSON.stringify({
37
+ from: message.from,
38
+ to: Array.isArray(message.to) ? message.to : [message.to],
39
+ subject: message.subject,
40
+ text: message.text,
41
+ html: message.html,
42
+ reply_to: message.replyTo,
43
+ headers: message.headers,
44
+ tags: message.tags,
45
+ }),
46
+ });
47
+ if (!response.ok) {
48
+ const error = await response.json().catch(() => ({ message: response.statusText }));
49
+ return {
50
+ success: false,
51
+ error: error.message || 'Failed to send email',
52
+ raw: error,
53
+ };
54
+ }
55
+ const result = (await response.json());
56
+ const sendResult = {
57
+ success: true,
58
+ raw: result,
59
+ };
60
+ if (result.id) {
61
+ sendResult.messageId = result.id;
62
+ }
63
+ return sendResult;
64
+ }
65
+ catch (error) {
66
+ return {
67
+ success: false,
68
+ error: error instanceof Error ? error.message : 'Unknown error',
69
+ };
70
+ }
71
+ },
72
+ async verify() {
73
+ try {
74
+ const response = await fetch(`${apiUrl}/domains`, {
75
+ headers: {
76
+ Authorization: `Bearer ${config.apiKey}`,
77
+ },
78
+ });
79
+ return response.ok;
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ },
85
+ };
86
+ }
87
+ // =============================================================================
88
+ // Email Templates
89
+ // =============================================================================
90
+ /**
91
+ * Default CSS styles for email templates
92
+ */
93
+ const DEFAULT_STYLES = `
94
+ body {
95
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
96
+ line-height: 1.6;
97
+ color: #333;
98
+ max-width: 600px;
99
+ margin: 0 auto;
100
+ padding: 20px;
101
+ }
102
+ .container {
103
+ background: #ffffff;
104
+ border: 1px solid #e5e5e5;
105
+ border-radius: 8px;
106
+ padding: 24px;
107
+ }
108
+ .header {
109
+ border-bottom: 1px solid #e5e5e5;
110
+ padding-bottom: 16px;
111
+ margin-bottom: 20px;
112
+ }
113
+ .header h1 {
114
+ margin: 0;
115
+ font-size: 20px;
116
+ color: #111;
117
+ }
118
+ .content {
119
+ margin-bottom: 24px;
120
+ }
121
+ .content p {
122
+ margin: 0 0 16px;
123
+ }
124
+ .context {
125
+ background: #f9f9f9;
126
+ border-radius: 6px;
127
+ padding: 16px;
128
+ margin: 16px 0;
129
+ }
130
+ .context-item {
131
+ display: flex;
132
+ margin-bottom: 8px;
133
+ }
134
+ .context-label {
135
+ font-weight: 600;
136
+ min-width: 120px;
137
+ color: #666;
138
+ }
139
+ .actions {
140
+ display: flex;
141
+ gap: 12px;
142
+ margin-top: 24px;
143
+ }
144
+ .btn {
145
+ display: inline-block;
146
+ padding: 12px 24px;
147
+ border-radius: 6px;
148
+ text-decoration: none;
149
+ font-weight: 600;
150
+ text-align: center;
151
+ }
152
+ .btn-primary {
153
+ background: #0066cc;
154
+ color: #ffffff;
155
+ }
156
+ .btn-danger {
157
+ background: #dc3545;
158
+ color: #ffffff;
159
+ }
160
+ .btn-secondary {
161
+ background: #6c757d;
162
+ color: #ffffff;
163
+ }
164
+ .footer {
165
+ margin-top: 24px;
166
+ padding-top: 16px;
167
+ border-top: 1px solid #e5e5e5;
168
+ font-size: 12px;
169
+ color: #666;
170
+ }
171
+ .reply-instructions {
172
+ background: #fff3cd;
173
+ border: 1px solid #ffc107;
174
+ border-radius: 6px;
175
+ padding: 12px;
176
+ margin-top: 16px;
177
+ font-size: 13px;
178
+ }
179
+ `;
180
+ /**
181
+ * Generate notification email HTML
182
+ */
183
+ export function generateNotificationEmail(message, options = {}) {
184
+ const priority = options.priority ?? 'normal';
185
+ const metadata = options.metadata;
186
+ const templates = options.templates ?? {};
187
+ const styles = templates.styles || DEFAULT_STYLES;
188
+ const brandName = templates.brandName || 'Digital Workers';
189
+ const footerText = templates.footerText || 'Sent via Digital Workers notification system';
190
+ const priorityBadge = priority === 'urgent' || priority === 'high'
191
+ ? `<span style="background: #dc3545; color: white; padding: 2px 8px; border-radius: 4px; font-size: 12px; margin-left: 8px;">${priority.toUpperCase()}</span>`
192
+ : '';
193
+ const subject = options.subject || `[${brandName}] Notification${priority === 'urgent' ? ' - URGENT' : ''}`;
194
+ const contextHtml = metadata
195
+ ? `
196
+ <div class="context">
197
+ ${Object.entries(metadata)
198
+ .map(([key, value]) => `<div class="context-item"><span class="context-label">${key}:</span><span>${String(value)}</span></div>`)
199
+ .join('')}
200
+ </div>
201
+ `
202
+ : '';
203
+ const html = `
204
+ <!DOCTYPE html>
205
+ <html>
206
+ <head>
207
+ <meta charset="utf-8">
208
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
209
+ <style>${styles}</style>
210
+ </head>
211
+ <body>
212
+ <div class="container">
213
+ <div class="header">
214
+ <h1>Notification${priorityBadge}</h1>
215
+ </div>
216
+ <div class="content">
217
+ <p>${escapeHtml(message)}</p>
218
+ ${contextHtml}
219
+ </div>
220
+ <div class="footer">
221
+ <p>${footerText}</p>
222
+ </div>
223
+ </div>
224
+ </body>
225
+ </html>
226
+ `;
227
+ const text = `${brandName} Notification\n\n${message}\n\n${metadata
228
+ ? Object.entries(metadata)
229
+ .map(([k, v]) => `${k}: ${v}`)
230
+ .join('\n')
231
+ : ''}\n\n${footerText}`;
232
+ return { subject, html, text };
233
+ }
234
+ /**
235
+ * Generate approval request email HTML
236
+ */
237
+ export function generateApprovalEmail(request, requestData, options = {}) {
238
+ const { approveUrl, rejectUrl } = options;
239
+ const templates = options.templates ?? {};
240
+ const styles = templates.styles || DEFAULT_STYLES;
241
+ const brandName = templates.brandName || 'Digital Workers';
242
+ const footerText = templates.footerText || 'Sent via Digital Workers approval system';
243
+ const subject = `[${brandName}] Approval Required: ${truncate(request, 50)}`;
244
+ const contextHtml = requestData.context
245
+ ? `
246
+ <div class="context">
247
+ <strong>Additional Context:</strong>
248
+ ${Object.entries(requestData.context)
249
+ .map(([key, value]) => `<div class="context-item"><span class="context-label">${key}:</span><span>${String(value)}</span></div>`)
250
+ .join('')}
251
+ </div>
252
+ `
253
+ : '';
254
+ const actionsHtml = approveUrl && rejectUrl
255
+ ? `
256
+ <div class="actions">
257
+ <a href="${approveUrl}" class="btn btn-primary">Approve</a>
258
+ <a href="${rejectUrl}" class="btn btn-danger">Reject</a>
259
+ </div>
260
+ `
261
+ : '';
262
+ const replyInstructions = `
263
+ <div class="reply-instructions">
264
+ <strong>Reply via Email:</strong> You can also respond by replying to this email with:
265
+ <ul style="margin: 8px 0; padding-left: 20px;">
266
+ <li><strong>APPROVED</strong> - to approve this request</li>
267
+ <li><strong>REJECTED</strong> - to reject this request</li>
268
+ </ul>
269
+ Add any notes after your decision.
270
+ </div>
271
+ `;
272
+ const expiresHtml = requestData.expiresAt
273
+ ? `<p style="color: #dc3545; font-size: 13px;">This request expires at ${new Date(requestData.expiresAt).toLocaleString()}</p>`
274
+ : '';
275
+ const html = `
276
+ <!DOCTYPE html>
277
+ <html>
278
+ <head>
279
+ <meta charset="utf-8">
280
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
281
+ <style>${styles}</style>
282
+ </head>
283
+ <body>
284
+ <div class="container">
285
+ <div class="header">
286
+ <h1>Approval Required</h1>
287
+ </div>
288
+ <div class="content">
289
+ <p><strong>Request:</strong></p>
290
+ <p>${escapeHtml(request)}</p>
291
+ ${contextHtml}
292
+ ${expiresHtml}
293
+ ${actionsHtml}
294
+ ${replyInstructions}
295
+ </div>
296
+ <div class="footer">
297
+ <p>${footerText}</p>
298
+ <p style="font-size: 10px; color: #999;">Request ID: ${requestData.requestId}</p>
299
+ </div>
300
+ </div>
301
+ </body>
302
+ </html>
303
+ `;
304
+ const text = `${brandName} - Approval Required
305
+
306
+ Request: ${request}
307
+
308
+ ${requestData.context
309
+ ? Object.entries(requestData.context)
310
+ .map(([k, v]) => `${k}: ${v}`)
311
+ .join('\n')
312
+ : ''}
313
+
314
+ To respond, reply to this email with:
315
+ - APPROVED - to approve this request
316
+ - REJECTED - to reject this request
317
+
318
+ Add any notes after your decision.
319
+
320
+ ${requestData.expiresAt
321
+ ? `This request expires at ${new Date(requestData.expiresAt).toLocaleString()}`
322
+ : ''}
323
+
324
+ ${footerText}
325
+ Request ID: ${requestData.requestId}`;
326
+ return { subject, html, text };
327
+ }
328
+ // =============================================================================
329
+ // Email Reply Parser
330
+ // =============================================================================
331
+ /**
332
+ * Parse an email reply for approval response
333
+ */
334
+ export function parseApprovalReply(email) {
335
+ const content = email.text || stripHtml(email.html || '');
336
+ const contentLower = content.toLowerCase().trim();
337
+ // Extract request ID from subject or references
338
+ const requestIdMatch = email.subject?.match(/Request ID:\s*([a-zA-Z0-9_-]+)/i) ||
339
+ content.match(/Request ID:\s*([a-zA-Z0-9_-]+)/i);
340
+ // Check for approval/rejection keywords
341
+ const approvedPatterns = [/^approved\b/i, /\bapprove\b/i, /\byes\b/i, /\blgtm\b/i, /\bok\b/i];
342
+ const rejectedPatterns = [/^rejected\b/i, /\breject\b/i, /\bno\b/i, /\bdeny\b/i, /\bdecline\b/i];
343
+ // Get the first meaningful line (skip quoted content)
344
+ const lines = content.split('\n').filter((line) => !line.startsWith('>') && line.trim());
345
+ const firstLine = lines[0] || '';
346
+ const firstLineLower = firstLine.toLowerCase().trim();
347
+ let isApprovalResponse = false;
348
+ let approved;
349
+ // Check first line for explicit approval/rejection
350
+ for (const pattern of approvedPatterns) {
351
+ if (pattern.test(firstLineLower)) {
352
+ isApprovalResponse = true;
353
+ approved = true;
354
+ break;
355
+ }
356
+ }
357
+ if (!isApprovalResponse) {
358
+ for (const pattern of rejectedPatterns) {
359
+ if (pattern.test(firstLineLower)) {
360
+ isApprovalResponse = true;
361
+ approved = false;
362
+ break;
363
+ }
364
+ }
365
+ }
366
+ // Extract notes (everything after the decision keyword)
367
+ let notes;
368
+ if (isApprovalResponse && lines.length > 1) {
369
+ notes = lines.slice(1).join('\n').trim();
370
+ }
371
+ else if (isApprovalResponse) {
372
+ // Notes might be on the same line after the keyword
373
+ const keywordMatch = firstLine.match(/^(approved|rejected|approve|reject|yes|no|lgtm|ok)\b[:\s]*(.*)/i);
374
+ if (keywordMatch && keywordMatch[2]) {
375
+ notes = keywordMatch[2].trim();
376
+ }
377
+ }
378
+ // Build result with only defined properties
379
+ const result = {
380
+ isApprovalResponse,
381
+ from: email.from,
382
+ repliedAt: new Date(),
383
+ rawContent: content,
384
+ };
385
+ if (approved !== undefined) {
386
+ result.approved = approved;
387
+ }
388
+ if (requestIdMatch?.[1]) {
389
+ result.requestId = requestIdMatch[1];
390
+ }
391
+ if (notes) {
392
+ result.notes = notes;
393
+ }
394
+ return result;
395
+ }
396
+ // =============================================================================
397
+ // EmailTransport Class
398
+ // =============================================================================
399
+ /**
400
+ * Email transport for digital-workers notifications and approvals
401
+ *
402
+ * @example
403
+ * ```ts
404
+ * // Create with Resend
405
+ * const transport = new EmailTransport({
406
+ * apiKey: process.env.RESEND_API_KEY,
407
+ * from: 'notifications@example.com',
408
+ * approvalBaseUrl: 'https://app.example.com/approvals',
409
+ * })
410
+ *
411
+ * // Send notification
412
+ * await transport.sendNotification({
413
+ * to: 'user@example.com',
414
+ * message: 'Deployment completed',
415
+ * priority: 'normal',
416
+ * })
417
+ *
418
+ * // Send approval request
419
+ * await transport.sendApprovalRequest({
420
+ * to: 'manager@example.com',
421
+ * request: 'Expense: $500 for cloud services',
422
+ * requestId: 'apr_123',
423
+ * context: { amount: 500, category: 'Infrastructure' },
424
+ * })
425
+ * ```
426
+ */
427
+ export class EmailTransport {
428
+ provider;
429
+ config;
430
+ constructor(config) {
431
+ this.config = config;
432
+ // Initialize provider
433
+ if (config.customProvider) {
434
+ this.provider = config.customProvider;
435
+ }
436
+ else if (config.apiKey) {
437
+ // Default to Resend
438
+ const providerConfig = {
439
+ apiKey: config.apiKey,
440
+ };
441
+ if (config.apiUrl) {
442
+ providerConfig.apiUrl = config.apiUrl;
443
+ }
444
+ this.provider = createResendProvider(providerConfig);
445
+ }
446
+ else {
447
+ throw new Error('Email transport requires either apiKey or customProvider');
448
+ }
449
+ }
450
+ /**
451
+ * Get the underlying email provider
452
+ */
453
+ getProvider() {
454
+ return this.provider;
455
+ }
456
+ /**
457
+ * Get the transport configuration
458
+ */
459
+ getConfig() {
460
+ return this.config;
461
+ }
462
+ /**
463
+ * Send a notification email
464
+ */
465
+ async sendNotification(options) {
466
+ const templateOptions = {};
467
+ if (options.subject) {
468
+ templateOptions.subject = options.subject;
469
+ }
470
+ if (options.priority) {
471
+ templateOptions.priority = options.priority;
472
+ }
473
+ if (options.metadata) {
474
+ templateOptions.metadata = options.metadata;
475
+ }
476
+ if (this.config.templates) {
477
+ templateOptions.templates = this.config.templates;
478
+ }
479
+ const { subject, html, text } = generateNotificationEmail(options.message, templateOptions);
480
+ const emailMessage = {
481
+ to: options.to,
482
+ from: options.from || this.config.from || 'notifications@example.com',
483
+ subject,
484
+ html,
485
+ text,
486
+ tags: [
487
+ { name: 'type', value: 'notification' },
488
+ { name: 'priority', value: options.priority || 'normal' },
489
+ ],
490
+ };
491
+ const replyTo = options.replyTo || this.config.replyTo;
492
+ if (replyTo) {
493
+ emailMessage.replyTo = replyTo;
494
+ }
495
+ const result = await this.provider.send(emailMessage);
496
+ const deliveryResult = {
497
+ success: result.success,
498
+ transport: 'email',
499
+ metadata: { provider: this.provider.name, raw: result.raw },
500
+ };
501
+ if (result.messageId) {
502
+ deliveryResult.messageId = result.messageId;
503
+ }
504
+ if (result.error) {
505
+ deliveryResult.error = result.error;
506
+ }
507
+ return deliveryResult;
508
+ }
509
+ /**
510
+ * Send an approval request email
511
+ */
512
+ async sendApprovalRequest(options) {
513
+ const requestData = {
514
+ requestId: options.requestId,
515
+ request: options.request,
516
+ };
517
+ if (options.requestedBy) {
518
+ requestData.requestedBy = options.requestedBy;
519
+ }
520
+ if (options.context) {
521
+ requestData.context = options.context;
522
+ }
523
+ if (options.expiresAt) {
524
+ requestData.expiresAt =
525
+ options.expiresAt instanceof Date ? options.expiresAt.getTime() : options.expiresAt;
526
+ }
527
+ if (options.callbackUrl) {
528
+ requestData.callbackUrl = options.callbackUrl;
529
+ }
530
+ // Generate approval/reject URLs if base URL is configured
531
+ let approveUrl;
532
+ let rejectUrl;
533
+ if (this.config.approvalBaseUrl) {
534
+ const baseUrl = this.config.approvalBaseUrl.replace(/\/$/, '');
535
+ approveUrl = `${baseUrl}/${options.requestId}/approve`;
536
+ rejectUrl = `${baseUrl}/${options.requestId}/reject`;
537
+ }
538
+ const templateOptions = {};
539
+ if (approveUrl) {
540
+ templateOptions.approveUrl = approveUrl;
541
+ }
542
+ if (rejectUrl) {
543
+ templateOptions.rejectUrl = rejectUrl;
544
+ }
545
+ if (this.config.templates) {
546
+ templateOptions.templates = this.config.templates;
547
+ }
548
+ const { subject, html, text } = generateApprovalEmail(options.request, requestData, templateOptions);
549
+ const emailMessage = {
550
+ to: options.to,
551
+ from: options.from || this.config.from || 'approvals@example.com',
552
+ subject,
553
+ html,
554
+ text,
555
+ headers: {
556
+ 'X-Approval-Request-Id': options.requestId,
557
+ },
558
+ tags: [
559
+ { name: 'type', value: 'approval' },
560
+ { name: 'request_id', value: options.requestId },
561
+ ],
562
+ };
563
+ const replyTo = options.replyTo || this.config.replyTo;
564
+ if (replyTo) {
565
+ emailMessage.replyTo = replyTo;
566
+ }
567
+ const result = await this.provider.send(emailMessage);
568
+ const deliveryMetadata = {
569
+ provider: this.provider.name,
570
+ requestId: options.requestId,
571
+ raw: result.raw,
572
+ };
573
+ if (approveUrl) {
574
+ deliveryMetadata['approveUrl'] = approveUrl;
575
+ }
576
+ if (rejectUrl) {
577
+ deliveryMetadata['rejectUrl'] = rejectUrl;
578
+ }
579
+ const deliveryResult = {
580
+ success: result.success,
581
+ transport: 'email',
582
+ metadata: deliveryMetadata,
583
+ };
584
+ if (result.messageId) {
585
+ deliveryResult.messageId = result.messageId;
586
+ }
587
+ if (result.error) {
588
+ deliveryResult.error = result.error;
589
+ }
590
+ return deliveryResult;
591
+ }
592
+ /**
593
+ * Parse an email reply for approval response
594
+ */
595
+ parseReply(email) {
596
+ return parseApprovalReply(email);
597
+ }
598
+ /**
599
+ * Convert parsed reply to ApprovalResult
600
+ */
601
+ toApprovalResult(reply, approver) {
602
+ const result = {
603
+ approved: reply.approved ?? false,
604
+ via: 'email',
605
+ };
606
+ if (approver) {
607
+ result.approvedBy = approver;
608
+ }
609
+ else if (reply.from) {
610
+ result.approvedBy = { id: reply.from };
611
+ }
612
+ if (reply.repliedAt) {
613
+ result.approvedAt = reply.repliedAt;
614
+ }
615
+ if (reply.notes) {
616
+ result.notes = reply.notes;
617
+ }
618
+ return result;
619
+ }
620
+ /**
621
+ * Create transport handler for registration
622
+ */
623
+ createHandler() {
624
+ return async (payload, _config) => {
625
+ const to = Array.isArray(payload.to) ? payload.to : [payload.to];
626
+ if (payload.type === 'notification') {
627
+ const notifyOptions = {
628
+ to,
629
+ message: payload.body,
630
+ };
631
+ if (payload.subject) {
632
+ notifyOptions.subject = payload.subject;
633
+ }
634
+ if (payload.priority) {
635
+ notifyOptions.priority = payload.priority;
636
+ }
637
+ if (payload.metadata) {
638
+ notifyOptions.metadata = payload.metadata;
639
+ }
640
+ if (payload.from) {
641
+ notifyOptions.from = payload.from;
642
+ }
643
+ if (payload.replyTo) {
644
+ notifyOptions.replyTo = payload.replyTo;
645
+ }
646
+ return this.sendNotification(notifyOptions);
647
+ }
648
+ if (payload.type === 'approval') {
649
+ const requestId = payload.metadata?.['requestId'] || generateRequestId('apr');
650
+ const approvalOptions = {
651
+ to,
652
+ request: payload.body,
653
+ requestId,
654
+ };
655
+ if (payload.metadata) {
656
+ approvalOptions.context = payload.metadata;
657
+ }
658
+ if (payload.from) {
659
+ approvalOptions.from = payload.from;
660
+ }
661
+ if (payload.replyTo) {
662
+ approvalOptions.replyTo = payload.replyTo;
663
+ }
664
+ return this.sendApprovalRequest(approvalOptions);
665
+ }
666
+ // Default: send as notification
667
+ const defaultOptions = {
668
+ to,
669
+ message: payload.body,
670
+ };
671
+ if (payload.subject) {
672
+ defaultOptions.subject = payload.subject;
673
+ }
674
+ if (payload.metadata) {
675
+ defaultOptions.metadata = payload.metadata;
676
+ }
677
+ if (payload.from) {
678
+ defaultOptions.from = payload.from;
679
+ }
680
+ if (payload.replyTo) {
681
+ defaultOptions.replyTo = payload.replyTo;
682
+ }
683
+ return this.sendNotification(defaultOptions);
684
+ };
685
+ }
686
+ /**
687
+ * Register this transport with the transport registry
688
+ */
689
+ register() {
690
+ registerTransport('email', this.createHandler());
691
+ }
692
+ }
693
+ // =============================================================================
694
+ // Factory Functions
695
+ // =============================================================================
696
+ /**
697
+ * Create an email transport with Resend
698
+ */
699
+ export function createEmailTransport(config) {
700
+ return new EmailTransport({ ...config, transport: 'email' });
701
+ }
702
+ /**
703
+ * Create an email transport with a custom provider
704
+ */
705
+ export function createEmailTransportWithProvider(provider, config) {
706
+ return new EmailTransport({
707
+ transport: 'email',
708
+ customProvider: provider,
709
+ ...config,
710
+ });
711
+ }
712
+ // =============================================================================
713
+ // Utility Functions
714
+ // =============================================================================
715
+ /**
716
+ * Escape HTML special characters
717
+ */
718
+ function escapeHtml(text) {
719
+ const map = {
720
+ '&': '&amp;',
721
+ '<': '&lt;',
722
+ '>': '&gt;',
723
+ '"': '&quot;',
724
+ "'": '&#039;',
725
+ };
726
+ return text.replace(/[&<>"']/g, (char) => map[char] || char);
727
+ }
728
+ /**
729
+ * Strip HTML tags from content
730
+ */
731
+ function stripHtml(html) {
732
+ return (html
733
+ // Add newlines before block elements
734
+ .replace(/<(p|div|br|li|h[1-6]|tr)[^>]*>/gi, '\n')
735
+ // Remove all HTML tags
736
+ .replace(/<[^>]*>/g, '')
737
+ // Decode HTML entities
738
+ .replace(/&nbsp;/g, ' ')
739
+ .replace(/&amp;/g, '&')
740
+ .replace(/&lt;/g, '<')
741
+ .replace(/&gt;/g, '>')
742
+ .replace(/&quot;/g, '"')
743
+ .replace(/&#039;/g, "'")
744
+ // Normalize whitespace
745
+ .replace(/\n\s*\n/g, '\n')
746
+ .trim());
747
+ }
748
+ /**
749
+ * Truncate text to a maximum length
750
+ */
751
+ function truncate(text, maxLength) {
752
+ if (text.length <= maxLength)
753
+ return text;
754
+ return text.substring(0, maxLength - 3) + '...';
755
+ }
756
+ // =============================================================================
757
+ // Type Guards
758
+ // =============================================================================
759
+ /**
760
+ * Check if an object is an EmailTransportConfig
761
+ */
762
+ export function isEmailTransportConfig(config) {
763
+ return (typeof config === 'object' &&
764
+ config !== null &&
765
+ config.transport === 'email');
766
+ }
767
+ /**
768
+ * Check if a parsed reply indicates approval
769
+ */
770
+ export function isApproved(reply) {
771
+ return reply.isApprovalResponse && reply.approved === true;
772
+ }
773
+ /**
774
+ * Check if a parsed reply indicates rejection
775
+ */
776
+ export function isRejected(reply) {
777
+ return reply.isApprovalResponse && reply.approved === false;
778
+ }
779
+ //# sourceMappingURL=email.js.map