@stacksjs/ts-cloud 0.1.9 → 0.1.14

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 (150) hide show
  1. package/README.md +39 -377
  2. package/dist/bin/cli.js +1047 -424
  3. package/dist/index.d.ts +36 -3
  4. package/dist/index.js +76430 -7096
  5. package/package.json +7 -8
  6. package/dist/aws/acm.d.ts +0 -129
  7. package/dist/aws/application-autoscaling.d.ts +0 -282
  8. package/dist/aws/bedrock.d.ts +0 -2292
  9. package/dist/aws/client.d.ts +0 -79
  10. package/dist/aws/cloudformation.d.ts +0 -105
  11. package/dist/aws/cloudfront.d.ts +0 -265
  12. package/dist/aws/cloudwatch-logs.d.ts +0 -48
  13. package/dist/aws/comprehend.d.ts +0 -505
  14. package/dist/aws/connect.d.ts +0 -377
  15. package/dist/aws/deploy-imap.d.ts +0 -14
  16. package/dist/aws/dynamodb.d.ts +0 -176
  17. package/dist/aws/ec2.d.ts +0 -272
  18. package/dist/aws/ecr.d.ts +0 -149
  19. package/dist/aws/ecs.d.ts +0 -162
  20. package/dist/aws/elasticache.d.ts +0 -71
  21. package/dist/aws/elbv2.d.ts +0 -248
  22. package/dist/aws/email.d.ts +0 -175
  23. package/dist/aws/eventbridge.d.ts +0 -142
  24. package/dist/aws/iam.d.ts +0 -638
  25. package/dist/aws/imap-server.d.ts +0 -119
  26. package/dist/aws/index.d.ts +0 -192
  27. package/dist/aws/kendra.d.ts +0 -782
  28. package/dist/aws/lambda.d.ts +0 -232
  29. package/dist/aws/opensearch.d.ts +0 -87
  30. package/dist/aws/personalize.d.ts +0 -516
  31. package/dist/aws/polly.d.ts +0 -214
  32. package/dist/aws/rds.d.ts +0 -240
  33. package/dist/aws/rekognition.d.ts +0 -543
  34. package/dist/aws/route53-domains.d.ts +0 -113
  35. package/dist/aws/route53.d.ts +0 -215
  36. package/dist/aws/s3.d.ts +0 -212
  37. package/dist/aws/scheduler.d.ts +0 -140
  38. package/dist/aws/secrets-manager.d.ts +0 -170
  39. package/dist/aws/ses.d.ts +0 -288
  40. package/dist/aws/setup-phone.d.ts +0 -0
  41. package/dist/aws/setup-sms.d.ts +0 -115
  42. package/dist/aws/sms.d.ts +0 -304
  43. package/dist/aws/smtp-server.d.ts +0 -61
  44. package/dist/aws/sns.d.ts +0 -117
  45. package/dist/aws/sqs.d.ts +0 -65
  46. package/dist/aws/ssm.d.ts +0 -179
  47. package/dist/aws/sts.d.ts +0 -15
  48. package/dist/aws/support.d.ts +0 -104
  49. package/dist/aws/test-imap.d.ts +0 -0
  50. package/dist/aws/textract.d.ts +0 -403
  51. package/dist/aws/transcribe.d.ts +0 -60
  52. package/dist/aws/translate.d.ts +0 -358
  53. package/dist/aws/voice.d.ts +0 -219
  54. package/dist/config.d.ts +0 -7
  55. package/dist/deploy/index.d.ts +0 -2
  56. package/dist/deploy/static-site-external-dns.d.ts +0 -51
  57. package/dist/deploy/static-site.d.ts +0 -71
  58. package/dist/dns/cloudflare.d.ts +0 -52
  59. package/dist/dns/godaddy.d.ts +0 -38
  60. package/dist/dns/index.d.ts +0 -45
  61. package/dist/dns/porkbun.d.ts +0 -18
  62. package/dist/dns/route53-adapter.d.ts +0 -38
  63. package/dist/dns/types.d.ts +0 -77
  64. package/dist/dns/validator.d.ts +0 -78
  65. package/dist/generators/index.d.ts +0 -1
  66. package/dist/generators/infrastructure.d.ts +0 -30
  67. package/dist/push/apns.d.ts +0 -60
  68. package/dist/push/fcm.d.ts +0 -117
  69. package/dist/push/index.d.ts +0 -14
  70. package/dist/security/pre-deploy-scanner.d.ts +0 -69
  71. package/dist/ssl/acme-client.d.ts +0 -67
  72. package/dist/ssl/index.d.ts +0 -2
  73. package/dist/ssl/letsencrypt.d.ts +0 -48
  74. package/dist/types.d.ts +0 -1
  75. package/dist/utils/cli.d.ts +0 -123
  76. package/dist/validation/index.d.ts +0 -1
  77. package/dist/validation/template.d.ts +0 -23
  78. package/src/aws/acm.ts +0 -768
  79. package/src/aws/application-autoscaling.ts +0 -845
  80. package/src/aws/bedrock.ts +0 -4074
  81. package/src/aws/client.ts +0 -891
  82. package/src/aws/cloudformation.ts +0 -896
  83. package/src/aws/cloudfront.ts +0 -1531
  84. package/src/aws/cloudwatch-logs.ts +0 -154
  85. package/src/aws/comprehend.ts +0 -839
  86. package/src/aws/connect.ts +0 -1056
  87. package/src/aws/deploy-imap.ts +0 -384
  88. package/src/aws/dynamodb.ts +0 -340
  89. package/src/aws/ec2.ts +0 -1385
  90. package/src/aws/ecr.ts +0 -621
  91. package/src/aws/ecs.ts +0 -615
  92. package/src/aws/elasticache.ts +0 -301
  93. package/src/aws/elbv2.ts +0 -942
  94. package/src/aws/email.ts +0 -928
  95. package/src/aws/eventbridge.ts +0 -248
  96. package/src/aws/iam.ts +0 -1689
  97. package/src/aws/imap-server.ts +0 -2100
  98. package/src/aws/index.ts +0 -213
  99. package/src/aws/kendra.ts +0 -1097
  100. package/src/aws/lambda.ts +0 -786
  101. package/src/aws/opensearch.ts +0 -158
  102. package/src/aws/personalize.ts +0 -977
  103. package/src/aws/polly.ts +0 -559
  104. package/src/aws/rds.ts +0 -888
  105. package/src/aws/rekognition.ts +0 -846
  106. package/src/aws/route53-domains.ts +0 -359
  107. package/src/aws/route53.ts +0 -1046
  108. package/src/aws/s3.ts +0 -2334
  109. package/src/aws/scheduler.ts +0 -571
  110. package/src/aws/secrets-manager.ts +0 -769
  111. package/src/aws/ses.ts +0 -1081
  112. package/src/aws/setup-phone.ts +0 -104
  113. package/src/aws/setup-sms.ts +0 -580
  114. package/src/aws/sms.ts +0 -1735
  115. package/src/aws/smtp-server.ts +0 -531
  116. package/src/aws/sns.ts +0 -758
  117. package/src/aws/sqs.ts +0 -382
  118. package/src/aws/ssm.ts +0 -807
  119. package/src/aws/sts.ts +0 -92
  120. package/src/aws/support.ts +0 -391
  121. package/src/aws/test-imap.ts +0 -86
  122. package/src/aws/textract.ts +0 -780
  123. package/src/aws/transcribe.ts +0 -108
  124. package/src/aws/translate.ts +0 -641
  125. package/src/aws/voice.ts +0 -1379
  126. package/src/config.ts +0 -35
  127. package/src/deploy/index.ts +0 -7
  128. package/src/deploy/static-site-external-dns.ts +0 -945
  129. package/src/deploy/static-site.ts +0 -1175
  130. package/src/dns/cloudflare.ts +0 -548
  131. package/src/dns/godaddy.ts +0 -412
  132. package/src/dns/index.ts +0 -205
  133. package/src/dns/porkbun.ts +0 -362
  134. package/src/dns/route53-adapter.ts +0 -414
  135. package/src/dns/types.ts +0 -119
  136. package/src/dns/validator.ts +0 -369
  137. package/src/generators/index.ts +0 -5
  138. package/src/generators/infrastructure.ts +0 -1660
  139. package/src/index.ts +0 -163
  140. package/src/push/apns.ts +0 -452
  141. package/src/push/fcm.ts +0 -506
  142. package/src/push/index.ts +0 -58
  143. package/src/security/pre-deploy-scanner.ts +0 -655
  144. package/src/ssl/acme-client.ts +0 -478
  145. package/src/ssl/index.ts +0 -7
  146. package/src/ssl/letsencrypt.ts +0 -747
  147. package/src/types.ts +0 -2
  148. package/src/utils/cli.ts +0 -398
  149. package/src/validation/index.ts +0 -5
  150. package/src/validation/template.ts +0 -405
package/src/aws/client.ts DELETED
@@ -1,891 +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 = Object.keys(queryParams)
444
- .map(k => `${this.uriEncode(k)}=${this.uriEncode(queryParams[k])}`)
445
- .join('&')
446
- url += `?${queryString}`
447
- }
448
-
449
- return url
450
- }
451
-
452
- /**
453
- * Sign the request using AWS Signature Version 4
454
- */
455
- private signRequest(options: AWSRequestOptions, credentials: AWSCredentials): Record<string, string> {
456
- const { service, region, method, path, queryParams, body } = options
457
-
458
- const now = new Date()
459
- const amzDate = this.getAmzDate(now)
460
- const dateStamp = this.getDateStamp(now)
461
-
462
- let host: string
463
- if (service === 's3') {
464
- if (options.bucket) {
465
- host = `${options.bucket}.s3.${region}.amazonaws.com`
466
- } else {
467
- host = `s3.${region}.amazonaws.com`
468
- }
469
- }
470
- else if (service === 'cloudfront') {
471
- host = 'cloudfront.amazonaws.com'
472
- }
473
- else if (service === 'iam') {
474
- // IAM is a global service - no region in the endpoint
475
- host = 'iam.amazonaws.com'
476
- }
477
- else if (service === 'route53') {
478
- // Route53 is a global service
479
- host = 'route53.amazonaws.com'
480
- }
481
- else if (service === 'route53domains') {
482
- // Route53 Domains is only available in us-east-1
483
- host = 'route53domains.us-east-1.amazonaws.com'
484
- }
485
- else if (service === 'ecr') {
486
- // ECR uses api.ecr subdomain for the JSON API
487
- host = `api.ecr.${region}.amazonaws.com`
488
- }
489
- else if (service === 'ses') {
490
- // SES v1 API uses email subdomain
491
- host = `email.${region}.amazonaws.com`
492
- }
493
- else {
494
- host = `${service}.${region}.amazonaws.com`
495
- }
496
-
497
- // Build canonical headers
498
- const headers: Record<string, string> = {
499
- 'host': host,
500
- 'x-amz-date': amzDate,
501
- ...(options.headers || {}),
502
- }
503
-
504
- if (credentials.sessionToken) {
505
- headers['x-amz-security-token'] = credentials.sessionToken
506
- }
507
-
508
- if (body) {
509
- // Don't override content-type if already set (case-insensitive check)
510
- const hasContentType = Object.keys(headers).some(
511
- k => k.toLowerCase() === 'content-type'
512
- )
513
- if (!hasContentType) {
514
- headers['content-type'] = 'application/x-www-form-urlencoded'
515
- }
516
- headers['content-length'] = Buffer.byteLength(body).toString()
517
- }
518
-
519
- // Create canonical request
520
- const payloadHash = this.sha256(body || '')
521
- headers['x-amz-content-sha256'] = payloadHash
522
-
523
- const canonicalUri = path
524
- const canonicalQueryString = queryParams
525
- ? Object.keys(queryParams).sort().map(k =>
526
- `${this.uriEncode(k)}=${this.uriEncode(queryParams[k])}`
527
- ).join('&')
528
- : ''
529
-
530
- // Sort headers by lowercase key name (AWS SigV4 requirement)
531
- const sortedHeaderKeys = Object.keys(headers).sort((a, b) =>
532
- a.toLowerCase().localeCompare(b.toLowerCase())
533
- )
534
-
535
- const canonicalHeaders = sortedHeaderKeys
536
- .map(key => `${key.toLowerCase()}:${headers[key].trim()}\n`)
537
- .join('')
538
-
539
- const signedHeaders = sortedHeaderKeys
540
- .map(key => key.toLowerCase())
541
- .join(';')
542
-
543
- const canonicalRequest = [
544
- method,
545
- canonicalUri,
546
- canonicalQueryString,
547
- canonicalHeaders,
548
- signedHeaders,
549
- payloadHash,
550
- ].join('\n')
551
-
552
- // Create string to sign
553
- const algorithm = 'AWS4-HMAC-SHA256'
554
- // Some AWS services use different service names for endpoint vs credential scope
555
- // SES v2 API uses 'email' as endpoint but 'ses' for credential scope
556
- let signingService = service
557
- if (service === 'email') signingService = 'ses'
558
- const credentialScope = `${dateStamp}/${region}/${signingService}/aws4_request`
559
- const stringToSign = [
560
- algorithm,
561
- amzDate,
562
- credentialScope,
563
- this.sha256(canonicalRequest),
564
- ].join('\n')
565
-
566
- // Calculate signature
567
- const signingKey = this.getSignatureKey(credentials.secretAccessKey, dateStamp, region, signingService)
568
- const signature = this.hmac(signingKey, stringToSign)
569
-
570
- // Build authorization header
571
- const authorizationHeader = `${algorithm} Credential=${credentials.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
572
-
573
- return {
574
- ...headers,
575
- 'Authorization': authorizationHeader,
576
- }
577
- }
578
-
579
- /**
580
- * Get AMZ date format (YYYYMMDDTHHMMSSZ)
581
- */
582
- private getAmzDate(date: Date): string {
583
- return date.toISOString().replace(/[:-]|\.\d{3}/g, '')
584
- }
585
-
586
- /**
587
- * Get date stamp (YYYYMMDD)
588
- */
589
- private getDateStamp(date: Date): string {
590
- return date.toISOString().slice(0, 10).replace(/-/g, '')
591
- }
592
-
593
- /**
594
- * URI-encode a string per RFC 3986
595
- */
596
- private uriEncode(str: string): string {
597
- return encodeURIComponent(str).replace(/[!'()*]/g, c =>
598
- `%${c.charCodeAt(0).toString(16).toUpperCase()}`
599
- )
600
- }
601
-
602
- /**
603
- * SHA256 hash
604
- */
605
- private sha256(data: string): string {
606
- return crypto.createHash('sha256').update(data, 'utf8').digest('hex')
607
- }
608
-
609
- /**
610
- * HMAC SHA256
611
- */
612
- private hmac(key: Buffer | string, data: string): string {
613
- return crypto.createHmac('sha256', key).update(data, 'utf8').digest('hex')
614
- }
615
-
616
- /**
617
- * Get signature key
618
- */
619
- private getSignatureKey(key: string, dateStamp: string, region: string, service: string): Buffer {
620
- const kDate = crypto.createHmac('sha256', `AWS4${key}`).update(dateStamp).digest()
621
- const kRegion = crypto.createHmac('sha256', kDate).update(region).digest()
622
- const kService = crypto.createHmac('sha256', kRegion).update(service).digest()
623
- const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest()
624
- return kSigning
625
- }
626
-
627
- /**
628
- * Parse XML response using fast-xml-parser
629
- */
630
- private parseXmlResponse(xml: string): any {
631
- try {
632
- const parsed = this.xmlParser.parse(xml)
633
-
634
- // Extract the main result from common AWS response wrappers
635
- if (parsed.ErrorResponse) {
636
- throw this.createErrorFromXml(parsed.ErrorResponse.Error)
637
- }
638
-
639
- // Remove XML wrapper nodes to get to actual data
640
- const keys = Object.keys(parsed)
641
- if (keys.length === 1) {
642
- return parsed[keys[0]]
643
- }
644
-
645
- return parsed
646
- }
647
- catch (error: any) {
648
- // If parsing fails, return empty object
649
- if (error.code || error.statusCode) {
650
- throw error
651
- }
652
- return {}
653
- }
654
- }
655
-
656
- /**
657
- * Parse error response and create detailed error object
658
- */
659
- private parseError(responseText: string, statusCode: number, headers: Headers): AWSError {
660
- const error: AWSError = new Error() as AWSError
661
- error.statusCode = statusCode
662
- error.requestId = headers.get('x-amzn-requestid') || headers.get('x-amz-request-id') || undefined
663
-
664
- // Try to parse XML error
665
- if (responseText.startsWith('<')) {
666
- try {
667
- const parsed = this.xmlParser.parse(responseText)
668
-
669
- if (parsed.ErrorResponse?.Error) {
670
- const awsError = parsed.ErrorResponse.Error
671
- error.code = awsError.Code
672
- error.message = `AWS Error [${awsError.Code}]: ${awsError.Message || 'Unknown error'}`
673
- error.type = awsError.Type
674
- return error
675
- }
676
-
677
- if (parsed.Error) {
678
- error.code = parsed.Error.Code
679
- error.message = `AWS Error [${parsed.Error.Code}]: ${parsed.Error.Message || 'Unknown error'}`
680
- return error
681
- }
682
- }
683
- catch {
684
- // Fall through to generic error
685
- }
686
- }
687
-
688
- // Try to parse JSON error
689
- try {
690
- const json = JSON.parse(responseText)
691
- if (json.__type || json.code) {
692
- error.code = json.__type || json.code
693
- error.message = `AWS Error [${error.code}]: ${json.message || json.Message || 'Unknown error'}`
694
- return error
695
- }
696
- }
697
- catch {
698
- // Fall through to generic error
699
- }
700
-
701
- // Generic error
702
- error.message = `AWS API Error (${statusCode}): ${responseText}`
703
- return error
704
- }
705
-
706
- /**
707
- * Create error from XML error object
708
- */
709
- private createErrorFromXml(errorData: any): AWSError {
710
- const error: AWSError = new Error() as AWSError
711
- error.code = errorData.Code
712
- error.message = `AWS Error [${errorData.Code}]: ${errorData.Message || 'Unknown error'}`
713
- error.type = errorData.Type
714
- return error
715
- }
716
-
717
- /**
718
- * Check if error code is retryable
719
- */
720
- private isRetryableError(code: string): boolean {
721
- const retryableCodes = [
722
- // Timeout errors
723
- 'RequestTimeout',
724
- 'RequestTimeoutException',
725
- 'PriorRequestNotComplete',
726
- 'ConnectionError',
727
- // Throttling errors
728
- 'ThrottlingException',
729
- 'Throttling',
730
- 'TooManyRequestsException',
731
- 'ProvisionedThroughputExceededException',
732
- 'RequestLimitExceeded',
733
- 'BandwidthLimitExceeded',
734
- 'SlowDown',
735
- // Service errors
736
- 'ServiceUnavailable',
737
- 'ServiceUnavailableException',
738
- 'InternalError',
739
- 'InternalFailure',
740
- 'InternalServerError',
741
- 'InternalServiceException',
742
- // Transient errors
743
- 'TransientError',
744
- 'TransientFailure',
745
- 'IDPCommunicationErrorException',
746
- // Network errors
747
- 'NetworkError',
748
- 'ConnectionRefusedException',
749
- 'EC2ThrottledException',
750
- // S3 specific
751
- '503 Slow Down',
752
- '500 InternalError',
753
- ]
754
- return retryableCodes.includes(code)
755
- }
756
-
757
- /**
758
- * Check if HTTP status code is retryable
759
- */
760
- private isRetryableStatusCode(statusCode: number): boolean {
761
- return statusCode >= 500 || statusCode === 429 // Server errors or Too Many Requests
762
- }
763
-
764
- /**
765
- * Determine if an error should be retried
766
- */
767
- private shouldRetry(error: any): boolean {
768
- // Network/fetch errors should be retried
769
- if (error.name === 'FetchError' || error.name === 'TypeError' || error.code === 'ECONNRESET') {
770
- return true
771
- }
772
-
773
- // Check by error code
774
- if (error.code && this.isRetryableError(error.code)) {
775
- return true
776
- }
777
-
778
- // Check by status code
779
- if (error.statusCode && this.isRetryableStatusCode(error.statusCode)) {
780
- return true
781
- }
782
-
783
- // Don't retry client errors (4xx) except 429
784
- if (error.statusCode && error.statusCode >= 400 && error.statusCode < 500 && error.statusCode !== 429) {
785
- return false
786
- }
787
-
788
- // Default: don't retry if we have a status code
789
- if (error.statusCode) {
790
- return false
791
- }
792
-
793
- // Unknown errors: retry once in case it's a transient issue
794
- return true
795
- }
796
-
797
- /**
798
- * Calculate retry delay with exponential backoff
799
- */
800
- private calculateRetryDelay(attempt: number): number {
801
- const baseDelay = this.config.retryDelay ?? 1000
802
- const maxDelay = 30000 // 30 seconds max
803
- const delay = Math.min(baseDelay * (2 ** attempt), maxDelay)
804
- // Add jitter to avoid thundering herd
805
- const jitter = Math.random() * 0.3 * delay
806
- return delay + jitter
807
- }
808
-
809
- /**
810
- * Sleep for specified milliseconds
811
- */
812
- private sleep(ms: number): Promise<void> {
813
- return new Promise(resolve => setTimeout(resolve, ms))
814
- }
815
-
816
- /**
817
- * Get value from cache
818
- */
819
- private getFromCache(key: string): any | null {
820
- const entry = this.cache.get(key)
821
- if (!entry) {
822
- return null
823
- }
824
-
825
- // Check if expired
826
- if (Date.now() > entry.expires) {
827
- this.cache.delete(key)
828
- return null
829
- }
830
-
831
- return entry.data
832
- }
833
-
834
- /**
835
- * Set value in cache
836
- */
837
- private setInCache(key: string, data: any, ttl: number): void {
838
- this.cache.set(key, {
839
- data,
840
- expires: Date.now() + ttl,
841
- })
842
- }
843
-
844
- /**
845
- * Clear cache
846
- */
847
- clearCache(): void {
848
- this.cache.clear()
849
- }
850
-
851
- /**
852
- * Clear expired cache entries
853
- */
854
- clearExpiredCache(): void {
855
- const now = Date.now()
856
- for (const [key, entry] of this.cache.entries()) {
857
- if (now > entry.expires) {
858
- this.cache.delete(key)
859
- }
860
- }
861
- }
862
- }
863
-
864
- /**
865
- * Build query string for AWS API calls
866
- */
867
- export function buildQueryParams(params: Record<string, any>): Record<string, string> {
868
- const result: Record<string, string> = {}
869
-
870
- for (const [key, value] of Object.entries(params)) {
871
- if (value === undefined || value === null) {
872
- continue
873
- }
874
-
875
- if (Array.isArray(value)) {
876
- value.forEach((item, index) => {
877
- result[`${key}.${index + 1}`] = String(item)
878
- })
879
- }
880
- else if (typeof value === 'object') {
881
- for (const [subKey, subValue] of Object.entries(value)) {
882
- result[`${key}.${subKey}`] = String(subValue)
883
- }
884
- }
885
- else {
886
- result[key] = String(value)
887
- }
888
- }
889
-
890
- return result
891
- }