digital-workers 2.1.1 → 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.
- package/CHANGELOG.md +23 -0
- package/README.md +136 -180
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +34 -21
- package/dist/actions.js.map +1 -1
- package/dist/agent-comms.d.ts +438 -0
- package/dist/agent-comms.d.ts.map +1 -0
- package/dist/agent-comms.js +677 -0
- package/dist/agent-comms.js.map +1 -0
- package/dist/approve.d.ts +40 -8
- package/dist/approve.d.ts.map +1 -1
- package/dist/approve.js +86 -20
- package/dist/approve.js.map +1 -1
- package/dist/ask.d.ts +38 -7
- package/dist/ask.d.ts.map +1 -1
- package/dist/ask.js +85 -25
- package/dist/ask.js.map +1 -1
- package/dist/browse.d.ts +223 -0
- package/dist/browse.d.ts.map +1 -0
- package/dist/browse.js +392 -0
- package/dist/browse.js.map +1 -0
- package/dist/capability-tiers.d.ts +230 -0
- package/dist/capability-tiers.d.ts.map +1 -0
- package/dist/capability-tiers.js +388 -0
- package/dist/capability-tiers.js.map +1 -0
- package/dist/cascade-context.d.ts +523 -0
- package/dist/cascade-context.d.ts.map +1 -0
- package/dist/cascade-context.js +494 -0
- package/dist/cascade-context.js.map +1 -0
- package/dist/client.d.ts +162 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +64 -0
- package/dist/client.js.map +1 -0
- package/dist/decide.d.ts +42 -6
- package/dist/decide.d.ts.map +1 -1
- package/dist/decide.js +54 -11
- package/dist/decide.js.map +1 -1
- package/dist/do.d.ts +36 -7
- package/dist/do.d.ts.map +1 -1
- package/dist/do.js +82 -39
- package/dist/do.js.map +1 -1
- package/dist/error-escalation.d.ts +416 -0
- package/dist/error-escalation.d.ts.map +1 -0
- package/dist/error-escalation.js +656 -0
- package/dist/error-escalation.js.map +1 -0
- package/dist/generate.d.ts +48 -7
- package/dist/generate.d.ts.map +1 -1
- package/dist/generate.js +49 -8
- package/dist/generate.js.map +1 -1
- package/dist/goals.d.ts +10 -9
- package/dist/goals.d.ts.map +1 -1
- package/dist/goals.js +30 -24
- package/dist/goals.js.map +1 -1
- package/dist/image.d.ts +189 -0
- package/dist/image.d.ts.map +1 -0
- package/dist/image.js +528 -0
- package/dist/image.js.map +1 -0
- package/dist/index.d.ts +59 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +92 -2
- package/dist/index.js.map +1 -1
- package/dist/is.d.ts +45 -10
- package/dist/is.d.ts.map +1 -1
- package/dist/is.js +56 -21
- package/dist/is.js.map +1 -1
- package/dist/kpis.d.ts +24 -15
- package/dist/kpis.d.ts.map +1 -1
- package/dist/kpis.js +16 -14
- package/dist/kpis.js.map +1 -1
- package/dist/load-balancing.d.ts +395 -0
- package/dist/load-balancing.d.ts.map +1 -0
- package/dist/load-balancing.js +991 -0
- package/dist/load-balancing.js.map +1 -0
- package/dist/logger.d.ts +76 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +39 -0
- package/dist/logger.js.map +1 -0
- package/dist/notify.d.ts +38 -9
- package/dist/notify.d.ts.map +1 -1
- package/dist/notify.js +72 -17
- package/dist/notify.js.map +1 -1
- package/dist/role.d.ts +5 -4
- package/dist/role.d.ts.map +1 -1
- package/dist/role.js +13 -10
- package/dist/role.js.map +1 -1
- package/dist/runtime.d.ts +310 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +510 -0
- package/dist/runtime.js.map +1 -0
- package/dist/team.d.ts +11 -6
- package/dist/team.d.ts.map +1 -1
- package/dist/team.js +22 -15
- package/dist/team.js.map +1 -1
- package/dist/transports/email.d.ts +318 -0
- package/dist/transports/email.d.ts.map +1 -0
- package/dist/transports/email.js +779 -0
- package/dist/transports/email.js.map +1 -0
- package/dist/transports/slack.d.ts +515 -0
- package/dist/transports/slack.d.ts.map +1 -0
- package/dist/transports/slack.js +844 -0
- package/dist/transports/slack.js.map +1 -0
- package/dist/transports.d.ts.map +1 -1
- package/dist/transports.js +44 -25
- package/dist/transports.js.map +1 -1
- package/dist/types.d.ts +149 -19
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -1
- package/dist/utils/id.d.ts +19 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +21 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/video.d.ts +203 -0
- package/dist/video.d.ts.map +1 -0
- package/dist/video.js +528 -0
- package/dist/video.js.map +1 -0
- package/dist/worker.d.ts +343 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +698 -0
- package/dist/worker.js.map +1 -0
- package/package.json +24 -5
- package/src/actions.ts +48 -38
- package/src/agent-comms.ts +1200 -0
- package/src/approve.ts +91 -20
- package/src/ask.ts +99 -25
- package/src/browse.ts +627 -0
- package/src/capability-tiers.ts +545 -0
- package/src/cascade-context.ts +648 -0
- package/src/client.ts +221 -0
- package/src/decide.ts +81 -35
- package/src/do.ts +98 -52
- package/src/error-escalation.ts +1123 -0
- package/src/generate.ts +52 -18
- package/src/goals.ts +36 -27
- package/src/image.ts +816 -0
- package/src/index.ts +410 -2
- package/src/is.ts +59 -25
- package/src/kpis.ts +41 -36
- package/src/load-balancing.ts +1467 -0
- package/src/logger.ts +93 -0
- package/src/notify.ts +78 -17
- package/src/role.ts +30 -20
- package/src/runtime.ts +796 -0
- package/src/team.ts +24 -19
- package/src/transports/email.ts +1160 -0
- package/src/transports/slack.ts +1320 -0
- package/src/transports.ts +58 -43
- package/src/types.ts +182 -46
- package/src/utils/id.ts +21 -0
- package/src/video.ts +906 -0
- package/src/worker.ts +1007 -0
- package/test/agent-comms.test.ts +1397 -0
- package/test/approve.test.ts +305 -0
- package/test/ask.test.ts +274 -0
- package/test/browse.test.ts +361 -0
- package/test/capability-tiers.test.ts +631 -0
- package/test/cascade-context.test.ts +692 -0
- package/test/decide.test.ts +252 -0
- package/test/do.test.ts +144 -0
- package/test/error-escalation.test.ts +1205 -0
- package/test/error-logging.test.ts +357 -0
- package/test/generate.test.ts +319 -0
- package/test/image.test.ts +398 -0
- package/test/is.test.ts +287 -0
- package/test/load-balancing-safety.test.ts +404 -0
- package/test/load-balancing-thread-safety.test.ts +464 -0
- package/test/load-balancing.test.ts +1145 -0
- package/test/notify.test.ts +434 -0
- package/test/primitives.test.ts +320 -0
- package/test/runtime-integration.test.ts +892 -0
- package/test/transports/crypto.test.ts +230 -0
- package/test/transports/email.test.ts +866 -0
- package/test/transports/id-generation.test.ts +91 -0
- package/test/transports/slack.test.ts +760 -0
- package/test/type-safety.test.ts +834 -0
- package/test/types.test.ts +95 -2
- package/test/video.test.ts +530 -0
- package/test/worker.test.ts +1433 -0
- package/tsconfig.json +4 -1
- package/vitest.config.ts +42 -0
- package/wrangler.jsonc +36 -0
- package/.turbo/turbo-build.log +0 -5
- package/src/actions.js +0 -436
- package/src/approve.js +0 -234
- package/src/ask.js +0 -226
- package/src/decide.js +0 -244
- package/src/do.js +0 -227
- package/src/generate.js +0 -298
- package/src/goals.js +0 -205
- package/src/index.js +0 -68
- package/src/is.js +0 -317
- package/src/kpis.js +0 -270
- package/src/notify.js +0 -219
- package/src/role.js +0 -110
- package/src/team.js +0 -130
- package/src/transports.js +0 -357
- 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
|
+
'&': '&',
|
|
721
|
+
'<': '<',
|
|
722
|
+
'>': '>',
|
|
723
|
+
'"': '"',
|
|
724
|
+
"'": ''',
|
|
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(/ /g, ' ')
|
|
739
|
+
.replace(/&/g, '&')
|
|
740
|
+
.replace(/</g, '<')
|
|
741
|
+
.replace(/>/g, '>')
|
|
742
|
+
.replace(/"/g, '"')
|
|
743
|
+
.replace(/'/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
|