@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,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMS Send Lambda Handler
|
|
3
|
+
*
|
|
4
|
+
* Sends SMS messages:
|
|
5
|
+
* - Send SMS via Pinpoint/SNS
|
|
6
|
+
* - Handle templated messages
|
|
7
|
+
* - Track delivery status
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const handler = `
|
|
11
|
+
const { PinpointClient, SendMessagesCommand } = require('@aws-sdk/client-pinpoint');
|
|
12
|
+
const { SNSClient, PublishCommand } = require('@aws-sdk/client-sns');
|
|
13
|
+
const { DynamoDBClient, PutItemCommand } = require('@aws-sdk/client-dynamodb');
|
|
14
|
+
|
|
15
|
+
const pinpoint = new PinpointClient({});
|
|
16
|
+
const sns = new SNSClient({});
|
|
17
|
+
const dynamodb = new DynamoDBClient({});
|
|
18
|
+
|
|
19
|
+
exports.handler = async (event) => {
|
|
20
|
+
console.log('SMS send event:', JSON.stringify(event, null, 2));
|
|
21
|
+
|
|
22
|
+
const applicationId = process.env.PINPOINT_APP_ID;
|
|
23
|
+
const messageLogTable = process.env.MESSAGE_LOG_TABLE;
|
|
24
|
+
const senderId = process.env.SMS_SENDER_ID;
|
|
25
|
+
const originationNumber = process.env.SMS_ORIGINATION_NUMBER;
|
|
26
|
+
|
|
27
|
+
// Handle both direct invocation and SQS/SNS events
|
|
28
|
+
const messages = event.Records
|
|
29
|
+
? event.Records.map(r => JSON.parse(r.body || r.Sns?.Message || '{}'))
|
|
30
|
+
: [event];
|
|
31
|
+
|
|
32
|
+
const results = [];
|
|
33
|
+
|
|
34
|
+
for (const message of messages) {
|
|
35
|
+
try {
|
|
36
|
+
const {
|
|
37
|
+
to,
|
|
38
|
+
body,
|
|
39
|
+
template,
|
|
40
|
+
templateData,
|
|
41
|
+
messageType = 'TRANSACTIONAL',
|
|
42
|
+
} = message;
|
|
43
|
+
|
|
44
|
+
if (!to || (!body && !template)) {
|
|
45
|
+
console.log('Missing required fields (to, body/template)');
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Resolve template if provided
|
|
50
|
+
let messageBody = body;
|
|
51
|
+
if (template && templateData) {
|
|
52
|
+
messageBody = resolveTemplate(template, templateData);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const messageId = \`sms-\${Date.now()}-\${Math.random().toString(36).substr(2)}\`;
|
|
56
|
+
|
|
57
|
+
// Send via Pinpoint if app ID is configured
|
|
58
|
+
if (applicationId) {
|
|
59
|
+
const sendResult = await pinpoint.send(new SendMessagesCommand({
|
|
60
|
+
ApplicationId: applicationId,
|
|
61
|
+
MessageRequest: {
|
|
62
|
+
Addresses: {
|
|
63
|
+
[to]: {
|
|
64
|
+
ChannelType: 'SMS',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
MessageConfiguration: {
|
|
68
|
+
SMSMessage: {
|
|
69
|
+
Body: messageBody,
|
|
70
|
+
MessageType: messageType,
|
|
71
|
+
SenderId: senderId,
|
|
72
|
+
OriginationNumber: originationNumber,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
const result = sendResult.MessageResponse?.Result?.[to] || {};
|
|
79
|
+
|
|
80
|
+
// Log message
|
|
81
|
+
if (messageLogTable) {
|
|
82
|
+
await dynamodb.send(new PutItemCommand({
|
|
83
|
+
TableName: messageLogTable,
|
|
84
|
+
Item: {
|
|
85
|
+
messageId: { S: result.MessageId || messageId },
|
|
86
|
+
to: { S: to },
|
|
87
|
+
body: { S: messageBody },
|
|
88
|
+
messageType: { S: messageType },
|
|
89
|
+
deliveryStatus: { S: result.DeliveryStatus || 'UNKNOWN' },
|
|
90
|
+
statusCode: { N: String(result.StatusCode || 0) },
|
|
91
|
+
statusMessage: { S: result.StatusMessage || '' },
|
|
92
|
+
sentAt: { S: new Date().toISOString() },
|
|
93
|
+
ttl: { N: String(Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60) },
|
|
94
|
+
},
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
results.push({
|
|
99
|
+
to,
|
|
100
|
+
messageId: result.MessageId || messageId,
|
|
101
|
+
status: result.DeliveryStatus,
|
|
102
|
+
statusCode: result.StatusCode,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
} else {
|
|
106
|
+
// Fallback to SNS
|
|
107
|
+
const snsResult = await sns.send(new PublishCommand({
|
|
108
|
+
PhoneNumber: to,
|
|
109
|
+
Message: messageBody,
|
|
110
|
+
MessageAttributes: {
|
|
111
|
+
'AWS.SNS.SMS.SMSType': {
|
|
112
|
+
DataType: 'String',
|
|
113
|
+
StringValue: messageType === 'PROMOTIONAL' ? 'Promotional' : 'Transactional',
|
|
114
|
+
},
|
|
115
|
+
...(senderId && {
|
|
116
|
+
'AWS.SNS.SMS.SenderID': {
|
|
117
|
+
DataType: 'String',
|
|
118
|
+
StringValue: senderId,
|
|
119
|
+
},
|
|
120
|
+
}),
|
|
121
|
+
},
|
|
122
|
+
}));
|
|
123
|
+
|
|
124
|
+
// Log message
|
|
125
|
+
if (messageLogTable) {
|
|
126
|
+
await dynamodb.send(new PutItemCommand({
|
|
127
|
+
TableName: messageLogTable,
|
|
128
|
+
Item: {
|
|
129
|
+
messageId: { S: snsResult.MessageId || messageId },
|
|
130
|
+
to: { S: to },
|
|
131
|
+
body: { S: messageBody },
|
|
132
|
+
messageType: { S: messageType },
|
|
133
|
+
deliveryStatus: { S: 'SENT' },
|
|
134
|
+
sentAt: { S: new Date().toISOString() },
|
|
135
|
+
ttl: { N: String(Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60) },
|
|
136
|
+
},
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
results.push({
|
|
141
|
+
to,
|
|
142
|
+
messageId: snsResult.MessageId || messageId,
|
|
143
|
+
status: 'SENT',
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(\`SMS sent to \${to}: \${results[results.length - 1].status}\`);
|
|
148
|
+
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error('Error sending SMS:', error);
|
|
151
|
+
results.push({
|
|
152
|
+
to: message.to,
|
|
153
|
+
error: error.message,
|
|
154
|
+
status: 'FAILED',
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
statusCode: 200,
|
|
161
|
+
body: JSON.stringify({ results }),
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
function resolveTemplate(template, data) {
|
|
166
|
+
let result = template;
|
|
167
|
+
for (const [key, value] of Object.entries(data)) {
|
|
168
|
+
result = result.replace(new RegExp(\`{{\\\\s*\${key}\\\\s*}}\`, 'g'), value);
|
|
169
|
+
}
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
`
|
|
173
|
+
|
|
174
|
+
export default handler
|
package/src/sms/index.ts
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stack Diff Analyzer
|
|
3
|
+
* Analyzes differences between CloudFormation templates for stack updates
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { CloudFormationResource, CloudFormationTemplate } from '@stacksjs/ts-cloud-aws-types'
|
|
7
|
+
|
|
8
|
+
export interface ResourceDiff {
|
|
9
|
+
logicalId: string
|
|
10
|
+
action: 'add' | 'remove' | 'update' | 'replace'
|
|
11
|
+
resourceType: string
|
|
12
|
+
changes?: PropertyChange[]
|
|
13
|
+
reason?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PropertyChange {
|
|
17
|
+
path: string
|
|
18
|
+
oldValue: any
|
|
19
|
+
newValue: any
|
|
20
|
+
requiresReplacement?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface StackDiff {
|
|
24
|
+
added: ResourceDiff[]
|
|
25
|
+
removed: ResourceDiff[]
|
|
26
|
+
updated: ResourceDiff[]
|
|
27
|
+
replaced: ResourceDiff[]
|
|
28
|
+
unchanged: string[]
|
|
29
|
+
summary: {
|
|
30
|
+
totalChanges: number
|
|
31
|
+
requiresReplacement: boolean
|
|
32
|
+
dangerousChanges: string[]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Analyze differences between two CloudFormation templates
|
|
38
|
+
*/
|
|
39
|
+
export function analyzeStackDiff(
|
|
40
|
+
oldTemplate: CloudFormationTemplate,
|
|
41
|
+
newTemplate: CloudFormationTemplate,
|
|
42
|
+
): StackDiff {
|
|
43
|
+
const added: ResourceDiff[] = []
|
|
44
|
+
const removed: ResourceDiff[] = []
|
|
45
|
+
const updated: ResourceDiff[] = []
|
|
46
|
+
const replaced: ResourceDiff[] = []
|
|
47
|
+
const unchanged: string[] = []
|
|
48
|
+
|
|
49
|
+
const oldResources = oldTemplate.Resources || {}
|
|
50
|
+
const newResources = newTemplate.Resources || {}
|
|
51
|
+
|
|
52
|
+
const allLogicalIds = new Set([
|
|
53
|
+
...Object.keys(oldResources),
|
|
54
|
+
...Object.keys(newResources),
|
|
55
|
+
])
|
|
56
|
+
|
|
57
|
+
for (const logicalId of allLogicalIds) {
|
|
58
|
+
const oldResource = oldResources[logicalId]
|
|
59
|
+
const newResource = newResources[logicalId]
|
|
60
|
+
|
|
61
|
+
if (!oldResource && newResource) {
|
|
62
|
+
// Resource added
|
|
63
|
+
added.push({
|
|
64
|
+
logicalId,
|
|
65
|
+
action: 'add',
|
|
66
|
+
resourceType: newResource.Type,
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
else if (oldResource && !newResource) {
|
|
70
|
+
// Resource removed
|
|
71
|
+
removed.push({
|
|
72
|
+
logicalId,
|
|
73
|
+
action: 'remove',
|
|
74
|
+
resourceType: oldResource.Type,
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
else if (oldResource && newResource) {
|
|
78
|
+
// Resource exists in both - check for changes
|
|
79
|
+
if (oldResource.Type !== newResource.Type) {
|
|
80
|
+
// Type changed - this is a replacement
|
|
81
|
+
replaced.push({
|
|
82
|
+
logicalId,
|
|
83
|
+
action: 'replace',
|
|
84
|
+
resourceType: newResource.Type,
|
|
85
|
+
reason: `Type changed from ${oldResource.Type} to ${newResource.Type}`,
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const changes = compareProperties(
|
|
90
|
+
oldResource.Properties || {},
|
|
91
|
+
newResource.Properties || {},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if (changes.length > 0) {
|
|
95
|
+
const requiresReplacement = checkIfReplacementRequired(
|
|
96
|
+
oldResource.Type,
|
|
97
|
+
changes,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if (requiresReplacement) {
|
|
101
|
+
replaced.push({
|
|
102
|
+
logicalId,
|
|
103
|
+
action: 'replace',
|
|
104
|
+
resourceType: newResource.Type,
|
|
105
|
+
changes,
|
|
106
|
+
reason: 'Property changes require replacement',
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
updated.push({
|
|
111
|
+
logicalId,
|
|
112
|
+
action: 'update',
|
|
113
|
+
resourceType: newResource.Type,
|
|
114
|
+
changes,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
unchanged.push(logicalId)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const dangerousChanges = identifyDangerousChanges([
|
|
126
|
+
...removed,
|
|
127
|
+
...replaced,
|
|
128
|
+
...updated,
|
|
129
|
+
])
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
added,
|
|
133
|
+
removed,
|
|
134
|
+
updated,
|
|
135
|
+
replaced,
|
|
136
|
+
unchanged,
|
|
137
|
+
summary: {
|
|
138
|
+
totalChanges: added.length + removed.length + updated.length + replaced.length,
|
|
139
|
+
requiresReplacement: replaced.length > 0,
|
|
140
|
+
dangerousChanges,
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Compare properties of two resources
|
|
147
|
+
*/
|
|
148
|
+
function compareProperties(
|
|
149
|
+
oldProps: Record<string, any>,
|
|
150
|
+
newProps: Record<string, any>,
|
|
151
|
+
path = '',
|
|
152
|
+
): PropertyChange[] {
|
|
153
|
+
const changes: PropertyChange[] = []
|
|
154
|
+
const allKeys = new Set([...Object.keys(oldProps), ...Object.keys(newProps)])
|
|
155
|
+
|
|
156
|
+
for (const key of allKeys) {
|
|
157
|
+
const currentPath = path ? `${path}.${key}` : key
|
|
158
|
+
const oldValue = oldProps[key]
|
|
159
|
+
const newValue = newProps[key]
|
|
160
|
+
|
|
161
|
+
if (oldValue === undefined && newValue !== undefined) {
|
|
162
|
+
changes.push({
|
|
163
|
+
path: currentPath,
|
|
164
|
+
oldValue: undefined,
|
|
165
|
+
newValue,
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
else if (oldValue !== undefined && newValue === undefined) {
|
|
169
|
+
changes.push({
|
|
170
|
+
path: currentPath,
|
|
171
|
+
oldValue,
|
|
172
|
+
newValue: undefined,
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
else if (typeof oldValue !== typeof newValue) {
|
|
176
|
+
changes.push({
|
|
177
|
+
path: currentPath,
|
|
178
|
+
oldValue,
|
|
179
|
+
newValue,
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
else if (typeof oldValue === 'object' && oldValue !== null) {
|
|
183
|
+
if (Array.isArray(oldValue) && Array.isArray(newValue)) {
|
|
184
|
+
if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
|
|
185
|
+
changes.push({
|
|
186
|
+
path: currentPath,
|
|
187
|
+
oldValue,
|
|
188
|
+
newValue,
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else if (!Array.isArray(oldValue) && !Array.isArray(newValue)) {
|
|
193
|
+
const nestedChanges = compareProperties(oldValue, newValue, currentPath)
|
|
194
|
+
changes.push(...nestedChanges)
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
changes.push({
|
|
198
|
+
path: currentPath,
|
|
199
|
+
oldValue,
|
|
200
|
+
newValue,
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else if (oldValue !== newValue) {
|
|
205
|
+
changes.push({
|
|
206
|
+
path: currentPath,
|
|
207
|
+
oldValue,
|
|
208
|
+
newValue,
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return changes
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Check if property changes require resource replacement
|
|
218
|
+
* Based on CloudFormation resource specifications
|
|
219
|
+
*/
|
|
220
|
+
function checkIfReplacementRequired(
|
|
221
|
+
resourceType: string,
|
|
222
|
+
changes: PropertyChange[],
|
|
223
|
+
): boolean {
|
|
224
|
+
// Map of resource types to properties that require replacement
|
|
225
|
+
const replacementProperties: Record<string, Set<string>> = {
|
|
226
|
+
'AWS::S3::Bucket': new Set(['BucketName']),
|
|
227
|
+
'AWS::DynamoDB::Table': new Set(['TableName', 'KeySchema']),
|
|
228
|
+
'AWS::RDS::DBInstance': new Set(['DBInstanceIdentifier', 'DBName', 'Engine']),
|
|
229
|
+
'AWS::Lambda::Function': new Set(['FunctionName']), // Changed in some cases
|
|
230
|
+
'AWS::EC2::Instance': new Set(['ImageId', 'InstanceType', 'KeyName']),
|
|
231
|
+
'AWS::ECS::TaskDefinition': new Set(['Family', 'ContainerDefinitions']),
|
|
232
|
+
'AWS::ElastiCache::CacheCluster': new Set(['CacheNodeType', 'Engine']),
|
|
233
|
+
'AWS::CloudFront::Distribution': new Set([]), // Most properties can be updated
|
|
234
|
+
'AWS::Route53::HostedZone': new Set(['Name']),
|
|
235
|
+
'AWS::IAM::Role': new Set(['RoleName', 'Path']),
|
|
236
|
+
'AWS::KMS::Key': new Set(['KeyPolicy']), // Some properties require replacement
|
|
237
|
+
'AWS::Cognito::UserPool': new Set(['UserPoolName']),
|
|
238
|
+
'AWS::OpenSearchService::Domain': new Set(['DomainName']),
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const replaceProps = replacementProperties[resourceType]
|
|
242
|
+
if (!replaceProps) {
|
|
243
|
+
// Unknown resource type - assume updates might require replacement
|
|
244
|
+
return false
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (const change of changes) {
|
|
248
|
+
const topLevelProp = change.path.split('.')[0]
|
|
249
|
+
if (replaceProps.has(topLevelProp)) {
|
|
250
|
+
return true
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return false
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Identify potentially dangerous changes
|
|
259
|
+
*/
|
|
260
|
+
function identifyDangerousChanges(diffs: ResourceDiff[]): string[] {
|
|
261
|
+
const dangerous: string[] = []
|
|
262
|
+
|
|
263
|
+
for (const diff of diffs) {
|
|
264
|
+
// Removing databases is dangerous
|
|
265
|
+
if (diff.action === 'remove' && (
|
|
266
|
+
diff.resourceType === 'AWS::RDS::DBInstance'
|
|
267
|
+
|| diff.resourceType === 'AWS::DynamoDB::Table'
|
|
268
|
+
|| diff.resourceType === 'AWS::ElastiCache::CacheCluster'
|
|
269
|
+
)) {
|
|
270
|
+
dangerous.push(`Removing ${diff.resourceType} ${diff.logicalId} - data loss risk!`)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Replacing databases is dangerous
|
|
274
|
+
if (diff.action === 'replace' && (
|
|
275
|
+
diff.resourceType === 'AWS::RDS::DBInstance'
|
|
276
|
+
|| diff.resourceType === 'AWS::DynamoDB::Table'
|
|
277
|
+
)) {
|
|
278
|
+
dangerous.push(`Replacing ${diff.resourceType} ${diff.logicalId} - data loss risk!`)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Removing/replacing S3 buckets
|
|
282
|
+
if ((diff.action === 'remove' || diff.action === 'replace')
|
|
283
|
+
&& diff.resourceType === 'AWS::S3::Bucket') {
|
|
284
|
+
dangerous.push(`${diff.action === 'remove' ? 'Removing' : 'Replacing'} S3 bucket ${diff.logicalId} - data loss risk!`)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Replacing EC2 instances
|
|
288
|
+
if (diff.action === 'replace' && diff.resourceType === 'AWS::EC2::Instance') {
|
|
289
|
+
dangerous.push(`Replacing EC2 instance ${diff.logicalId} - downtime expected`)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Security group changes
|
|
293
|
+
if (diff.action === 'update' && diff.resourceType === 'AWS::EC2::SecurityGroup') {
|
|
294
|
+
dangerous.push(`Updating security group ${diff.logicalId} - may affect connectivity`)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// IAM role/policy changes
|
|
298
|
+
if ((diff.action === 'update' || diff.action === 'replace') && (
|
|
299
|
+
diff.resourceType === 'AWS::IAM::Role'
|
|
300
|
+
|| diff.resourceType === 'AWS::IAM::Policy'
|
|
301
|
+
)) {
|
|
302
|
+
dangerous.push(`Modifying IAM ${diff.resourceType.split('::')[2]} ${diff.logicalId} - may affect permissions`)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return dangerous
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Format diff for display
|
|
311
|
+
*/
|
|
312
|
+
export function formatDiff(diff: StackDiff): string {
|
|
313
|
+
const lines: string[] = []
|
|
314
|
+
|
|
315
|
+
lines.push('=== Stack Update Analysis ===\n')
|
|
316
|
+
|
|
317
|
+
if (diff.summary.totalChanges === 0) {
|
|
318
|
+
lines.push('No changes detected.\n')
|
|
319
|
+
return lines.join('\n')
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
lines.push(`Total changes: ${diff.summary.totalChanges}`)
|
|
323
|
+
lines.push(`Requires replacement: ${diff.summary.requiresReplacement ? 'Yes' : 'No'}\n`)
|
|
324
|
+
|
|
325
|
+
if (diff.summary.dangerousChanges.length > 0) {
|
|
326
|
+
lines.push('⚠️ DANGEROUS CHANGES DETECTED:')
|
|
327
|
+
for (const warning of diff.summary.dangerousChanges) {
|
|
328
|
+
lines.push(` • ${warning}`)
|
|
329
|
+
}
|
|
330
|
+
lines.push('')
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (diff.added.length > 0) {
|
|
334
|
+
lines.push(`✅ Resources to Add (${diff.added.length}):`)
|
|
335
|
+
for (const resource of diff.added) {
|
|
336
|
+
lines.push(` + ${resource.logicalId} (${resource.resourceType})`)
|
|
337
|
+
}
|
|
338
|
+
lines.push('')
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (diff.removed.length > 0) {
|
|
342
|
+
lines.push(`❌ Resources to Remove (${diff.removed.length}):`)
|
|
343
|
+
for (const resource of diff.removed) {
|
|
344
|
+
lines.push(` - ${resource.logicalId} (${resource.resourceType})`)
|
|
345
|
+
}
|
|
346
|
+
lines.push('')
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (diff.replaced.length > 0) {
|
|
350
|
+
lines.push(`🔄 Resources to Replace (${diff.replaced.length}):`)
|
|
351
|
+
for (const resource of diff.replaced) {
|
|
352
|
+
lines.push(` ~ ${resource.logicalId} (${resource.resourceType})`)
|
|
353
|
+
if (resource.reason) {
|
|
354
|
+
lines.push(` Reason: ${resource.reason}`)
|
|
355
|
+
}
|
|
356
|
+
if (resource.changes) {
|
|
357
|
+
for (const change of resource.changes.slice(0, 3)) {
|
|
358
|
+
lines.push(` • ${change.path}: ${JSON.stringify(change.oldValue)} → ${JSON.stringify(change.newValue)}`)
|
|
359
|
+
}
|
|
360
|
+
if (resource.changes.length > 3) {
|
|
361
|
+
lines.push(` ... and ${resource.changes.length - 3} more changes`)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
lines.push('')
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (diff.updated.length > 0) {
|
|
369
|
+
lines.push(`📝 Resources to Update (${diff.updated.length}):`)
|
|
370
|
+
for (const resource of diff.updated) {
|
|
371
|
+
lines.push(` ~ ${resource.logicalId} (${resource.resourceType})`)
|
|
372
|
+
if (resource.changes) {
|
|
373
|
+
for (const change of resource.changes.slice(0, 3)) {
|
|
374
|
+
lines.push(` • ${change.path}: ${JSON.stringify(change.oldValue)} → ${JSON.stringify(change.newValue)}`)
|
|
375
|
+
}
|
|
376
|
+
if (resource.changes.length > 3) {
|
|
377
|
+
lines.push(` ... and ${resource.changes.length - 3} more changes`)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
lines.push('')
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (diff.unchanged.length > 0) {
|
|
385
|
+
lines.push(`⚪ Unchanged Resources (${diff.unchanged.length})`)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return lines.join('\n')
|
|
389
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static Site Advanced Features
|
|
3
|
+
* Asset optimization, image optimization, SSG support, and prerendering
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface AssetOptimization {
|
|
7
|
+
id: string
|
|
8
|
+
name: string
|
|
9
|
+
minify: boolean
|
|
10
|
+
compress: boolean
|
|
11
|
+
compressionType: 'gzip' | 'brotli' | 'both'
|
|
12
|
+
sourceMaps: boolean
|
|
13
|
+
cacheControl: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ImageOptimization {
|
|
17
|
+
id: string
|
|
18
|
+
formats: Array<'webp' | 'avif' | 'jpeg' | 'png'>
|
|
19
|
+
quality: number
|
|
20
|
+
responsive: boolean
|
|
21
|
+
lazy: boolean
|
|
22
|
+
sizes: number[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SSGConfig {
|
|
26
|
+
id: string
|
|
27
|
+
framework: 'next' | 'gatsby' | 'astro' | 'hugo' | 'eleventy'
|
|
28
|
+
outputDir: string
|
|
29
|
+
buildCommand: string
|
|
30
|
+
routes: string[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface PrerenderConfig {
|
|
34
|
+
id: string
|
|
35
|
+
routes: string[]
|
|
36
|
+
fallback: 'blocking' | 'static' | false
|
|
37
|
+
revalidate?: number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class StaticSiteManager {
|
|
41
|
+
private optimizations: Map<string, AssetOptimization> = new Map()
|
|
42
|
+
private imageConfigs: Map<string, ImageOptimization> = new Map()
|
|
43
|
+
private ssgConfigs: Map<string, SSGConfig> = new Map()
|
|
44
|
+
private prerenderConfigs: Map<string, PrerenderConfig> = new Map()
|
|
45
|
+
private counter = 0
|
|
46
|
+
|
|
47
|
+
createAssetOptimization(config: Omit<AssetOptimization, 'id'>): AssetOptimization {
|
|
48
|
+
const id = `asset-opt-${Date.now()}-${this.counter++}`
|
|
49
|
+
const optimization = { id, ...config }
|
|
50
|
+
this.optimizations.set(id, optimization)
|
|
51
|
+
return optimization
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
createImageOptimization(config: Omit<ImageOptimization, 'id'>): ImageOptimization {
|
|
55
|
+
const id = `image-opt-${Date.now()}-${this.counter++}`
|
|
56
|
+
const optimization = { id, ...config }
|
|
57
|
+
this.imageConfigs.set(id, optimization)
|
|
58
|
+
return optimization
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
createSSGConfig(config: Omit<SSGConfig, 'id'>): SSGConfig {
|
|
62
|
+
const id = `ssg-${Date.now()}-${this.counter++}`
|
|
63
|
+
const ssgConfig = { id, ...config }
|
|
64
|
+
this.ssgConfigs.set(id, ssgConfig)
|
|
65
|
+
return ssgConfig
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
createPrerenderConfig(config: Omit<PrerenderConfig, 'id'>): PrerenderConfig {
|
|
69
|
+
const id = `prerender-${Date.now()}-${this.counter++}`
|
|
70
|
+
const prerenderConfig = { id, ...config }
|
|
71
|
+
this.prerenderConfigs.set(id, prerenderConfig)
|
|
72
|
+
return prerenderConfig
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
listOptimizations(): AssetOptimization[] { return Array.from(this.optimizations.values()) }
|
|
76
|
+
listImageConfigs(): ImageOptimization[] { return Array.from(this.imageConfigs.values()) }
|
|
77
|
+
clear(): void {
|
|
78
|
+
this.optimizations.clear()
|
|
79
|
+
this.imageConfigs.clear()
|
|
80
|
+
this.ssgConfigs.clear()
|
|
81
|
+
this.prerenderConfigs.clear()
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const staticSiteManager: StaticSiteManager = new StaticSiteManager()
|