@stacksjs/ts-cloud 0.1.2 → 0.1.5

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 (187) hide show
  1. package/README.md +98 -13
  2. package/dist/aws/acm.d.ts +129 -0
  3. package/dist/aws/application-autoscaling.d.ts +282 -0
  4. package/dist/aws/bedrock.d.ts +2292 -0
  5. package/dist/aws/client.d.ts +79 -0
  6. package/dist/aws/cloudformation.d.ts +105 -0
  7. package/dist/aws/cloudfront.d.ts +265 -0
  8. package/dist/aws/cloudwatch-logs.d.ts +48 -0
  9. package/dist/aws/comprehend.d.ts +505 -0
  10. package/dist/aws/connect.d.ts +377 -0
  11. package/dist/aws/deploy-imap.d.ts +14 -0
  12. package/dist/aws/dynamodb.d.ts +176 -0
  13. package/dist/aws/ec2.d.ts +272 -0
  14. package/dist/aws/ecr.d.ts +149 -0
  15. package/dist/aws/ecs.d.ts +162 -0
  16. package/dist/aws/elasticache.d.ts +71 -0
  17. package/dist/aws/elbv2.d.ts +248 -0
  18. package/dist/aws/email.d.ts +175 -0
  19. package/dist/aws/eventbridge.d.ts +142 -0
  20. package/dist/aws/iam.d.ts +638 -0
  21. package/dist/aws/imap-server.d.ts +119 -0
  22. package/{src/aws/index.ts → dist/aws/index.d.ts} +62 -83
  23. package/{src/aws/kendra.ts → dist/aws/kendra.d.ts} +71 -386
  24. package/dist/aws/lambda.d.ts +232 -0
  25. package/dist/aws/opensearch.d.ts +87 -0
  26. package/dist/aws/personalize.d.ts +516 -0
  27. package/dist/aws/polly.d.ts +214 -0
  28. package/dist/aws/rds.d.ts +240 -0
  29. package/dist/aws/rekognition.d.ts +543 -0
  30. package/dist/aws/route53-domains.d.ts +113 -0
  31. package/dist/aws/route53.d.ts +215 -0
  32. package/dist/aws/s3.d.ts +212 -0
  33. package/dist/aws/scheduler.d.ts +140 -0
  34. package/dist/aws/secrets-manager.d.ts +170 -0
  35. package/dist/aws/ses.d.ts +288 -0
  36. package/dist/aws/setup-phone.d.ts +0 -0
  37. package/dist/aws/setup-sms.d.ts +115 -0
  38. package/dist/aws/sms.d.ts +304 -0
  39. package/dist/aws/smtp-server.d.ts +61 -0
  40. package/dist/aws/sns.d.ts +117 -0
  41. package/dist/aws/sqs.d.ts +65 -0
  42. package/dist/aws/ssm.d.ts +179 -0
  43. package/dist/aws/sts.d.ts +15 -0
  44. package/dist/aws/support.d.ts +104 -0
  45. package/dist/aws/test-imap.d.ts +0 -0
  46. package/dist/aws/textract.d.ts +403 -0
  47. package/dist/aws/transcribe.d.ts +60 -0
  48. package/dist/aws/translate.d.ts +358 -0
  49. package/dist/aws/voice.d.ts +219 -0
  50. package/dist/bin/cli.js +1724 -0
  51. package/dist/config.d.ts +7 -0
  52. package/dist/deploy/index.d.ts +2 -0
  53. package/dist/deploy/static-site-external-dns.d.ts +51 -0
  54. package/dist/deploy/static-site.d.ts +71 -0
  55. package/dist/dns/cloudflare.d.ts +52 -0
  56. package/dist/dns/godaddy.d.ts +38 -0
  57. package/dist/dns/index.d.ts +45 -0
  58. package/dist/dns/porkbun.d.ts +18 -0
  59. package/dist/dns/route53-adapter.d.ts +38 -0
  60. package/{src/dns/types.ts → dist/dns/types.d.ts} +26 -63
  61. package/dist/dns/validator.d.ts +78 -0
  62. package/dist/generators/index.d.ts +1 -0
  63. package/dist/generators/infrastructure.d.ts +30 -0
  64. package/{src/index.ts → dist/index.d.ts} +70 -93
  65. package/dist/index.js +7881 -0
  66. package/dist/push/apns.d.ts +60 -0
  67. package/dist/push/fcm.d.ts +117 -0
  68. package/dist/push/index.d.ts +14 -0
  69. package/dist/security/pre-deploy-scanner.d.ts +69 -0
  70. package/dist/ssl/acme-client.d.ts +67 -0
  71. package/dist/ssl/index.d.ts +2 -0
  72. package/dist/ssl/letsencrypt.d.ts +48 -0
  73. package/dist/types.d.ts +1 -0
  74. package/dist/utils/cli.d.ts +123 -0
  75. package/dist/validation/index.d.ts +1 -0
  76. package/dist/validation/template.d.ts +23 -0
  77. package/package.json +8 -8
  78. package/bin/cli.ts +0 -133
  79. package/bin/commands/analytics.ts +0 -328
  80. package/bin/commands/api.ts +0 -379
  81. package/bin/commands/assets.ts +0 -221
  82. package/bin/commands/audit.ts +0 -501
  83. package/bin/commands/backup.ts +0 -682
  84. package/bin/commands/cache.ts +0 -294
  85. package/bin/commands/cdn.ts +0 -281
  86. package/bin/commands/config.ts +0 -202
  87. package/bin/commands/container.ts +0 -105
  88. package/bin/commands/cost.ts +0 -208
  89. package/bin/commands/database.ts +0 -401
  90. package/bin/commands/deploy.ts +0 -674
  91. package/bin/commands/domain.ts +0 -397
  92. package/bin/commands/email.ts +0 -423
  93. package/bin/commands/environment.ts +0 -285
  94. package/bin/commands/events.ts +0 -424
  95. package/bin/commands/firewall.ts +0 -145
  96. package/bin/commands/function.ts +0 -116
  97. package/bin/commands/generate.ts +0 -280
  98. package/bin/commands/git.ts +0 -139
  99. package/bin/commands/iam.ts +0 -464
  100. package/bin/commands/index.ts +0 -48
  101. package/bin/commands/init.ts +0 -120
  102. package/bin/commands/logs.ts +0 -148
  103. package/bin/commands/network.ts +0 -579
  104. package/bin/commands/notify.ts +0 -489
  105. package/bin/commands/queue.ts +0 -407
  106. package/bin/commands/scheduler.ts +0 -370
  107. package/bin/commands/secrets.ts +0 -54
  108. package/bin/commands/server.ts +0 -629
  109. package/bin/commands/shared.ts +0 -97
  110. package/bin/commands/ssl.ts +0 -138
  111. package/bin/commands/stack.ts +0 -325
  112. package/bin/commands/status.ts +0 -385
  113. package/bin/commands/storage.ts +0 -450
  114. package/bin/commands/team.ts +0 -96
  115. package/bin/commands/tunnel.ts +0 -489
  116. package/bin/commands/utils.ts +0 -202
  117. package/build.ts +0 -15
  118. package/cloud +0 -2
  119. package/src/aws/acm.ts +0 -768
  120. package/src/aws/application-autoscaling.ts +0 -845
  121. package/src/aws/bedrock.ts +0 -4074
  122. package/src/aws/client.ts +0 -878
  123. package/src/aws/cloudformation.ts +0 -896
  124. package/src/aws/cloudfront.ts +0 -1531
  125. package/src/aws/cloudwatch-logs.ts +0 -154
  126. package/src/aws/comprehend.ts +0 -839
  127. package/src/aws/connect.ts +0 -1056
  128. package/src/aws/deploy-imap.ts +0 -384
  129. package/src/aws/dynamodb.ts +0 -340
  130. package/src/aws/ec2.ts +0 -1385
  131. package/src/aws/ecr.ts +0 -621
  132. package/src/aws/ecs.ts +0 -615
  133. package/src/aws/elasticache.ts +0 -301
  134. package/src/aws/elbv2.ts +0 -942
  135. package/src/aws/email.ts +0 -928
  136. package/src/aws/eventbridge.ts +0 -248
  137. package/src/aws/iam.ts +0 -1689
  138. package/src/aws/imap-server.ts +0 -2100
  139. package/src/aws/lambda.ts +0 -786
  140. package/src/aws/opensearch.ts +0 -158
  141. package/src/aws/personalize.ts +0 -977
  142. package/src/aws/polly.ts +0 -559
  143. package/src/aws/rds.ts +0 -888
  144. package/src/aws/rekognition.ts +0 -846
  145. package/src/aws/route53-domains.ts +0 -359
  146. package/src/aws/route53.ts +0 -1046
  147. package/src/aws/s3.ts +0 -2318
  148. package/src/aws/scheduler.ts +0 -571
  149. package/src/aws/secrets-manager.ts +0 -769
  150. package/src/aws/ses.ts +0 -1081
  151. package/src/aws/setup-phone.ts +0 -104
  152. package/src/aws/setup-sms.ts +0 -580
  153. package/src/aws/sms.ts +0 -1735
  154. package/src/aws/smtp-server.ts +0 -531
  155. package/src/aws/sns.ts +0 -758
  156. package/src/aws/sqs.ts +0 -382
  157. package/src/aws/ssm.ts +0 -807
  158. package/src/aws/sts.ts +0 -92
  159. package/src/aws/support.ts +0 -391
  160. package/src/aws/test-imap.ts +0 -86
  161. package/src/aws/textract.ts +0 -780
  162. package/src/aws/transcribe.ts +0 -108
  163. package/src/aws/translate.ts +0 -641
  164. package/src/aws/voice.ts +0 -1379
  165. package/src/config.ts +0 -35
  166. package/src/deploy/index.ts +0 -7
  167. package/src/deploy/static-site-external-dns.ts +0 -906
  168. package/src/deploy/static-site.ts +0 -1125
  169. package/src/dns/godaddy.ts +0 -412
  170. package/src/dns/index.ts +0 -183
  171. package/src/dns/porkbun.ts +0 -362
  172. package/src/dns/route53-adapter.ts +0 -414
  173. package/src/dns/validator.ts +0 -369
  174. package/src/generators/index.ts +0 -5
  175. package/src/generators/infrastructure.ts +0 -1660
  176. package/src/push/apns.ts +0 -452
  177. package/src/push/fcm.ts +0 -506
  178. package/src/push/index.ts +0 -58
  179. package/src/ssl/acme-client.ts +0 -478
  180. package/src/ssl/index.ts +0 -7
  181. package/src/ssl/letsencrypt.ts +0 -747
  182. package/src/types.ts +0 -2
  183. package/src/utils/cli.ts +0 -398
  184. package/src/validation/index.ts +0 -5
  185. package/src/validation/template.ts +0 -405
  186. package/test/index.test.ts +0 -128
  187. package/tsconfig.json +0 -18
package/src/aws/voice.ts DELETED
@@ -1,1379 +0,0 @@
1
- /**
2
- * Unified Voice Module
3
- * Provides voice calling and voicemail with S3 storage
4
- *
5
- * Similar to the Email and SMS modules, this provides:
6
- * - Making outbound calls via Connect
7
- * - Receiving voicemails stored in S3
8
- * - Voicemail management (list, read, delete)
9
- * - Call recording storage
10
- * - Text-to-speech for voice messages
11
- */
12
-
13
- import { ConnectClient } from './connect'
14
- import { S3Client } from './s3'
15
- import { TranscribeClient } from './transcribe'
16
-
17
- export interface VoiceClientConfig {
18
- region?: string
19
- // S3 bucket for storing voicemails and recordings
20
- voicemailBucket?: string
21
- voicemailPrefix?: string
22
- recordingsPrefix?: string
23
- // Connect instance for full call center features
24
- connectInstanceId?: string
25
- connectContactFlowId?: string
26
- // Default caller ID
27
- defaultCallerId?: string
28
- // Enable automatic transcription of voicemails
29
- enableTranscription?: boolean
30
- // Language for transcription
31
- transcriptionLanguage?: string
32
- }
33
-
34
- export interface Voicemail {
35
- key: string
36
- from: string
37
- to: string
38
- duration: number
39
- timestamp: Date
40
- transcription?: string
41
- transcriptionStatus?: 'pending' | 'processing' | 'completed' | 'failed'
42
- transcriptionJobName?: string
43
- audioUrl?: string
44
- // Read/unread status
45
- read?: boolean
46
- readAt?: Date
47
- // Recording file info
48
- contentType?: string
49
- size?: number
50
- // Raw metadata
51
- raw?: any
52
- }
53
-
54
- export interface CallRecording {
55
- key: string
56
- contactId: string
57
- from?: string
58
- to?: string
59
- duration: number
60
- timestamp: Date
61
- audioUrl?: string
62
- contentType?: string
63
- size?: number
64
- }
65
-
66
- export interface MakeCallOptions {
67
- to: string
68
- from?: string
69
- // Text message to speak (TTS)
70
- message?: string
71
- // Voice for TTS
72
- voiceId?: string
73
- // Audio URL to play
74
- audioUrl?: string
75
- // Connect-specific options
76
- contactFlowId?: string
77
- attributes?: Record<string, string>
78
- }
79
-
80
- export interface SendVoiceMessageOptions {
81
- to: string
82
- from?: string
83
- message: string
84
- voiceId?: string
85
- languageCode?: string
86
- }
87
-
88
- export interface VoicemailGreeting {
89
- id: string
90
- name: string
91
- type: 'default' | 'busy' | 'unavailable' | 'custom'
92
- // Text for TTS or audio file key
93
- text?: string
94
- audioKey?: string
95
- audioUrl?: string
96
- voiceId?: string
97
- languageCode?: string
98
- isActive: boolean
99
- createdAt: Date
100
- updatedAt?: Date
101
- }
102
-
103
- export interface CallForwardingRule {
104
- id: string
105
- name: string
106
- enabled: boolean
107
- // When to forward
108
- condition: 'always' | 'busy' | 'no_answer' | 'unreachable' | 'after_hours'
109
- // Where to forward
110
- forwardTo: string
111
- // After how many seconds (for no_answer)
112
- ringTimeout?: number
113
- // Business hours (for after_hours condition)
114
- businessHours?: {
115
- timezone: string
116
- schedule: Array<{
117
- day: 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' | 'sun'
118
- start: string // HH:MM format
119
- end: string
120
- }>
121
- }
122
- // Order of priority (lower = higher priority)
123
- priority: number
124
- createdAt: Date
125
- updatedAt?: Date
126
- }
127
-
128
- /**
129
- * Voice Client with S3 voicemail storage
130
- */
131
- export class VoiceClient {
132
- private config: VoiceClientConfig
133
- private connect?: ConnectClient
134
- private s3?: S3Client
135
- private transcribe?: TranscribeClient
136
-
137
- constructor(config: VoiceClientConfig = {}) {
138
- this.config = {
139
- region: 'us-east-1',
140
- voicemailPrefix: 'voicemail/',
141
- recordingsPrefix: 'recordings/',
142
- transcriptionLanguage: 'en-US',
143
- ...config,
144
- }
145
-
146
- if (this.config.connectInstanceId) {
147
- this.connect = new ConnectClient(this.config.region!)
148
- }
149
-
150
- if (this.config.voicemailBucket) {
151
- this.s3 = new S3Client(this.config.region!)
152
- }
153
-
154
- if (this.config.enableTranscription) {
155
- this.transcribe = new TranscribeClient(this.config.region!)
156
- }
157
- }
158
-
159
- // ============================================
160
- // Making Calls
161
- // ============================================
162
-
163
- /**
164
- * Make an outbound voice call via Connect
165
- */
166
- async call(options: MakeCallOptions): Promise<{ callId: string }> {
167
- if (!this.connect || !this.config.connectInstanceId) {
168
- throw new Error('Connect instance ID required for voice calls. Set connectInstanceId in config.')
169
- }
170
-
171
- const from = options.from || this.config.defaultCallerId
172
- const contactFlowId = options.contactFlowId || this.config.connectContactFlowId
173
-
174
- if (!contactFlowId) {
175
- throw new Error('Contact flow ID required for Connect calls')
176
- }
177
-
178
- const result = await this.connect.makeCall({
179
- instanceId: this.config.connectInstanceId,
180
- contactFlowId,
181
- to: options.to,
182
- from,
183
- attributes: options.attributes,
184
- })
185
-
186
- return { callId: result.ContactId || '' }
187
- }
188
-
189
- /**
190
- * Send a voice message (one-way TTS call) via Connect
191
- *
192
- * Note: This requires a Contact Flow configured for TTS playback.
193
- * The message is passed as an attribute that the Contact Flow can use.
194
- */
195
- async sendVoiceMessage(options: SendVoiceMessageOptions): Promise<{ messageId: string }> {
196
- const result = await this.call({
197
- to: options.to,
198
- from: options.from,
199
- attributes: {
200
- message: options.message,
201
- voiceId: options.voiceId || 'Joanna',
202
- languageCode: options.languageCode || 'en-US',
203
- },
204
- })
205
-
206
- return { messageId: result.callId }
207
- }
208
-
209
- /**
210
- * Send a TTS voice message (alias for sendVoiceMessage)
211
- */
212
- async speak(to: string, message: string, voiceId?: string): Promise<{ messageId: string }> {
213
- return this.sendVoiceMessage({ to, message, voiceId })
214
- }
215
-
216
- // ============================================
217
- // Voicemail Management (S3 Storage)
218
- // ============================================
219
-
220
- /**
221
- * Get voicemails from S3
222
- */
223
- async getVoicemails(options: {
224
- prefix?: string
225
- maxResults?: number
226
- } = {}): Promise<Voicemail[]> {
227
- if (!this.s3 || !this.config.voicemailBucket) {
228
- throw new Error('Voicemail bucket not configured')
229
- }
230
-
231
- const prefix = options.prefix || this.config.voicemailPrefix || 'voicemail/'
232
- const objects = await this.s3.list({
233
- bucket: this.config.voicemailBucket,
234
- prefix,
235
- maxKeys: options.maxResults || 100,
236
- })
237
-
238
- const voicemails: Voicemail[] = []
239
-
240
- for (const obj of objects || []) {
241
- if (!obj.Key) continue
242
-
243
- // Skip non-audio files (metadata files, etc.)
244
- if (obj.Key.endsWith('.json')) {
245
- continue
246
- }
247
-
248
- try {
249
- const voicemail = await this.getVoicemailMetadata(obj.Key)
250
- if (voicemail) {
251
- voicemails.push({
252
- ...voicemail,
253
- size: obj.Size,
254
- })
255
- }
256
- } catch (err) {
257
- console.error(`Failed to read voicemail ${obj.Key}:`, err)
258
- }
259
- }
260
-
261
- // Sort by timestamp descending (newest first)
262
- return voicemails.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
263
- }
264
-
265
- /**
266
- * Get a specific voicemail
267
- */
268
- async getVoicemail(key: string): Promise<Voicemail | null> {
269
- if (!this.s3 || !this.config.voicemailBucket) {
270
- throw new Error('Voicemail bucket not configured')
271
- }
272
-
273
- return this.getVoicemailMetadata(key)
274
- }
275
-
276
- /**
277
- * Get voicemail audio as a signed URL
278
- */
279
- async getVoicemailAudioUrl(key: string, expiresIn: number = 3600): Promise<string> {
280
- if (!this.s3 || !this.config.voicemailBucket) {
281
- throw new Error('Voicemail bucket not configured')
282
- }
283
-
284
- // Generate a presigned URL for the audio file
285
- return this.s3.getSignedUrl({
286
- bucket: this.config.voicemailBucket,
287
- key,
288
- expiresIn,
289
- })
290
- }
291
-
292
- /**
293
- * Delete a voicemail
294
- */
295
- async deleteVoicemail(key: string): Promise<void> {
296
- if (!this.s3 || !this.config.voicemailBucket) {
297
- throw new Error('Voicemail bucket not configured')
298
- }
299
-
300
- // Delete audio file
301
- await this.s3.deleteObject(this.config.voicemailBucket, key)
302
-
303
- // Delete metadata file if exists
304
- const metadataKey = key.replace(/\.[^/.]+$/, '.json')
305
- try {
306
- await this.s3.deleteObject(this.config.voicemailBucket, metadataKey)
307
- } catch {
308
- // Metadata might not exist
309
- }
310
- }
311
-
312
- /**
313
- * Archive a voicemail
314
- */
315
- async archiveVoicemail(key: string): Promise<string> {
316
- if (!this.s3 || !this.config.voicemailBucket) {
317
- throw new Error('Voicemail bucket not configured')
318
- }
319
-
320
- const filename = key.split('/').pop() || `${Date.now()}.wav`
321
- const newKey = `voicemail/archive/${filename}`
322
-
323
- // Copy to archive
324
- await this.s3.copyObject({
325
- sourceBucket: this.config.voicemailBucket,
326
- sourceKey: key,
327
- destinationBucket: this.config.voicemailBucket,
328
- destinationKey: newKey,
329
- })
330
-
331
- // Delete original
332
- await this.s3.deleteObject(this.config.voicemailBucket, key)
333
-
334
- return newKey
335
- }
336
-
337
- /**
338
- * Get voicemail count
339
- */
340
- async getVoicemailCount(): Promise<number> {
341
- if (!this.s3 || !this.config.voicemailBucket) {
342
- throw new Error('Voicemail bucket not configured')
343
- }
344
-
345
- const objects = await this.s3.list({
346
- bucket: this.config.voicemailBucket,
347
- prefix: this.config.voicemailPrefix || 'voicemail/',
348
- maxKeys: 1000,
349
- })
350
-
351
- // Count only audio files, not metadata
352
- return (objects || []).filter(obj =>
353
- obj.Key && !obj.Key.endsWith('.json') && !obj.Key.endsWith('/')
354
- ).length
355
- }
356
-
357
- /**
358
- * Get unread voicemail count
359
- */
360
- async getUnreadCount(): Promise<number> {
361
- const voicemails = await this.getVoicemails({ maxResults: 1000 })
362
- return voicemails.filter(v => !v.read).length
363
- }
364
-
365
- /**
366
- * Mark a voicemail as read
367
- */
368
- async markAsRead(key: string): Promise<void> {
369
- if (!this.s3 || !this.config.voicemailBucket) {
370
- throw new Error('Voicemail bucket not configured')
371
- }
372
-
373
- const metadataKey = key.replace(/\.[^/.]+$/, '.json')
374
- try {
375
- const content = await this.s3.getObject(this.config.voicemailBucket, metadataKey)
376
- const data = JSON.parse(content)
377
- data.read = true
378
- data.readAt = new Date().toISOString()
379
-
380
- await this.s3.putObject({
381
- bucket: this.config.voicemailBucket,
382
- key: metadataKey,
383
- body: JSON.stringify(data, null, 2),
384
- contentType: 'application/json',
385
- })
386
- } catch {
387
- // Create metadata if it doesn't exist
388
- await this.s3.putObject({
389
- bucket: this.config.voicemailBucket,
390
- key: metadataKey,
391
- body: JSON.stringify({ read: true, readAt: new Date().toISOString() }, null, 2),
392
- contentType: 'application/json',
393
- })
394
- }
395
- }
396
-
397
- /**
398
- * Mark a voicemail as unread
399
- */
400
- async markAsUnread(key: string): Promise<void> {
401
- if (!this.s3 || !this.config.voicemailBucket) {
402
- throw new Error('Voicemail bucket not configured')
403
- }
404
-
405
- const metadataKey = key.replace(/\.[^/.]+$/, '.json')
406
- try {
407
- const content = await this.s3.getObject(this.config.voicemailBucket, metadataKey)
408
- const data = JSON.parse(content)
409
- data.read = false
410
- delete data.readAt
411
-
412
- await this.s3.putObject({
413
- bucket: this.config.voicemailBucket,
414
- key: metadataKey,
415
- body: JSON.stringify(data, null, 2),
416
- contentType: 'application/json',
417
- })
418
- } catch {
419
- // Ignore if metadata doesn't exist
420
- }
421
- }
422
-
423
- /**
424
- * Batch mark voicemails as read
425
- */
426
- async markManyAsRead(keys: string[]): Promise<void> {
427
- await Promise.all(keys.map(key => this.markAsRead(key)))
428
- }
429
-
430
- /**
431
- * Batch delete voicemails
432
- */
433
- async deleteMany(keys: string[]): Promise<void> {
434
- await Promise.all(keys.map(key => this.deleteVoicemail(key)))
435
- }
436
-
437
- // ============================================
438
- // Transcription
439
- // ============================================
440
-
441
- /**
442
- * Start transcription for a voicemail
443
- */
444
- async transcribeVoicemail(key: string): Promise<{ jobName: string }> {
445
- if (!this.transcribe) {
446
- throw new Error('Transcription not enabled. Set enableTranscription: true in config')
447
- }
448
- if (!this.s3 || !this.config.voicemailBucket) {
449
- throw new Error('Voicemail bucket not configured')
450
- }
451
-
452
- const jobName = `voicemail-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
453
- const mediaUri = `s3://${this.config.voicemailBucket}/${key}`
454
-
455
- // Determine media format from extension
456
- const ext = key.split('.').pop()?.toLowerCase()
457
- const mediaFormat = ext === 'mp3' ? 'mp3' : ext === 'wav' ? 'wav' : 'wav'
458
-
459
- await this.transcribe.startTranscriptionJob({
460
- TranscriptionJobName: jobName,
461
- LanguageCode: this.config.transcriptionLanguage || 'en-US',
462
- Media: { MediaFileUri: mediaUri },
463
- MediaFormat: mediaFormat,
464
- OutputBucketName: this.config.voicemailBucket,
465
- OutputKey: key.replace(/\.[^/.]+$/, '-transcript.json'),
466
- })
467
-
468
- // Update metadata with job info
469
- const metadataKey = key.replace(/\.[^/.]+$/, '.json')
470
- try {
471
- const content = await this.s3.getObject(this.config.voicemailBucket, metadataKey)
472
- const data = JSON.parse(content)
473
- data.transcriptionJobName = jobName
474
- data.transcriptionStatus = 'processing'
475
-
476
- await this.s3.putObject({
477
- bucket: this.config.voicemailBucket,
478
- key: metadataKey,
479
- body: JSON.stringify(data, null, 2),
480
- contentType: 'application/json',
481
- })
482
- } catch {
483
- // Create metadata if it doesn't exist
484
- await this.s3.putObject({
485
- bucket: this.config.voicemailBucket,
486
- key: metadataKey,
487
- body: JSON.stringify({
488
- transcriptionJobName: jobName,
489
- transcriptionStatus: 'processing',
490
- }, null, 2),
491
- contentType: 'application/json',
492
- })
493
- }
494
-
495
- return { jobName }
496
- }
497
-
498
- /**
499
- * Get transcription status for a voicemail
500
- */
501
- async getTranscriptionStatus(jobName: string): Promise<{
502
- status: 'IN_PROGRESS' | 'COMPLETED' | 'FAILED'
503
- transcription?: string
504
- }> {
505
- if (!this.transcribe) {
506
- throw new Error('Transcription not enabled')
507
- }
508
-
509
- const job = await this.transcribe.getTranscriptionJob({ TranscriptionJobName: jobName })
510
- const status = job.TranscriptionJob?.TranscriptionJobStatus
511
-
512
- if (status === 'COMPLETED' && job.TranscriptionJob?.Transcript?.TranscriptFileUri) {
513
- // Fetch the transcript from S3
514
- const transcriptUri = job.TranscriptionJob.Transcript.TranscriptFileUri
515
- // The URI is like s3://bucket/key or https://s3.region.amazonaws.com/bucket/key
516
- // We need to extract the key
517
- try {
518
- if (this.s3 && this.config.voicemailBucket && transcriptUri.includes(this.config.voicemailBucket)) {
519
- const key = transcriptUri.split(this.config.voicemailBucket + '/')[1]
520
- if (key) {
521
- const content = await this.s3.getObject(this.config.voicemailBucket, key)
522
- const transcript = JSON.parse(content)
523
- return {
524
- status: 'COMPLETED',
525
- transcription: transcript.results?.transcripts?.[0]?.transcript || '',
526
- }
527
- }
528
- }
529
- } catch {
530
- // Could not fetch transcript
531
- }
532
- }
533
-
534
- return { status: status as 'IN_PROGRESS' | 'COMPLETED' | 'FAILED' }
535
- }
536
-
537
- /**
538
- * Update voicemail metadata with completed transcription
539
- */
540
- async updateTranscription(key: string, transcription: string): Promise<void> {
541
- if (!this.s3 || !this.config.voicemailBucket) {
542
- throw new Error('Voicemail bucket not configured')
543
- }
544
-
545
- const metadataKey = key.replace(/\.[^/.]+$/, '.json')
546
- try {
547
- const content = await this.s3.getObject(this.config.voicemailBucket, metadataKey)
548
- const data = JSON.parse(content)
549
- data.transcription = transcription
550
- data.transcriptionStatus = 'completed'
551
-
552
- await this.s3.putObject({
553
- bucket: this.config.voicemailBucket,
554
- key: metadataKey,
555
- body: JSON.stringify(data, null, 2),
556
- contentType: 'application/json',
557
- })
558
- } catch {
559
- await this.s3.putObject({
560
- bucket: this.config.voicemailBucket,
561
- key: metadataKey,
562
- body: JSON.stringify({
563
- transcription,
564
- transcriptionStatus: 'completed',
565
- }, null, 2),
566
- contentType: 'application/json',
567
- })
568
- }
569
- }
570
-
571
- // ============================================
572
- // Voicemail Greetings
573
- // ============================================
574
-
575
- /**
576
- * Create a voicemail greeting
577
- */
578
- async createGreeting(greeting: {
579
- name: string
580
- type: 'default' | 'busy' | 'unavailable' | 'custom'
581
- text?: string
582
- audioData?: Buffer | string
583
- voiceId?: string
584
- languageCode?: string
585
- setActive?: boolean
586
- }): Promise<VoicemailGreeting> {
587
- if (!this.s3 || !this.config.voicemailBucket) {
588
- throw new Error('Voicemail bucket not configured')
589
- }
590
-
591
- const id = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
592
- let audioKey: string | undefined
593
-
594
- // If audio data provided, store it
595
- if (greeting.audioData) {
596
- audioKey = `greetings/${id}.wav`
597
- await this.s3.putObject({
598
- bucket: this.config.voicemailBucket,
599
- key: audioKey,
600
- body: greeting.audioData,
601
- contentType: 'audio/wav',
602
- })
603
- }
604
-
605
- const newGreeting: VoicemailGreeting = {
606
- id,
607
- name: greeting.name,
608
- type: greeting.type,
609
- text: greeting.text,
610
- audioKey,
611
- voiceId: greeting.voiceId || 'Joanna',
612
- languageCode: greeting.languageCode || 'en-US',
613
- isActive: greeting.setActive || false,
614
- createdAt: new Date(),
615
- }
616
-
617
- // If setting as active, deactivate other greetings of same type
618
- if (greeting.setActive) {
619
- await this.deactivateGreetingsOfType(greeting.type)
620
- newGreeting.isActive = true
621
- }
622
-
623
- // Store metadata
624
- await this.s3.putObject({
625
- bucket: this.config.voicemailBucket,
626
- key: `greetings/${id}.json`,
627
- body: JSON.stringify(newGreeting, null, 2),
628
- contentType: 'application/json',
629
- })
630
-
631
- return newGreeting
632
- }
633
-
634
- /**
635
- * Get all voicemail greetings
636
- */
637
- async getGreetings(): Promise<VoicemailGreeting[]> {
638
- if (!this.s3 || !this.config.voicemailBucket) {
639
- throw new Error('Voicemail bucket not configured')
640
- }
641
-
642
- const objects = await this.s3.list({
643
- bucket: this.config.voicemailBucket,
644
- prefix: 'greetings/',
645
- maxKeys: 100,
646
- })
647
-
648
- const greetings: VoicemailGreeting[] = []
649
- for (const obj of objects || []) {
650
- if (!obj.Key || !obj.Key.endsWith('.json')) continue
651
- try {
652
- const content = await this.s3.getObject(this.config.voicemailBucket, obj.Key)
653
- const greeting = JSON.parse(content) as VoicemailGreeting
654
- greeting.createdAt = new Date(greeting.createdAt)
655
- if (greeting.updatedAt) greeting.updatedAt = new Date(greeting.updatedAt)
656
- greetings.push(greeting)
657
- } catch {
658
- // Skip invalid
659
- }
660
- }
661
-
662
- return greetings.sort((a, b) => a.name.localeCompare(b.name))
663
- }
664
-
665
- /**
666
- * Get a specific greeting
667
- */
668
- async getGreeting(id: string): Promise<VoicemailGreeting | null> {
669
- if (!this.s3 || !this.config.voicemailBucket) {
670
- return null
671
- }
672
-
673
- try {
674
- const content = await this.s3.getObject(this.config.voicemailBucket, `greetings/${id}.json`)
675
- const greeting = JSON.parse(content) as VoicemailGreeting
676
- greeting.createdAt = new Date(greeting.createdAt)
677
- if (greeting.updatedAt) greeting.updatedAt = new Date(greeting.updatedAt)
678
- return greeting
679
- } catch {
680
- return null
681
- }
682
- }
683
-
684
- /**
685
- * Get the active greeting of a specific type
686
- */
687
- async getActiveGreeting(type: 'default' | 'busy' | 'unavailable' | 'custom'): Promise<VoicemailGreeting | null> {
688
- const greetings = await this.getGreetings()
689
- return greetings.find(g => g.type === type && g.isActive) || null
690
- }
691
-
692
- /**
693
- * Set a greeting as active
694
- */
695
- async setActiveGreeting(id: string): Promise<void> {
696
- if (!this.s3 || !this.config.voicemailBucket) {
697
- throw new Error('Voicemail bucket not configured')
698
- }
699
-
700
- const greeting = await this.getGreeting(id)
701
- if (!greeting) throw new Error(`Greeting ${id} not found`)
702
-
703
- // Deactivate other greetings of same type
704
- await this.deactivateGreetingsOfType(greeting.type)
705
-
706
- // Activate this greeting
707
- greeting.isActive = true
708
- greeting.updatedAt = new Date()
709
-
710
- await this.s3.putObject({
711
- bucket: this.config.voicemailBucket,
712
- key: `greetings/${id}.json`,
713
- body: JSON.stringify(greeting, null, 2),
714
- contentType: 'application/json',
715
- })
716
- }
717
-
718
- /**
719
- * Deactivate all greetings of a type
720
- */
721
- private async deactivateGreetingsOfType(type: string): Promise<void> {
722
- const greetings = await this.getGreetings()
723
- for (const g of greetings) {
724
- if (g.type === type && g.isActive) {
725
- g.isActive = false
726
- g.updatedAt = new Date()
727
- await this.s3!.putObject({
728
- bucket: this.config.voicemailBucket!,
729
- key: `greetings/${g.id}.json`,
730
- body: JSON.stringify(g, null, 2),
731
- contentType: 'application/json',
732
- })
733
- }
734
- }
735
- }
736
-
737
- /**
738
- * Update a greeting
739
- */
740
- async updateGreeting(
741
- id: string,
742
- updates: { name?: string; text?: string; audioData?: Buffer | string },
743
- ): Promise<VoicemailGreeting> {
744
- if (!this.s3 || !this.config.voicemailBucket) {
745
- throw new Error('Voicemail bucket not configured')
746
- }
747
-
748
- const greeting = await this.getGreeting(id)
749
- if (!greeting) throw new Error(`Greeting ${id} not found`)
750
-
751
- if (updates.name) greeting.name = updates.name
752
- if (updates.text) greeting.text = updates.text
753
- if (updates.audioData) {
754
- const audioKey = `greetings/${id}.wav`
755
- await this.s3.putObject({
756
- bucket: this.config.voicemailBucket,
757
- key: audioKey,
758
- body: updates.audioData,
759
- contentType: 'audio/wav',
760
- })
761
- greeting.audioKey = audioKey
762
- }
763
- greeting.updatedAt = new Date()
764
-
765
- await this.s3.putObject({
766
- bucket: this.config.voicemailBucket,
767
- key: `greetings/${id}.json`,
768
- body: JSON.stringify(greeting, null, 2),
769
- contentType: 'application/json',
770
- })
771
-
772
- return greeting
773
- }
774
-
775
- /**
776
- * Delete a greeting
777
- */
778
- async deleteGreeting(id: string): Promise<void> {
779
- if (!this.s3 || !this.config.voicemailBucket) {
780
- throw new Error('Voicemail bucket not configured')
781
- }
782
-
783
- const greeting = await this.getGreeting(id)
784
- if (greeting?.audioKey) {
785
- try {
786
- await this.s3.deleteObject(this.config.voicemailBucket, greeting.audioKey)
787
- } catch {
788
- // Audio may not exist
789
- }
790
- }
791
-
792
- await this.s3.deleteObject(this.config.voicemailBucket, `greetings/${id}.json`)
793
- }
794
-
795
- /**
796
- * Get a signed URL for a greeting audio
797
- */
798
- async getGreetingAudioUrl(id: string, expiresIn: number = 3600): Promise<string | null> {
799
- const greeting = await this.getGreeting(id)
800
- if (!greeting?.audioKey || !this.s3 || !this.config.voicemailBucket) {
801
- return null
802
- }
803
-
804
- return this.s3.getSignedUrl({
805
- bucket: this.config.voicemailBucket,
806
- key: greeting.audioKey,
807
- expiresIn,
808
- })
809
- }
810
-
811
- // ============================================
812
- // Call Forwarding Rules
813
- // ============================================
814
-
815
- /**
816
- * Create a call forwarding rule
817
- */
818
- async createForwardingRule(rule: {
819
- name: string
820
- condition: 'always' | 'busy' | 'no_answer' | 'unreachable' | 'after_hours'
821
- forwardTo: string
822
- ringTimeout?: number
823
- businessHours?: CallForwardingRule['businessHours']
824
- priority?: number
825
- enabled?: boolean
826
- }): Promise<CallForwardingRule> {
827
- if (!this.s3 || !this.config.voicemailBucket) {
828
- throw new Error('Voicemail bucket not configured')
829
- }
830
-
831
- const id = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
832
-
833
- const newRule: CallForwardingRule = {
834
- id,
835
- name: rule.name,
836
- enabled: rule.enabled !== false,
837
- condition: rule.condition,
838
- forwardTo: rule.forwardTo,
839
- ringTimeout: rule.ringTimeout || 20,
840
- businessHours: rule.businessHours,
841
- priority: rule.priority || 100,
842
- createdAt: new Date(),
843
- }
844
-
845
- await this.s3.putObject({
846
- bucket: this.config.voicemailBucket,
847
- key: `forwarding/${id}.json`,
848
- body: JSON.stringify(newRule, null, 2),
849
- contentType: 'application/json',
850
- })
851
-
852
- return newRule
853
- }
854
-
855
- /**
856
- * Get all forwarding rules
857
- */
858
- async getForwardingRules(): Promise<CallForwardingRule[]> {
859
- if (!this.s3 || !this.config.voicemailBucket) {
860
- throw new Error('Voicemail bucket not configured')
861
- }
862
-
863
- const objects = await this.s3.list({
864
- bucket: this.config.voicemailBucket,
865
- prefix: 'forwarding/',
866
- maxKeys: 100,
867
- })
868
-
869
- const rules: CallForwardingRule[] = []
870
- for (const obj of objects || []) {
871
- if (!obj.Key || !obj.Key.endsWith('.json')) continue
872
- try {
873
- const content = await this.s3.getObject(this.config.voicemailBucket, obj.Key)
874
- const rule = JSON.parse(content) as CallForwardingRule
875
- rule.createdAt = new Date(rule.createdAt)
876
- if (rule.updatedAt) rule.updatedAt = new Date(rule.updatedAt)
877
- rules.push(rule)
878
- } catch {
879
- // Skip invalid
880
- }
881
- }
882
-
883
- return rules.sort((a, b) => a.priority - b.priority)
884
- }
885
-
886
- /**
887
- * Get a specific forwarding rule
888
- */
889
- async getForwardingRule(id: string): Promise<CallForwardingRule | null> {
890
- if (!this.s3 || !this.config.voicemailBucket) {
891
- return null
892
- }
893
-
894
- try {
895
- const content = await this.s3.getObject(this.config.voicemailBucket, `forwarding/${id}.json`)
896
- const rule = JSON.parse(content) as CallForwardingRule
897
- rule.createdAt = new Date(rule.createdAt)
898
- if (rule.updatedAt) rule.updatedAt = new Date(rule.updatedAt)
899
- return rule
900
- } catch {
901
- return null
902
- }
903
- }
904
-
905
- /**
906
- * Update a forwarding rule
907
- */
908
- async updateForwardingRule(
909
- id: string,
910
- updates: Partial<Omit<CallForwardingRule, 'id' | 'createdAt' | 'updatedAt'>>,
911
- ): Promise<CallForwardingRule> {
912
- if (!this.s3 || !this.config.voicemailBucket) {
913
- throw new Error('Voicemail bucket not configured')
914
- }
915
-
916
- const rule = await this.getForwardingRule(id)
917
- if (!rule) throw new Error(`Forwarding rule ${id} not found`)
918
-
919
- if (updates.name !== undefined) rule.name = updates.name
920
- if (updates.enabled !== undefined) rule.enabled = updates.enabled
921
- if (updates.condition !== undefined) rule.condition = updates.condition
922
- if (updates.forwardTo !== undefined) rule.forwardTo = updates.forwardTo
923
- if (updates.ringTimeout !== undefined) rule.ringTimeout = updates.ringTimeout
924
- if (updates.businessHours !== undefined) rule.businessHours = updates.businessHours
925
- if (updates.priority !== undefined) rule.priority = updates.priority
926
- rule.updatedAt = new Date()
927
-
928
- await this.s3.putObject({
929
- bucket: this.config.voicemailBucket,
930
- key: `forwarding/${id}.json`,
931
- body: JSON.stringify(rule, null, 2),
932
- contentType: 'application/json',
933
- })
934
-
935
- return rule
936
- }
937
-
938
- /**
939
- * Enable/disable a forwarding rule
940
- */
941
- async setForwardingRuleEnabled(id: string, enabled: boolean): Promise<void> {
942
- await this.updateForwardingRule(id, { enabled })
943
- }
944
-
945
- /**
946
- * Delete a forwarding rule
947
- */
948
- async deleteForwardingRule(id: string): Promise<void> {
949
- if (!this.s3 || !this.config.voicemailBucket) {
950
- throw new Error('Voicemail bucket not configured')
951
- }
952
-
953
- await this.s3.deleteObject(this.config.voicemailBucket, `forwarding/${id}.json`)
954
- }
955
-
956
- /**
957
- * Get the applicable forwarding rule for current conditions
958
- * Returns the highest priority enabled rule that matches current conditions
959
- */
960
- async getApplicableForwardingRule(
961
- currentConditions: {
962
- isBusy?: boolean
963
- isUnreachable?: boolean
964
- noAnswer?: boolean
965
- } = {},
966
- ): Promise<CallForwardingRule | null> {
967
- const rules = await this.getForwardingRules()
968
- const now = new Date()
969
-
970
- for (const rule of rules) {
971
- if (!rule.enabled) continue
972
-
973
- switch (rule.condition) {
974
- case 'always':
975
- return rule
976
- case 'busy':
977
- if (currentConditions.isBusy) return rule
978
- break
979
- case 'no_answer':
980
- if (currentConditions.noAnswer) return rule
981
- break
982
- case 'unreachable':
983
- if (currentConditions.isUnreachable) return rule
984
- break
985
- case 'after_hours':
986
- if (rule.businessHours && !this.isWithinBusinessHours(now, rule.businessHours)) {
987
- return rule
988
- }
989
- break
990
- }
991
- }
992
-
993
- return null
994
- }
995
-
996
- /**
997
- * Check if a time is within business hours
998
- */
999
- private isWithinBusinessHours(
1000
- date: Date,
1001
- businessHours: NonNullable<CallForwardingRule['businessHours']>,
1002
- ): boolean {
1003
- const days = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] as const
1004
-
1005
- // Convert to business timezone if needed (simplified - uses local time)
1006
- const dayName = days[date.getDay()]
1007
- const timeStr = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
1008
-
1009
- for (const schedule of businessHours.schedule) {
1010
- if (schedule.day === dayName) {
1011
- if (timeStr >= schedule.start && timeStr <= schedule.end) {
1012
- return true
1013
- }
1014
- }
1015
- }
1016
-
1017
- return false
1018
- }
1019
-
1020
- // ============================================
1021
- // Call Recordings (Connect)
1022
- // ============================================
1023
-
1024
- /**
1025
- * Get call recordings from S3
1026
- */
1027
- async getRecordings(options: {
1028
- prefix?: string
1029
- maxResults?: number
1030
- } = {}): Promise<CallRecording[]> {
1031
- if (!this.s3 || !this.config.voicemailBucket) {
1032
- throw new Error('Recordings bucket not configured')
1033
- }
1034
-
1035
- const prefix = options.prefix || this.config.recordingsPrefix || 'recordings/'
1036
- const objects = await this.s3.list({
1037
- bucket: this.config.voicemailBucket,
1038
- prefix,
1039
- maxKeys: options.maxResults || 100,
1040
- })
1041
-
1042
- const recordings: CallRecording[] = []
1043
-
1044
- for (const obj of objects || []) {
1045
- if (!obj.Key || obj.Key.endsWith('.json') || obj.Key.endsWith('/')) continue
1046
-
1047
- try {
1048
- // Try to get metadata
1049
- const metadataKey = obj.Key.replace(/\.[^/.]+$/, '.json')
1050
- let metadata: any = {}
1051
-
1052
- try {
1053
- const metadataContent = await this.s3.getObject(this.config.voicemailBucket!, metadataKey)
1054
- metadata = JSON.parse(metadataContent)
1055
- } catch {
1056
- // No metadata file
1057
- }
1058
-
1059
- recordings.push({
1060
- key: obj.Key,
1061
- contactId: metadata.contactId || obj.Key.split('/').pop()?.split('.')[0] || '',
1062
- from: metadata.from,
1063
- to: metadata.to,
1064
- duration: metadata.duration || 0,
1065
- timestamp: new Date(obj.LastModified || Date.now()),
1066
- contentType: 'audio/wav',
1067
- size: obj.Size,
1068
- })
1069
- } catch (err) {
1070
- console.error(`Failed to read recording ${obj.Key}:`, err)
1071
- }
1072
- }
1073
-
1074
- return recordings.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
1075
- }
1076
-
1077
- /**
1078
- * Get a recording audio URL
1079
- */
1080
- async getRecordingUrl(key: string, expiresIn: number = 3600): Promise<string> {
1081
- if (!this.s3 || !this.config.voicemailBucket) {
1082
- throw new Error('Recordings bucket not configured')
1083
- }
1084
-
1085
- return this.s3.getSignedUrl({
1086
- bucket: this.config.voicemailBucket,
1087
- key,
1088
- expiresIn,
1089
- })
1090
- }
1091
-
1092
- // ============================================
1093
- // Voicemail Ingestion
1094
- // ============================================
1095
-
1096
- /**
1097
- * Store an incoming voicemail to S3
1098
- * This is typically called from a Lambda handler
1099
- */
1100
- async storeVoicemail(voicemail: {
1101
- from: string
1102
- to: string
1103
- audioData: Buffer | string
1104
- duration?: number
1105
- transcription?: string
1106
- contentType?: string
1107
- timestamp?: Date
1108
- }): Promise<string> {
1109
- if (!this.s3 || !this.config.voicemailBucket) {
1110
- throw new Error('Voicemail bucket not configured')
1111
- }
1112
-
1113
- const timestamp = voicemail.timestamp || new Date()
1114
- const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`
1115
- const ext = voicemail.contentType === 'audio/mp3' ? 'mp3' : 'wav'
1116
- const audioKey = `${this.config.voicemailPrefix}${timestamp.toISOString().split('T')[0]}/${id}.${ext}`
1117
- const metadataKey = `${this.config.voicemailPrefix}${timestamp.toISOString().split('T')[0]}/${id}.json`
1118
-
1119
- // Store audio file
1120
- await this.s3.putObject({
1121
- bucket: this.config.voicemailBucket,
1122
- key: audioKey,
1123
- body: voicemail.audioData,
1124
- contentType: voicemail.contentType || 'audio/wav',
1125
- })
1126
-
1127
- // Store metadata
1128
- const metadata = {
1129
- from: voicemail.from,
1130
- to: voicemail.to,
1131
- duration: voicemail.duration || 0,
1132
- transcription: voicemail.transcription,
1133
- timestamp: timestamp.toISOString(),
1134
- audioKey,
1135
- }
1136
-
1137
- await this.s3.putObject({
1138
- bucket: this.config.voicemailBucket,
1139
- key: metadataKey,
1140
- body: JSON.stringify(metadata, null, 2),
1141
- contentType: 'application/json',
1142
- })
1143
-
1144
- return audioKey
1145
- }
1146
-
1147
- /**
1148
- * Store a call recording to S3
1149
- */
1150
- async storeRecording(recording: {
1151
- contactId: string
1152
- from?: string
1153
- to?: string
1154
- audioData: Buffer | string
1155
- duration?: number
1156
- contentType?: string
1157
- timestamp?: Date
1158
- }): Promise<string> {
1159
- if (!this.s3 || !this.config.voicemailBucket) {
1160
- throw new Error('Recordings bucket not configured')
1161
- }
1162
-
1163
- const timestamp = recording.timestamp || new Date()
1164
- const ext = recording.contentType === 'audio/mp3' ? 'mp3' : 'wav'
1165
- const audioKey = `${this.config.recordingsPrefix}${timestamp.toISOString().split('T')[0]}/${recording.contactId}.${ext}`
1166
- const metadataKey = `${this.config.recordingsPrefix}${timestamp.toISOString().split('T')[0]}/${recording.contactId}.json`
1167
-
1168
- // Store audio file
1169
- await this.s3.putObject({
1170
- bucket: this.config.voicemailBucket,
1171
- key: audioKey,
1172
- body: recording.audioData,
1173
- contentType: recording.contentType || 'audio/wav',
1174
- })
1175
-
1176
- // Store metadata
1177
- const metadata = {
1178
- contactId: recording.contactId,
1179
- from: recording.from,
1180
- to: recording.to,
1181
- duration: recording.duration || 0,
1182
- timestamp: timestamp.toISOString(),
1183
- audioKey,
1184
- }
1185
-
1186
- await this.s3.putObject({
1187
- bucket: this.config.voicemailBucket,
1188
- key: metadataKey,
1189
- body: JSON.stringify(metadata, null, 2),
1190
- contentType: 'application/json',
1191
- })
1192
-
1193
- return audioKey
1194
- }
1195
-
1196
- // ============================================
1197
- // Private Helpers
1198
- // ============================================
1199
-
1200
- /**
1201
- * Get voicemail metadata from S3
1202
- */
1203
- private async getVoicemailMetadata(audioKey: string): Promise<Voicemail | null> {
1204
- if (!this.s3 || !this.config.voicemailBucket) {
1205
- return null
1206
- }
1207
-
1208
- // Try to get metadata file
1209
- const metadataKey = audioKey.replace(/\.[^/.]+$/, '.json')
1210
-
1211
- try {
1212
- const metadataContent = await this.s3.getObject(this.config.voicemailBucket, metadataKey)
1213
- const metadata = JSON.parse(metadataContent)
1214
-
1215
- return {
1216
- key: audioKey,
1217
- from: metadata.from || 'unknown',
1218
- to: metadata.to || 'unknown',
1219
- duration: metadata.duration || 0,
1220
- timestamp: new Date(metadata.timestamp || Date.now()),
1221
- transcription: metadata.transcription,
1222
- contentType: audioKey.endsWith('.mp3') ? 'audio/mp3' : 'audio/wav',
1223
- raw: metadata,
1224
- }
1225
- } catch {
1226
- // No metadata file, return basic info
1227
- return {
1228
- key: audioKey,
1229
- from: 'unknown',
1230
- to: 'unknown',
1231
- duration: 0,
1232
- timestamp: new Date(),
1233
- contentType: audioKey.endsWith('.mp3') ? 'audio/mp3' : 'audio/wav',
1234
- }
1235
- }
1236
- }
1237
- }
1238
-
1239
- // ============================================
1240
- // Lambda Handlers
1241
- // ============================================
1242
-
1243
- /**
1244
- * Create a Lambda handler for processing incoming voicemails
1245
- * Use this with Connect's voicemail feature or custom IVR
1246
- *
1247
- * @example
1248
- * ```typescript
1249
- * // lambda.ts
1250
- * import { createVoicemailHandler } from 'ts-cloud/aws/voice'
1251
- *
1252
- * export const handler = createVoicemailHandler({
1253
- * bucket: 'my-voicemail-bucket',
1254
- * prefix: 'voicemail/',
1255
- * region: 'us-east-1',
1256
- * })
1257
- * ```
1258
- */
1259
- export function createVoicemailHandler(config: {
1260
- bucket: string
1261
- prefix?: string
1262
- region?: string
1263
- onVoicemail?: (voicemail: Voicemail) => Promise<void>
1264
- }) {
1265
- const voiceClient = new VoiceClient({
1266
- region: config.region || 'us-east-1',
1267
- voicemailBucket: config.bucket,
1268
- voicemailPrefix: config.prefix || 'voicemail/',
1269
- })
1270
-
1271
- return async (event: any): Promise<any> => {
1272
- console.log('Incoming voicemail event:', JSON.stringify(event))
1273
-
1274
- // Handle S3 event (audio file uploaded)
1275
- if (event.Records) {
1276
- for (const record of event.Records) {
1277
- if (record.s3) {
1278
- const bucket = record.s3.bucket.name
1279
- const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, ' '))
1280
-
1281
- // Skip metadata files
1282
- if (key.endsWith('.json')) continue
1283
-
1284
- console.log(`Processing voicemail: ${bucket}/${key}`)
1285
-
1286
- if (config.onVoicemail) {
1287
- const voicemail = await voiceClient.getVoicemail(key)
1288
- if (voicemail) {
1289
- await config.onVoicemail(voicemail)
1290
- }
1291
- }
1292
- }
1293
- }
1294
- }
1295
-
1296
- // Handle Connect event (from contact flow)
1297
- if (event.Details?.ContactData) {
1298
- const contactData = event.Details.ContactData
1299
- console.log(`Connect voicemail from: ${contactData.CustomerEndpoint?.Address}`)
1300
-
1301
- // The actual audio would need to be fetched from Connect's recording API
1302
- // This is triggered after the voicemail is recorded
1303
-
1304
- if (config.onVoicemail && event.audioData) {
1305
- const key = await voiceClient.storeVoicemail({
1306
- from: contactData.CustomerEndpoint?.Address || 'unknown',
1307
- to: contactData.SystemEndpoint?.Address || 'unknown',
1308
- audioData: event.audioData,
1309
- duration: event.duration,
1310
- transcription: event.transcription,
1311
- })
1312
-
1313
- const voicemail = await voiceClient.getVoicemail(key)
1314
- if (voicemail) {
1315
- await config.onVoicemail(voicemail)
1316
- }
1317
- }
1318
- }
1319
-
1320
- return {
1321
- statusCode: 200,
1322
- body: JSON.stringify({ message: 'Voicemail processed' }),
1323
- }
1324
- }
1325
- }
1326
-
1327
- /**
1328
- * Create a Lambda handler for Connect recording events
1329
- * This processes call recordings uploaded to S3 by Connect
1330
- */
1331
- export function createRecordingHandler(config: {
1332
- bucket: string
1333
- prefix?: string
1334
- region?: string
1335
- onRecording?: (recording: CallRecording) => Promise<void>
1336
- }) {
1337
- const voiceClient = new VoiceClient({
1338
- region: config.region || 'us-east-1',
1339
- voicemailBucket: config.bucket,
1340
- recordingsPrefix: config.prefix || 'recordings/',
1341
- })
1342
-
1343
- return async (event: any): Promise<any> => {
1344
- console.log('Recording event:', JSON.stringify(event))
1345
-
1346
- // Handle S3 event
1347
- if (event.Records) {
1348
- for (const record of event.Records) {
1349
- if (record.s3) {
1350
- const bucket = record.s3.bucket.name
1351
- const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, ' '))
1352
-
1353
- if (key.endsWith('.json')) continue
1354
-
1355
- console.log(`Processing recording: ${bucket}/${key}`)
1356
-
1357
- if (config.onRecording) {
1358
- const recordings = await voiceClient.getRecordings({ prefix: key })
1359
- if (recordings.length > 0) {
1360
- await config.onRecording(recordings[0])
1361
- }
1362
- }
1363
- }
1364
- }
1365
- }
1366
-
1367
- return {
1368
- statusCode: 200,
1369
- body: JSON.stringify({ message: 'Recording processed' }),
1370
- }
1371
- }
1372
- }
1373
-
1374
- /**
1375
- * Convenience function to create a voice client
1376
- */
1377
- export function createVoiceClient(config?: VoiceClientConfig): VoiceClient {
1378
- return new VoiceClient(config)
1379
- }