@stacksjs/ts-cloud-core 0.1.7 → 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/package.json +7 -6
- 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
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWS CloudFormation API Client
|
|
3
|
+
* Direct API calls without AWS SDK dependency
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AWSCredentials } from './credentials'
|
|
7
|
+
import { resolveCredentials } from './credentials'
|
|
8
|
+
import { makeAWSRequest, parseXMLResponse } from './signature'
|
|
9
|
+
|
|
10
|
+
export interface CloudFormationStack {
|
|
11
|
+
StackName: string
|
|
12
|
+
StackId?: string
|
|
13
|
+
StackStatus?: string
|
|
14
|
+
CreationTime?: string
|
|
15
|
+
LastUpdatedTime?: string
|
|
16
|
+
StackStatusReason?: string
|
|
17
|
+
Description?: string
|
|
18
|
+
Parameters?: Array<{ ParameterKey: string, ParameterValue: string }>
|
|
19
|
+
Outputs?: Array<{ OutputKey: string, OutputValue: string, Description?: string }>
|
|
20
|
+
Tags?: Array<{ Key: string, Value: string }>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CreateStackOptions {
|
|
24
|
+
stackName: string
|
|
25
|
+
templateBody?: string
|
|
26
|
+
templateURL?: string
|
|
27
|
+
parameters?: Record<string, string>
|
|
28
|
+
capabilities?: string[]
|
|
29
|
+
tags?: Record<string, string>
|
|
30
|
+
timeoutInMinutes?: number
|
|
31
|
+
onFailure?: 'DO_NOTHING' | 'ROLLBACK' | 'DELETE'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface UpdateStackOptions {
|
|
35
|
+
stackName: string
|
|
36
|
+
templateBody?: string
|
|
37
|
+
templateURL?: string
|
|
38
|
+
parameters?: Record<string, string>
|
|
39
|
+
capabilities?: string[]
|
|
40
|
+
tags?: Record<string, string>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface StackEvent {
|
|
44
|
+
EventId: string
|
|
45
|
+
StackName: string
|
|
46
|
+
LogicalResourceId: string
|
|
47
|
+
PhysicalResourceId?: string
|
|
48
|
+
ResourceType: string
|
|
49
|
+
Timestamp: string
|
|
50
|
+
ResourceStatus: string
|
|
51
|
+
ResourceStatusReason?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* CloudFormation API Client
|
|
56
|
+
*/
|
|
57
|
+
export class CloudFormationClient {
|
|
58
|
+
private credentials: AWSCredentials | null = null
|
|
59
|
+
private region: string
|
|
60
|
+
|
|
61
|
+
constructor(
|
|
62
|
+
region: string = 'us-east-1',
|
|
63
|
+
private readonly profile: string = 'default',
|
|
64
|
+
) {
|
|
65
|
+
this.region = region
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Initialize client with credentials
|
|
70
|
+
*/
|
|
71
|
+
async init(): Promise<void> {
|
|
72
|
+
this.credentials = await resolveCredentials(this.profile)
|
|
73
|
+
if (this.credentials.region) {
|
|
74
|
+
this.region = this.credentials.region
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Ensure credentials are loaded
|
|
80
|
+
*/
|
|
81
|
+
private async ensureCredentials(): Promise<AWSCredentials> {
|
|
82
|
+
if (!this.credentials) {
|
|
83
|
+
await this.init()
|
|
84
|
+
}
|
|
85
|
+
return this.credentials!
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Make a CloudFormation API request
|
|
90
|
+
*/
|
|
91
|
+
private async request(action: string, params: Record<string, any>): Promise<any> {
|
|
92
|
+
const credentials = await this.ensureCredentials()
|
|
93
|
+
|
|
94
|
+
// Build query string
|
|
95
|
+
const queryParams: Record<string, string> = {
|
|
96
|
+
Action: action,
|
|
97
|
+
Version: '2010-05-15',
|
|
98
|
+
...flattenParams(params),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const queryString = new URLSearchParams(queryParams).toString()
|
|
102
|
+
|
|
103
|
+
const response = await makeAWSRequest({
|
|
104
|
+
method: 'POST',
|
|
105
|
+
url: `https://cloudformation.${this.region}.amazonaws.com/`,
|
|
106
|
+
service: 'cloudformation',
|
|
107
|
+
region: this.region,
|
|
108
|
+
headers: {
|
|
109
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
110
|
+
},
|
|
111
|
+
body: queryString,
|
|
112
|
+
accessKeyId: credentials.accessKeyId,
|
|
113
|
+
secretAccessKey: credentials.secretAccessKey,
|
|
114
|
+
sessionToken: credentials.sessionToken,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
return await parseXMLResponse(response)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Create a new CloudFormation stack
|
|
122
|
+
*/
|
|
123
|
+
async createStack(options: CreateStackOptions): Promise<string> {
|
|
124
|
+
const params: Record<string, any> = {
|
|
125
|
+
StackName: options.stackName,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (options.templateBody) {
|
|
129
|
+
params.TemplateBody = options.templateBody
|
|
130
|
+
}
|
|
131
|
+
else if (options.templateURL) {
|
|
132
|
+
params.TemplateURL = options.templateURL
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
throw new Error('Either templateBody or templateURL must be provided')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (options.parameters) {
|
|
139
|
+
params.Parameters = Object.entries(options.parameters).map(([key, value], index) => ({
|
|
140
|
+
[`Parameters.member.${index + 1}.ParameterKey`]: key,
|
|
141
|
+
[`Parameters.member.${index + 1}.ParameterValue`]: value,
|
|
142
|
+
}))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (options.capabilities) {
|
|
146
|
+
params.Capabilities = options.capabilities.map((cap, index) => ({
|
|
147
|
+
[`Capabilities.member.${index + 1}`]: cap,
|
|
148
|
+
}))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (options.tags) {
|
|
152
|
+
params.Tags = Object.entries(options.tags).map(([key, value], index) => ({
|
|
153
|
+
[`Tags.member.${index + 1}.Key`]: key,
|
|
154
|
+
[`Tags.member.${index + 1}.Value`]: value,
|
|
155
|
+
}))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (options.timeoutInMinutes) {
|
|
159
|
+
params.TimeoutInMinutes = options.timeoutInMinutes
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (options.onFailure) {
|
|
163
|
+
params.OnFailure = options.onFailure
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const result = await this.request('CreateStack', params)
|
|
167
|
+
return result.StackId
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Update an existing CloudFormation stack
|
|
172
|
+
*/
|
|
173
|
+
async updateStack(options: UpdateStackOptions): Promise<string> {
|
|
174
|
+
const params: Record<string, any> = {
|
|
175
|
+
StackName: options.stackName,
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (options.templateBody) {
|
|
179
|
+
params.TemplateBody = options.templateBody
|
|
180
|
+
}
|
|
181
|
+
else if (options.templateURL) {
|
|
182
|
+
params.TemplateURL = options.templateURL
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (options.parameters) {
|
|
186
|
+
params.Parameters = Object.entries(options.parameters).map(([key, value], index) => ({
|
|
187
|
+
[`Parameters.member.${index + 1}.ParameterKey`]: key,
|
|
188
|
+
[`Parameters.member.${index + 1}.ParameterValue`]: value,
|
|
189
|
+
}))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (options.capabilities) {
|
|
193
|
+
params.Capabilities = options.capabilities.map((cap, index) => ({
|
|
194
|
+
[`Capabilities.member.${index + 1}`]: cap,
|
|
195
|
+
}))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (options.tags) {
|
|
199
|
+
params.Tags = Object.entries(options.tags).map(([key, value], index) => ({
|
|
200
|
+
[`Tags.member.${index + 1}.Key`]: key,
|
|
201
|
+
[`Tags.member.${index + 1}.Value`]: value,
|
|
202
|
+
}))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const result = await this.request('UpdateStack', params)
|
|
206
|
+
return result.StackId
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Delete a CloudFormation stack
|
|
211
|
+
*/
|
|
212
|
+
async deleteStack(stackName: string): Promise<void> {
|
|
213
|
+
await this.request('DeleteStack', {
|
|
214
|
+
StackName: stackName,
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Describe a CloudFormation stack
|
|
220
|
+
*/
|
|
221
|
+
async describeStack(stackName: string): Promise<CloudFormationStack> {
|
|
222
|
+
const result = await this.request('DescribeStacks', {
|
|
223
|
+
StackName: stackName,
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
// Parse stack from XML response
|
|
227
|
+
return parseStack(result)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* List all CloudFormation stacks
|
|
232
|
+
*/
|
|
233
|
+
async listStacks(statusFilter?: string[]): Promise<CloudFormationStack[]> {
|
|
234
|
+
const params: Record<string, any> = {}
|
|
235
|
+
|
|
236
|
+
if (statusFilter) {
|
|
237
|
+
params.StackStatusFilter = statusFilter.map((status, index) => ({
|
|
238
|
+
[`StackStatusFilter.member.${index + 1}`]: status,
|
|
239
|
+
}))
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const result = await this.request('ListStacks', params)
|
|
243
|
+
return parseStackList(result)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Get stack events
|
|
248
|
+
*/
|
|
249
|
+
async describeStackEvents(stackName: string): Promise<StackEvent[]> {
|
|
250
|
+
const result = await this.request('DescribeStackEvents', {
|
|
251
|
+
StackName: stackName,
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
return parseStackEvents(result)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Wait for stack to reach a terminal state
|
|
259
|
+
*/
|
|
260
|
+
async waitForStack(
|
|
261
|
+
stackName: string,
|
|
262
|
+
desiredStates: string[],
|
|
263
|
+
onProgress?: (event: StackEvent) => void,
|
|
264
|
+
): Promise<CloudFormationStack> {
|
|
265
|
+
const pollInterval = 5000 // 5 seconds
|
|
266
|
+
const maxAttempts = 360 // 30 minutes maximum
|
|
267
|
+
let attempts = 0
|
|
268
|
+
let lastEventId: string | null = null
|
|
269
|
+
|
|
270
|
+
while (attempts < maxAttempts) {
|
|
271
|
+
attempts++
|
|
272
|
+
|
|
273
|
+
const stack = await this.describeStack(stackName)
|
|
274
|
+
|
|
275
|
+
// Get latest events
|
|
276
|
+
if (onProgress) {
|
|
277
|
+
const events = await this.describeStackEvents(stackName)
|
|
278
|
+
const newEvents = lastEventId
|
|
279
|
+
? events.filter(e => e.EventId !== lastEventId).reverse()
|
|
280
|
+
: [events[0]]
|
|
281
|
+
|
|
282
|
+
newEvents.forEach(event => onProgress(event))
|
|
283
|
+
|
|
284
|
+
if (events.length > 0) {
|
|
285
|
+
lastEventId = events[0].EventId
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Check if stack reached desired state
|
|
290
|
+
if (stack.StackStatus && desiredStates.includes(stack.StackStatus)) {
|
|
291
|
+
return stack
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Check for failure states
|
|
295
|
+
if (stack.StackStatus?.includes('FAILED') || stack.StackStatus?.includes('ROLLBACK')) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
`Stack reached failed state: ${stack.StackStatus}\n`
|
|
298
|
+
+ `Reason: ${stack.StackStatusReason || 'Unknown'}`,
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Wait before next poll
|
|
303
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval))
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
throw new Error(`Timeout waiting for stack ${stackName} to complete`)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Create a change set
|
|
311
|
+
*/
|
|
312
|
+
async createChangeSet(options: {
|
|
313
|
+
stackName: string
|
|
314
|
+
changeSetName: string
|
|
315
|
+
templateBody?: string
|
|
316
|
+
templateURL?: string
|
|
317
|
+
parameters?: Record<string, string>
|
|
318
|
+
capabilities?: string[]
|
|
319
|
+
}): Promise<string> {
|
|
320
|
+
const params: Record<string, any> = {
|
|
321
|
+
StackName: options.stackName,
|
|
322
|
+
ChangeSetName: options.changeSetName,
|
|
323
|
+
ChangeSetType: 'UPDATE',
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (options.templateBody) {
|
|
327
|
+
params.TemplateBody = options.templateBody
|
|
328
|
+
}
|
|
329
|
+
else if (options.templateURL) {
|
|
330
|
+
params.TemplateURL = options.templateURL
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (options.parameters) {
|
|
334
|
+
params.Parameters = Object.entries(options.parameters).map(([key, value], index) => ({
|
|
335
|
+
[`Parameters.member.${index + 1}.ParameterKey`]: key,
|
|
336
|
+
[`Parameters.member.${index + 1}.ParameterValue`]: value,
|
|
337
|
+
}))
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (options.capabilities) {
|
|
341
|
+
params.Capabilities = options.capabilities.map((cap, index) => ({
|
|
342
|
+
[`Capabilities.member.${index + 1}`]: cap,
|
|
343
|
+
}))
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const result = await this.request('CreateChangeSet', params)
|
|
347
|
+
return result.ChangeSetId
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Execute a change set
|
|
352
|
+
*/
|
|
353
|
+
async executeChangeSet(changeSetName: string, stackName: string): Promise<void> {
|
|
354
|
+
await this.request('ExecuteChangeSet', {
|
|
355
|
+
ChangeSetName: changeSetName,
|
|
356
|
+
StackName: stackName,
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Flatten nested parameters for AWS API query string
|
|
363
|
+
*/
|
|
364
|
+
function flattenParams(params: Record<string, any>, prefix: string = ''): Record<string, string> {
|
|
365
|
+
const result: Record<string, string> = {}
|
|
366
|
+
|
|
367
|
+
for (const [key, value] of Object.entries(params)) {
|
|
368
|
+
const fullKey = prefix ? `${prefix}.${key}` : key
|
|
369
|
+
|
|
370
|
+
if (Array.isArray(value)) {
|
|
371
|
+
value.forEach((item, index) => {
|
|
372
|
+
if (typeof item === 'object') {
|
|
373
|
+
Object.assign(result, flattenParams(item, `${fullKey}.${index + 1}`))
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
result[`${fullKey}.${index + 1}`] = String(item)
|
|
377
|
+
}
|
|
378
|
+
})
|
|
379
|
+
}
|
|
380
|
+
else if (typeof value === 'object' && value !== null) {
|
|
381
|
+
Object.assign(result, flattenParams(value, fullKey))
|
|
382
|
+
}
|
|
383
|
+
else if (value !== undefined && value !== null) {
|
|
384
|
+
result[fullKey] = String(value)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return result
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Parse stack from XML response
|
|
393
|
+
*/
|
|
394
|
+
function parseStack(data: any): CloudFormationStack {
|
|
395
|
+
// Simplified parsing - in production, use a proper XML parser
|
|
396
|
+
return {
|
|
397
|
+
StackName: data.StackName || '',
|
|
398
|
+
StackId: data.StackId,
|
|
399
|
+
StackStatus: data.StackStatus,
|
|
400
|
+
CreationTime: data.CreationTime,
|
|
401
|
+
LastUpdatedTime: data.LastUpdatedTime,
|
|
402
|
+
StackStatusReason: data.StackStatusReason,
|
|
403
|
+
Description: data.Description,
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Parse stack list from XML response
|
|
409
|
+
*/
|
|
410
|
+
function parseStackList(data: any): CloudFormationStack[] {
|
|
411
|
+
// Simplified parsing
|
|
412
|
+
return []
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Parse stack events from XML response
|
|
417
|
+
*/
|
|
418
|
+
function parseStackEvents(data: any): StackEvent[] {
|
|
419
|
+
// Simplified parsing
|
|
420
|
+
return []
|
|
421
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWS CloudFront API Client
|
|
3
|
+
* Direct API calls for CloudFront invalidations without AWS SDK
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AWSCredentials } from './credentials'
|
|
7
|
+
import { resolveCredentials } from './credentials'
|
|
8
|
+
import { makeAWSRequest, parseXMLResponse } from './signature'
|
|
9
|
+
|
|
10
|
+
export interface InvalidationOptions {
|
|
11
|
+
distributionId: string
|
|
12
|
+
paths: string[]
|
|
13
|
+
callerReference?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* CloudFront API Client
|
|
18
|
+
*/
|
|
19
|
+
export class CloudFrontClient {
|
|
20
|
+
private credentials: AWSCredentials | null = null
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
private readonly profile: string = 'default',
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Initialize client with credentials
|
|
28
|
+
*/
|
|
29
|
+
async init(): Promise<void> {
|
|
30
|
+
this.credentials = await resolveCredentials(this.profile)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Ensure credentials are loaded
|
|
35
|
+
*/
|
|
36
|
+
private async ensureCredentials(): Promise<AWSCredentials> {
|
|
37
|
+
if (!this.credentials) {
|
|
38
|
+
await this.init()
|
|
39
|
+
}
|
|
40
|
+
return this.credentials!
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create a cache invalidation
|
|
45
|
+
*/
|
|
46
|
+
async createInvalidation(options: InvalidationOptions): Promise<string> {
|
|
47
|
+
const credentials = await this.ensureCredentials()
|
|
48
|
+
|
|
49
|
+
const callerReference = options.callerReference || `invalidation-${Date.now()}`
|
|
50
|
+
|
|
51
|
+
const body = `<?xml version="1.0" encoding="UTF-8"?>
|
|
52
|
+
<InvalidationBatch>
|
|
53
|
+
<Paths>
|
|
54
|
+
<Quantity>${options.paths.length}</Quantity>
|
|
55
|
+
<Items>
|
|
56
|
+
${options.paths.map(path => `<Path>${path}</Path>`).join('')}
|
|
57
|
+
</Items>
|
|
58
|
+
</Paths>
|
|
59
|
+
<CallerReference>${callerReference}</CallerReference>
|
|
60
|
+
</InvalidationBatch>`
|
|
61
|
+
|
|
62
|
+
const url = `https://cloudfront.amazonaws.com/2020-05-31/distribution/${options.distributionId}/invalidation`
|
|
63
|
+
|
|
64
|
+
const response = await makeAWSRequest({
|
|
65
|
+
method: 'POST',
|
|
66
|
+
url,
|
|
67
|
+
service: 'cloudfront',
|
|
68
|
+
region: 'us-east-1', // CloudFront is global, but uses us-east-1
|
|
69
|
+
headers: {
|
|
70
|
+
'Content-Type': 'text/xml',
|
|
71
|
+
},
|
|
72
|
+
body,
|
|
73
|
+
accessKeyId: credentials.accessKeyId,
|
|
74
|
+
secretAccessKey: credentials.secretAccessKey,
|
|
75
|
+
sessionToken: credentials.sessionToken,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const data = await parseXMLResponse(response)
|
|
79
|
+
return data.Id
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get invalidation status
|
|
84
|
+
*/
|
|
85
|
+
async getInvalidation(distributionId: string, invalidationId: string): Promise<any> {
|
|
86
|
+
const credentials = await this.ensureCredentials()
|
|
87
|
+
|
|
88
|
+
const url = `https://cloudfront.amazonaws.com/2020-05-31/distribution/${distributionId}/invalidation/${invalidationId}`
|
|
89
|
+
|
|
90
|
+
const response = await makeAWSRequest({
|
|
91
|
+
method: 'GET',
|
|
92
|
+
url,
|
|
93
|
+
service: 'cloudfront',
|
|
94
|
+
region: 'us-east-1',
|
|
95
|
+
accessKeyId: credentials.accessKeyId,
|
|
96
|
+
secretAccessKey: credentials.secretAccessKey,
|
|
97
|
+
sessionToken: credentials.sessionToken,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
return await parseXMLResponse(response)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* List invalidations for a distribution
|
|
105
|
+
*/
|
|
106
|
+
async listInvalidations(distributionId: string): Promise<any[]> {
|
|
107
|
+
const credentials = await this.ensureCredentials()
|
|
108
|
+
|
|
109
|
+
const url = `https://cloudfront.amazonaws.com/2020-05-31/distribution/${distributionId}/invalidation`
|
|
110
|
+
|
|
111
|
+
const response = await makeAWSRequest({
|
|
112
|
+
method: 'GET',
|
|
113
|
+
url,
|
|
114
|
+
service: 'cloudfront',
|
|
115
|
+
region: 'us-east-1',
|
|
116
|
+
accessKeyId: credentials.accessKeyId,
|
|
117
|
+
secretAccessKey: credentials.secretAccessKey,
|
|
118
|
+
sessionToken: credentials.sessionToken,
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const data = await parseXMLResponse(response)
|
|
122
|
+
return data.Items || []
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Wait for invalidation to complete
|
|
127
|
+
*/
|
|
128
|
+
async waitForInvalidation(
|
|
129
|
+
distributionId: string,
|
|
130
|
+
invalidationId: string,
|
|
131
|
+
maxAttempts: number = 60,
|
|
132
|
+
pollInterval: number = 5000,
|
|
133
|
+
): Promise<void> {
|
|
134
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
135
|
+
const invalidation = await this.getInvalidation(distributionId, invalidationId)
|
|
136
|
+
|
|
137
|
+
if (invalidation.Status === 'Completed') {
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (i < maxAttempts - 1) {
|
|
142
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval))
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
throw new Error(`Invalidation ${invalidationId} did not complete within the expected time`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Invalidate all files in a distribution
|
|
151
|
+
*/
|
|
152
|
+
async invalidateAll(distributionId: string): Promise<string> {
|
|
153
|
+
return await this.createInvalidation({
|
|
154
|
+
distributionId,
|
|
155
|
+
paths: ['/*'],
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWS Credentials Provider Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it, beforeEach, afterEach } from 'bun:test'
|
|
6
|
+
import {
|
|
7
|
+
fromEnvironment,
|
|
8
|
+
fromSharedCredentials,
|
|
9
|
+
createCredentialProvider,
|
|
10
|
+
} from './credentials'
|
|
11
|
+
|
|
12
|
+
describe('Credential Providers', () => {
|
|
13
|
+
// Store original env
|
|
14
|
+
const originalEnv = { ...process.env }
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
// Reset env before each test
|
|
18
|
+
process.env = { ...originalEnv }
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
// Restore original env
|
|
23
|
+
process.env = originalEnv
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('fromEnvironment', () => {
|
|
27
|
+
it('should return credentials from environment variables', () => {
|
|
28
|
+
process.env.AWS_ACCESS_KEY_ID = 'AKIATEST123'
|
|
29
|
+
process.env.AWS_SECRET_ACCESS_KEY = 'secret123'
|
|
30
|
+
|
|
31
|
+
const creds = fromEnvironment()
|
|
32
|
+
|
|
33
|
+
expect(creds).not.toBeNull()
|
|
34
|
+
expect(creds?.accessKeyId).toBe('AKIATEST123')
|
|
35
|
+
expect(creds?.secretAccessKey).toBe('secret123')
|
|
36
|
+
expect(creds?.sessionToken).toBeUndefined()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should include session token if present', () => {
|
|
40
|
+
process.env.AWS_ACCESS_KEY_ID = 'AKIATEST123'
|
|
41
|
+
process.env.AWS_SECRET_ACCESS_KEY = 'secret123'
|
|
42
|
+
process.env.AWS_SESSION_TOKEN = 'token123'
|
|
43
|
+
|
|
44
|
+
const creds = fromEnvironment()
|
|
45
|
+
|
|
46
|
+
expect(creds).not.toBeNull()
|
|
47
|
+
expect(creds?.sessionToken).toBe('token123')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should return null if access key is missing', () => {
|
|
51
|
+
process.env.AWS_SECRET_ACCESS_KEY = 'secret123'
|
|
52
|
+
delete process.env.AWS_ACCESS_KEY_ID
|
|
53
|
+
|
|
54
|
+
const creds = fromEnvironment()
|
|
55
|
+
|
|
56
|
+
expect(creds).toBeNull()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should return null if secret key is missing', () => {
|
|
60
|
+
process.env.AWS_ACCESS_KEY_ID = 'AKIATEST123'
|
|
61
|
+
delete process.env.AWS_SECRET_ACCESS_KEY
|
|
62
|
+
|
|
63
|
+
const creds = fromEnvironment()
|
|
64
|
+
|
|
65
|
+
expect(creds).toBeNull()
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('fromSharedCredentials', () => {
|
|
70
|
+
it('should return null for non-existent file', () => {
|
|
71
|
+
const creds = fromSharedCredentials({
|
|
72
|
+
credentialsFile: '/nonexistent/path/credentials',
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
expect(creds).toBeNull()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('should parse credentials from a valid file', () => {
|
|
79
|
+
// This test would need a mock file or temp file
|
|
80
|
+
// For now, just verify the function doesn't crash
|
|
81
|
+
const creds = fromSharedCredentials({
|
|
82
|
+
credentialsFile: '/tmp/test-aws-credentials-nonexistent',
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
expect(creds).toBeNull()
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('createCredentialProvider', () => {
|
|
90
|
+
it('should create a provider function', () => {
|
|
91
|
+
const provider = createCredentialProvider()
|
|
92
|
+
expect(typeof provider).toBe('function')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('should cache credentials', async () => {
|
|
96
|
+
process.env.AWS_ACCESS_KEY_ID = 'AKIATEST123'
|
|
97
|
+
process.env.AWS_SECRET_ACCESS_KEY = 'secret123'
|
|
98
|
+
|
|
99
|
+
const provider = createCredentialProvider()
|
|
100
|
+
|
|
101
|
+
const creds1 = await provider()
|
|
102
|
+
const creds2 = await provider()
|
|
103
|
+
|
|
104
|
+
expect(creds1).toEqual(creds2)
|
|
105
|
+
expect(creds1.accessKeyId).toBe('AKIATEST123')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('should throw if no credentials found', async () => {
|
|
109
|
+
delete process.env.AWS_ACCESS_KEY_ID
|
|
110
|
+
delete process.env.AWS_SECRET_ACCESS_KEY
|
|
111
|
+
delete process.env.AWS_PROFILE
|
|
112
|
+
|
|
113
|
+
const provider = createCredentialProvider({
|
|
114
|
+
credentialsFile: '/nonexistent/path',
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// This will try all providers and fail
|
|
118
|
+
// In real tests, you'd mock the metadata endpoints
|
|
119
|
+
await expect(provider()).rejects.toThrow('Could not find AWS credentials')
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
describe('Credential File Parsing', () => {
|
|
125
|
+
it('should handle empty credentials gracefully', () => {
|
|
126
|
+
delete process.env.AWS_ACCESS_KEY_ID
|
|
127
|
+
delete process.env.AWS_SECRET_ACCESS_KEY
|
|
128
|
+
|
|
129
|
+
const creds = fromEnvironment()
|
|
130
|
+
expect(creds).toBeNull()
|
|
131
|
+
})
|
|
132
|
+
})
|