@stacksjs/ts-cloud-core 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +321 -0
- package/package.json +31 -0
- package/src/advanced-features.test.ts +465 -0
- package/src/aws/cloudformation.ts +421 -0
- package/src/aws/cloudfront.ts +158 -0
- package/src/aws/credentials.test.ts +132 -0
- package/src/aws/credentials.ts +545 -0
- package/src/aws/index.ts +87 -0
- package/src/aws/s3.test.ts +188 -0
- package/src/aws/s3.ts +1088 -0
- package/src/aws/signature.test.ts +670 -0
- package/src/aws/signature.ts +1155 -0
- package/src/backup/disaster-recovery.test.ts +726 -0
- package/src/backup/disaster-recovery.ts +500 -0
- package/src/backup/index.ts +34 -0
- package/src/backup/manager.test.ts +498 -0
- package/src/backup/manager.ts +432 -0
- package/src/cicd/circleci.ts +430 -0
- package/src/cicd/github-actions.ts +424 -0
- package/src/cicd/gitlab-ci.ts +255 -0
- package/src/cicd/index.ts +8 -0
- package/src/cli/history.ts +396 -0
- package/src/cli/index.ts +10 -0
- package/src/cli/progress.ts +458 -0
- package/src/cli/repl.ts +454 -0
- package/src/cli/suggestions.ts +327 -0
- package/src/cli/table.test.ts +319 -0
- package/src/cli/table.ts +332 -0
- package/src/cloudformation/builder.test.ts +327 -0
- package/src/cloudformation/builder.ts +378 -0
- package/src/cloudformation/builders/api-gateway.ts +449 -0
- package/src/cloudformation/builders/cache.ts +334 -0
- package/src/cloudformation/builders/cdn.ts +278 -0
- package/src/cloudformation/builders/compute.ts +485 -0
- package/src/cloudformation/builders/database.ts +392 -0
- package/src/cloudformation/builders/functions.ts +343 -0
- package/src/cloudformation/builders/messaging.ts +140 -0
- package/src/cloudformation/builders/monitoring.ts +300 -0
- package/src/cloudformation/builders/network.ts +264 -0
- package/src/cloudformation/builders/queue.ts +147 -0
- package/src/cloudformation/builders/security.ts +399 -0
- package/src/cloudformation/builders/storage.ts +285 -0
- package/src/cloudformation/index.ts +30 -0
- package/src/cloudformation/types.ts +173 -0
- package/src/compliance/aws-config.ts +543 -0
- package/src/compliance/cloudtrail.ts +376 -0
- package/src/compliance/compliance.test.ts +423 -0
- package/src/compliance/guardduty.ts +446 -0
- package/src/compliance/index.ts +66 -0
- package/src/compliance/security-hub.ts +456 -0
- package/src/containers/build-optimization.ts +416 -0
- package/src/containers/containers.test.ts +508 -0
- package/src/containers/image-scanning.ts +360 -0
- package/src/containers/index.ts +9 -0
- package/src/containers/registry.ts +293 -0
- package/src/containers/service-mesh.ts +520 -0
- package/src/database/database.test.ts +762 -0
- package/src/database/index.ts +9 -0
- package/src/database/migrations.ts +444 -0
- package/src/database/performance.ts +528 -0
- package/src/database/replicas.ts +534 -0
- package/src/database/users.ts +494 -0
- package/src/dependency-graph.ts +143 -0
- package/src/deployment/ab-testing.ts +582 -0
- package/src/deployment/blue-green.ts +452 -0
- package/src/deployment/canary.ts +500 -0
- package/src/deployment/deployment.test.ts +526 -0
- package/src/deployment/index.ts +61 -0
- package/src/deployment/progressive.ts +62 -0
- package/src/dns/dns.test.ts +641 -0
- package/src/dns/dnssec.ts +315 -0
- package/src/dns/index.ts +8 -0
- package/src/dns/resolver.ts +496 -0
- package/src/dns/routing.ts +593 -0
- package/src/email/advanced/analytics.ts +445 -0
- package/src/email/advanced/index.ts +11 -0
- package/src/email/advanced/rules.ts +465 -0
- package/src/email/advanced/scheduling.ts +352 -0
- package/src/email/advanced/search.ts +412 -0
- package/src/email/advanced/shared-mailboxes.ts +404 -0
- package/src/email/advanced/templates.ts +455 -0
- package/src/email/advanced/threading.ts +281 -0
- package/src/email/analytics.ts +467 -0
- package/src/email/bounce-handling.ts +425 -0
- package/src/email/email.test.ts +431 -0
- package/src/email/handlers/__tests__/inbound.test.ts +38 -0
- package/src/email/handlers/__tests__/outbound.test.ts +37 -0
- package/src/email/handlers/converter.ts +227 -0
- package/src/email/handlers/feedback.ts +228 -0
- package/src/email/handlers/inbound.ts +169 -0
- package/src/email/handlers/outbound.ts +178 -0
- package/src/email/index.ts +15 -0
- package/src/email/reputation.ts +303 -0
- package/src/email/templates.ts +352 -0
- package/src/errors/index.test.ts +434 -0
- package/src/errors/index.ts +416 -0
- package/src/health-checks/index.ts +40 -0
- package/src/index.ts +360 -0
- package/src/intrinsic-functions.ts +118 -0
- package/src/lambda/concurrency.ts +330 -0
- package/src/lambda/destinations.ts +345 -0
- package/src/lambda/dlq.ts +425 -0
- package/src/lambda/index.ts +11 -0
- package/src/lambda/lambda.test.ts +840 -0
- package/src/lambda/layers.ts +263 -0
- package/src/lambda/versions.ts +376 -0
- package/src/lambda/vpc.ts +399 -0
- package/src/local/config.ts +114 -0
- package/src/local/index.ts +6 -0
- package/src/local/mock-aws.ts +351 -0
- package/src/modules/ai.ts +340 -0
- package/src/modules/api.ts +478 -0
- package/src/modules/auth.ts +805 -0
- package/src/modules/cache.ts +417 -0
- package/src/modules/cdn.ts +1062 -0
- package/src/modules/communication.ts +1094 -0
- package/src/modules/compute.ts +3348 -0
- package/src/modules/database.ts +554 -0
- package/src/modules/deployment.ts +1079 -0
- package/src/modules/dns.ts +337 -0
- package/src/modules/email.ts +1538 -0
- package/src/modules/filesystem.ts +515 -0
- package/src/modules/index.ts +32 -0
- package/src/modules/messaging.ts +486 -0
- package/src/modules/monitoring.ts +2086 -0
- package/src/modules/network.ts +664 -0
- package/src/modules/parameter-store.ts +325 -0
- package/src/modules/permissions.ts +1081 -0
- package/src/modules/phone.ts +494 -0
- package/src/modules/queue.ts +1260 -0
- package/src/modules/redirects.ts +464 -0
- package/src/modules/registry.ts +699 -0
- package/src/modules/search.ts +401 -0
- package/src/modules/secrets.ts +416 -0
- package/src/modules/security.ts +731 -0
- package/src/modules/sms.ts +389 -0
- package/src/modules/storage.ts +1120 -0
- package/src/modules/workflow.ts +680 -0
- package/src/multi-account/config.ts +521 -0
- package/src/multi-account/index.ts +7 -0
- package/src/multi-account/manager.ts +427 -0
- package/src/multi-region/cross-region.ts +410 -0
- package/src/multi-region/index.ts +8 -0
- package/src/multi-region/manager.ts +483 -0
- package/src/multi-region/regions.ts +435 -0
- package/src/network-security/index.ts +48 -0
- package/src/observability/index.ts +9 -0
- package/src/observability/logs.ts +522 -0
- package/src/observability/metrics.ts +460 -0
- package/src/observability/observability.test.ts +782 -0
- package/src/observability/synthetics.ts +568 -0
- package/src/observability/xray.ts +358 -0
- package/src/phone/advanced/analytics.ts +349 -0
- package/src/phone/advanced/callbacks.ts +428 -0
- package/src/phone/advanced/index.ts +8 -0
- package/src/phone/advanced/ivr-builder.ts +504 -0
- package/src/phone/advanced/recording.ts +310 -0
- package/src/phone/handlers/__tests__/incoming-call.test.ts +40 -0
- package/src/phone/handlers/incoming-call.ts +117 -0
- package/src/phone/handlers/missed-call.ts +116 -0
- package/src/phone/handlers/voicemail.ts +179 -0
- package/src/phone/index.ts +9 -0
- package/src/presets/api-backend.ts +134 -0
- package/src/presets/data-pipeline.ts +204 -0
- package/src/presets/extend.test.ts +295 -0
- package/src/presets/extend.ts +297 -0
- package/src/presets/fullstack-app.ts +144 -0
- package/src/presets/index.ts +27 -0
- package/src/presets/jamstack.ts +135 -0
- package/src/presets/microservices.ts +167 -0
- package/src/presets/ml-api.ts +208 -0
- package/src/presets/nodejs-server.ts +104 -0
- package/src/presets/nodejs-serverless.ts +114 -0
- package/src/presets/realtime-app.ts +184 -0
- package/src/presets/static-site.ts +64 -0
- package/src/presets/traditional-web-app.ts +339 -0
- package/src/presets/wordpress.ts +138 -0
- package/src/preview/github.test.ts +249 -0
- package/src/preview/github.ts +297 -0
- package/src/preview/index.ts +37 -0
- package/src/preview/manager.test.ts +440 -0
- package/src/preview/manager.ts +326 -0
- package/src/preview/notifications.test.ts +582 -0
- package/src/preview/notifications.ts +341 -0
- package/src/queue/batch-processing.ts +402 -0
- package/src/queue/dlq-monitoring.ts +402 -0
- package/src/queue/fifo.ts +342 -0
- package/src/queue/index.ts +9 -0
- package/src/queue/management.ts +428 -0
- package/src/queue/queue.test.ts +429 -0
- package/src/resource-mgmt/index.ts +39 -0
- package/src/resource-naming.ts +62 -0
- package/src/s3/index.ts +523 -0
- package/src/schema/cloud-config.schema.json +554 -0
- package/src/schema/index.ts +68 -0
- package/src/security/certificate-manager.ts +492 -0
- package/src/security/index.ts +9 -0
- package/src/security/scanning.ts +545 -0
- package/src/security/secrets-manager.ts +476 -0
- package/src/security/secrets-rotation.ts +456 -0
- package/src/security/security.test.ts +738 -0
- package/src/sms/advanced/ab-testing.ts +389 -0
- package/src/sms/advanced/analytics.ts +336 -0
- package/src/sms/advanced/campaigns.ts +523 -0
- package/src/sms/advanced/chatbot.ts +224 -0
- package/src/sms/advanced/index.ts +10 -0
- package/src/sms/advanced/link-tracking.ts +248 -0
- package/src/sms/advanced/mms.ts +308 -0
- package/src/sms/handlers/__tests__/send.test.ts +40 -0
- package/src/sms/handlers/delivery-status.ts +133 -0
- package/src/sms/handlers/receive.ts +162 -0
- package/src/sms/handlers/send.ts +174 -0
- package/src/sms/index.ts +9 -0
- package/src/stack-diff.ts +389 -0
- package/src/static-site/index.ts +85 -0
- package/src/template-builder.ts +110 -0
- package/src/template-validator.ts +574 -0
- package/src/utils/cache.ts +291 -0
- package/src/utils/diff.ts +269 -0
- package/src/utils/hash.ts +227 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/parallel.ts +294 -0
- package/src/validators/credentials.test.ts +274 -0
- package/src/validators/credentials.ts +233 -0
- package/src/validators/quotas.test.ts +434 -0
- package/src/validators/quotas.ts +217 -0
- package/test/ai.test.ts +327 -0
- package/test/api.test.ts +511 -0
- package/test/auth.test.ts +632 -0
- package/test/cache.test.ts +406 -0
- package/test/cdn.test.ts +247 -0
- package/test/compute.test.ts +861 -0
- package/test/database.test.ts +523 -0
- package/test/deployment.test.ts +499 -0
- package/test/dns.test.ts +270 -0
- package/test/email.test.ts +439 -0
- package/test/filesystem.test.ts +382 -0
- package/test/integration.test.ts +350 -0
- package/test/messaging.test.ts +514 -0
- package/test/monitoring.test.ts +634 -0
- package/test/network.test.ts +425 -0
- package/test/permissions.test.ts +488 -0
- package/test/queue.test.ts +484 -0
- package/test/registry.test.ts +306 -0
- package/test/security.test.ts +462 -0
- package/test/storage.test.ts +463 -0
- package/test/template-validator.test.ts +559 -0
- package/test/workflow.test.ts +592 -0
- package/tsconfig.json +16 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Call Recording with Transcription
|
|
3
|
+
*
|
|
4
|
+
* Provides call recording storage and transcription
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface CallRecording {
|
|
8
|
+
recordingId: string
|
|
9
|
+
contactId: string
|
|
10
|
+
startTime: string
|
|
11
|
+
endTime: string
|
|
12
|
+
duration: number
|
|
13
|
+
participants: string[]
|
|
14
|
+
recordingUrl: string
|
|
15
|
+
transcription?: CallTranscription
|
|
16
|
+
status: 'recording' | 'processing' | 'completed' | 'failed'
|
|
17
|
+
createdAt: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CallTranscription {
|
|
21
|
+
transcriptId: string
|
|
22
|
+
text: string
|
|
23
|
+
segments: TranscriptionSegment[]
|
|
24
|
+
language: string
|
|
25
|
+
confidence: number
|
|
26
|
+
status: 'pending' | 'processing' | 'completed' | 'failed'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface TranscriptionSegment {
|
|
30
|
+
speaker: string
|
|
31
|
+
startTime: number
|
|
32
|
+
endTime: number
|
|
33
|
+
text: string
|
|
34
|
+
confidence: number
|
|
35
|
+
sentiment?: 'positive' | 'neutral' | 'negative'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Call Recording Module
|
|
40
|
+
*/
|
|
41
|
+
export class CallRecording {
|
|
42
|
+
/**
|
|
43
|
+
* Lambda code for processing call recordings
|
|
44
|
+
*/
|
|
45
|
+
static RecordingProcessorCode = `
|
|
46
|
+
const { S3Client, GetObjectCommand, PutObjectCommand, CopyObjectCommand } = require('@aws-sdk/client-s3');
|
|
47
|
+
const { TranscribeClient, StartTranscriptionJobCommand, GetTranscriptionJobCommand } = require('@aws-sdk/client-transcribe');
|
|
48
|
+
const { DynamoDBClient, PutItemCommand, UpdateItemCommand } = require('@aws-sdk/client-dynamodb');
|
|
49
|
+
|
|
50
|
+
const s3 = new S3Client({});
|
|
51
|
+
const transcribe = new TranscribeClient({});
|
|
52
|
+
const dynamodb = new DynamoDBClient({});
|
|
53
|
+
|
|
54
|
+
const RECORDING_BUCKET = process.env.RECORDING_BUCKET;
|
|
55
|
+
const RECORDINGS_TABLE = process.env.RECORDINGS_TABLE;
|
|
56
|
+
|
|
57
|
+
exports.handler = async (event) => {
|
|
58
|
+
console.log('Recording processor event:', JSON.stringify(event, null, 2));
|
|
59
|
+
|
|
60
|
+
for (const record of event.Records) {
|
|
61
|
+
try {
|
|
62
|
+
const bucket = record.s3?.bucket?.name || RECORDING_BUCKET;
|
|
63
|
+
const key = decodeURIComponent(record.s3?.object?.key?.replace(/\\+/g, ' ') || '');
|
|
64
|
+
|
|
65
|
+
// Only process audio files
|
|
66
|
+
if (!key.match(/\\.(wav|mp3|mp4|flac|ogg|webm)$/i)) continue;
|
|
67
|
+
|
|
68
|
+
const recordingId = key.split('/').pop().replace(/\\.[^.]+$/, '');
|
|
69
|
+
const now = new Date().toISOString();
|
|
70
|
+
|
|
71
|
+
// Save recording metadata
|
|
72
|
+
await dynamodb.send(new PutItemCommand({
|
|
73
|
+
TableName: RECORDINGS_TABLE,
|
|
74
|
+
Item: {
|
|
75
|
+
recordingId: { S: recordingId },
|
|
76
|
+
bucket: { S: bucket },
|
|
77
|
+
key: { S: key },
|
|
78
|
+
status: { S: 'processing' },
|
|
79
|
+
createdAt: { S: now },
|
|
80
|
+
ttl: { N: String(Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60) },
|
|
81
|
+
},
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
// Start transcription job
|
|
85
|
+
const transcriptionJobName = \`transcribe-\${recordingId}-\${Date.now()}\`;
|
|
86
|
+
const mediaUri = \`s3://\${bucket}/\${key}\`;
|
|
87
|
+
|
|
88
|
+
await transcribe.send(new StartTranscriptionJobCommand({
|
|
89
|
+
TranscriptionJobName: transcriptionJobName,
|
|
90
|
+
LanguageCode: 'en-US',
|
|
91
|
+
MediaFormat: key.split('.').pop().toLowerCase(),
|
|
92
|
+
Media: { MediaFileUri: mediaUri },
|
|
93
|
+
OutputBucketName: bucket,
|
|
94
|
+
OutputKey: \`transcriptions/\${recordingId}.json\`,
|
|
95
|
+
Settings: {
|
|
96
|
+
ShowSpeakerLabels: true,
|
|
97
|
+
MaxSpeakerLabels: 10,
|
|
98
|
+
ChannelIdentification: false,
|
|
99
|
+
},
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
// Update status
|
|
103
|
+
await dynamodb.send(new UpdateItemCommand({
|
|
104
|
+
TableName: RECORDINGS_TABLE,
|
|
105
|
+
Key: { recordingId: { S: recordingId } },
|
|
106
|
+
UpdateExpression: 'SET transcriptionJobName = :job, #status = :status',
|
|
107
|
+
ExpressionAttributeNames: { '#status': 'status' },
|
|
108
|
+
ExpressionAttributeValues: {
|
|
109
|
+
':job': { S: transcriptionJobName },
|
|
110
|
+
':status': { S: 'transcribing' },
|
|
111
|
+
},
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
console.log(\`Started transcription for: \${recordingId}\`);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error('Error processing recording:', error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { statusCode: 200 };
|
|
121
|
+
};
|
|
122
|
+
`
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Lambda code for transcription completion
|
|
126
|
+
*/
|
|
127
|
+
static TranscriptionCompleteCode = `
|
|
128
|
+
const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
|
|
129
|
+
const { DynamoDBClient, UpdateItemCommand, QueryCommand } = require('@aws-sdk/client-dynamodb');
|
|
130
|
+
|
|
131
|
+
const s3 = new S3Client({});
|
|
132
|
+
const dynamodb = new DynamoDBClient({});
|
|
133
|
+
|
|
134
|
+
const RECORDING_BUCKET = process.env.RECORDING_BUCKET;
|
|
135
|
+
const RECORDINGS_TABLE = process.env.RECORDINGS_TABLE;
|
|
136
|
+
|
|
137
|
+
exports.handler = async (event) => {
|
|
138
|
+
console.log('Transcription complete event:', JSON.stringify(event, null, 2));
|
|
139
|
+
|
|
140
|
+
const { detail } = event;
|
|
141
|
+
const jobName = detail?.TranscriptionJobName;
|
|
142
|
+
const jobStatus = detail?.TranscriptionJobStatus;
|
|
143
|
+
|
|
144
|
+
if (!jobName) return { statusCode: 400 };
|
|
145
|
+
|
|
146
|
+
// Find recording by job name
|
|
147
|
+
const queryResult = await dynamodb.send(new QueryCommand({
|
|
148
|
+
TableName: RECORDINGS_TABLE,
|
|
149
|
+
IndexName: 'job-index',
|
|
150
|
+
KeyConditionExpression: 'transcriptionJobName = :job',
|
|
151
|
+
ExpressionAttributeValues: {
|
|
152
|
+
':job': { S: jobName },
|
|
153
|
+
},
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
const recording = queryResult.Items?.[0];
|
|
157
|
+
if (!recording) {
|
|
158
|
+
console.log('Recording not found for job:', jobName);
|
|
159
|
+
return { statusCode: 404 };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const recordingId = recording.recordingId.S;
|
|
163
|
+
|
|
164
|
+
if (jobStatus === 'COMPLETED') {
|
|
165
|
+
// Get transcription result
|
|
166
|
+
const transcriptKey = \`transcriptions/\${recordingId}.json\`;
|
|
167
|
+
const result = await s3.send(new GetObjectCommand({
|
|
168
|
+
Bucket: RECORDING_BUCKET,
|
|
169
|
+
Key: transcriptKey,
|
|
170
|
+
}));
|
|
171
|
+
|
|
172
|
+
const transcriptData = JSON.parse(await result.Body.transformToString());
|
|
173
|
+
|
|
174
|
+
// Extract segments with speaker labels
|
|
175
|
+
const segments = [];
|
|
176
|
+
const items = transcriptData.results?.items || [];
|
|
177
|
+
let currentSegment = null;
|
|
178
|
+
|
|
179
|
+
for (const item of items) {
|
|
180
|
+
if (item.type === 'pronunciation') {
|
|
181
|
+
const speaker = item.speaker_label || 'spk_0';
|
|
182
|
+
const startTime = parseFloat(item.start_time || 0);
|
|
183
|
+
const endTime = parseFloat(item.end_time || 0);
|
|
184
|
+
const content = item.alternatives?.[0]?.content || '';
|
|
185
|
+
const confidence = parseFloat(item.alternatives?.[0]?.confidence || 0);
|
|
186
|
+
|
|
187
|
+
if (!currentSegment || currentSegment.speaker !== speaker) {
|
|
188
|
+
if (currentSegment) segments.push(currentSegment);
|
|
189
|
+
currentSegment = {
|
|
190
|
+
speaker,
|
|
191
|
+
startTime,
|
|
192
|
+
endTime,
|
|
193
|
+
text: content,
|
|
194
|
+
confidence,
|
|
195
|
+
};
|
|
196
|
+
} else {
|
|
197
|
+
currentSegment.endTime = endTime;
|
|
198
|
+
currentSegment.text += ' ' + content;
|
|
199
|
+
currentSegment.confidence = (currentSegment.confidence + confidence) / 2;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (currentSegment) segments.push(currentSegment);
|
|
204
|
+
|
|
205
|
+
// Update recording with transcription
|
|
206
|
+
await dynamodb.send(new UpdateItemCommand({
|
|
207
|
+
TableName: RECORDINGS_TABLE,
|
|
208
|
+
Key: { recordingId: { S: recordingId } },
|
|
209
|
+
UpdateExpression: 'SET #status = :status, transcription = :transcript, completedAt = :now',
|
|
210
|
+
ExpressionAttributeNames: { '#status': 'status' },
|
|
211
|
+
ExpressionAttributeValues: {
|
|
212
|
+
':status': { S: 'completed' },
|
|
213
|
+
':transcript': { S: JSON.stringify({
|
|
214
|
+
text: transcriptData.results?.transcripts?.[0]?.transcript || '',
|
|
215
|
+
segments,
|
|
216
|
+
language: 'en-US',
|
|
217
|
+
confidence: segments.reduce((sum, s) => sum + s.confidence, 0) / segments.length || 0,
|
|
218
|
+
})},
|
|
219
|
+
':now': { S: new Date().toISOString() },
|
|
220
|
+
},
|
|
221
|
+
}));
|
|
222
|
+
|
|
223
|
+
console.log(\`Transcription completed for: \${recordingId}\`);
|
|
224
|
+
} else if (jobStatus === 'FAILED') {
|
|
225
|
+
await dynamodb.send(new UpdateItemCommand({
|
|
226
|
+
TableName: RECORDINGS_TABLE,
|
|
227
|
+
Key: { recordingId: { S: recordingId } },
|
|
228
|
+
UpdateExpression: 'SET #status = :status, error = :error',
|
|
229
|
+
ExpressionAttributeNames: { '#status': 'status' },
|
|
230
|
+
ExpressionAttributeValues: {
|
|
231
|
+
':status': { S: 'failed' },
|
|
232
|
+
':error': { S: detail?.FailureReason || 'Unknown error' },
|
|
233
|
+
},
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return { statusCode: 200 };
|
|
238
|
+
};
|
|
239
|
+
`
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Create recordings DynamoDB table
|
|
243
|
+
*/
|
|
244
|
+
static createRecordingsTable(config: { slug: string }): Record<string, any> {
|
|
245
|
+
return {
|
|
246
|
+
[`${config.slug}CallRecordingsTable`]: {
|
|
247
|
+
Type: 'AWS::DynamoDB::Table',
|
|
248
|
+
Properties: {
|
|
249
|
+
TableName: `${config.slug}-call-recordings`,
|
|
250
|
+
BillingMode: 'PAY_PER_REQUEST',
|
|
251
|
+
AttributeDefinitions: [
|
|
252
|
+
{ AttributeName: 'recordingId', AttributeType: 'S' },
|
|
253
|
+
{ AttributeName: 'transcriptionJobName', AttributeType: 'S' },
|
|
254
|
+
],
|
|
255
|
+
KeySchema: [
|
|
256
|
+
{ AttributeName: 'recordingId', KeyType: 'HASH' },
|
|
257
|
+
],
|
|
258
|
+
GlobalSecondaryIndexes: [
|
|
259
|
+
{
|
|
260
|
+
IndexName: 'job-index',
|
|
261
|
+
KeySchema: [
|
|
262
|
+
{ AttributeName: 'transcriptionJobName', KeyType: 'HASH' },
|
|
263
|
+
],
|
|
264
|
+
Projection: { ProjectionType: 'ALL' },
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
TimeToLiveSpecification: {
|
|
268
|
+
AttributeName: 'ttl',
|
|
269
|
+
Enabled: true,
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Create recording processor Lambda
|
|
278
|
+
*/
|
|
279
|
+
static createRecordingProcessorLambda(config: {
|
|
280
|
+
slug: string
|
|
281
|
+
roleArn: string
|
|
282
|
+
recordingBucket: string
|
|
283
|
+
recordingsTable: string
|
|
284
|
+
}): Record<string, any> {
|
|
285
|
+
return {
|
|
286
|
+
[`${config.slug}RecordingProcessorLambda`]: {
|
|
287
|
+
Type: 'AWS::Lambda::Function',
|
|
288
|
+
Properties: {
|
|
289
|
+
FunctionName: `${config.slug}-recording-processor`,
|
|
290
|
+
Runtime: 'nodejs20.x',
|
|
291
|
+
Handler: 'index.handler',
|
|
292
|
+
Role: config.roleArn,
|
|
293
|
+
Timeout: 60,
|
|
294
|
+
MemorySize: 256,
|
|
295
|
+
Code: {
|
|
296
|
+
ZipFile: CallRecording.RecordingProcessorCode,
|
|
297
|
+
},
|
|
298
|
+
Environment: {
|
|
299
|
+
Variables: {
|
|
300
|
+
RECORDING_BUCKET: config.recordingBucket,
|
|
301
|
+
RECORDINGS_TABLE: config.recordingsTable,
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export default CallRecording
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { handler } from '../incoming-call'
|
|
3
|
+
|
|
4
|
+
describe('Incoming Call Handler', () => {
|
|
5
|
+
it('should export handler code as string', () => {
|
|
6
|
+
expect(typeof handler).toBe('string')
|
|
7
|
+
expect(handler).toContain('exports.handler')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('should contain SNS client import', () => {
|
|
11
|
+
expect(handler).toContain('SNSClient')
|
|
12
|
+
expect(handler).toContain('PublishCommand')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('should contain DynamoDB client import', () => {
|
|
16
|
+
expect(handler).toContain('DynamoDBClient')
|
|
17
|
+
expect(handler).toContain('PutItemCommand')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('should extract contact data', () => {
|
|
21
|
+
expect(handler).toContain('ContactData')
|
|
22
|
+
expect(handler).toContain('CustomerEndpoint')
|
|
23
|
+
expect(handler).toContain('SystemEndpoint')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should log calls to DynamoDB', () => {
|
|
27
|
+
expect(handler).toContain('CALL_LOG_TABLE')
|
|
28
|
+
expect(handler).toContain('contactId')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should send notifications', () => {
|
|
32
|
+
expect(handler).toContain('NOTIFICATION_TOPIC_ARN')
|
|
33
|
+
expect(handler).toContain('incoming_call')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should support webhook notifications', () => {
|
|
37
|
+
expect(handler).toContain('WEBHOOK_URL')
|
|
38
|
+
expect(handler).toContain('fetch')
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incoming Call Lambda Handler
|
|
3
|
+
*
|
|
4
|
+
* Processes incoming calls from Amazon Connect:
|
|
5
|
+
* - Logs call details
|
|
6
|
+
* - Sends notifications (SNS, webhook)
|
|
7
|
+
* - Routes based on caller ID
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const handler = `
|
|
11
|
+
const { SNSClient, PublishCommand } = require('@aws-sdk/client-sns');
|
|
12
|
+
const { DynamoDBClient, PutItemCommand } = require('@aws-sdk/client-dynamodb');
|
|
13
|
+
|
|
14
|
+
const sns = new SNSClient({});
|
|
15
|
+
const dynamodb = new DynamoDBClient({});
|
|
16
|
+
|
|
17
|
+
exports.handler = async (event) => {
|
|
18
|
+
console.log('Incoming call event:', JSON.stringify(event, null, 2));
|
|
19
|
+
|
|
20
|
+
const notificationTopicArn = process.env.NOTIFICATION_TOPIC_ARN;
|
|
21
|
+
const callLogTable = process.env.CALL_LOG_TABLE;
|
|
22
|
+
const webhookUrl = process.env.WEBHOOK_URL;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// Extract call details from Connect event
|
|
26
|
+
const contactData = event.Details?.ContactData || {};
|
|
27
|
+
const parameters = event.Details?.Parameters || {};
|
|
28
|
+
|
|
29
|
+
const callDetails = {
|
|
30
|
+
contactId: contactData.ContactId,
|
|
31
|
+
channel: contactData.Channel || 'VOICE',
|
|
32
|
+
initiationMethod: contactData.InitiationMethod,
|
|
33
|
+
customerEndpoint: contactData.CustomerEndpoint?.Address,
|
|
34
|
+
systemEndpoint: contactData.SystemEndpoint?.Address,
|
|
35
|
+
queue: contactData.Queue?.Name,
|
|
36
|
+
attributes: contactData.Attributes || {},
|
|
37
|
+
timestamp: new Date().toISOString(),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
console.log('Call details:', callDetails);
|
|
41
|
+
|
|
42
|
+
// Log call to DynamoDB
|
|
43
|
+
if (callLogTable) {
|
|
44
|
+
await dynamodb.send(new PutItemCommand({
|
|
45
|
+
TableName: callLogTable,
|
|
46
|
+
Item: {
|
|
47
|
+
contactId: { S: callDetails.contactId },
|
|
48
|
+
timestamp: { S: callDetails.timestamp },
|
|
49
|
+
callerNumber: { S: callDetails.customerEndpoint || 'unknown' },
|
|
50
|
+
calledNumber: { S: callDetails.systemEndpoint || 'unknown' },
|
|
51
|
+
channel: { S: callDetails.channel },
|
|
52
|
+
initiationMethod: { S: callDetails.initiationMethod || 'unknown' },
|
|
53
|
+
status: { S: 'incoming' },
|
|
54
|
+
ttl: { N: String(Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60) }, // 90 days
|
|
55
|
+
},
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Send SNS notification
|
|
60
|
+
if (notificationTopicArn) {
|
|
61
|
+
await sns.send(new PublishCommand({
|
|
62
|
+
TopicArn: notificationTopicArn,
|
|
63
|
+
Subject: 'Incoming Call',
|
|
64
|
+
Message: JSON.stringify({
|
|
65
|
+
type: 'incoming_call',
|
|
66
|
+
from: callDetails.customerEndpoint,
|
|
67
|
+
to: callDetails.systemEndpoint,
|
|
68
|
+
contactId: callDetails.contactId,
|
|
69
|
+
timestamp: callDetails.timestamp,
|
|
70
|
+
}, null, 2),
|
|
71
|
+
MessageAttributes: {
|
|
72
|
+
eventType: {
|
|
73
|
+
DataType: 'String',
|
|
74
|
+
StringValue: 'incoming_call',
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Send webhook notification
|
|
81
|
+
if (webhookUrl) {
|
|
82
|
+
try {
|
|
83
|
+
await fetch(webhookUrl, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { 'Content-Type': 'application/json' },
|
|
86
|
+
body: JSON.stringify({
|
|
87
|
+
event: 'incoming_call',
|
|
88
|
+
data: callDetails,
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error('Webhook notification failed:', err.message);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Return routing decision
|
|
97
|
+
// Can be customized based on caller ID, time of day, etc.
|
|
98
|
+
return {
|
|
99
|
+
statusCode: 200,
|
|
100
|
+
route: parameters.defaultRoute || 'main_queue',
|
|
101
|
+
callerInfo: {
|
|
102
|
+
number: callDetails.customerEndpoint,
|
|
103
|
+
isKnown: false, // Could lookup in CRM
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error('Error processing incoming call:', error);
|
|
109
|
+
return {
|
|
110
|
+
statusCode: 500,
|
|
111
|
+
error: error.message,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
`
|
|
116
|
+
|
|
117
|
+
export default handler
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Missed Call Lambda Handler
|
|
3
|
+
*
|
|
4
|
+
* Processes missed call events:
|
|
5
|
+
* - Logs missed call
|
|
6
|
+
* - Sends notification
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const handler = `
|
|
10
|
+
const { SNSClient, PublishCommand } = require('@aws-sdk/client-sns');
|
|
11
|
+
const { DynamoDBClient, UpdateItemCommand } = require('@aws-sdk/client-dynamodb');
|
|
12
|
+
|
|
13
|
+
const sns = new SNSClient({});
|
|
14
|
+
const dynamodb = new DynamoDBClient({});
|
|
15
|
+
|
|
16
|
+
exports.handler = async (event) => {
|
|
17
|
+
console.log('Missed call event:', JSON.stringify(event, null, 2));
|
|
18
|
+
|
|
19
|
+
const notificationTopicArn = process.env.NOTIFICATION_TOPIC_ARN;
|
|
20
|
+
const callLogTable = process.env.CALL_LOG_TABLE;
|
|
21
|
+
const webhookUrl = process.env.WEBHOOK_URL;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
// Extract call details from Connect event
|
|
25
|
+
const contactData = event.Details?.ContactData || {};
|
|
26
|
+
|
|
27
|
+
const callDetails = {
|
|
28
|
+
contactId: contactData.ContactId,
|
|
29
|
+
customerEndpoint: contactData.CustomerEndpoint?.Address,
|
|
30
|
+
systemEndpoint: contactData.SystemEndpoint?.Address,
|
|
31
|
+
queue: contactData.Queue?.Name,
|
|
32
|
+
waitTime: contactData.Queue?.EnqueueTimestamp
|
|
33
|
+
? Math.floor((Date.now() - new Date(contactData.Queue.EnqueueTimestamp).getTime()) / 1000)
|
|
34
|
+
: 0,
|
|
35
|
+
disconnectReason: event.Details?.Parameters?.disconnectReason || 'customer_abandoned',
|
|
36
|
+
timestamp: new Date().toISOString(),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
console.log('Missed call details:', callDetails);
|
|
40
|
+
|
|
41
|
+
// Update call log
|
|
42
|
+
if (callLogTable && callDetails.contactId) {
|
|
43
|
+
await dynamodb.send(new UpdateItemCommand({
|
|
44
|
+
TableName: callLogTable,
|
|
45
|
+
Key: {
|
|
46
|
+
contactId: { S: callDetails.contactId },
|
|
47
|
+
},
|
|
48
|
+
UpdateExpression: 'SET #status = :status, disconnectReason = :reason, waitTime = :wait, missedAt = :at',
|
|
49
|
+
ExpressionAttributeNames: {
|
|
50
|
+
'#status': 'status',
|
|
51
|
+
},
|
|
52
|
+
ExpressionAttributeValues: {
|
|
53
|
+
':status': { S: 'missed' },
|
|
54
|
+
':reason': { S: callDetails.disconnectReason },
|
|
55
|
+
':wait': { N: String(callDetails.waitTime) },
|
|
56
|
+
':at': { S: callDetails.timestamp },
|
|
57
|
+
},
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Send SNS notification
|
|
62
|
+
if (notificationTopicArn) {
|
|
63
|
+
await sns.send(new PublishCommand({
|
|
64
|
+
TopicArn: notificationTopicArn,
|
|
65
|
+
Subject: 'Missed Call Alert',
|
|
66
|
+
Message: JSON.stringify({
|
|
67
|
+
type: 'missed_call',
|
|
68
|
+
from: callDetails.customerEndpoint,
|
|
69
|
+
to: callDetails.systemEndpoint,
|
|
70
|
+
queue: callDetails.queue,
|
|
71
|
+
waitTime: callDetails.waitTime,
|
|
72
|
+
reason: callDetails.disconnectReason,
|
|
73
|
+
contactId: callDetails.contactId,
|
|
74
|
+
timestamp: callDetails.timestamp,
|
|
75
|
+
}, null, 2),
|
|
76
|
+
MessageAttributes: {
|
|
77
|
+
eventType: {
|
|
78
|
+
DataType: 'String',
|
|
79
|
+
StringValue: 'missed_call',
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Send webhook notification
|
|
86
|
+
if (webhookUrl) {
|
|
87
|
+
try {
|
|
88
|
+
await fetch(webhookUrl, {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
headers: { 'Content-Type': 'application/json' },
|
|
91
|
+
body: JSON.stringify({
|
|
92
|
+
event: 'missed_call',
|
|
93
|
+
data: callDetails,
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error('Webhook notification failed:', err.message);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
statusCode: 200,
|
|
103
|
+
message: 'Missed call logged',
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error('Error processing missed call:', error);
|
|
108
|
+
return {
|
|
109
|
+
statusCode: 500,
|
|
110
|
+
error: error.message,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
`
|
|
115
|
+
|
|
116
|
+
export default handler
|