digital-workers 2.1.3 → 2.4.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +17 -0
- package/README.md +2 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +33 -21
- package/dist/actions.js.map +1 -1
- package/dist/agent-comms.d.ts.map +1 -1
- package/dist/agent-comms.js +36 -25
- package/dist/agent-comms.js.map +1 -1
- 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.js +3 -3
- package/dist/capability-tiers.js.map +1 -1
- package/dist/cascade-context.d.ts +28 -28
- 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.map +1 -1
- package/dist/error-escalation.js +38 -38
- package/dist/error-escalation.js.map +1 -1
- 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 +49 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +58 -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.map +1 -1
- package/dist/load-balancing.js +124 -38
- package/dist/load-balancing.js.map +1 -1
- 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 +141 -19
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +5 -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 +32 -14
- package/src/actions.ts +39 -30
- package/src/agent-comms.ts +54 -92
- package/src/approve.ts +91 -20
- package/src/ask.ts +99 -25
- package/src/browse.ts +627 -0
- package/src/capability-tiers.ts +5 -5
- package/src/client.ts +221 -0
- package/src/decide.ts +81 -35
- package/src/do.ts +98 -52
- package/src/error-escalation.ts +55 -67
- package/src/generate.ts +52 -18
- package/src/goals.ts +36 -27
- package/src/image.ts +816 -0
- package/src/index.ts +187 -2
- package/src/is.ts +59 -25
- package/src/kpis.ts +41 -36
- package/src/load-balancing.ts +132 -46
- 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 +174 -46
- package/src/utils/id.ts +21 -0
- package/src/video.ts +906 -0
- package/src/worker.ts +1007 -0
- package/test/approve.test.ts +305 -0
- package/test/ask.test.ts +274 -0
- package/test/browse.test.ts +361 -0
- package/test/decide.test.ts +252 -0
- package/test/do.test.ts +144 -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/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 +60 -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/LICENSE +0 -21
- 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,1160 @@
|
|
|
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
|
+
|
|
11
|
+
import type {
|
|
12
|
+
Transport,
|
|
13
|
+
TransportConfig,
|
|
14
|
+
MessagePayload,
|
|
15
|
+
DeliveryResult,
|
|
16
|
+
TransportHandler,
|
|
17
|
+
} from '../transports.js'
|
|
18
|
+
import { registerTransport } from '../transports.js'
|
|
19
|
+
import type { WorkerRef, ApprovalResult, ContactChannel } from '../types.js'
|
|
20
|
+
import { generateRequestId } from '../utils/id.js'
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// Email Provider Interface (Provider-Agnostic)
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Email message to be sent
|
|
28
|
+
*/
|
|
29
|
+
export interface EmailMessage {
|
|
30
|
+
/** Recipient email address(es) */
|
|
31
|
+
to: string | string[]
|
|
32
|
+
/** Sender email address */
|
|
33
|
+
from: string
|
|
34
|
+
/** Reply-to address */
|
|
35
|
+
replyTo?: string
|
|
36
|
+
/** Email subject line */
|
|
37
|
+
subject: string
|
|
38
|
+
/** Plain text body */
|
|
39
|
+
text?: string
|
|
40
|
+
/** HTML body */
|
|
41
|
+
html?: string
|
|
42
|
+
/** Custom headers */
|
|
43
|
+
headers?: Record<string, string>
|
|
44
|
+
/** Attachments */
|
|
45
|
+
attachments?: EmailAttachment[]
|
|
46
|
+
/** Tags for categorization */
|
|
47
|
+
tags?: EmailTag[]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Email attachment
|
|
52
|
+
*/
|
|
53
|
+
export interface EmailAttachment {
|
|
54
|
+
/** Filename */
|
|
55
|
+
filename: string
|
|
56
|
+
/** Content as base64 or Buffer */
|
|
57
|
+
content: string | ArrayBuffer
|
|
58
|
+
/** MIME type */
|
|
59
|
+
contentType?: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Email tag for categorization
|
|
64
|
+
*/
|
|
65
|
+
export interface EmailTag {
|
|
66
|
+
/** Tag name */
|
|
67
|
+
name: string
|
|
68
|
+
/** Tag value */
|
|
69
|
+
value: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Result of sending an email
|
|
74
|
+
*/
|
|
75
|
+
export interface EmailSendResult {
|
|
76
|
+
/** Whether the send was successful */
|
|
77
|
+
success: boolean
|
|
78
|
+
/** Provider-specific message ID */
|
|
79
|
+
messageId?: string
|
|
80
|
+
/** Error message if failed */
|
|
81
|
+
error?: string
|
|
82
|
+
/** Raw provider response */
|
|
83
|
+
raw?: unknown
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Email provider interface - implement this for different email services
|
|
88
|
+
*/
|
|
89
|
+
export interface EmailProvider {
|
|
90
|
+
/** Provider name (e.g., 'resend', 'sendgrid', 'ses') */
|
|
91
|
+
name: string
|
|
92
|
+
/** Send an email */
|
|
93
|
+
send(message: EmailMessage): Promise<EmailSendResult>
|
|
94
|
+
/** Verify the provider is configured correctly */
|
|
95
|
+
verify?(): Promise<boolean>
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// =============================================================================
|
|
99
|
+
// Email Transport Configuration
|
|
100
|
+
// =============================================================================
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Email transport configuration
|
|
104
|
+
*/
|
|
105
|
+
export interface EmailTransportConfig extends TransportConfig {
|
|
106
|
+
transport: 'email'
|
|
107
|
+
/** Email provider to use */
|
|
108
|
+
provider?: 'resend' | 'sendgrid' | 'ses' | 'smtp' | 'custom'
|
|
109
|
+
/** API key for the provider */
|
|
110
|
+
apiKey?: string
|
|
111
|
+
/** API URL (for custom providers) */
|
|
112
|
+
apiUrl?: string
|
|
113
|
+
/** Default sender address */
|
|
114
|
+
from?: string
|
|
115
|
+
/** Default reply-to address */
|
|
116
|
+
replyTo?: string
|
|
117
|
+
/** Base URL for approval links */
|
|
118
|
+
approvalBaseUrl?: string
|
|
119
|
+
/** Custom email provider instance */
|
|
120
|
+
customProvider?: EmailProvider
|
|
121
|
+
/** Template options */
|
|
122
|
+
templates?: EmailTemplateOptions
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Template customization options
|
|
127
|
+
*/
|
|
128
|
+
export interface EmailTemplateOptions {
|
|
129
|
+
/** Custom CSS styles */
|
|
130
|
+
styles?: string
|
|
131
|
+
/** Company/product name */
|
|
132
|
+
brandName?: string
|
|
133
|
+
/** Logo URL */
|
|
134
|
+
logoUrl?: string
|
|
135
|
+
/** Primary brand color */
|
|
136
|
+
primaryColor?: string
|
|
137
|
+
/** Footer text */
|
|
138
|
+
footerText?: string
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// =============================================================================
|
|
142
|
+
// Approval Request/Response Types
|
|
143
|
+
// =============================================================================
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Approval request data encoded in email
|
|
147
|
+
*/
|
|
148
|
+
export interface ApprovalRequestData {
|
|
149
|
+
/** Unique approval request ID */
|
|
150
|
+
requestId: string
|
|
151
|
+
/** What is being requested */
|
|
152
|
+
request: string
|
|
153
|
+
/** Who initiated the request */
|
|
154
|
+
requestedBy?: WorkerRef | string
|
|
155
|
+
/** Additional context */
|
|
156
|
+
context?: Record<string, unknown>
|
|
157
|
+
/** Expiration timestamp */
|
|
158
|
+
expiresAt?: number
|
|
159
|
+
/** Callback URL for webhook notifications */
|
|
160
|
+
callbackUrl?: string
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Parsed email reply for approval
|
|
165
|
+
*/
|
|
166
|
+
export interface ParsedEmailReply {
|
|
167
|
+
/** Whether this is an approval response */
|
|
168
|
+
isApprovalResponse: boolean
|
|
169
|
+
/** The decision */
|
|
170
|
+
approved?: boolean
|
|
171
|
+
/** The approval request ID */
|
|
172
|
+
requestId?: string
|
|
173
|
+
/** Any notes/comments from the approver */
|
|
174
|
+
notes?: string
|
|
175
|
+
/** Who replied */
|
|
176
|
+
from?: string
|
|
177
|
+
/** When they replied */
|
|
178
|
+
repliedAt?: Date
|
|
179
|
+
/** Raw email content */
|
|
180
|
+
rawContent?: string
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Inbound email for parsing
|
|
185
|
+
*/
|
|
186
|
+
export interface InboundEmail {
|
|
187
|
+
/** Sender address */
|
|
188
|
+
from: string
|
|
189
|
+
/** Recipient address */
|
|
190
|
+
to: string | string[]
|
|
191
|
+
/** Email subject */
|
|
192
|
+
subject: string
|
|
193
|
+
/** Plain text body */
|
|
194
|
+
text?: string
|
|
195
|
+
/** HTML body */
|
|
196
|
+
html?: string
|
|
197
|
+
/** In-Reply-To header */
|
|
198
|
+
inReplyTo?: string
|
|
199
|
+
/** References header */
|
|
200
|
+
references?: string
|
|
201
|
+
/** Custom headers */
|
|
202
|
+
headers?: Record<string, string>
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// =============================================================================
|
|
206
|
+
// Resend Provider Implementation
|
|
207
|
+
// =============================================================================
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Resend email provider
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* ```ts
|
|
214
|
+
* const resend = createResendProvider({ apiKey: 'your-api-key' })
|
|
215
|
+
* const transport = new EmailTransport({ provider: resend })
|
|
216
|
+
* ```
|
|
217
|
+
*/
|
|
218
|
+
export function createResendProvider(config: { apiKey: string; apiUrl?: string }): EmailProvider {
|
|
219
|
+
const apiUrl = config.apiUrl || 'https://api.resend.com'
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
name: 'resend',
|
|
223
|
+
|
|
224
|
+
async send(message: EmailMessage): Promise<EmailSendResult> {
|
|
225
|
+
try {
|
|
226
|
+
const response = await fetch(`${apiUrl}/emails`, {
|
|
227
|
+
method: 'POST',
|
|
228
|
+
headers: {
|
|
229
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
230
|
+
'Content-Type': 'application/json',
|
|
231
|
+
},
|
|
232
|
+
body: JSON.stringify({
|
|
233
|
+
from: message.from,
|
|
234
|
+
to: Array.isArray(message.to) ? message.to : [message.to],
|
|
235
|
+
subject: message.subject,
|
|
236
|
+
text: message.text,
|
|
237
|
+
html: message.html,
|
|
238
|
+
reply_to: message.replyTo,
|
|
239
|
+
headers: message.headers,
|
|
240
|
+
tags: message.tags,
|
|
241
|
+
}),
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
if (!response.ok) {
|
|
245
|
+
const error = await response.json().catch(() => ({ message: response.statusText }))
|
|
246
|
+
return {
|
|
247
|
+
success: false,
|
|
248
|
+
error: (error as { message?: string }).message || 'Failed to send email',
|
|
249
|
+
raw: error,
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const result = (await response.json()) as { id?: string }
|
|
254
|
+
const sendResult: EmailSendResult = {
|
|
255
|
+
success: true,
|
|
256
|
+
raw: result,
|
|
257
|
+
}
|
|
258
|
+
if (result.id) {
|
|
259
|
+
sendResult.messageId = result.id
|
|
260
|
+
}
|
|
261
|
+
return sendResult
|
|
262
|
+
} catch (error) {
|
|
263
|
+
return {
|
|
264
|
+
success: false,
|
|
265
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
async verify(): Promise<boolean> {
|
|
271
|
+
try {
|
|
272
|
+
const response = await fetch(`${apiUrl}/domains`, {
|
|
273
|
+
headers: {
|
|
274
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
275
|
+
},
|
|
276
|
+
})
|
|
277
|
+
return response.ok
|
|
278
|
+
} catch {
|
|
279
|
+
return false
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// =============================================================================
|
|
286
|
+
// Email Templates
|
|
287
|
+
// =============================================================================
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Default CSS styles for email templates
|
|
291
|
+
*/
|
|
292
|
+
const DEFAULT_STYLES = `
|
|
293
|
+
body {
|
|
294
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
295
|
+
line-height: 1.6;
|
|
296
|
+
color: #333;
|
|
297
|
+
max-width: 600px;
|
|
298
|
+
margin: 0 auto;
|
|
299
|
+
padding: 20px;
|
|
300
|
+
}
|
|
301
|
+
.container {
|
|
302
|
+
background: #ffffff;
|
|
303
|
+
border: 1px solid #e5e5e5;
|
|
304
|
+
border-radius: 8px;
|
|
305
|
+
padding: 24px;
|
|
306
|
+
}
|
|
307
|
+
.header {
|
|
308
|
+
border-bottom: 1px solid #e5e5e5;
|
|
309
|
+
padding-bottom: 16px;
|
|
310
|
+
margin-bottom: 20px;
|
|
311
|
+
}
|
|
312
|
+
.header h1 {
|
|
313
|
+
margin: 0;
|
|
314
|
+
font-size: 20px;
|
|
315
|
+
color: #111;
|
|
316
|
+
}
|
|
317
|
+
.content {
|
|
318
|
+
margin-bottom: 24px;
|
|
319
|
+
}
|
|
320
|
+
.content p {
|
|
321
|
+
margin: 0 0 16px;
|
|
322
|
+
}
|
|
323
|
+
.context {
|
|
324
|
+
background: #f9f9f9;
|
|
325
|
+
border-radius: 6px;
|
|
326
|
+
padding: 16px;
|
|
327
|
+
margin: 16px 0;
|
|
328
|
+
}
|
|
329
|
+
.context-item {
|
|
330
|
+
display: flex;
|
|
331
|
+
margin-bottom: 8px;
|
|
332
|
+
}
|
|
333
|
+
.context-label {
|
|
334
|
+
font-weight: 600;
|
|
335
|
+
min-width: 120px;
|
|
336
|
+
color: #666;
|
|
337
|
+
}
|
|
338
|
+
.actions {
|
|
339
|
+
display: flex;
|
|
340
|
+
gap: 12px;
|
|
341
|
+
margin-top: 24px;
|
|
342
|
+
}
|
|
343
|
+
.btn {
|
|
344
|
+
display: inline-block;
|
|
345
|
+
padding: 12px 24px;
|
|
346
|
+
border-radius: 6px;
|
|
347
|
+
text-decoration: none;
|
|
348
|
+
font-weight: 600;
|
|
349
|
+
text-align: center;
|
|
350
|
+
}
|
|
351
|
+
.btn-primary {
|
|
352
|
+
background: #0066cc;
|
|
353
|
+
color: #ffffff;
|
|
354
|
+
}
|
|
355
|
+
.btn-danger {
|
|
356
|
+
background: #dc3545;
|
|
357
|
+
color: #ffffff;
|
|
358
|
+
}
|
|
359
|
+
.btn-secondary {
|
|
360
|
+
background: #6c757d;
|
|
361
|
+
color: #ffffff;
|
|
362
|
+
}
|
|
363
|
+
.footer {
|
|
364
|
+
margin-top: 24px;
|
|
365
|
+
padding-top: 16px;
|
|
366
|
+
border-top: 1px solid #e5e5e5;
|
|
367
|
+
font-size: 12px;
|
|
368
|
+
color: #666;
|
|
369
|
+
}
|
|
370
|
+
.reply-instructions {
|
|
371
|
+
background: #fff3cd;
|
|
372
|
+
border: 1px solid #ffc107;
|
|
373
|
+
border-radius: 6px;
|
|
374
|
+
padding: 12px;
|
|
375
|
+
margin-top: 16px;
|
|
376
|
+
font-size: 13px;
|
|
377
|
+
}
|
|
378
|
+
`
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Generate notification email HTML
|
|
382
|
+
*/
|
|
383
|
+
export function generateNotificationEmail(
|
|
384
|
+
message: string,
|
|
385
|
+
options: {
|
|
386
|
+
subject?: string
|
|
387
|
+
priority?: 'low' | 'normal' | 'high' | 'urgent'
|
|
388
|
+
metadata?: Record<string, unknown>
|
|
389
|
+
templates?: EmailTemplateOptions
|
|
390
|
+
} = {}
|
|
391
|
+
): { subject: string; html: string; text: string } {
|
|
392
|
+
const priority = options.priority ?? 'normal'
|
|
393
|
+
const metadata = options.metadata
|
|
394
|
+
const templates = options.templates ?? {}
|
|
395
|
+
const styles = templates.styles || DEFAULT_STYLES
|
|
396
|
+
const brandName = templates.brandName || 'Digital Workers'
|
|
397
|
+
const footerText = templates.footerText || 'Sent via Digital Workers notification system'
|
|
398
|
+
|
|
399
|
+
const priorityBadge =
|
|
400
|
+
priority === 'urgent' || priority === 'high'
|
|
401
|
+
? `<span style="background: #dc3545; color: white; padding: 2px 8px; border-radius: 4px; font-size: 12px; margin-left: 8px;">${priority.toUpperCase()}</span>`
|
|
402
|
+
: ''
|
|
403
|
+
|
|
404
|
+
const subject =
|
|
405
|
+
options.subject || `[${brandName}] Notification${priority === 'urgent' ? ' - URGENT' : ''}`
|
|
406
|
+
|
|
407
|
+
const contextHtml = metadata
|
|
408
|
+
? `
|
|
409
|
+
<div class="context">
|
|
410
|
+
${Object.entries(metadata)
|
|
411
|
+
.map(
|
|
412
|
+
([key, value]) =>
|
|
413
|
+
`<div class="context-item"><span class="context-label">${key}:</span><span>${String(
|
|
414
|
+
value
|
|
415
|
+
)}</span></div>`
|
|
416
|
+
)
|
|
417
|
+
.join('')}
|
|
418
|
+
</div>
|
|
419
|
+
`
|
|
420
|
+
: ''
|
|
421
|
+
|
|
422
|
+
const html = `
|
|
423
|
+
<!DOCTYPE html>
|
|
424
|
+
<html>
|
|
425
|
+
<head>
|
|
426
|
+
<meta charset="utf-8">
|
|
427
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
428
|
+
<style>${styles}</style>
|
|
429
|
+
</head>
|
|
430
|
+
<body>
|
|
431
|
+
<div class="container">
|
|
432
|
+
<div class="header">
|
|
433
|
+
<h1>Notification${priorityBadge}</h1>
|
|
434
|
+
</div>
|
|
435
|
+
<div class="content">
|
|
436
|
+
<p>${escapeHtml(message)}</p>
|
|
437
|
+
${contextHtml}
|
|
438
|
+
</div>
|
|
439
|
+
<div class="footer">
|
|
440
|
+
<p>${footerText}</p>
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
443
|
+
</body>
|
|
444
|
+
</html>
|
|
445
|
+
`
|
|
446
|
+
|
|
447
|
+
const text = `${brandName} Notification\n\n${message}\n\n${
|
|
448
|
+
metadata
|
|
449
|
+
? Object.entries(metadata)
|
|
450
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
451
|
+
.join('\n')
|
|
452
|
+
: ''
|
|
453
|
+
}\n\n${footerText}`
|
|
454
|
+
|
|
455
|
+
return { subject, html, text }
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Generate approval request email HTML
|
|
460
|
+
*/
|
|
461
|
+
export function generateApprovalEmail(
|
|
462
|
+
request: string,
|
|
463
|
+
requestData: ApprovalRequestData,
|
|
464
|
+
options: {
|
|
465
|
+
approveUrl?: string
|
|
466
|
+
rejectUrl?: string
|
|
467
|
+
templates?: EmailTemplateOptions
|
|
468
|
+
} = {}
|
|
469
|
+
): { subject: string; html: string; text: string } {
|
|
470
|
+
const { approveUrl, rejectUrl } = options
|
|
471
|
+
const templates = options.templates ?? {}
|
|
472
|
+
const styles = templates.styles || DEFAULT_STYLES
|
|
473
|
+
const brandName = templates.brandName || 'Digital Workers'
|
|
474
|
+
const footerText = templates.footerText || 'Sent via Digital Workers approval system'
|
|
475
|
+
|
|
476
|
+
const subject = `[${brandName}] Approval Required: ${truncate(request, 50)}`
|
|
477
|
+
|
|
478
|
+
const contextHtml = requestData.context
|
|
479
|
+
? `
|
|
480
|
+
<div class="context">
|
|
481
|
+
<strong>Additional Context:</strong>
|
|
482
|
+
${Object.entries(requestData.context)
|
|
483
|
+
.map(
|
|
484
|
+
([key, value]) =>
|
|
485
|
+
`<div class="context-item"><span class="context-label">${key}:</span><span>${String(
|
|
486
|
+
value
|
|
487
|
+
)}</span></div>`
|
|
488
|
+
)
|
|
489
|
+
.join('')}
|
|
490
|
+
</div>
|
|
491
|
+
`
|
|
492
|
+
: ''
|
|
493
|
+
|
|
494
|
+
const actionsHtml =
|
|
495
|
+
approveUrl && rejectUrl
|
|
496
|
+
? `
|
|
497
|
+
<div class="actions">
|
|
498
|
+
<a href="${approveUrl}" class="btn btn-primary">Approve</a>
|
|
499
|
+
<a href="${rejectUrl}" class="btn btn-danger">Reject</a>
|
|
500
|
+
</div>
|
|
501
|
+
`
|
|
502
|
+
: ''
|
|
503
|
+
|
|
504
|
+
const replyInstructions = `
|
|
505
|
+
<div class="reply-instructions">
|
|
506
|
+
<strong>Reply via Email:</strong> You can also respond by replying to this email with:
|
|
507
|
+
<ul style="margin: 8px 0; padding-left: 20px;">
|
|
508
|
+
<li><strong>APPROVED</strong> - to approve this request</li>
|
|
509
|
+
<li><strong>REJECTED</strong> - to reject this request</li>
|
|
510
|
+
</ul>
|
|
511
|
+
Add any notes after your decision.
|
|
512
|
+
</div>
|
|
513
|
+
`
|
|
514
|
+
|
|
515
|
+
const expiresHtml = requestData.expiresAt
|
|
516
|
+
? `<p style="color: #dc3545; font-size: 13px;">This request expires at ${new Date(
|
|
517
|
+
requestData.expiresAt
|
|
518
|
+
).toLocaleString()}</p>`
|
|
519
|
+
: ''
|
|
520
|
+
|
|
521
|
+
const html = `
|
|
522
|
+
<!DOCTYPE html>
|
|
523
|
+
<html>
|
|
524
|
+
<head>
|
|
525
|
+
<meta charset="utf-8">
|
|
526
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
527
|
+
<style>${styles}</style>
|
|
528
|
+
</head>
|
|
529
|
+
<body>
|
|
530
|
+
<div class="container">
|
|
531
|
+
<div class="header">
|
|
532
|
+
<h1>Approval Required</h1>
|
|
533
|
+
</div>
|
|
534
|
+
<div class="content">
|
|
535
|
+
<p><strong>Request:</strong></p>
|
|
536
|
+
<p>${escapeHtml(request)}</p>
|
|
537
|
+
${contextHtml}
|
|
538
|
+
${expiresHtml}
|
|
539
|
+
${actionsHtml}
|
|
540
|
+
${replyInstructions}
|
|
541
|
+
</div>
|
|
542
|
+
<div class="footer">
|
|
543
|
+
<p>${footerText}</p>
|
|
544
|
+
<p style="font-size: 10px; color: #999;">Request ID: ${requestData.requestId}</p>
|
|
545
|
+
</div>
|
|
546
|
+
</div>
|
|
547
|
+
</body>
|
|
548
|
+
</html>
|
|
549
|
+
`
|
|
550
|
+
|
|
551
|
+
const text = `${brandName} - Approval Required
|
|
552
|
+
|
|
553
|
+
Request: ${request}
|
|
554
|
+
|
|
555
|
+
${
|
|
556
|
+
requestData.context
|
|
557
|
+
? Object.entries(requestData.context)
|
|
558
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
559
|
+
.join('\n')
|
|
560
|
+
: ''
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
To respond, reply to this email with:
|
|
564
|
+
- APPROVED - to approve this request
|
|
565
|
+
- REJECTED - to reject this request
|
|
566
|
+
|
|
567
|
+
Add any notes after your decision.
|
|
568
|
+
|
|
569
|
+
${
|
|
570
|
+
requestData.expiresAt
|
|
571
|
+
? `This request expires at ${new Date(requestData.expiresAt).toLocaleString()}`
|
|
572
|
+
: ''
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
${footerText}
|
|
576
|
+
Request ID: ${requestData.requestId}`
|
|
577
|
+
|
|
578
|
+
return { subject, html, text }
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// =============================================================================
|
|
582
|
+
// Email Reply Parser
|
|
583
|
+
// =============================================================================
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Parse an email reply for approval response
|
|
587
|
+
*/
|
|
588
|
+
export function parseApprovalReply(email: InboundEmail): ParsedEmailReply {
|
|
589
|
+
const content = email.text || stripHtml(email.html || '')
|
|
590
|
+
const contentLower = content.toLowerCase().trim()
|
|
591
|
+
|
|
592
|
+
// Extract request ID from subject or references
|
|
593
|
+
const requestIdMatch =
|
|
594
|
+
email.subject?.match(/Request ID:\s*([a-zA-Z0-9_-]+)/i) ||
|
|
595
|
+
content.match(/Request ID:\s*([a-zA-Z0-9_-]+)/i)
|
|
596
|
+
|
|
597
|
+
// Check for approval/rejection keywords
|
|
598
|
+
const approvedPatterns = [/^approved\b/i, /\bapprove\b/i, /\byes\b/i, /\blgtm\b/i, /\bok\b/i]
|
|
599
|
+
const rejectedPatterns = [/^rejected\b/i, /\breject\b/i, /\bno\b/i, /\bdeny\b/i, /\bdecline\b/i]
|
|
600
|
+
|
|
601
|
+
// Get the first meaningful line (skip quoted content)
|
|
602
|
+
const lines = content.split('\n').filter((line) => !line.startsWith('>') && line.trim())
|
|
603
|
+
const firstLine = lines[0] || ''
|
|
604
|
+
const firstLineLower = firstLine.toLowerCase().trim()
|
|
605
|
+
|
|
606
|
+
let isApprovalResponse = false
|
|
607
|
+
let approved: boolean | undefined
|
|
608
|
+
|
|
609
|
+
// Check first line for explicit approval/rejection
|
|
610
|
+
for (const pattern of approvedPatterns) {
|
|
611
|
+
if (pattern.test(firstLineLower)) {
|
|
612
|
+
isApprovalResponse = true
|
|
613
|
+
approved = true
|
|
614
|
+
break
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (!isApprovalResponse) {
|
|
619
|
+
for (const pattern of rejectedPatterns) {
|
|
620
|
+
if (pattern.test(firstLineLower)) {
|
|
621
|
+
isApprovalResponse = true
|
|
622
|
+
approved = false
|
|
623
|
+
break
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Extract notes (everything after the decision keyword)
|
|
629
|
+
let notes: string | undefined
|
|
630
|
+
if (isApprovalResponse && lines.length > 1) {
|
|
631
|
+
notes = lines.slice(1).join('\n').trim()
|
|
632
|
+
} else if (isApprovalResponse) {
|
|
633
|
+
// Notes might be on the same line after the keyword
|
|
634
|
+
const keywordMatch = firstLine.match(
|
|
635
|
+
/^(approved|rejected|approve|reject|yes|no|lgtm|ok)\b[:\s]*(.*)/i
|
|
636
|
+
)
|
|
637
|
+
if (keywordMatch && keywordMatch[2]) {
|
|
638
|
+
notes = keywordMatch[2].trim()
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Build result with only defined properties
|
|
643
|
+
const result: ParsedEmailReply = {
|
|
644
|
+
isApprovalResponse,
|
|
645
|
+
from: email.from,
|
|
646
|
+
repliedAt: new Date(),
|
|
647
|
+
rawContent: content,
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (approved !== undefined) {
|
|
651
|
+
result.approved = approved
|
|
652
|
+
}
|
|
653
|
+
if (requestIdMatch?.[1]) {
|
|
654
|
+
result.requestId = requestIdMatch[1]
|
|
655
|
+
}
|
|
656
|
+
if (notes) {
|
|
657
|
+
result.notes = notes
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return result
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// =============================================================================
|
|
664
|
+
// EmailTransport Class
|
|
665
|
+
// =============================================================================
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Email transport for digital-workers notifications and approvals
|
|
669
|
+
*
|
|
670
|
+
* @example
|
|
671
|
+
* ```ts
|
|
672
|
+
* // Create with Resend
|
|
673
|
+
* const transport = new EmailTransport({
|
|
674
|
+
* apiKey: process.env.RESEND_API_KEY,
|
|
675
|
+
* from: 'notifications@example.com',
|
|
676
|
+
* approvalBaseUrl: 'https://app.example.com/approvals',
|
|
677
|
+
* })
|
|
678
|
+
*
|
|
679
|
+
* // Send notification
|
|
680
|
+
* await transport.sendNotification({
|
|
681
|
+
* to: 'user@example.com',
|
|
682
|
+
* message: 'Deployment completed',
|
|
683
|
+
* priority: 'normal',
|
|
684
|
+
* })
|
|
685
|
+
*
|
|
686
|
+
* // Send approval request
|
|
687
|
+
* await transport.sendApprovalRequest({
|
|
688
|
+
* to: 'manager@example.com',
|
|
689
|
+
* request: 'Expense: $500 for cloud services',
|
|
690
|
+
* requestId: 'apr_123',
|
|
691
|
+
* context: { amount: 500, category: 'Infrastructure' },
|
|
692
|
+
* })
|
|
693
|
+
* ```
|
|
694
|
+
*/
|
|
695
|
+
export class EmailTransport {
|
|
696
|
+
private provider: EmailProvider
|
|
697
|
+
private config: EmailTransportConfig
|
|
698
|
+
|
|
699
|
+
constructor(config: EmailTransportConfig) {
|
|
700
|
+
this.config = config
|
|
701
|
+
|
|
702
|
+
// Initialize provider
|
|
703
|
+
if (config.customProvider) {
|
|
704
|
+
this.provider = config.customProvider
|
|
705
|
+
} else if (config.apiKey) {
|
|
706
|
+
// Default to Resend
|
|
707
|
+
const providerConfig: { apiKey: string; apiUrl?: string } = {
|
|
708
|
+
apiKey: config.apiKey,
|
|
709
|
+
}
|
|
710
|
+
if (config.apiUrl) {
|
|
711
|
+
providerConfig.apiUrl = config.apiUrl
|
|
712
|
+
}
|
|
713
|
+
this.provider = createResendProvider(providerConfig)
|
|
714
|
+
} else {
|
|
715
|
+
throw new Error('Email transport requires either apiKey or customProvider')
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Get the underlying email provider
|
|
721
|
+
*/
|
|
722
|
+
getProvider(): EmailProvider {
|
|
723
|
+
return this.provider
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Get the transport configuration
|
|
728
|
+
*/
|
|
729
|
+
getConfig(): EmailTransportConfig {
|
|
730
|
+
return this.config
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Send a notification email
|
|
735
|
+
*/
|
|
736
|
+
async sendNotification(options: {
|
|
737
|
+
to: string | string[]
|
|
738
|
+
message: string
|
|
739
|
+
subject?: string
|
|
740
|
+
priority?: 'low' | 'normal' | 'high' | 'urgent'
|
|
741
|
+
metadata?: Record<string, unknown>
|
|
742
|
+
from?: string
|
|
743
|
+
replyTo?: string
|
|
744
|
+
}): Promise<DeliveryResult> {
|
|
745
|
+
const templateOptions: {
|
|
746
|
+
subject?: string
|
|
747
|
+
priority?: 'low' | 'normal' | 'high' | 'urgent'
|
|
748
|
+
metadata?: Record<string, unknown>
|
|
749
|
+
templates?: EmailTemplateOptions
|
|
750
|
+
} = {}
|
|
751
|
+
|
|
752
|
+
if (options.subject) {
|
|
753
|
+
templateOptions.subject = options.subject
|
|
754
|
+
}
|
|
755
|
+
if (options.priority) {
|
|
756
|
+
templateOptions.priority = options.priority
|
|
757
|
+
}
|
|
758
|
+
if (options.metadata) {
|
|
759
|
+
templateOptions.metadata = options.metadata
|
|
760
|
+
}
|
|
761
|
+
if (this.config.templates) {
|
|
762
|
+
templateOptions.templates = this.config.templates
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const { subject, html, text } = generateNotificationEmail(options.message, templateOptions)
|
|
766
|
+
|
|
767
|
+
const emailMessage: EmailMessage = {
|
|
768
|
+
to: options.to,
|
|
769
|
+
from: options.from || this.config.from || 'notifications@example.com',
|
|
770
|
+
subject,
|
|
771
|
+
html,
|
|
772
|
+
text,
|
|
773
|
+
tags: [
|
|
774
|
+
{ name: 'type', value: 'notification' },
|
|
775
|
+
{ name: 'priority', value: options.priority || 'normal' },
|
|
776
|
+
],
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const replyTo = options.replyTo || this.config.replyTo
|
|
780
|
+
if (replyTo) {
|
|
781
|
+
emailMessage.replyTo = replyTo
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const result = await this.provider.send(emailMessage)
|
|
785
|
+
|
|
786
|
+
const deliveryResult: DeliveryResult = {
|
|
787
|
+
success: result.success,
|
|
788
|
+
transport: 'email',
|
|
789
|
+
metadata: { provider: this.provider.name, raw: result.raw },
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if (result.messageId) {
|
|
793
|
+
deliveryResult.messageId = result.messageId
|
|
794
|
+
}
|
|
795
|
+
if (result.error) {
|
|
796
|
+
deliveryResult.error = result.error
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return deliveryResult
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Send an approval request email
|
|
804
|
+
*/
|
|
805
|
+
async sendApprovalRequest(options: {
|
|
806
|
+
to: string | string[]
|
|
807
|
+
request: string
|
|
808
|
+
requestId: string
|
|
809
|
+
requestedBy?: WorkerRef | string
|
|
810
|
+
context?: Record<string, unknown>
|
|
811
|
+
expiresAt?: Date | number
|
|
812
|
+
callbackUrl?: string
|
|
813
|
+
from?: string
|
|
814
|
+
replyTo?: string
|
|
815
|
+
}): Promise<DeliveryResult> {
|
|
816
|
+
const requestData: ApprovalRequestData = {
|
|
817
|
+
requestId: options.requestId,
|
|
818
|
+
request: options.request,
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (options.requestedBy) {
|
|
822
|
+
requestData.requestedBy = options.requestedBy
|
|
823
|
+
}
|
|
824
|
+
if (options.context) {
|
|
825
|
+
requestData.context = options.context
|
|
826
|
+
}
|
|
827
|
+
if (options.expiresAt) {
|
|
828
|
+
requestData.expiresAt =
|
|
829
|
+
options.expiresAt instanceof Date ? options.expiresAt.getTime() : options.expiresAt
|
|
830
|
+
}
|
|
831
|
+
if (options.callbackUrl) {
|
|
832
|
+
requestData.callbackUrl = options.callbackUrl
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Generate approval/reject URLs if base URL is configured
|
|
836
|
+
let approveUrl: string | undefined
|
|
837
|
+
let rejectUrl: string | undefined
|
|
838
|
+
|
|
839
|
+
if (this.config.approvalBaseUrl) {
|
|
840
|
+
const baseUrl = this.config.approvalBaseUrl.replace(/\/$/, '')
|
|
841
|
+
approveUrl = `${baseUrl}/${options.requestId}/approve`
|
|
842
|
+
rejectUrl = `${baseUrl}/${options.requestId}/reject`
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const templateOptions: {
|
|
846
|
+
approveUrl?: string
|
|
847
|
+
rejectUrl?: string
|
|
848
|
+
templates?: EmailTemplateOptions
|
|
849
|
+
} = {}
|
|
850
|
+
|
|
851
|
+
if (approveUrl) {
|
|
852
|
+
templateOptions.approveUrl = approveUrl
|
|
853
|
+
}
|
|
854
|
+
if (rejectUrl) {
|
|
855
|
+
templateOptions.rejectUrl = rejectUrl
|
|
856
|
+
}
|
|
857
|
+
if (this.config.templates) {
|
|
858
|
+
templateOptions.templates = this.config.templates
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const { subject, html, text } = generateApprovalEmail(
|
|
862
|
+
options.request,
|
|
863
|
+
requestData,
|
|
864
|
+
templateOptions
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
const emailMessage: EmailMessage = {
|
|
868
|
+
to: options.to,
|
|
869
|
+
from: options.from || this.config.from || 'approvals@example.com',
|
|
870
|
+
subject,
|
|
871
|
+
html,
|
|
872
|
+
text,
|
|
873
|
+
headers: {
|
|
874
|
+
'X-Approval-Request-Id': options.requestId,
|
|
875
|
+
},
|
|
876
|
+
tags: [
|
|
877
|
+
{ name: 'type', value: 'approval' },
|
|
878
|
+
{ name: 'request_id', value: options.requestId },
|
|
879
|
+
],
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const replyTo = options.replyTo || this.config.replyTo
|
|
883
|
+
if (replyTo) {
|
|
884
|
+
emailMessage.replyTo = replyTo
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const result = await this.provider.send(emailMessage)
|
|
888
|
+
|
|
889
|
+
const deliveryMetadata: Record<string, unknown> = {
|
|
890
|
+
provider: this.provider.name,
|
|
891
|
+
requestId: options.requestId,
|
|
892
|
+
raw: result.raw,
|
|
893
|
+
}
|
|
894
|
+
if (approveUrl) {
|
|
895
|
+
deliveryMetadata['approveUrl'] = approveUrl
|
|
896
|
+
}
|
|
897
|
+
if (rejectUrl) {
|
|
898
|
+
deliveryMetadata['rejectUrl'] = rejectUrl
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const deliveryResult: DeliveryResult = {
|
|
902
|
+
success: result.success,
|
|
903
|
+
transport: 'email',
|
|
904
|
+
metadata: deliveryMetadata,
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
if (result.messageId) {
|
|
908
|
+
deliveryResult.messageId = result.messageId
|
|
909
|
+
}
|
|
910
|
+
if (result.error) {
|
|
911
|
+
deliveryResult.error = result.error
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
return deliveryResult
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Parse an email reply for approval response
|
|
919
|
+
*/
|
|
920
|
+
parseReply(email: InboundEmail): ParsedEmailReply {
|
|
921
|
+
return parseApprovalReply(email)
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Convert parsed reply to ApprovalResult
|
|
926
|
+
*/
|
|
927
|
+
toApprovalResult(reply: ParsedEmailReply, approver?: WorkerRef): ApprovalResult {
|
|
928
|
+
const result: ApprovalResult = {
|
|
929
|
+
approved: reply.approved ?? false,
|
|
930
|
+
via: 'email' as ContactChannel,
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if (approver) {
|
|
934
|
+
result.approvedBy = approver
|
|
935
|
+
} else if (reply.from) {
|
|
936
|
+
result.approvedBy = { id: reply.from }
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
if (reply.repliedAt) {
|
|
940
|
+
result.approvedAt = reply.repliedAt
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (reply.notes) {
|
|
944
|
+
result.notes = reply.notes
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
return result
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Create transport handler for registration
|
|
952
|
+
*/
|
|
953
|
+
createHandler(): TransportHandler {
|
|
954
|
+
return async (payload: MessagePayload, _config: TransportConfig): Promise<DeliveryResult> => {
|
|
955
|
+
const to = Array.isArray(payload.to) ? payload.to : [payload.to]
|
|
956
|
+
|
|
957
|
+
if (payload.type === 'notification') {
|
|
958
|
+
const notifyOptions: {
|
|
959
|
+
to: string[]
|
|
960
|
+
message: string
|
|
961
|
+
subject?: string
|
|
962
|
+
priority?: 'low' | 'normal' | 'high' | 'urgent'
|
|
963
|
+
metadata?: Record<string, unknown>
|
|
964
|
+
from?: string
|
|
965
|
+
replyTo?: string
|
|
966
|
+
} = {
|
|
967
|
+
to,
|
|
968
|
+
message: payload.body,
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (payload.subject) {
|
|
972
|
+
notifyOptions.subject = payload.subject
|
|
973
|
+
}
|
|
974
|
+
if (payload.priority) {
|
|
975
|
+
notifyOptions.priority = payload.priority
|
|
976
|
+
}
|
|
977
|
+
if (payload.metadata) {
|
|
978
|
+
notifyOptions.metadata = payload.metadata
|
|
979
|
+
}
|
|
980
|
+
if (payload.from) {
|
|
981
|
+
notifyOptions.from = payload.from
|
|
982
|
+
}
|
|
983
|
+
if (payload.replyTo) {
|
|
984
|
+
notifyOptions.replyTo = payload.replyTo
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
return this.sendNotification(notifyOptions)
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
if (payload.type === 'approval') {
|
|
991
|
+
const requestId = (payload.metadata?.['requestId'] as string) || generateRequestId('apr')
|
|
992
|
+
|
|
993
|
+
const approvalOptions: {
|
|
994
|
+
to: string[]
|
|
995
|
+
request: string
|
|
996
|
+
requestId: string
|
|
997
|
+
context?: Record<string, unknown>
|
|
998
|
+
from?: string
|
|
999
|
+
replyTo?: string
|
|
1000
|
+
} = {
|
|
1001
|
+
to,
|
|
1002
|
+
request: payload.body,
|
|
1003
|
+
requestId,
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
if (payload.metadata) {
|
|
1007
|
+
approvalOptions.context = payload.metadata
|
|
1008
|
+
}
|
|
1009
|
+
if (payload.from) {
|
|
1010
|
+
approvalOptions.from = payload.from
|
|
1011
|
+
}
|
|
1012
|
+
if (payload.replyTo) {
|
|
1013
|
+
approvalOptions.replyTo = payload.replyTo
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
return this.sendApprovalRequest(approvalOptions)
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Default: send as notification
|
|
1020
|
+
const defaultOptions: {
|
|
1021
|
+
to: string[]
|
|
1022
|
+
message: string
|
|
1023
|
+
subject?: string
|
|
1024
|
+
metadata?: Record<string, unknown>
|
|
1025
|
+
from?: string
|
|
1026
|
+
replyTo?: string
|
|
1027
|
+
} = {
|
|
1028
|
+
to,
|
|
1029
|
+
message: payload.body,
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
if (payload.subject) {
|
|
1033
|
+
defaultOptions.subject = payload.subject
|
|
1034
|
+
}
|
|
1035
|
+
if (payload.metadata) {
|
|
1036
|
+
defaultOptions.metadata = payload.metadata
|
|
1037
|
+
}
|
|
1038
|
+
if (payload.from) {
|
|
1039
|
+
defaultOptions.from = payload.from
|
|
1040
|
+
}
|
|
1041
|
+
if (payload.replyTo) {
|
|
1042
|
+
defaultOptions.replyTo = payload.replyTo
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
return this.sendNotification(defaultOptions)
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* Register this transport with the transport registry
|
|
1051
|
+
*/
|
|
1052
|
+
register(): void {
|
|
1053
|
+
registerTransport('email', this.createHandler())
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// =============================================================================
|
|
1058
|
+
// Factory Functions
|
|
1059
|
+
// =============================================================================
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Create an email transport with Resend
|
|
1063
|
+
*/
|
|
1064
|
+
export function createEmailTransport(
|
|
1065
|
+
config: Omit<EmailTransportConfig, 'transport'>
|
|
1066
|
+
): EmailTransport {
|
|
1067
|
+
return new EmailTransport({ ...config, transport: 'email' })
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Create an email transport with a custom provider
|
|
1072
|
+
*/
|
|
1073
|
+
export function createEmailTransportWithProvider(
|
|
1074
|
+
provider: EmailProvider,
|
|
1075
|
+
config?: Partial<Omit<EmailTransportConfig, 'transport' | 'customProvider'>>
|
|
1076
|
+
): EmailTransport {
|
|
1077
|
+
return new EmailTransport({
|
|
1078
|
+
transport: 'email',
|
|
1079
|
+
customProvider: provider,
|
|
1080
|
+
...config,
|
|
1081
|
+
})
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// =============================================================================
|
|
1085
|
+
// Utility Functions
|
|
1086
|
+
// =============================================================================
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* Escape HTML special characters
|
|
1090
|
+
*/
|
|
1091
|
+
function escapeHtml(text: string): string {
|
|
1092
|
+
const map: Record<string, string> = {
|
|
1093
|
+
'&': '&',
|
|
1094
|
+
'<': '<',
|
|
1095
|
+
'>': '>',
|
|
1096
|
+
'"': '"',
|
|
1097
|
+
"'": ''',
|
|
1098
|
+
}
|
|
1099
|
+
return text.replace(/[&<>"']/g, (char) => map[char] || char)
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/**
|
|
1103
|
+
* Strip HTML tags from content
|
|
1104
|
+
*/
|
|
1105
|
+
function stripHtml(html: string): string {
|
|
1106
|
+
return (
|
|
1107
|
+
html
|
|
1108
|
+
// Add newlines before block elements
|
|
1109
|
+
.replace(/<(p|div|br|li|h[1-6]|tr)[^>]*>/gi, '\n')
|
|
1110
|
+
// Remove all HTML tags
|
|
1111
|
+
.replace(/<[^>]*>/g, '')
|
|
1112
|
+
// Decode HTML entities
|
|
1113
|
+
.replace(/ /g, ' ')
|
|
1114
|
+
.replace(/&/g, '&')
|
|
1115
|
+
.replace(/</g, '<')
|
|
1116
|
+
.replace(/>/g, '>')
|
|
1117
|
+
.replace(/"/g, '"')
|
|
1118
|
+
.replace(/'/g, "'")
|
|
1119
|
+
// Normalize whitespace
|
|
1120
|
+
.replace(/\n\s*\n/g, '\n')
|
|
1121
|
+
.trim()
|
|
1122
|
+
)
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* Truncate text to a maximum length
|
|
1127
|
+
*/
|
|
1128
|
+
function truncate(text: string, maxLength: number): string {
|
|
1129
|
+
if (text.length <= maxLength) return text
|
|
1130
|
+
return text.substring(0, maxLength - 3) + '...'
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// =============================================================================
|
|
1134
|
+
// Type Guards
|
|
1135
|
+
// =============================================================================
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* Check if an object is an EmailTransportConfig
|
|
1139
|
+
*/
|
|
1140
|
+
export function isEmailTransportConfig(config: unknown): config is EmailTransportConfig {
|
|
1141
|
+
return (
|
|
1142
|
+
typeof config === 'object' &&
|
|
1143
|
+
config !== null &&
|
|
1144
|
+
(config as TransportConfig).transport === 'email'
|
|
1145
|
+
)
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/**
|
|
1149
|
+
* Check if a parsed reply indicates approval
|
|
1150
|
+
*/
|
|
1151
|
+
export function isApproved(reply: ParsedEmailReply): boolean {
|
|
1152
|
+
return reply.isApprovalResponse && reply.approved === true
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Check if a parsed reply indicates rejection
|
|
1157
|
+
*/
|
|
1158
|
+
export function isRejected(reply: ParsedEmailReply): boolean {
|
|
1159
|
+
return reply.isApprovalResponse && reply.approved === false
|
|
1160
|
+
}
|