@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,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMS Chatbot Integration
|
|
3
|
+
*
|
|
4
|
+
* Provides conversational SMS with AI/rule-based responses
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface ChatbotConfig {
|
|
8
|
+
enabled: boolean
|
|
9
|
+
provider: 'rules' | 'bedrock' | 'openai' | 'custom'
|
|
10
|
+
welcomeMessage?: string
|
|
11
|
+
fallbackMessage?: string
|
|
12
|
+
sessionTimeout?: number
|
|
13
|
+
maxTurns?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ChatSession {
|
|
17
|
+
sessionId: string
|
|
18
|
+
phoneNumber: string
|
|
19
|
+
startedAt: string
|
|
20
|
+
lastMessageAt: string
|
|
21
|
+
turnCount: number
|
|
22
|
+
context: Record<string, any>
|
|
23
|
+
status: 'active' | 'ended' | 'timeout'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ChatRule {
|
|
27
|
+
id: string
|
|
28
|
+
priority: number
|
|
29
|
+
patterns: string[]
|
|
30
|
+
response: string
|
|
31
|
+
action?: 'respond' | 'transfer' | 'end' | 'webhook'
|
|
32
|
+
actionParams?: Record<string, any>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* SMS Chatbot Module
|
|
37
|
+
*/
|
|
38
|
+
export class SmsChatbot {
|
|
39
|
+
/**
|
|
40
|
+
* Lambda code for chatbot processing
|
|
41
|
+
*/
|
|
42
|
+
static ChatbotProcessorCode: string = [
|
|
43
|
+
`const { DynamoDBClient, GetItemCommand, PutItemCommand, UpdateItemCommand, ScanCommand } = require('@aws-sdk/client-dynamodb');`,
|
|
44
|
+
"const { BedrockRuntimeClient, InvokeModelCommand } = require('@aws-sdk/client-bedrock-runtime');",
|
|
45
|
+
"const { SNSClient, PublishCommand } = require('@aws-sdk/client-sns');",
|
|
46
|
+
"",
|
|
47
|
+
"const dynamodb = new DynamoDBClient({});",
|
|
48
|
+
"const bedrock = new BedrockRuntimeClient({});",
|
|
49
|
+
"const sns = new SNSClient({});",
|
|
50
|
+
"",
|
|
51
|
+
"const SESSIONS_TABLE = process.env.SESSIONS_TABLE;",
|
|
52
|
+
"const RULES_TABLE = process.env.RULES_TABLE;",
|
|
53
|
+
"const CHATBOT_CONFIG = JSON.parse(process.env.CHATBOT_CONFIG || '{}');",
|
|
54
|
+
"",
|
|
55
|
+
"exports.handler = async (event) => {",
|
|
56
|
+
" console.log('Chatbot event:', JSON.stringify(event, null, 2));",
|
|
57
|
+
" for (const record of event.Records) {",
|
|
58
|
+
" try {",
|
|
59
|
+
" const message = JSON.parse(record.Sns?.Message || record.body || '{}');",
|
|
60
|
+
" const { originationNumber, messageBody } = message;",
|
|
61
|
+
" if (!originationNumber || !messageBody) continue;",
|
|
62
|
+
" const response = await processMessage(originationNumber, messageBody.trim());",
|
|
63
|
+
" await sendResponse(originationNumber, response);",
|
|
64
|
+
" } catch (error) {",
|
|
65
|
+
" console.error('Error:', error);",
|
|
66
|
+
" }",
|
|
67
|
+
" }",
|
|
68
|
+
" return { statusCode: 200 };",
|
|
69
|
+
"};",
|
|
70
|
+
"",
|
|
71
|
+
"async function processMessage(phoneNumber, message) {",
|
|
72
|
+
" const rules = await getRules();",
|
|
73
|
+
" const lowerMessage = message.toLowerCase();",
|
|
74
|
+
" for (const rule of rules) {",
|
|
75
|
+
" for (const pattern of rule.patterns) {",
|
|
76
|
+
" if (new RegExp(pattern, 'i').test(lowerMessage)) {",
|
|
77
|
+
" return rule.response;",
|
|
78
|
+
" }",
|
|
79
|
+
" }",
|
|
80
|
+
" }",
|
|
81
|
+
" return CHATBOT_CONFIG.fallbackMessage || 'Sorry, I did not understand. Please try again.';",
|
|
82
|
+
"}",
|
|
83
|
+
"",
|
|
84
|
+
"async function getRules() {",
|
|
85
|
+
" const result = await dynamodb.send(new ScanCommand({ TableName: RULES_TABLE }));",
|
|
86
|
+
" return (result.Items || []).map(item => ({",
|
|
87
|
+
" patterns: JSON.parse(item.patterns?.S || '[]'),",
|
|
88
|
+
" response: item.response?.S || '',",
|
|
89
|
+
" })).sort((a, b) => (a.priority || 0) - (b.priority || 0));",
|
|
90
|
+
"}",
|
|
91
|
+
"",
|
|
92
|
+
"async function sendResponse(phoneNumber, message) {",
|
|
93
|
+
" await sns.send(new PublishCommand({",
|
|
94
|
+
" PhoneNumber: phoneNumber,",
|
|
95
|
+
" Message: message.substring(0, 1600),",
|
|
96
|
+
" MessageAttributes: {",
|
|
97
|
+
" 'AWS.SNS.SMS.SMSType': { DataType: 'String', StringValue: 'Transactional' },",
|
|
98
|
+
" },",
|
|
99
|
+
" }));",
|
|
100
|
+
"}",
|
|
101
|
+
].join('\n')
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Create sessions DynamoDB table
|
|
105
|
+
*/
|
|
106
|
+
static createSessionsTable(config: { slug: string }): Record<string, any> {
|
|
107
|
+
return {
|
|
108
|
+
[`${config.slug}ChatbotSessionsTable`]: {
|
|
109
|
+
Type: 'AWS::DynamoDB::Table',
|
|
110
|
+
Properties: {
|
|
111
|
+
TableName: `${config.slug}-chatbot-sessions`,
|
|
112
|
+
BillingMode: 'PAY_PER_REQUEST',
|
|
113
|
+
AttributeDefinitions: [
|
|
114
|
+
{ AttributeName: 'phoneNumber', AttributeType: 'S' },
|
|
115
|
+
],
|
|
116
|
+
KeySchema: [
|
|
117
|
+
{ AttributeName: 'phoneNumber', KeyType: 'HASH' },
|
|
118
|
+
],
|
|
119
|
+
TimeToLiveSpecification: {
|
|
120
|
+
AttributeName: 'ttl',
|
|
121
|
+
Enabled: true,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Create chatbot rules table
|
|
130
|
+
*/
|
|
131
|
+
static createRulesTable(config: { slug: string }): Record<string, any> {
|
|
132
|
+
return {
|
|
133
|
+
[`${config.slug}ChatbotRulesTable`]: {
|
|
134
|
+
Type: 'AWS::DynamoDB::Table',
|
|
135
|
+
Properties: {
|
|
136
|
+
TableName: `${config.slug}-chatbot-rules`,
|
|
137
|
+
BillingMode: 'PAY_PER_REQUEST',
|
|
138
|
+
AttributeDefinitions: [
|
|
139
|
+
{ AttributeName: 'id', AttributeType: 'S' },
|
|
140
|
+
],
|
|
141
|
+
KeySchema: [
|
|
142
|
+
{ AttributeName: 'id', KeyType: 'HASH' },
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Create chatbot processor Lambda
|
|
151
|
+
*/
|
|
152
|
+
static createChatbotProcessorLambda(config: {
|
|
153
|
+
slug: string
|
|
154
|
+
roleArn: string
|
|
155
|
+
sessionsTable: string
|
|
156
|
+
rulesTable: string
|
|
157
|
+
chatbotConfig: ChatbotConfig
|
|
158
|
+
originationNumber?: string
|
|
159
|
+
}): Record<string, any> {
|
|
160
|
+
return {
|
|
161
|
+
[`${config.slug}ChatbotProcessorLambda`]: {
|
|
162
|
+
Type: 'AWS::Lambda::Function',
|
|
163
|
+
Properties: {
|
|
164
|
+
FunctionName: `${config.slug}-chatbot-processor`,
|
|
165
|
+
Runtime: 'nodejs20.x',
|
|
166
|
+
Handler: 'index.handler',
|
|
167
|
+
Role: config.roleArn,
|
|
168
|
+
Timeout: 30,
|
|
169
|
+
MemorySize: 256,
|
|
170
|
+
Code: {
|
|
171
|
+
ZipFile: SmsChatbot.ChatbotProcessorCode,
|
|
172
|
+
},
|
|
173
|
+
Environment: {
|
|
174
|
+
Variables: {
|
|
175
|
+
SESSIONS_TABLE: config.sessionsTable,
|
|
176
|
+
RULES_TABLE: config.rulesTable,
|
|
177
|
+
CHATBOT_CONFIG: JSON.stringify(config.chatbotConfig),
|
|
178
|
+
ORIGINATION_NUMBER: config.originationNumber || '',
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Built-in chatbot rules
|
|
188
|
+
*/
|
|
189
|
+
static readonly DefaultRules: ChatRule[] = [
|
|
190
|
+
{
|
|
191
|
+
id: 'greeting',
|
|
192
|
+
priority: 1,
|
|
193
|
+
patterns: ['^(hi|hello|hey|howdy)$', '^good (morning|afternoon|evening)'],
|
|
194
|
+
response: 'Hello! How can I help you today?',
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: 'help',
|
|
198
|
+
priority: 2,
|
|
199
|
+
patterns: ['^help$', 'what can you do', 'how does this work'],
|
|
200
|
+
response: 'I can help with: 1) Account info 2) Support 3) Hours. Reply with a number.',
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: 'hours',
|
|
204
|
+
priority: 3,
|
|
205
|
+
patterns: ['hours', 'open', 'when are you'],
|
|
206
|
+
response: 'We are open Mon-Fri 9am-5pm EST. Closed weekends.',
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: 'thanks',
|
|
210
|
+
priority: 4,
|
|
211
|
+
patterns: ['^(thanks|thank you|thx)'],
|
|
212
|
+
response: "You're welcome! Is there anything else I can help with?",
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: 'bye',
|
|
216
|
+
priority: 5,
|
|
217
|
+
patterns: ['^(bye|goodbye|quit|exit|stop)$'],
|
|
218
|
+
response: 'Goodbye! Text us anytime if you need help.',
|
|
219
|
+
action: 'end',
|
|
220
|
+
},
|
|
221
|
+
]
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export default SmsChatbot
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Link Shortening and Tracking
|
|
3
|
+
*
|
|
4
|
+
* Provides URL shortening and click tracking for SMS
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface ShortenedLink {
|
|
8
|
+
id: string
|
|
9
|
+
originalUrl: string
|
|
10
|
+
shortUrl: string
|
|
11
|
+
campaignId?: string
|
|
12
|
+
messageId?: string
|
|
13
|
+
clicks: number
|
|
14
|
+
uniqueClicks: number
|
|
15
|
+
createdAt: string
|
|
16
|
+
expiresAt?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LinkClick {
|
|
20
|
+
linkId: string
|
|
21
|
+
clickedAt: string
|
|
22
|
+
userAgent?: string
|
|
23
|
+
ipAddress?: string
|
|
24
|
+
country?: string
|
|
25
|
+
device?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Link Tracking Module
|
|
30
|
+
*/
|
|
31
|
+
export class LinkTracking {
|
|
32
|
+
/**
|
|
33
|
+
* Lambda code for link shortening
|
|
34
|
+
*/
|
|
35
|
+
static LinkShortenerCode = `
|
|
36
|
+
const { DynamoDBClient, PutItemCommand, GetItemCommand, UpdateItemCommand } = require('@aws-sdk/client-dynamodb');
|
|
37
|
+
const crypto = require('crypto');
|
|
38
|
+
|
|
39
|
+
const dynamodb = new DynamoDBClient({});
|
|
40
|
+
const LINKS_TABLE = process.env.LINKS_TABLE;
|
|
41
|
+
const SHORT_DOMAIN = process.env.SHORT_DOMAIN;
|
|
42
|
+
|
|
43
|
+
exports.handler = async (event) => {
|
|
44
|
+
console.log('Link shortener event:', JSON.stringify(event, null, 2));
|
|
45
|
+
|
|
46
|
+
const { httpMethod, body, pathParameters } = event;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
if (httpMethod === 'POST') {
|
|
50
|
+
// Create short link
|
|
51
|
+
const data = JSON.parse(body || '{}');
|
|
52
|
+
return await createShortLink(data);
|
|
53
|
+
} else if (httpMethod === 'GET' && pathParameters?.id) {
|
|
54
|
+
// Redirect to original URL
|
|
55
|
+
return await handleRedirect(pathParameters.id, event);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { statusCode: 405, body: 'Method not allowed' };
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Error:', error);
|
|
61
|
+
return { statusCode: 500, body: JSON.stringify({ error: error.message }) };
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
async function createShortLink(data) {
|
|
66
|
+
const { url, campaignId, messageId, expiresIn } = data;
|
|
67
|
+
|
|
68
|
+
if (!url) {
|
|
69
|
+
return { statusCode: 400, body: JSON.stringify({ error: 'URL is required' }) };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Generate short ID
|
|
73
|
+
const id = generateShortId();
|
|
74
|
+
const now = new Date().toISOString();
|
|
75
|
+
const expiresAt = expiresIn
|
|
76
|
+
? new Date(Date.now() + expiresIn * 1000).toISOString()
|
|
77
|
+
: null;
|
|
78
|
+
|
|
79
|
+
await dynamodb.send(new PutItemCommand({
|
|
80
|
+
TableName: LINKS_TABLE,
|
|
81
|
+
Item: {
|
|
82
|
+
id: { S: id },
|
|
83
|
+
originalUrl: { S: url },
|
|
84
|
+
campaignId: { S: campaignId || '' },
|
|
85
|
+
messageId: { S: messageId || '' },
|
|
86
|
+
clicks: { N: '0' },
|
|
87
|
+
uniqueClicks: { N: '0' },
|
|
88
|
+
visitors: { SS: ['_placeholder'] }, // DynamoDB requires non-empty set
|
|
89
|
+
createdAt: { S: now },
|
|
90
|
+
...(expiresAt && { expiresAt: { S: expiresAt } }),
|
|
91
|
+
ttl: { N: String(Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60) },
|
|
92
|
+
},
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
const shortUrl = SHORT_DOMAIN ? 'https://' + SHORT_DOMAIN + '/l/' + id : '/l/' + id;
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
statusCode: 201,
|
|
99
|
+
headers: { 'Content-Type': 'application/json' },
|
|
100
|
+
body: JSON.stringify({
|
|
101
|
+
id,
|
|
102
|
+
shortUrl,
|
|
103
|
+
originalUrl: url,
|
|
104
|
+
createdAt: now,
|
|
105
|
+
}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function handleRedirect(id, event) {
|
|
110
|
+
const result = await dynamodb.send(new GetItemCommand({
|
|
111
|
+
TableName: LINKS_TABLE,
|
|
112
|
+
Key: { id: { S: id } },
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
if (!result.Item) {
|
|
116
|
+
return { statusCode: 404, body: 'Link not found' };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const link = result.Item;
|
|
120
|
+
|
|
121
|
+
// Check expiration
|
|
122
|
+
if (link.expiresAt?.S && new Date(link.expiresAt.S) < new Date()) {
|
|
123
|
+
return { statusCode: 410, body: 'Link has expired' };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const originalUrl = link.originalUrl.S;
|
|
127
|
+
|
|
128
|
+
// Track click
|
|
129
|
+
const ipAddress = event.requestContext?.identity?.sourceIp || 'unknown';
|
|
130
|
+
const userAgent = event.headers?.['user-agent'] || '';
|
|
131
|
+
const visitorId = crypto.createHash('md5').update(ipAddress + userAgent).digest('hex').substring(0, 8);
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
await dynamodb.send(new UpdateItemCommand({
|
|
135
|
+
TableName: LINKS_TABLE,
|
|
136
|
+
Key: { id: { S: id } },
|
|
137
|
+
UpdateExpression: 'SET clicks = clicks + :one, visitors = list_append(if_not_exists(visitors, :empty), :visitor)',
|
|
138
|
+
ExpressionAttributeValues: {
|
|
139
|
+
':one': { N: '1' },
|
|
140
|
+
':empty': { L: [] },
|
|
141
|
+
':visitor': { L: [{ S: visitorId }] },
|
|
142
|
+
},
|
|
143
|
+
}));
|
|
144
|
+
} catch (e) {
|
|
145
|
+
console.error('Error tracking click:', e);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
statusCode: 302,
|
|
150
|
+
headers: {
|
|
151
|
+
'Location': originalUrl,
|
|
152
|
+
'Cache-Control': 'no-store',
|
|
153
|
+
},
|
|
154
|
+
body: '',
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function generateShortId() {
|
|
159
|
+
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
160
|
+
let id = '';
|
|
161
|
+
for (let i = 0; i < 6; i++) {
|
|
162
|
+
id += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
163
|
+
}
|
|
164
|
+
return id;
|
|
165
|
+
}
|
|
166
|
+
`
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Create links DynamoDB table
|
|
170
|
+
*/
|
|
171
|
+
static createLinksTable(config: { slug: string }): Record<string, any> {
|
|
172
|
+
return {
|
|
173
|
+
[`${config.slug}ShortLinksTable`]: {
|
|
174
|
+
Type: 'AWS::DynamoDB::Table',
|
|
175
|
+
Properties: {
|
|
176
|
+
TableName: `${config.slug}-short-links`,
|
|
177
|
+
BillingMode: 'PAY_PER_REQUEST',
|
|
178
|
+
AttributeDefinitions: [
|
|
179
|
+
{ AttributeName: 'id', AttributeType: 'S' },
|
|
180
|
+
],
|
|
181
|
+
KeySchema: [
|
|
182
|
+
{ AttributeName: 'id', KeyType: 'HASH' },
|
|
183
|
+
],
|
|
184
|
+
TimeToLiveSpecification: {
|
|
185
|
+
AttributeName: 'ttl',
|
|
186
|
+
Enabled: true,
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Create link shortener Lambda
|
|
195
|
+
*/
|
|
196
|
+
static createLinkShortenerLambda(config: {
|
|
197
|
+
slug: string
|
|
198
|
+
roleArn: string
|
|
199
|
+
linksTable: string
|
|
200
|
+
shortDomain?: string
|
|
201
|
+
}): Record<string, any> {
|
|
202
|
+
return {
|
|
203
|
+
[`${config.slug}LinkShortenerLambda`]: {
|
|
204
|
+
Type: 'AWS::Lambda::Function',
|
|
205
|
+
Properties: {
|
|
206
|
+
FunctionName: `${config.slug}-link-shortener`,
|
|
207
|
+
Runtime: 'nodejs20.x',
|
|
208
|
+
Handler: 'index.handler',
|
|
209
|
+
Role: config.roleArn,
|
|
210
|
+
Timeout: 10,
|
|
211
|
+
MemorySize: 128,
|
|
212
|
+
Code: {
|
|
213
|
+
ZipFile: LinkTracking.LinkShortenerCode,
|
|
214
|
+
},
|
|
215
|
+
Environment: {
|
|
216
|
+
Variables: {
|
|
217
|
+
LINKS_TABLE: config.linksTable,
|
|
218
|
+
SHORT_DOMAIN: config.shortDomain || '',
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Shorten URLs in message text
|
|
228
|
+
*/
|
|
229
|
+
static shortenUrlsInMessage(message: string, shortDomain: string, linkIdPrefix: string): {
|
|
230
|
+
message: string
|
|
231
|
+
links: Array<{ original: string; short: string; id: string }>
|
|
232
|
+
} {
|
|
233
|
+
const urlRegex = /(https?:\/\/[^\s]+)/g
|
|
234
|
+
const links: Array<{ original: string; short: string; id: string }> = []
|
|
235
|
+
let index = 0
|
|
236
|
+
|
|
237
|
+
const shortenedMessage = message.replace(urlRegex, (url) => {
|
|
238
|
+
const id = `${linkIdPrefix}-${index++}`
|
|
239
|
+
const shortUrl = `https://${shortDomain}/l/${id}`
|
|
240
|
+
links.push({ original: url, short: shortUrl, id })
|
|
241
|
+
return shortUrl
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
return { message: shortenedMessage, links }
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export default LinkTracking
|