@stacksjs/ts-cloud 0.1.8 → 0.1.9
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/dist/bin/cli.js +1 -1
- package/package.json +18 -16
- package/src/aws/acm.ts +768 -0
- package/src/aws/application-autoscaling.ts +845 -0
- package/src/aws/bedrock.ts +4074 -0
- package/src/aws/client.ts +891 -0
- package/src/aws/cloudformation.ts +896 -0
- package/src/aws/cloudfront.ts +1531 -0
- package/src/aws/cloudwatch-logs.ts +154 -0
- package/src/aws/comprehend.ts +839 -0
- package/src/aws/connect.ts +1056 -0
- package/src/aws/deploy-imap.ts +384 -0
- package/src/aws/dynamodb.ts +340 -0
- package/src/aws/ec2.ts +1385 -0
- package/src/aws/ecr.ts +621 -0
- package/src/aws/ecs.ts +615 -0
- package/src/aws/elasticache.ts +301 -0
- package/src/aws/elbv2.ts +942 -0
- package/src/aws/email.ts +928 -0
- package/src/aws/eventbridge.ts +248 -0
- package/src/aws/iam.ts +1689 -0
- package/src/aws/imap-server.ts +2100 -0
- package/src/aws/index.ts +213 -0
- package/src/aws/kendra.ts +1097 -0
- package/src/aws/lambda.ts +786 -0
- package/src/aws/opensearch.ts +158 -0
- package/src/aws/personalize.ts +977 -0
- package/src/aws/polly.ts +559 -0
- package/src/aws/rds.ts +888 -0
- package/src/aws/rekognition.ts +846 -0
- package/src/aws/route53-domains.ts +359 -0
- package/src/aws/route53.ts +1046 -0
- package/src/aws/s3.ts +2334 -0
- package/src/aws/scheduler.ts +571 -0
- package/src/aws/secrets-manager.ts +769 -0
- package/src/aws/ses.ts +1081 -0
- package/src/aws/setup-phone.ts +104 -0
- package/src/aws/setup-sms.ts +580 -0
- package/src/aws/sms.ts +1735 -0
- package/src/aws/smtp-server.ts +531 -0
- package/src/aws/sns.ts +758 -0
- package/src/aws/sqs.ts +382 -0
- package/src/aws/ssm.ts +807 -0
- package/src/aws/sts.ts +92 -0
- package/src/aws/support.ts +391 -0
- package/src/aws/test-imap.ts +86 -0
- package/src/aws/textract.ts +780 -0
- package/src/aws/transcribe.ts +108 -0
- package/src/aws/translate.ts +641 -0
- package/src/aws/voice.ts +1379 -0
- package/src/config.ts +35 -0
- package/src/deploy/index.ts +7 -0
- package/src/deploy/static-site-external-dns.ts +945 -0
- package/src/deploy/static-site.ts +1175 -0
- package/src/dns/cloudflare.ts +548 -0
- package/src/dns/godaddy.ts +412 -0
- package/src/dns/index.ts +205 -0
- package/src/dns/porkbun.ts +362 -0
- package/src/dns/route53-adapter.ts +414 -0
- package/src/dns/types.ts +119 -0
- package/src/dns/validator.ts +369 -0
- package/src/generators/index.ts +5 -0
- package/src/generators/infrastructure.ts +1660 -0
- package/src/index.ts +163 -0
- package/src/push/apns.ts +452 -0
- package/src/push/fcm.ts +506 -0
- package/src/push/index.ts +58 -0
- package/src/security/pre-deploy-scanner.ts +655 -0
- package/src/ssl/acme-client.ts +478 -0
- package/src/ssl/index.ts +7 -0
- package/src/ssl/letsencrypt.ts +747 -0
- package/src/types.ts +2 -0
- package/src/utils/cli.ts +398 -0
- package/src/validation/index.ts +5 -0
- package/src/validation/template.ts +405 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CloudFormation Template Validation
|
|
3
|
+
* Validates templates before deployment
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { CloudFormationTemplate } from '@stacksjs/ts-cloud-aws-types'
|
|
7
|
+
|
|
8
|
+
export interface ValidationError {
|
|
9
|
+
path: string
|
|
10
|
+
message: string
|
|
11
|
+
severity: 'error' | 'warning'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ValidationResult {
|
|
15
|
+
valid: boolean
|
|
16
|
+
errors: ValidationError[]
|
|
17
|
+
warnings: ValidationError[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validate a CloudFormation template
|
|
22
|
+
*/
|
|
23
|
+
export function validateTemplate(template: CloudFormationTemplate): ValidationResult {
|
|
24
|
+
const errors: ValidationError[] = []
|
|
25
|
+
const warnings: ValidationError[] = []
|
|
26
|
+
|
|
27
|
+
// Check required fields
|
|
28
|
+
if (!template.AWSTemplateFormatVersion) {
|
|
29
|
+
errors.push({
|
|
30
|
+
path: 'AWSTemplateFormatVersion',
|
|
31
|
+
message: 'AWSTemplateFormatVersion is required',
|
|
32
|
+
severity: 'error',
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
else if (template.AWSTemplateFormatVersion !== '2010-09-09') {
|
|
36
|
+
warnings.push({
|
|
37
|
+
path: 'AWSTemplateFormatVersion',
|
|
38
|
+
message: 'AWSTemplateFormatVersion should be "2010-09-09"',
|
|
39
|
+
severity: 'warning',
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check Resources section
|
|
44
|
+
if (!template.Resources || Object.keys(template.Resources).length === 0) {
|
|
45
|
+
errors.push({
|
|
46
|
+
path: 'Resources',
|
|
47
|
+
message: 'At least one resource is required',
|
|
48
|
+
severity: 'error',
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// Validate each resource
|
|
53
|
+
for (const [logicalId, resource] of Object.entries(template.Resources)) {
|
|
54
|
+
validateResource(logicalId, resource, errors, warnings)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check for circular dependencies
|
|
59
|
+
if (template.Resources) {
|
|
60
|
+
const circularDeps = findCircularDependencies(template.Resources)
|
|
61
|
+
if (circularDeps.length > 0) {
|
|
62
|
+
errors.push({
|
|
63
|
+
path: 'Resources',
|
|
64
|
+
message: `Circular dependencies detected: ${circularDeps.join(' -> ')}`,
|
|
65
|
+
severity: 'error',
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Validate Parameters if present
|
|
71
|
+
if (template.Parameters) {
|
|
72
|
+
for (const [paramName, param] of Object.entries(template.Parameters)) {
|
|
73
|
+
validateParameter(paramName, param, errors, warnings)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Validate Outputs if present
|
|
78
|
+
if (template.Outputs) {
|
|
79
|
+
for (const [outputName, output] of Object.entries(template.Outputs)) {
|
|
80
|
+
validateOutput(outputName, output, errors, warnings)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
valid: errors.length === 0,
|
|
86
|
+
errors,
|
|
87
|
+
warnings,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Validate a single resource
|
|
93
|
+
*/
|
|
94
|
+
function validateResource(
|
|
95
|
+
logicalId: string,
|
|
96
|
+
resource: any,
|
|
97
|
+
errors: ValidationError[],
|
|
98
|
+
warnings: ValidationError[],
|
|
99
|
+
): void {
|
|
100
|
+
// Check required fields
|
|
101
|
+
if (!resource.Type) {
|
|
102
|
+
errors.push({
|
|
103
|
+
path: `Resources.${logicalId}.Type`,
|
|
104
|
+
message: 'Resource Type is required',
|
|
105
|
+
severity: 'error',
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check logical ID format
|
|
110
|
+
if (!/^[a-zA-Z0-9]+$/.test(logicalId)) {
|
|
111
|
+
warnings.push({
|
|
112
|
+
path: `Resources.${logicalId}`,
|
|
113
|
+
message: 'Logical ID should only contain alphanumeric characters',
|
|
114
|
+
severity: 'warning',
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Validate resource type format
|
|
119
|
+
if (resource.Type && !resource.Type.startsWith('AWS::') && !resource.Type.startsWith('Custom::')) {
|
|
120
|
+
errors.push({
|
|
121
|
+
path: `Resources.${logicalId}.Type`,
|
|
122
|
+
message: 'Resource Type must start with "AWS::" or "Custom::"',
|
|
123
|
+
severity: 'error',
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check for common mistakes
|
|
128
|
+
if (resource.Properties) {
|
|
129
|
+
// Check for undefined or null values
|
|
130
|
+
for (const [propName, propValue] of Object.entries(resource.Properties)) {
|
|
131
|
+
if (propValue === undefined || propValue === null) {
|
|
132
|
+
warnings.push({
|
|
133
|
+
path: `Resources.${logicalId}.Properties.${propName}`,
|
|
134
|
+
message: 'Property has undefined or null value',
|
|
135
|
+
severity: 'warning',
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Validate DependsOn
|
|
142
|
+
if (resource.DependsOn) {
|
|
143
|
+
if (typeof resource.DependsOn === 'string') {
|
|
144
|
+
if (resource.DependsOn === logicalId) {
|
|
145
|
+
errors.push({
|
|
146
|
+
path: `Resources.${logicalId}.DependsOn`,
|
|
147
|
+
message: 'Resource cannot depend on itself',
|
|
148
|
+
severity: 'error',
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else if (Array.isArray(resource.DependsOn)) {
|
|
153
|
+
if (resource.DependsOn.includes(logicalId)) {
|
|
154
|
+
errors.push({
|
|
155
|
+
path: `Resources.${logicalId}.DependsOn`,
|
|
156
|
+
message: 'Resource cannot depend on itself',
|
|
157
|
+
severity: 'error',
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Validate a parameter
|
|
166
|
+
*/
|
|
167
|
+
function validateParameter(
|
|
168
|
+
paramName: string,
|
|
169
|
+
param: any,
|
|
170
|
+
errors: ValidationError[],
|
|
171
|
+
warnings: ValidationError[],
|
|
172
|
+
): void {
|
|
173
|
+
if (!param.Type) {
|
|
174
|
+
errors.push({
|
|
175
|
+
path: `Parameters.${paramName}.Type`,
|
|
176
|
+
message: 'Parameter Type is required',
|
|
177
|
+
severity: 'error',
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const validTypes = ['String', 'Number', 'List<Number>', 'CommaDelimitedList', 'AWS::SSM::Parameter::Value<String>']
|
|
182
|
+
if (param.Type && !validTypes.includes(param.Type) && !param.Type.startsWith('AWS::')) {
|
|
183
|
+
warnings.push({
|
|
184
|
+
path: `Parameters.${paramName}.Type`,
|
|
185
|
+
message: `Uncommon parameter type: ${param.Type}`,
|
|
186
|
+
severity: 'warning',
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check for default value with NoEcho
|
|
191
|
+
if (param.NoEcho && param.Default) {
|
|
192
|
+
warnings.push({
|
|
193
|
+
path: `Parameters.${paramName}`,
|
|
194
|
+
message: 'NoEcho parameters should not have default values',
|
|
195
|
+
severity: 'warning',
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Validate an output
|
|
202
|
+
*/
|
|
203
|
+
function validateOutput(
|
|
204
|
+
outputName: string,
|
|
205
|
+
output: any,
|
|
206
|
+
errors: ValidationError[],
|
|
207
|
+
warnings: ValidationError[],
|
|
208
|
+
): void {
|
|
209
|
+
if (!output.Value) {
|
|
210
|
+
errors.push({
|
|
211
|
+
path: `Outputs.${outputName}.Value`,
|
|
212
|
+
message: 'Output Value is required',
|
|
213
|
+
severity: 'error',
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Find circular dependencies in resources
|
|
220
|
+
*/
|
|
221
|
+
function findCircularDependencies(resources: Record<string, any>): string[] {
|
|
222
|
+
const graph: Record<string, string[]> = {}
|
|
223
|
+
|
|
224
|
+
// Build dependency graph
|
|
225
|
+
for (const [logicalId, resource] of Object.entries(resources)) {
|
|
226
|
+
graph[logicalId] = []
|
|
227
|
+
|
|
228
|
+
// Explicit dependencies (DependsOn)
|
|
229
|
+
if (resource.DependsOn) {
|
|
230
|
+
if (typeof resource.DependsOn === 'string') {
|
|
231
|
+
graph[logicalId].push(resource.DependsOn)
|
|
232
|
+
}
|
|
233
|
+
else if (Array.isArray(resource.DependsOn)) {
|
|
234
|
+
graph[logicalId].push(...resource.DependsOn)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Implicit dependencies (Ref, GetAtt)
|
|
239
|
+
const deps = extractDependencies(resource)
|
|
240
|
+
graph[logicalId].push(...deps)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Detect cycles using DFS
|
|
244
|
+
const visited = new Set<string>()
|
|
245
|
+
const recursionStack = new Set<string>()
|
|
246
|
+
const cycle: string[] = []
|
|
247
|
+
|
|
248
|
+
function dfs(node: string, path: string[]): boolean {
|
|
249
|
+
visited.add(node)
|
|
250
|
+
recursionStack.add(node)
|
|
251
|
+
path.push(node)
|
|
252
|
+
|
|
253
|
+
const neighbors = graph[node] || []
|
|
254
|
+
for (const neighbor of neighbors) {
|
|
255
|
+
if (!visited.has(neighbor)) {
|
|
256
|
+
if (dfs(neighbor, path)) {
|
|
257
|
+
return true
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else if (recursionStack.has(neighbor)) {
|
|
261
|
+
// Cycle detected
|
|
262
|
+
const cycleStart = path.indexOf(neighbor)
|
|
263
|
+
cycle.push(...path.slice(cycleStart), neighbor)
|
|
264
|
+
return true
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
path.pop()
|
|
269
|
+
recursionStack.delete(node)
|
|
270
|
+
return false
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
for (const node of Object.keys(graph)) {
|
|
274
|
+
if (!visited.has(node)) {
|
|
275
|
+
if (dfs(node, [])) {
|
|
276
|
+
return cycle
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return []
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Extract dependencies from a resource (Ref, GetAtt, etc.)
|
|
286
|
+
*/
|
|
287
|
+
function extractDependencies(obj: any, deps: string[] = []): string[] {
|
|
288
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
289
|
+
return deps
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (Array.isArray(obj)) {
|
|
293
|
+
for (const item of obj) {
|
|
294
|
+
extractDependencies(item, deps)
|
|
295
|
+
}
|
|
296
|
+
return deps
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Check for Ref
|
|
300
|
+
if (obj.Ref && typeof obj.Ref === 'string' && !obj.Ref.startsWith('AWS::')) {
|
|
301
|
+
deps.push(obj.Ref)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Check for GetAtt
|
|
305
|
+
if (obj['Fn::GetAtt']) {
|
|
306
|
+
const getAtt = obj['Fn::GetAtt']
|
|
307
|
+
if (Array.isArray(getAtt) && getAtt.length > 0) {
|
|
308
|
+
deps.push(getAtt[0])
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Recurse into object properties
|
|
313
|
+
for (const value of Object.values(obj)) {
|
|
314
|
+
extractDependencies(value, deps)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return deps
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Validate template size
|
|
322
|
+
*/
|
|
323
|
+
export function validateTemplateSize(templateBody: string): ValidationResult {
|
|
324
|
+
const errors: ValidationError[] = []
|
|
325
|
+
const warnings: ValidationError[] = []
|
|
326
|
+
|
|
327
|
+
const sizeInBytes = new TextEncoder().encode(templateBody).length
|
|
328
|
+
|
|
329
|
+
// CloudFormation limits
|
|
330
|
+
const maxBodySize = 51200 // 51,200 bytes (50 KB)
|
|
331
|
+
const maxS3Size = 460800 // 460,800 bytes (450 KB)
|
|
332
|
+
|
|
333
|
+
if (sizeInBytes > maxBodySize) {
|
|
334
|
+
if (sizeInBytes > maxS3Size) {
|
|
335
|
+
errors.push({
|
|
336
|
+
path: 'template',
|
|
337
|
+
message: `Template size (${sizeInBytes} bytes) exceeds maximum allowed size of ${maxS3Size} bytes`,
|
|
338
|
+
severity: 'error',
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
warnings.push({
|
|
343
|
+
path: 'template',
|
|
344
|
+
message: `Template size (${sizeInBytes} bytes) exceeds direct upload limit (${maxBodySize} bytes). You must upload to S3 first.`,
|
|
345
|
+
severity: 'warning',
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
valid: errors.length === 0,
|
|
352
|
+
errors,
|
|
353
|
+
warnings,
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Validate template resource limits
|
|
359
|
+
*/
|
|
360
|
+
export function validateResourceLimits(template: CloudFormationTemplate): ValidationResult {
|
|
361
|
+
const errors: ValidationError[] = []
|
|
362
|
+
const warnings: ValidationError[] = []
|
|
363
|
+
|
|
364
|
+
const resourceCount = template.Resources ? Object.keys(template.Resources).length : 0
|
|
365
|
+
const parameterCount = template.Parameters ? Object.keys(template.Parameters).length : 0
|
|
366
|
+
const outputCount = template.Outputs ? Object.keys(template.Outputs).length : 0
|
|
367
|
+
|
|
368
|
+
// CloudFormation limits
|
|
369
|
+
if (resourceCount > 500) {
|
|
370
|
+
errors.push({
|
|
371
|
+
path: 'Resources',
|
|
372
|
+
message: `Template has ${resourceCount} resources, exceeding the limit of 500`,
|
|
373
|
+
severity: 'error',
|
|
374
|
+
})
|
|
375
|
+
}
|
|
376
|
+
else if (resourceCount > 200) {
|
|
377
|
+
warnings.push({
|
|
378
|
+
path: 'Resources',
|
|
379
|
+
message: `Template has ${resourceCount} resources. Consider using nested stacks for better organization.`,
|
|
380
|
+
severity: 'warning',
|
|
381
|
+
})
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (parameterCount > 200) {
|
|
385
|
+
errors.push({
|
|
386
|
+
path: 'Parameters',
|
|
387
|
+
message: `Template has ${parameterCount} parameters, exceeding the limit of 200`,
|
|
388
|
+
severity: 'error',
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (outputCount > 200) {
|
|
393
|
+
errors.push({
|
|
394
|
+
path: 'Outputs',
|
|
395
|
+
message: `Template has ${outputCount} outputs, exceeding the limit of 200`,
|
|
396
|
+
severity: 'error',
|
|
397
|
+
})
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
valid: errors.length === 0,
|
|
402
|
+
errors,
|
|
403
|
+
warnings,
|
|
404
|
+
}
|
|
405
|
+
}
|