@stacksjs/ts-cloud 0.1.2 → 0.1.5

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.
Files changed (187) hide show
  1. package/README.md +98 -13
  2. package/dist/aws/acm.d.ts +129 -0
  3. package/dist/aws/application-autoscaling.d.ts +282 -0
  4. package/dist/aws/bedrock.d.ts +2292 -0
  5. package/dist/aws/client.d.ts +79 -0
  6. package/dist/aws/cloudformation.d.ts +105 -0
  7. package/dist/aws/cloudfront.d.ts +265 -0
  8. package/dist/aws/cloudwatch-logs.d.ts +48 -0
  9. package/dist/aws/comprehend.d.ts +505 -0
  10. package/dist/aws/connect.d.ts +377 -0
  11. package/dist/aws/deploy-imap.d.ts +14 -0
  12. package/dist/aws/dynamodb.d.ts +176 -0
  13. package/dist/aws/ec2.d.ts +272 -0
  14. package/dist/aws/ecr.d.ts +149 -0
  15. package/dist/aws/ecs.d.ts +162 -0
  16. package/dist/aws/elasticache.d.ts +71 -0
  17. package/dist/aws/elbv2.d.ts +248 -0
  18. package/dist/aws/email.d.ts +175 -0
  19. package/dist/aws/eventbridge.d.ts +142 -0
  20. package/dist/aws/iam.d.ts +638 -0
  21. package/dist/aws/imap-server.d.ts +119 -0
  22. package/{src/aws/index.ts → dist/aws/index.d.ts} +62 -83
  23. package/{src/aws/kendra.ts → dist/aws/kendra.d.ts} +71 -386
  24. package/dist/aws/lambda.d.ts +232 -0
  25. package/dist/aws/opensearch.d.ts +87 -0
  26. package/dist/aws/personalize.d.ts +516 -0
  27. package/dist/aws/polly.d.ts +214 -0
  28. package/dist/aws/rds.d.ts +240 -0
  29. package/dist/aws/rekognition.d.ts +543 -0
  30. package/dist/aws/route53-domains.d.ts +113 -0
  31. package/dist/aws/route53.d.ts +215 -0
  32. package/dist/aws/s3.d.ts +212 -0
  33. package/dist/aws/scheduler.d.ts +140 -0
  34. package/dist/aws/secrets-manager.d.ts +170 -0
  35. package/dist/aws/ses.d.ts +288 -0
  36. package/dist/aws/setup-phone.d.ts +0 -0
  37. package/dist/aws/setup-sms.d.ts +115 -0
  38. package/dist/aws/sms.d.ts +304 -0
  39. package/dist/aws/smtp-server.d.ts +61 -0
  40. package/dist/aws/sns.d.ts +117 -0
  41. package/dist/aws/sqs.d.ts +65 -0
  42. package/dist/aws/ssm.d.ts +179 -0
  43. package/dist/aws/sts.d.ts +15 -0
  44. package/dist/aws/support.d.ts +104 -0
  45. package/dist/aws/test-imap.d.ts +0 -0
  46. package/dist/aws/textract.d.ts +403 -0
  47. package/dist/aws/transcribe.d.ts +60 -0
  48. package/dist/aws/translate.d.ts +358 -0
  49. package/dist/aws/voice.d.ts +219 -0
  50. package/dist/bin/cli.js +1724 -0
  51. package/dist/config.d.ts +7 -0
  52. package/dist/deploy/index.d.ts +2 -0
  53. package/dist/deploy/static-site-external-dns.d.ts +51 -0
  54. package/dist/deploy/static-site.d.ts +71 -0
  55. package/dist/dns/cloudflare.d.ts +52 -0
  56. package/dist/dns/godaddy.d.ts +38 -0
  57. package/dist/dns/index.d.ts +45 -0
  58. package/dist/dns/porkbun.d.ts +18 -0
  59. package/dist/dns/route53-adapter.d.ts +38 -0
  60. package/{src/dns/types.ts → dist/dns/types.d.ts} +26 -63
  61. package/dist/dns/validator.d.ts +78 -0
  62. package/dist/generators/index.d.ts +1 -0
  63. package/dist/generators/infrastructure.d.ts +30 -0
  64. package/{src/index.ts → dist/index.d.ts} +70 -93
  65. package/dist/index.js +7881 -0
  66. package/dist/push/apns.d.ts +60 -0
  67. package/dist/push/fcm.d.ts +117 -0
  68. package/dist/push/index.d.ts +14 -0
  69. package/dist/security/pre-deploy-scanner.d.ts +69 -0
  70. package/dist/ssl/acme-client.d.ts +67 -0
  71. package/dist/ssl/index.d.ts +2 -0
  72. package/dist/ssl/letsencrypt.d.ts +48 -0
  73. package/dist/types.d.ts +1 -0
  74. package/dist/utils/cli.d.ts +123 -0
  75. package/dist/validation/index.d.ts +1 -0
  76. package/dist/validation/template.d.ts +23 -0
  77. package/package.json +8 -8
  78. package/bin/cli.ts +0 -133
  79. package/bin/commands/analytics.ts +0 -328
  80. package/bin/commands/api.ts +0 -379
  81. package/bin/commands/assets.ts +0 -221
  82. package/bin/commands/audit.ts +0 -501
  83. package/bin/commands/backup.ts +0 -682
  84. package/bin/commands/cache.ts +0 -294
  85. package/bin/commands/cdn.ts +0 -281
  86. package/bin/commands/config.ts +0 -202
  87. package/bin/commands/container.ts +0 -105
  88. package/bin/commands/cost.ts +0 -208
  89. package/bin/commands/database.ts +0 -401
  90. package/bin/commands/deploy.ts +0 -674
  91. package/bin/commands/domain.ts +0 -397
  92. package/bin/commands/email.ts +0 -423
  93. package/bin/commands/environment.ts +0 -285
  94. package/bin/commands/events.ts +0 -424
  95. package/bin/commands/firewall.ts +0 -145
  96. package/bin/commands/function.ts +0 -116
  97. package/bin/commands/generate.ts +0 -280
  98. package/bin/commands/git.ts +0 -139
  99. package/bin/commands/iam.ts +0 -464
  100. package/bin/commands/index.ts +0 -48
  101. package/bin/commands/init.ts +0 -120
  102. package/bin/commands/logs.ts +0 -148
  103. package/bin/commands/network.ts +0 -579
  104. package/bin/commands/notify.ts +0 -489
  105. package/bin/commands/queue.ts +0 -407
  106. package/bin/commands/scheduler.ts +0 -370
  107. package/bin/commands/secrets.ts +0 -54
  108. package/bin/commands/server.ts +0 -629
  109. package/bin/commands/shared.ts +0 -97
  110. package/bin/commands/ssl.ts +0 -138
  111. package/bin/commands/stack.ts +0 -325
  112. package/bin/commands/status.ts +0 -385
  113. package/bin/commands/storage.ts +0 -450
  114. package/bin/commands/team.ts +0 -96
  115. package/bin/commands/tunnel.ts +0 -489
  116. package/bin/commands/utils.ts +0 -202
  117. package/build.ts +0 -15
  118. package/cloud +0 -2
  119. package/src/aws/acm.ts +0 -768
  120. package/src/aws/application-autoscaling.ts +0 -845
  121. package/src/aws/bedrock.ts +0 -4074
  122. package/src/aws/client.ts +0 -878
  123. package/src/aws/cloudformation.ts +0 -896
  124. package/src/aws/cloudfront.ts +0 -1531
  125. package/src/aws/cloudwatch-logs.ts +0 -154
  126. package/src/aws/comprehend.ts +0 -839
  127. package/src/aws/connect.ts +0 -1056
  128. package/src/aws/deploy-imap.ts +0 -384
  129. package/src/aws/dynamodb.ts +0 -340
  130. package/src/aws/ec2.ts +0 -1385
  131. package/src/aws/ecr.ts +0 -621
  132. package/src/aws/ecs.ts +0 -615
  133. package/src/aws/elasticache.ts +0 -301
  134. package/src/aws/elbv2.ts +0 -942
  135. package/src/aws/email.ts +0 -928
  136. package/src/aws/eventbridge.ts +0 -248
  137. package/src/aws/iam.ts +0 -1689
  138. package/src/aws/imap-server.ts +0 -2100
  139. package/src/aws/lambda.ts +0 -786
  140. package/src/aws/opensearch.ts +0 -158
  141. package/src/aws/personalize.ts +0 -977
  142. package/src/aws/polly.ts +0 -559
  143. package/src/aws/rds.ts +0 -888
  144. package/src/aws/rekognition.ts +0 -846
  145. package/src/aws/route53-domains.ts +0 -359
  146. package/src/aws/route53.ts +0 -1046
  147. package/src/aws/s3.ts +0 -2318
  148. package/src/aws/scheduler.ts +0 -571
  149. package/src/aws/secrets-manager.ts +0 -769
  150. package/src/aws/ses.ts +0 -1081
  151. package/src/aws/setup-phone.ts +0 -104
  152. package/src/aws/setup-sms.ts +0 -580
  153. package/src/aws/sms.ts +0 -1735
  154. package/src/aws/smtp-server.ts +0 -531
  155. package/src/aws/sns.ts +0 -758
  156. package/src/aws/sqs.ts +0 -382
  157. package/src/aws/ssm.ts +0 -807
  158. package/src/aws/sts.ts +0 -92
  159. package/src/aws/support.ts +0 -391
  160. package/src/aws/test-imap.ts +0 -86
  161. package/src/aws/textract.ts +0 -780
  162. package/src/aws/transcribe.ts +0 -108
  163. package/src/aws/translate.ts +0 -641
  164. package/src/aws/voice.ts +0 -1379
  165. package/src/config.ts +0 -35
  166. package/src/deploy/index.ts +0 -7
  167. package/src/deploy/static-site-external-dns.ts +0 -906
  168. package/src/deploy/static-site.ts +0 -1125
  169. package/src/dns/godaddy.ts +0 -412
  170. package/src/dns/index.ts +0 -183
  171. package/src/dns/porkbun.ts +0 -362
  172. package/src/dns/route53-adapter.ts +0 -414
  173. package/src/dns/validator.ts +0 -369
  174. package/src/generators/index.ts +0 -5
  175. package/src/generators/infrastructure.ts +0 -1660
  176. package/src/push/apns.ts +0 -452
  177. package/src/push/fcm.ts +0 -506
  178. package/src/push/index.ts +0 -58
  179. package/src/ssl/acme-client.ts +0 -478
  180. package/src/ssl/index.ts +0 -7
  181. package/src/ssl/letsencrypt.ts +0 -747
  182. package/src/types.ts +0 -2
  183. package/src/utils/cli.ts +0 -398
  184. package/src/validation/index.ts +0 -5
  185. package/src/validation/template.ts +0 -405
  186. package/test/index.test.ts +0 -128
  187. package/tsconfig.json +0 -18
package/src/aws/client.ts DELETED
@@ -1,878 +0,0 @@
1
- /**
2
- * AWS API Client - Direct API calls without AWS CLI
3
- * Implements AWS Signature Version 4 for authentication
4
- */
5
-
6
- import * as crypto from 'node:crypto'
7
- import { existsSync, readFileSync } from 'node:fs'
8
- import { homedir } from 'node:os'
9
- import { join } from 'node:path'
10
- import { XMLParser } from 'fast-xml-parser'
11
-
12
- export interface AWSCredentials {
13
- accessKeyId: string
14
- secretAccessKey: string
15
- sessionToken?: string
16
- }
17
-
18
- export interface AWSRequestOptions {
19
- service: string
20
- region: string
21
- method: string
22
- path: string
23
- queryParams?: Record<string, string>
24
- headers?: Record<string, string>
25
- body?: string
26
- credentials?: AWSCredentials
27
- retries?: number
28
- cacheKey?: string
29
- cacheTTL?: number
30
- returnHeaders?: boolean
31
- rawResponse?: boolean
32
- /** S3 bucket name for virtual-hosted style URLs */
33
- bucket?: string
34
- }
35
-
36
- export interface AWSClientConfig {
37
- maxRetries?: number
38
- retryDelay?: number
39
- cacheEnabled?: boolean
40
- defaultCacheTTL?: number
41
- }
42
-
43
- export interface AWSError extends Error {
44
- code?: string
45
- statusCode?: number
46
- requestId?: string
47
- type?: string
48
- }
49
-
50
- interface CacheEntry {
51
- data: any
52
- expires: number
53
- }
54
-
55
- /**
56
- * AWS API Client - Makes authenticated requests to AWS services
57
- */
58
- export class AWSClient {
59
- private credentials?: AWSCredentials
60
- private config: AWSClientConfig
61
- private cache: Map<string, CacheEntry>
62
- private xmlParser: XMLParser
63
-
64
- constructor(credentials?: AWSCredentials, config?: AWSClientConfig) {
65
- this.credentials = credentials || this.loadCredentials()
66
- this.config = {
67
- maxRetries: 3,
68
- retryDelay: 1000,
69
- cacheEnabled: true,
70
- defaultCacheTTL: 60000, // 1 minute
71
- ...config,
72
- }
73
- this.cache = new Map()
74
- this.xmlParser = new XMLParser({
75
- ignoreAttributes: false,
76
- attributeNamePrefix: '@_',
77
- textNodeName: '#text',
78
- parseAttributeValue: true,
79
- trimValues: true,
80
- })
81
- }
82
-
83
- /**
84
- * Load AWS credentials from environment variables, credentials file, or EC2 instance metadata
85
- */
86
- private loadCredentials(): AWSCredentials {
87
- // 1. Check environment variables first
88
- const accessKeyId = process.env.AWS_ACCESS_KEY_ID
89
- const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY
90
- const sessionToken = process.env.AWS_SESSION_TOKEN
91
-
92
- if (accessKeyId && secretAccessKey) {
93
- return {
94
- accessKeyId,
95
- secretAccessKey,
96
- sessionToken,
97
- }
98
- }
99
-
100
- // 2. Try to load from ~/.aws/credentials file
101
- const fileCredentials = this.loadCredentialsFromFile()
102
- if (fileCredentials) {
103
- return fileCredentials
104
- }
105
-
106
- // Return placeholder - will be loaded async from EC2 metadata
107
- // The actual credentials will be fetched in getCredentials()
108
- return {
109
- accessKeyId: '',
110
- secretAccessKey: '',
111
- }
112
- }
113
-
114
- /**
115
- * Load credentials from ~/.aws/credentials file
116
- */
117
- private loadCredentialsFromFile(): AWSCredentials | null {
118
- const profile = process.env.AWS_PROFILE || 'default'
119
- const credentialsPath = process.env.AWS_SHARED_CREDENTIALS_FILE || join(homedir(), '.aws', 'credentials')
120
-
121
- if (!existsSync(credentialsPath)) {
122
- return null
123
- }
124
-
125
- try {
126
- const content = readFileSync(credentialsPath, 'utf-8')
127
- const credentials = this.parseCredentialsFile(content, profile)
128
- if (credentials.accessKeyId && credentials.secretAccessKey) {
129
- return credentials
130
- }
131
- }
132
- catch {
133
- // Failed to read or parse credentials file
134
- }
135
-
136
- return null
137
- }
138
-
139
- /**
140
- * Parse AWS credentials file (INI format)
141
- */
142
- private parseCredentialsFile(content: string, profile: string): AWSCredentials {
143
- const lines = content.split('\n')
144
- let currentProfile = ''
145
- let accessKeyId = ''
146
- let secretAccessKey = ''
147
- let sessionToken: string | undefined
148
-
149
- for (const line of lines) {
150
- const trimmed = line.trim()
151
-
152
- // Skip empty lines and comments
153
- if (!trimmed || trimmed.startsWith('#')) {
154
- continue
155
- }
156
-
157
- // Check for profile header [profile-name]
158
- const profileMatch = trimmed.match(/^\[([^\]]+)\]$/)
159
- if (profileMatch) {
160
- currentProfile = profileMatch[1]
161
- continue
162
- }
163
-
164
- // Parse key=value pairs for the target profile
165
- if (currentProfile === profile) {
166
- const [key, ...valueParts] = trimmed.split('=')
167
- const value = valueParts.join('=').trim()
168
-
169
- if (key.trim() === 'aws_access_key_id') {
170
- accessKeyId = value
171
- }
172
- else if (key.trim() === 'aws_secret_access_key') {
173
- secretAccessKey = value
174
- }
175
- else if (key.trim() === 'aws_session_token') {
176
- sessionToken = value
177
- }
178
- }
179
- }
180
-
181
- return { accessKeyId, secretAccessKey, sessionToken }
182
- }
183
-
184
- /**
185
- * Cache for EC2 instance metadata credentials
186
- */
187
- private ec2CredentialsCache?: {
188
- credentials: AWSCredentials
189
- expiration: number
190
- }
191
-
192
- /**
193
- * Get credentials, fetching from credentials file or EC2 metadata if needed
194
- */
195
- private async getCredentials(): Promise<AWSCredentials> {
196
- // 1. Check environment variables first
197
- const accessKeyId = process.env.AWS_ACCESS_KEY_ID
198
- const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY
199
- if (accessKeyId && secretAccessKey) {
200
- return {
201
- accessKeyId,
202
- secretAccessKey,
203
- sessionToken: process.env.AWS_SESSION_TOKEN,
204
- }
205
- }
206
-
207
- // 2. Try to load from ~/.aws/credentials file
208
- const fileCredentials = this.loadCredentialsFromFile()
209
- if (fileCredentials) {
210
- return fileCredentials
211
- }
212
-
213
- // 3. Check if we have cached EC2 credentials that haven't expired
214
- if (this.ec2CredentialsCache) {
215
- const now = Date.now()
216
- // Refresh 5 minutes before expiration
217
- if (this.ec2CredentialsCache.expiration > now + 5 * 60 * 1000) {
218
- return this.ec2CredentialsCache.credentials
219
- }
220
- }
221
-
222
- // Fetch from EC2 instance metadata service (IMDSv2)
223
- try {
224
- // Get token for IMDSv2
225
- const tokenResponse = await fetch('http://169.254.169.254/latest/api/token', {
226
- method: 'PUT',
227
- headers: {
228
- 'X-aws-ec2-metadata-token-ttl-seconds': '21600',
229
- },
230
- })
231
- const token = await tokenResponse.text()
232
-
233
- // Get IAM role name
234
- const roleResponse = await fetch('http://169.254.169.254/latest/meta-data/iam/security-credentials/', {
235
- headers: { 'X-aws-ec2-metadata-token': token },
236
- })
237
- const roleName = await roleResponse.text()
238
-
239
- // Get credentials for the role
240
- const credsResponse = await fetch(`http://169.254.169.254/latest/meta-data/iam/security-credentials/${roleName}`, {
241
- headers: { 'X-aws-ec2-metadata-token': token },
242
- })
243
- const credsData = await credsResponse.json() as {
244
- AccessKeyId: string
245
- SecretAccessKey: string
246
- Token: string
247
- Expiration: string
248
- }
249
-
250
- const credentials: AWSCredentials = {
251
- accessKeyId: credsData.AccessKeyId,
252
- secretAccessKey: credsData.SecretAccessKey,
253
- sessionToken: credsData.Token,
254
- }
255
-
256
- // Cache the credentials
257
- this.ec2CredentialsCache = {
258
- credentials,
259
- expiration: new Date(credsData.Expiration).getTime(),
260
- }
261
-
262
- return credentials
263
- }
264
- catch (error) {
265
- throw new Error('AWS credentials not found. Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables, or run on an EC2 instance with an IAM role.')
266
- }
267
- }
268
-
269
- /**
270
- * Make a signed AWS API request with retry logic and caching
271
- */
272
- async request(options: AWSRequestOptions): Promise<any> {
273
- // Check cache first for GET requests
274
- if (options.method === 'GET' && this.config.cacheEnabled && options.cacheKey) {
275
- const cached = this.getFromCache(options.cacheKey)
276
- if (cached !== null) {
277
- return cached
278
- }
279
- }
280
-
281
- const maxRetries = options.retries ?? this.config.maxRetries ?? 3
282
- let lastError: Error | null = null
283
-
284
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
285
- try {
286
- const result = await this.makeRequest(options)
287
-
288
- // Cache successful GET requests
289
- if (options.method === 'GET' && this.config.cacheEnabled && options.cacheKey) {
290
- this.setInCache(
291
- options.cacheKey,
292
- result,
293
- options.cacheTTL ?? this.config.defaultCacheTTL ?? 60000,
294
- )
295
- }
296
-
297
- return result
298
- }
299
- catch (error: any) {
300
- lastError = error
301
-
302
- // Check if error is retryable
303
- const isRetryable = this.shouldRetry(error)
304
-
305
- // Don't retry non-retryable errors
306
- if (!isRetryable) {
307
- throw error
308
- }
309
-
310
- // Last attempt
311
- if (attempt === maxRetries) {
312
- throw error
313
- }
314
-
315
- // Wait before retrying with exponential backoff
316
- const delay = this.calculateRetryDelay(attempt)
317
- await this.sleep(delay)
318
- }
319
- }
320
-
321
- throw lastError || new Error('Request failed after retries')
322
- }
323
-
324
- /**
325
- * Make the actual HTTP request
326
- */
327
- private async makeRequest(options: AWSRequestOptions): Promise<any> {
328
- const credentials = options.credentials || await this.getCredentials()
329
- if (!credentials || !credentials.accessKeyId || !credentials.secretAccessKey) {
330
- throw new Error('AWS credentials not provided')
331
- }
332
-
333
- const url = this.buildUrl(options)
334
- const headers = this.signRequest(options, credentials)
335
-
336
- const response = await fetch(url, {
337
- method: options.method,
338
- headers,
339
- body: options.body,
340
- })
341
-
342
- const responseText = await response.text()
343
-
344
- if (!response.ok) {
345
- throw this.parseError(responseText, response.status, response.headers)
346
- }
347
-
348
- // Handle empty responses
349
- if (!responseText || responseText.trim() === '') {
350
- if (options.returnHeaders) {
351
- return { body: null, headers: this.headersToObject(response.headers) }
352
- }
353
- return null
354
- }
355
-
356
- // Return raw response if requested (useful for S3 getObject with non-XML content)
357
- if (options.rawResponse) {
358
- if (options.returnHeaders) {
359
- return { body: responseText, headers: this.headersToObject(response.headers) }
360
- }
361
- return responseText
362
- }
363
-
364
- // Parse XML or JSON response
365
- let body: any
366
- if (responseText.startsWith('<')) {
367
- body = this.parseXmlResponse(responseText)
368
- }
369
- else {
370
- try {
371
- body = JSON.parse(responseText)
372
- }
373
- catch {
374
- body = responseText
375
- }
376
- }
377
-
378
- // Return with headers if requested
379
- if (options.returnHeaders) {
380
- return { body, headers: this.headersToObject(response.headers) }
381
- }
382
-
383
- return body
384
- }
385
-
386
- /**
387
- * Convert Headers object to plain object
388
- */
389
- private headersToObject(headers: Headers): Record<string, string> {
390
- const result: Record<string, string> = {}
391
- headers.forEach((value, key) => {
392
- result[key] = value
393
- })
394
- return result
395
- }
396
-
397
- /**
398
- * Build the full URL for the request
399
- */
400
- private buildUrl(options: AWSRequestOptions): string {
401
- const { service, region, path, queryParams } = options
402
-
403
- let host: string
404
- if (service === 's3') {
405
- // Use virtual-hosted style for S3 buckets (required for newer buckets)
406
- if (options.bucket) {
407
- host = `${options.bucket}.s3.${region}.amazonaws.com`
408
- }
409
- else {
410
- host = `s3.${region}.amazonaws.com`
411
- }
412
- }
413
- else if (service === 'cloudfront') {
414
- host = 'cloudfront.amazonaws.com'
415
- }
416
- else if (service === 'iam') {
417
- // IAM is a global service - no region in the endpoint
418
- host = 'iam.amazonaws.com'
419
- }
420
- else if (service === 'route53') {
421
- // Route53 is a global service
422
- host = 'route53.amazonaws.com'
423
- }
424
- else if (service === 'route53domains') {
425
- // Route53 Domains is only available in us-east-1
426
- host = 'route53domains.us-east-1.amazonaws.com'
427
- }
428
- else if (service === 'ecr') {
429
- // ECR uses api.ecr subdomain for the JSON API
430
- host = `api.ecr.${region}.amazonaws.com`
431
- }
432
- else if (service === 'ses') {
433
- // SES v1 API uses email subdomain
434
- host = `email.${region}.amazonaws.com`
435
- }
436
- else {
437
- host = `${service}.${region}.amazonaws.com`
438
- }
439
-
440
- let url = `https://${host}${path}`
441
-
442
- if (queryParams && Object.keys(queryParams).length > 0) {
443
- const queryString = new URLSearchParams(queryParams).toString()
444
- url += `?${queryString}`
445
- }
446
-
447
- return url
448
- }
449
-
450
- /**
451
- * Sign the request using AWS Signature Version 4
452
- */
453
- private signRequest(options: AWSRequestOptions, credentials: AWSCredentials): Record<string, string> {
454
- const { service, region, method, path, queryParams, body } = options
455
-
456
- const now = new Date()
457
- const amzDate = this.getAmzDate(now)
458
- const dateStamp = this.getDateStamp(now)
459
-
460
- let host: string
461
- if (service === 's3') {
462
- if (options.bucket) {
463
- host = `${options.bucket}.s3.${region}.amazonaws.com`
464
- } else {
465
- host = `s3.${region}.amazonaws.com`
466
- }
467
- }
468
- else if (service === 'cloudfront') {
469
- host = 'cloudfront.amazonaws.com'
470
- }
471
- else if (service === 'iam') {
472
- // IAM is a global service - no region in the endpoint
473
- host = 'iam.amazonaws.com'
474
- }
475
- else if (service === 'route53') {
476
- // Route53 is a global service
477
- host = 'route53.amazonaws.com'
478
- }
479
- else if (service === 'route53domains') {
480
- // Route53 Domains is only available in us-east-1
481
- host = 'route53domains.us-east-1.amazonaws.com'
482
- }
483
- else if (service === 'ecr') {
484
- // ECR uses api.ecr subdomain for the JSON API
485
- host = `api.ecr.${region}.amazonaws.com`
486
- }
487
- else if (service === 'ses') {
488
- // SES v1 API uses email subdomain
489
- host = `email.${region}.amazonaws.com`
490
- }
491
- else {
492
- host = `${service}.${region}.amazonaws.com`
493
- }
494
-
495
- // Build canonical headers
496
- const headers: Record<string, string> = {
497
- 'host': host,
498
- 'x-amz-date': amzDate,
499
- ...(options.headers || {}),
500
- }
501
-
502
- if (credentials.sessionToken) {
503
- headers['x-amz-security-token'] = credentials.sessionToken
504
- }
505
-
506
- if (body) {
507
- // Don't override content-type if already set (case-insensitive check)
508
- const hasContentType = Object.keys(headers).some(
509
- k => k.toLowerCase() === 'content-type'
510
- )
511
- if (!hasContentType) {
512
- headers['content-type'] = 'application/x-www-form-urlencoded'
513
- }
514
- headers['content-length'] = Buffer.byteLength(body).toString()
515
- }
516
-
517
- // Create canonical request
518
- const payloadHash = this.sha256(body || '')
519
- headers['x-amz-content-sha256'] = payloadHash
520
-
521
- const canonicalUri = path
522
- const canonicalQueryString = queryParams
523
- ? new URLSearchParams(queryParams).toString()
524
- : ''
525
-
526
- // Sort headers by lowercase key name (AWS SigV4 requirement)
527
- const sortedHeaderKeys = Object.keys(headers).sort((a, b) =>
528
- a.toLowerCase().localeCompare(b.toLowerCase())
529
- )
530
-
531
- const canonicalHeaders = sortedHeaderKeys
532
- .map(key => `${key.toLowerCase()}:${headers[key].trim()}\n`)
533
- .join('')
534
-
535
- const signedHeaders = sortedHeaderKeys
536
- .map(key => key.toLowerCase())
537
- .join(';')
538
-
539
- const canonicalRequest = [
540
- method,
541
- canonicalUri,
542
- canonicalQueryString,
543
- canonicalHeaders,
544
- signedHeaders,
545
- payloadHash,
546
- ].join('\n')
547
-
548
- // Create string to sign
549
- const algorithm = 'AWS4-HMAC-SHA256'
550
- // Some AWS services use different service names for endpoint vs credential scope
551
- // SES v2 API uses 'email' as endpoint but 'ses' for credential scope
552
- let signingService = service
553
- if (service === 'email') signingService = 'ses'
554
- const credentialScope = `${dateStamp}/${region}/${signingService}/aws4_request`
555
- const stringToSign = [
556
- algorithm,
557
- amzDate,
558
- credentialScope,
559
- this.sha256(canonicalRequest),
560
- ].join('\n')
561
-
562
- // Calculate signature
563
- const signingKey = this.getSignatureKey(credentials.secretAccessKey, dateStamp, region, signingService)
564
- const signature = this.hmac(signingKey, stringToSign)
565
-
566
- // Build authorization header
567
- const authorizationHeader = `${algorithm} Credential=${credentials.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
568
-
569
- return {
570
- ...headers,
571
- 'Authorization': authorizationHeader,
572
- }
573
- }
574
-
575
- /**
576
- * Get AMZ date format (YYYYMMDDTHHMMSSZ)
577
- */
578
- private getAmzDate(date: Date): string {
579
- return date.toISOString().replace(/[:-]|\.\d{3}/g, '')
580
- }
581
-
582
- /**
583
- * Get date stamp (YYYYMMDD)
584
- */
585
- private getDateStamp(date: Date): string {
586
- return date.toISOString().slice(0, 10).replace(/-/g, '')
587
- }
588
-
589
- /**
590
- * SHA256 hash
591
- */
592
- private sha256(data: string): string {
593
- return crypto.createHash('sha256').update(data, 'utf8').digest('hex')
594
- }
595
-
596
- /**
597
- * HMAC SHA256
598
- */
599
- private hmac(key: Buffer | string, data: string): string {
600
- return crypto.createHmac('sha256', key).update(data, 'utf8').digest('hex')
601
- }
602
-
603
- /**
604
- * Get signature key
605
- */
606
- private getSignatureKey(key: string, dateStamp: string, region: string, service: string): Buffer {
607
- const kDate = crypto.createHmac('sha256', `AWS4${key}`).update(dateStamp).digest()
608
- const kRegion = crypto.createHmac('sha256', kDate).update(region).digest()
609
- const kService = crypto.createHmac('sha256', kRegion).update(service).digest()
610
- const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest()
611
- return kSigning
612
- }
613
-
614
- /**
615
- * Parse XML response using fast-xml-parser
616
- */
617
- private parseXmlResponse(xml: string): any {
618
- try {
619
- const parsed = this.xmlParser.parse(xml)
620
-
621
- // Extract the main result from common AWS response wrappers
622
- if (parsed.ErrorResponse) {
623
- throw this.createErrorFromXml(parsed.ErrorResponse.Error)
624
- }
625
-
626
- // Remove XML wrapper nodes to get to actual data
627
- const keys = Object.keys(parsed)
628
- if (keys.length === 1) {
629
- return parsed[keys[0]]
630
- }
631
-
632
- return parsed
633
- }
634
- catch (error: any) {
635
- // If parsing fails, return empty object
636
- if (error.code || error.statusCode) {
637
- throw error
638
- }
639
- return {}
640
- }
641
- }
642
-
643
- /**
644
- * Parse error response and create detailed error object
645
- */
646
- private parseError(responseText: string, statusCode: number, headers: Headers): AWSError {
647
- const error: AWSError = new Error() as AWSError
648
- error.statusCode = statusCode
649
- error.requestId = headers.get('x-amzn-requestid') || headers.get('x-amz-request-id') || undefined
650
-
651
- // Try to parse XML error
652
- if (responseText.startsWith('<')) {
653
- try {
654
- const parsed = this.xmlParser.parse(responseText)
655
-
656
- if (parsed.ErrorResponse?.Error) {
657
- const awsError = parsed.ErrorResponse.Error
658
- error.code = awsError.Code
659
- error.message = `AWS Error [${awsError.Code}]: ${awsError.Message || 'Unknown error'}`
660
- error.type = awsError.Type
661
- return error
662
- }
663
-
664
- if (parsed.Error) {
665
- error.code = parsed.Error.Code
666
- error.message = `AWS Error [${parsed.Error.Code}]: ${parsed.Error.Message || 'Unknown error'}`
667
- return error
668
- }
669
- }
670
- catch {
671
- // Fall through to generic error
672
- }
673
- }
674
-
675
- // Try to parse JSON error
676
- try {
677
- const json = JSON.parse(responseText)
678
- if (json.__type || json.code) {
679
- error.code = json.__type || json.code
680
- error.message = `AWS Error [${error.code}]: ${json.message || json.Message || 'Unknown error'}`
681
- return error
682
- }
683
- }
684
- catch {
685
- // Fall through to generic error
686
- }
687
-
688
- // Generic error
689
- error.message = `AWS API Error (${statusCode}): ${responseText}`
690
- return error
691
- }
692
-
693
- /**
694
- * Create error from XML error object
695
- */
696
- private createErrorFromXml(errorData: any): AWSError {
697
- const error: AWSError = new Error() as AWSError
698
- error.code = errorData.Code
699
- error.message = `AWS Error [${errorData.Code}]: ${errorData.Message || 'Unknown error'}`
700
- error.type = errorData.Type
701
- return error
702
- }
703
-
704
- /**
705
- * Check if error code is retryable
706
- */
707
- private isRetryableError(code: string): boolean {
708
- const retryableCodes = [
709
- // Timeout errors
710
- 'RequestTimeout',
711
- 'RequestTimeoutException',
712
- 'PriorRequestNotComplete',
713
- 'ConnectionError',
714
- // Throttling errors
715
- 'ThrottlingException',
716
- 'Throttling',
717
- 'TooManyRequestsException',
718
- 'ProvisionedThroughputExceededException',
719
- 'RequestLimitExceeded',
720
- 'BandwidthLimitExceeded',
721
- 'SlowDown',
722
- // Service errors
723
- 'ServiceUnavailable',
724
- 'ServiceUnavailableException',
725
- 'InternalError',
726
- 'InternalFailure',
727
- 'InternalServerError',
728
- 'InternalServiceException',
729
- // Transient errors
730
- 'TransientError',
731
- 'TransientFailure',
732
- 'IDPCommunicationErrorException',
733
- // Network errors
734
- 'NetworkError',
735
- 'ConnectionRefusedException',
736
- 'EC2ThrottledException',
737
- // S3 specific
738
- '503 Slow Down',
739
- '500 InternalError',
740
- ]
741
- return retryableCodes.includes(code)
742
- }
743
-
744
- /**
745
- * Check if HTTP status code is retryable
746
- */
747
- private isRetryableStatusCode(statusCode: number): boolean {
748
- return statusCode >= 500 || statusCode === 429 // Server errors or Too Many Requests
749
- }
750
-
751
- /**
752
- * Determine if an error should be retried
753
- */
754
- private shouldRetry(error: any): boolean {
755
- // Network/fetch errors should be retried
756
- if (error.name === 'FetchError' || error.name === 'TypeError' || error.code === 'ECONNRESET') {
757
- return true
758
- }
759
-
760
- // Check by error code
761
- if (error.code && this.isRetryableError(error.code)) {
762
- return true
763
- }
764
-
765
- // Check by status code
766
- if (error.statusCode && this.isRetryableStatusCode(error.statusCode)) {
767
- return true
768
- }
769
-
770
- // Don't retry client errors (4xx) except 429
771
- if (error.statusCode && error.statusCode >= 400 && error.statusCode < 500 && error.statusCode !== 429) {
772
- return false
773
- }
774
-
775
- // Default: don't retry if we have a status code
776
- if (error.statusCode) {
777
- return false
778
- }
779
-
780
- // Unknown errors: retry once in case it's a transient issue
781
- return true
782
- }
783
-
784
- /**
785
- * Calculate retry delay with exponential backoff
786
- */
787
- private calculateRetryDelay(attempt: number): number {
788
- const baseDelay = this.config.retryDelay ?? 1000
789
- const maxDelay = 30000 // 30 seconds max
790
- const delay = Math.min(baseDelay * (2 ** attempt), maxDelay)
791
- // Add jitter to avoid thundering herd
792
- const jitter = Math.random() * 0.3 * delay
793
- return delay + jitter
794
- }
795
-
796
- /**
797
- * Sleep for specified milliseconds
798
- */
799
- private sleep(ms: number): Promise<void> {
800
- return new Promise(resolve => setTimeout(resolve, ms))
801
- }
802
-
803
- /**
804
- * Get value from cache
805
- */
806
- private getFromCache(key: string): any | null {
807
- const entry = this.cache.get(key)
808
- if (!entry) {
809
- return null
810
- }
811
-
812
- // Check if expired
813
- if (Date.now() > entry.expires) {
814
- this.cache.delete(key)
815
- return null
816
- }
817
-
818
- return entry.data
819
- }
820
-
821
- /**
822
- * Set value in cache
823
- */
824
- private setInCache(key: string, data: any, ttl: number): void {
825
- this.cache.set(key, {
826
- data,
827
- expires: Date.now() + ttl,
828
- })
829
- }
830
-
831
- /**
832
- * Clear cache
833
- */
834
- clearCache(): void {
835
- this.cache.clear()
836
- }
837
-
838
- /**
839
- * Clear expired cache entries
840
- */
841
- clearExpiredCache(): void {
842
- const now = Date.now()
843
- for (const [key, entry] of this.cache.entries()) {
844
- if (now > entry.expires) {
845
- this.cache.delete(key)
846
- }
847
- }
848
- }
849
- }
850
-
851
- /**
852
- * Build query string for AWS API calls
853
- */
854
- export function buildQueryParams(params: Record<string, any>): Record<string, string> {
855
- const result: Record<string, string> = {}
856
-
857
- for (const [key, value] of Object.entries(params)) {
858
- if (value === undefined || value === null) {
859
- continue
860
- }
861
-
862
- if (Array.isArray(value)) {
863
- value.forEach((item, index) => {
864
- result[`${key}.${index + 1}`] = String(item)
865
- })
866
- }
867
- else if (typeof value === 'object') {
868
- for (const [subKey, subValue] of Object.entries(value)) {
869
- result[`${key}.${subKey}`] = String(subValue)
870
- }
871
- }
872
- else {
873
- result[key] = String(value)
874
- }
875
- }
876
-
877
- return result
878
- }