@stacksjs/ts-cloud-core 0.1.3 → 0.1.6

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 (250) hide show
  1. package/README.md +98 -13
  2. package/package.json +12 -3
  3. package/src/advanced-features.test.ts +0 -465
  4. package/src/aws/cloudformation.ts +0 -421
  5. package/src/aws/cloudfront.ts +0 -158
  6. package/src/aws/credentials.test.ts +0 -132
  7. package/src/aws/credentials.ts +0 -545
  8. package/src/aws/index.ts +0 -87
  9. package/src/aws/s3.test.ts +0 -188
  10. package/src/aws/s3.ts +0 -1088
  11. package/src/aws/signature.test.ts +0 -670
  12. package/src/aws/signature.ts +0 -1155
  13. package/src/backup/disaster-recovery.test.ts +0 -726
  14. package/src/backup/disaster-recovery.ts +0 -500
  15. package/src/backup/index.ts +0 -34
  16. package/src/backup/manager.test.ts +0 -498
  17. package/src/backup/manager.ts +0 -432
  18. package/src/cicd/circleci.ts +0 -430
  19. package/src/cicd/github-actions.ts +0 -424
  20. package/src/cicd/gitlab-ci.ts +0 -255
  21. package/src/cicd/index.ts +0 -8
  22. package/src/cli/history.ts +0 -396
  23. package/src/cli/index.ts +0 -10
  24. package/src/cli/progress.ts +0 -458
  25. package/src/cli/repl.ts +0 -454
  26. package/src/cli/suggestions.ts +0 -327
  27. package/src/cli/table.test.ts +0 -319
  28. package/src/cli/table.ts +0 -332
  29. package/src/cloudformation/builder.test.ts +0 -327
  30. package/src/cloudformation/builder.ts +0 -378
  31. package/src/cloudformation/builders/api-gateway.ts +0 -449
  32. package/src/cloudformation/builders/cache.ts +0 -334
  33. package/src/cloudformation/builders/cdn.ts +0 -278
  34. package/src/cloudformation/builders/compute.ts +0 -485
  35. package/src/cloudformation/builders/database.ts +0 -392
  36. package/src/cloudformation/builders/functions.ts +0 -343
  37. package/src/cloudformation/builders/messaging.ts +0 -140
  38. package/src/cloudformation/builders/monitoring.ts +0 -300
  39. package/src/cloudformation/builders/network.ts +0 -264
  40. package/src/cloudformation/builders/queue.ts +0 -147
  41. package/src/cloudformation/builders/security.ts +0 -399
  42. package/src/cloudformation/builders/storage.ts +0 -285
  43. package/src/cloudformation/index.ts +0 -30
  44. package/src/cloudformation/types.ts +0 -173
  45. package/src/compliance/aws-config.ts +0 -543
  46. package/src/compliance/cloudtrail.ts +0 -376
  47. package/src/compliance/compliance.test.ts +0 -423
  48. package/src/compliance/guardduty.ts +0 -446
  49. package/src/compliance/index.ts +0 -66
  50. package/src/compliance/security-hub.ts +0 -456
  51. package/src/containers/build-optimization.ts +0 -416
  52. package/src/containers/containers.test.ts +0 -508
  53. package/src/containers/image-scanning.ts +0 -360
  54. package/src/containers/index.ts +0 -9
  55. package/src/containers/registry.ts +0 -293
  56. package/src/containers/service-mesh.ts +0 -520
  57. package/src/database/database.test.ts +0 -762
  58. package/src/database/index.ts +0 -9
  59. package/src/database/migrations.ts +0 -444
  60. package/src/database/performance.ts +0 -528
  61. package/src/database/replicas.ts +0 -534
  62. package/src/database/users.ts +0 -494
  63. package/src/dependency-graph.ts +0 -143
  64. package/src/deployment/ab-testing.ts +0 -582
  65. package/src/deployment/blue-green.ts +0 -452
  66. package/src/deployment/canary.ts +0 -500
  67. package/src/deployment/deployment.test.ts +0 -526
  68. package/src/deployment/index.ts +0 -61
  69. package/src/deployment/progressive.ts +0 -62
  70. package/src/dns/dns.test.ts +0 -641
  71. package/src/dns/dnssec.ts +0 -315
  72. package/src/dns/index.ts +0 -8
  73. package/src/dns/resolver.ts +0 -496
  74. package/src/dns/routing.ts +0 -593
  75. package/src/email/advanced/analytics.ts +0 -445
  76. package/src/email/advanced/index.ts +0 -11
  77. package/src/email/advanced/rules.ts +0 -465
  78. package/src/email/advanced/scheduling.ts +0 -352
  79. package/src/email/advanced/search.ts +0 -412
  80. package/src/email/advanced/shared-mailboxes.ts +0 -404
  81. package/src/email/advanced/templates.ts +0 -455
  82. package/src/email/advanced/threading.ts +0 -281
  83. package/src/email/analytics.ts +0 -467
  84. package/src/email/bounce-handling.ts +0 -425
  85. package/src/email/email.test.ts +0 -431
  86. package/src/email/handlers/__tests__/inbound.test.ts +0 -38
  87. package/src/email/handlers/__tests__/outbound.test.ts +0 -37
  88. package/src/email/handlers/converter.ts +0 -227
  89. package/src/email/handlers/feedback.ts +0 -228
  90. package/src/email/handlers/inbound.ts +0 -169
  91. package/src/email/handlers/outbound.ts +0 -178
  92. package/src/email/index.ts +0 -15
  93. package/src/email/reputation.ts +0 -303
  94. package/src/email/templates.ts +0 -352
  95. package/src/errors/index.test.ts +0 -434
  96. package/src/errors/index.ts +0 -416
  97. package/src/health-checks/index.ts +0 -40
  98. package/src/index.ts +0 -360
  99. package/src/intrinsic-functions.ts +0 -118
  100. package/src/lambda/concurrency.ts +0 -330
  101. package/src/lambda/destinations.ts +0 -345
  102. package/src/lambda/dlq.ts +0 -425
  103. package/src/lambda/index.ts +0 -11
  104. package/src/lambda/lambda.test.ts +0 -840
  105. package/src/lambda/layers.ts +0 -263
  106. package/src/lambda/versions.ts +0 -376
  107. package/src/lambda/vpc.ts +0 -399
  108. package/src/local/config.ts +0 -114
  109. package/src/local/index.ts +0 -6
  110. package/src/local/mock-aws.ts +0 -351
  111. package/src/modules/ai.ts +0 -340
  112. package/src/modules/api.ts +0 -478
  113. package/src/modules/auth.ts +0 -805
  114. package/src/modules/cache.ts +0 -417
  115. package/src/modules/cdn.ts +0 -1062
  116. package/src/modules/communication.ts +0 -1094
  117. package/src/modules/compute.ts +0 -3348
  118. package/src/modules/database.ts +0 -554
  119. package/src/modules/deployment.ts +0 -1079
  120. package/src/modules/dns.ts +0 -337
  121. package/src/modules/email.ts +0 -1538
  122. package/src/modules/filesystem.ts +0 -515
  123. package/src/modules/index.ts +0 -32
  124. package/src/modules/messaging.ts +0 -486
  125. package/src/modules/monitoring.ts +0 -2086
  126. package/src/modules/network.ts +0 -664
  127. package/src/modules/parameter-store.ts +0 -325
  128. package/src/modules/permissions.ts +0 -1081
  129. package/src/modules/phone.ts +0 -494
  130. package/src/modules/queue.ts +0 -1260
  131. package/src/modules/redirects.ts +0 -464
  132. package/src/modules/registry.ts +0 -699
  133. package/src/modules/search.ts +0 -401
  134. package/src/modules/secrets.ts +0 -416
  135. package/src/modules/security.ts +0 -731
  136. package/src/modules/sms.ts +0 -389
  137. package/src/modules/storage.ts +0 -1120
  138. package/src/modules/workflow.ts +0 -680
  139. package/src/multi-account/config.ts +0 -521
  140. package/src/multi-account/index.ts +0 -7
  141. package/src/multi-account/manager.ts +0 -427
  142. package/src/multi-region/cross-region.ts +0 -410
  143. package/src/multi-region/index.ts +0 -8
  144. package/src/multi-region/manager.ts +0 -483
  145. package/src/multi-region/regions.ts +0 -435
  146. package/src/network-security/index.ts +0 -48
  147. package/src/observability/index.ts +0 -9
  148. package/src/observability/logs.ts +0 -522
  149. package/src/observability/metrics.ts +0 -460
  150. package/src/observability/observability.test.ts +0 -782
  151. package/src/observability/synthetics.ts +0 -568
  152. package/src/observability/xray.ts +0 -358
  153. package/src/phone/advanced/analytics.ts +0 -349
  154. package/src/phone/advanced/callbacks.ts +0 -428
  155. package/src/phone/advanced/index.ts +0 -8
  156. package/src/phone/advanced/ivr-builder.ts +0 -504
  157. package/src/phone/advanced/recording.ts +0 -310
  158. package/src/phone/handlers/__tests__/incoming-call.test.ts +0 -40
  159. package/src/phone/handlers/incoming-call.ts +0 -117
  160. package/src/phone/handlers/missed-call.ts +0 -116
  161. package/src/phone/handlers/voicemail.ts +0 -179
  162. package/src/phone/index.ts +0 -9
  163. package/src/presets/api-backend.ts +0 -134
  164. package/src/presets/data-pipeline.ts +0 -204
  165. package/src/presets/extend.test.ts +0 -295
  166. package/src/presets/extend.ts +0 -297
  167. package/src/presets/fullstack-app.ts +0 -144
  168. package/src/presets/index.ts +0 -27
  169. package/src/presets/jamstack.ts +0 -135
  170. package/src/presets/microservices.ts +0 -167
  171. package/src/presets/ml-api.ts +0 -208
  172. package/src/presets/nodejs-server.ts +0 -104
  173. package/src/presets/nodejs-serverless.ts +0 -114
  174. package/src/presets/realtime-app.ts +0 -184
  175. package/src/presets/static-site.ts +0 -64
  176. package/src/presets/traditional-web-app.ts +0 -339
  177. package/src/presets/wordpress.ts +0 -138
  178. package/src/preview/github.test.ts +0 -249
  179. package/src/preview/github.ts +0 -297
  180. package/src/preview/index.ts +0 -37
  181. package/src/preview/manager.test.ts +0 -440
  182. package/src/preview/manager.ts +0 -326
  183. package/src/preview/notifications.test.ts +0 -582
  184. package/src/preview/notifications.ts +0 -341
  185. package/src/queue/batch-processing.ts +0 -402
  186. package/src/queue/dlq-monitoring.ts +0 -402
  187. package/src/queue/fifo.ts +0 -342
  188. package/src/queue/index.ts +0 -9
  189. package/src/queue/management.ts +0 -428
  190. package/src/queue/queue.test.ts +0 -429
  191. package/src/resource-mgmt/index.ts +0 -39
  192. package/src/resource-naming.ts +0 -62
  193. package/src/s3/index.ts +0 -523
  194. package/src/schema/cloud-config.schema.json +0 -554
  195. package/src/schema/index.ts +0 -68
  196. package/src/security/certificate-manager.ts +0 -492
  197. package/src/security/index.ts +0 -9
  198. package/src/security/scanning.ts +0 -545
  199. package/src/security/secrets-manager.ts +0 -476
  200. package/src/security/secrets-rotation.ts +0 -456
  201. package/src/security/security.test.ts +0 -738
  202. package/src/sms/advanced/ab-testing.ts +0 -389
  203. package/src/sms/advanced/analytics.ts +0 -336
  204. package/src/sms/advanced/campaigns.ts +0 -523
  205. package/src/sms/advanced/chatbot.ts +0 -224
  206. package/src/sms/advanced/index.ts +0 -10
  207. package/src/sms/advanced/link-tracking.ts +0 -248
  208. package/src/sms/advanced/mms.ts +0 -308
  209. package/src/sms/handlers/__tests__/send.test.ts +0 -40
  210. package/src/sms/handlers/delivery-status.ts +0 -133
  211. package/src/sms/handlers/receive.ts +0 -162
  212. package/src/sms/handlers/send.ts +0 -174
  213. package/src/sms/index.ts +0 -9
  214. package/src/stack-diff.ts +0 -389
  215. package/src/static-site/index.ts +0 -85
  216. package/src/template-builder.ts +0 -110
  217. package/src/template-validator.ts +0 -574
  218. package/src/utils/cache.ts +0 -291
  219. package/src/utils/diff.ts +0 -269
  220. package/src/utils/hash.ts +0 -227
  221. package/src/utils/index.ts +0 -8
  222. package/src/utils/parallel.ts +0 -294
  223. package/src/validators/credentials.test.ts +0 -274
  224. package/src/validators/credentials.ts +0 -233
  225. package/src/validators/quotas.test.ts +0 -434
  226. package/src/validators/quotas.ts +0 -217
  227. package/test/ai.test.ts +0 -327
  228. package/test/api.test.ts +0 -511
  229. package/test/auth.test.ts +0 -632
  230. package/test/cache.test.ts +0 -406
  231. package/test/cdn.test.ts +0 -247
  232. package/test/compute.test.ts +0 -861
  233. package/test/database.test.ts +0 -523
  234. package/test/deployment.test.ts +0 -499
  235. package/test/dns.test.ts +0 -270
  236. package/test/email.test.ts +0 -439
  237. package/test/filesystem.test.ts +0 -382
  238. package/test/integration.test.ts +0 -350
  239. package/test/messaging.test.ts +0 -514
  240. package/test/monitoring.test.ts +0 -634
  241. package/test/network.test.ts +0 -425
  242. package/test/permissions.test.ts +0 -488
  243. package/test/queue.test.ts +0 -484
  244. package/test/registry.test.ts +0 -306
  245. package/test/security.test.ts +0 -462
  246. package/test/storage.test.ts +0 -463
  247. package/test/template-validator.test.ts +0 -559
  248. package/test/workflow.test.ts +0 -592
  249. package/tsconfig.json +0 -16
  250. package/tsconfig.tsbuildinfo +0 -1
@@ -1,1155 +0,0 @@
1
- /**
2
- * AWS Signature Version 4 Signing Process
3
- * Implements request signing for direct AWS API calls without SDK
4
- *
5
- * Reference: https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
6
- *
7
- * Browser compatible: Use async functions (signRequestAsync, createPresignedUrlAsync)
8
- * Node.js/Bun: Use sync functions for better performance (signRequest, createPresignedUrl)
9
- */
10
-
11
- // Conditional import for Node.js crypto - will be undefined in browser
12
- let nodeCrypto: typeof import('node:crypto') | undefined
13
- try {
14
- nodeCrypto = await import('node:crypto')
15
- } catch {
16
- // Running in browser - nodeCrypto stays undefined
17
- }
18
-
19
- /**
20
- * Signing key cache for improved performance on repeated requests
21
- * Keys are cached by: secretAccessKey + date + region + service
22
- * Supports both Buffer (Node.js) and Uint8Array (browser)
23
- */
24
- const signingKeyCache = new Map<string, Buffer | Uint8Array>()
25
- const MAX_CACHE_SIZE = 100
26
-
27
- /**
28
- * Service name mappings for hosts that don't follow standard naming
29
- */
30
- const HOST_SERVICES: Record<string, string> = {
31
- 'appstream2': 'appstream',
32
- 'cloudhsmv2': 'cloudhsm',
33
- 'email': 'ses',
34
- 'marketplace': 'aws-marketplace',
35
- 'mobile': 'AWSMobileHubService',
36
- 'pinpoint': 'mobiletargeting',
37
- 'queue': 'sqs',
38
- 'git-codecommit': 'codecommit',
39
- 'mturk-requester-sandbox': 'mturk-requester',
40
- 'personalize-runtime': 'personalize',
41
- }
42
-
43
- export interface SignatureOptions {
44
- method: string
45
- url: string
46
- /** Service name - if not provided, will be auto-detected from URL */
47
- service?: string
48
- /** Region - if not provided, will be auto-detected from URL */
49
- region?: string
50
- headers?: Record<string, string>
51
- body?: string
52
- accessKeyId: string
53
- secretAccessKey: string
54
- sessionToken?: string
55
- /**
56
- * Optional external cache for signing keys
57
- * If not provided, uses internal cache
58
- * Supports both Buffer (Node.js) and Uint8Array (browser)
59
- */
60
- cache?: Map<string, Buffer | Uint8Array>
61
- /**
62
- * Sign via query string instead of Authorization header
63
- * Used for presigned URLs (e.g., S3 presigned URLs)
64
- */
65
- signQuery?: boolean
66
- /**
67
- * Expiration time in seconds for presigned URLs (default: 86400 = 24 hours)
68
- * Only used when signQuery is true
69
- */
70
- expiresIn?: number
71
- /**
72
- * Custom datetime for signing (format: YYYYMMDDTHHMMSSZ)
73
- * If not provided, uses current time
74
- * Useful for testing and reproducibility
75
- */
76
- datetime?: string
77
- }
78
-
79
- export interface SignedRequest {
80
- url: string
81
- method: string
82
- headers: Record<string, string>
83
- body?: string
84
- }
85
-
86
- export interface PresignedUrlOptions {
87
- url: string
88
- method?: string
89
- accessKeyId: string
90
- secretAccessKey: string
91
- sessionToken?: string
92
- /** Service name - if not provided, will be auto-detected from URL */
93
- service?: string
94
- /** Region - if not provided, will be auto-detected from URL */
95
- region?: string
96
- /** Expiration time in seconds (default: 3600 = 1 hour, max: 604800 = 7 days) */
97
- expiresIn?: number
98
- /** Optional external cache for signing keys */
99
- cache?: Map<string, Buffer | Uint8Array>
100
- }
101
-
102
- export interface RetryOptions {
103
- /** Maximum number of retry attempts (default: 3) */
104
- maxRetries?: number
105
- /** Initial delay in ms before first retry (default: 100) */
106
- initialDelayMs?: number
107
- /** Maximum delay in ms between retries (default: 5000) */
108
- maxDelayMs?: number
109
- /** HTTP status codes that should trigger a retry (default: [429, 500, 502, 503, 504]) */
110
- retryableStatusCodes?: number[]
111
- /** Request timeout in milliseconds (default: 30000 = 30 seconds) */
112
- timeoutMs?: number
113
- }
114
-
115
- /**
116
- * Detect service and region from AWS URL
117
- * Supports standard AWS endpoints, Lambda URLs, R2, and Backblaze B2
118
- */
119
- export function detectServiceRegion(url: string | URL): { service: string, region: string } {
120
- const urlObj = typeof url === 'string' ? new URL(url) : url
121
- const { hostname, pathname } = urlObj
122
-
123
- // Lambda function URLs: xxx.lambda-url.region.on.aws
124
- if (hostname.endsWith('.on.aws')) {
125
- const match = hostname.match(/^[^.]+\.lambda-url\.([^.]+)\.on\.aws$/)
126
- if (match) {
127
- return { service: 'lambda', region: match[1] }
128
- }
129
- return { service: '', region: '' }
130
- }
131
-
132
- // Cloudflare R2: xxx.r2.cloudflarestorage.com
133
- if (hostname.endsWith('.r2.cloudflarestorage.com')) {
134
- return { service: 's3', region: 'auto' }
135
- }
136
-
137
- // Backblaze B2: xxx.s3.region.backblazeb2.com
138
- if (hostname.endsWith('.backblazeb2.com')) {
139
- const match = hostname.match(/^(?:[^.]+\.)?s3\.([^.]+)\.backblazeb2\.com$/)
140
- if (match) {
141
- return { service: 's3', region: match[1] }
142
- }
143
- return { service: '', region: '' }
144
- }
145
-
146
- // Standard AWS endpoints: service.region.amazonaws.com
147
- const match = hostname
148
- .replace('dualstack.', '')
149
- .match(/([^.]+)\.(?:([^.]+)\.)?amazonaws\.com(?:\.cn)?$/)
150
-
151
- if (!match) {
152
- return { service: '', region: '' }
153
- }
154
-
155
- let service = match[1]
156
- let region = match[2] || ''
157
-
158
- // Handle special cases
159
- if (region === 'us-gov') {
160
- region = 'us-gov-west-1'
161
- } else if (region === 's3' || region === 's3-accelerate') {
162
- region = 'us-east-1'
163
- service = 's3'
164
- } else if (service === 'iot') {
165
- if (hostname.startsWith('iot.')) {
166
- service = 'execute-api'
167
- } else if (hostname.startsWith('data.jobs.iot.')) {
168
- service = 'iot-jobs-data'
169
- } else {
170
- service = pathname === '/mqtt' ? 'iotdevicegateway' : 'iotdata'
171
- }
172
- } else if (service === 'autoscaling') {
173
- // Could be application-autoscaling or autoscaling-plans based on target
174
- // Default to autoscaling
175
- } else if (!region && service.startsWith('s3-')) {
176
- region = service.slice(3).replace(/^fips-|^external-1/, '')
177
- service = 's3'
178
- } else if (service.endsWith('-fips')) {
179
- service = service.slice(0, -5)
180
- } else if (region && /-\d$/.test(service) && !/-\d$/.test(region)) {
181
- // Swap service and region if they appear reversed
182
- [service, region] = [region, service]
183
- }
184
-
185
- // Apply service name mappings
186
- service = HOST_SERVICES[service] || service
187
-
188
- return { service, region: region || 'us-east-1' }
189
- }
190
-
191
- /**
192
- * Sign an AWS request using Signature Version 4
193
- */
194
- export function signRequest(options: SignatureOptions): SignedRequest {
195
- const {
196
- method,
197
- url,
198
- body = '',
199
- accessKeyId,
200
- secretAccessKey,
201
- sessionToken,
202
- signQuery = false,
203
- expiresIn = 86400,
204
- datetime,
205
- } = options
206
-
207
- const urlObj = new URL(url)
208
- const host = urlObj.hostname
209
-
210
- // Auto-detect service and region if not provided
211
- const detected = detectServiceRegion(urlObj)
212
- const service = options.service || detected.service
213
- const region = options.region || detected.region
214
-
215
- if (!service) {
216
- throw new Error('Could not detect service from URL. Please provide service explicitly.')
217
- }
218
- if (!region) {
219
- throw new Error('Could not detect region from URL. Please provide region explicitly.')
220
- }
221
-
222
- // Step 1: Create canonical request
223
- const timestamp = datetime || new Date().toISOString().replace(/[:-]|\.\d{3}/g, '')
224
- const date = timestamp.substring(0, 8)
225
- const credentialScope = [date, region, service, 'aws4_request'].join('/')
226
- const algorithm = 'AWS4-HMAC-SHA256'
227
-
228
- if (signQuery) {
229
- // Query string signing (for presigned URLs)
230
- return signWithQueryString({
231
- urlObj,
232
- method,
233
- body,
234
- accessKeyId,
235
- secretAccessKey,
236
- sessionToken,
237
- service,
238
- region,
239
- timestamp,
240
- date,
241
- credentialScope,
242
- algorithm,
243
- expiresIn,
244
- cache: options.cache,
245
- })
246
- }
247
-
248
- // Header-based signing
249
- const path = urlObj.pathname || '/'
250
- const query = canonicalQueryString(urlObj.searchParams)
251
-
252
- const headers: Record<string, string> = {
253
- 'host': host,
254
- 'x-amz-date': timestamp,
255
- ...options.headers,
256
- }
257
-
258
- if (sessionToken) {
259
- headers['x-amz-security-token'] = sessionToken
260
- }
261
-
262
- // Add content-type for requests with body
263
- if (body && !headers['content-type']) {
264
- headers['content-type'] = 'application/x-amz-json-1.0'
265
- }
266
-
267
- // For S3, add content hash header
268
- if (service === 's3' && !headers['x-amz-content-sha256']) {
269
- headers['x-amz-content-sha256'] = hash(body)
270
- }
271
-
272
- const canonicalHeaders = getCanonicalHeaders(headers)
273
- const signedHeaders = getSignedHeaders(headers)
274
- const payloadHash = headers['x-amz-content-sha256'] || hash(body)
275
-
276
- const canonicalRequest = [
277
- method,
278
- encodePath(path),
279
- query,
280
- canonicalHeaders,
281
- signedHeaders,
282
- payloadHash,
283
- ].join('\n')
284
-
285
- // Step 2: Create string to sign
286
- const canonicalRequestHash = hash(canonicalRequest)
287
-
288
- const stringToSign = [
289
- algorithm,
290
- timestamp,
291
- credentialScope,
292
- canonicalRequestHash,
293
- ].join('\n')
294
-
295
- // Step 3: Calculate signature (with key caching for performance)
296
- const cache = options.cache ?? signingKeyCache
297
- const signature = calculateSignature(
298
- secretAccessKey,
299
- date,
300
- region,
301
- service,
302
- stringToSign,
303
- cache,
304
- )
305
-
306
- // Step 4: Add authorization header
307
- const authorization = [
308
- `${algorithm} Credential=${accessKeyId}/${credentialScope}`,
309
- `SignedHeaders=${signedHeaders}`,
310
- `Signature=${signature}`,
311
- ].join(', ')
312
-
313
- headers['authorization'] = authorization
314
-
315
- return {
316
- url,
317
- method,
318
- headers,
319
- body: body || undefined,
320
- }
321
- }
322
-
323
- /**
324
- * Sign an AWS request using Signature Version 4 (async - browser compatible)
325
- * Use this in browser environments where crypto.subtle is available
326
- */
327
- export async function signRequestAsync(options: SignatureOptions): Promise<SignedRequest> {
328
- const {
329
- method,
330
- url,
331
- body = '',
332
- accessKeyId,
333
- secretAccessKey,
334
- sessionToken,
335
- signQuery = false,
336
- expiresIn = 86400,
337
- datetime,
338
- } = options
339
-
340
- const urlObj = new URL(url)
341
- const host = urlObj.hostname
342
-
343
- // Auto-detect service and region if not provided
344
- const detected = detectServiceRegion(urlObj)
345
- const service = options.service || detected.service
346
- const region = options.region || detected.region
347
-
348
- if (!service) {
349
- throw new Error('Could not detect service from URL. Please provide service explicitly.')
350
- }
351
- if (!region) {
352
- throw new Error('Could not detect region from URL. Please provide region explicitly.')
353
- }
354
-
355
- // Step 1: Create canonical request
356
- const timestamp = datetime || new Date().toISOString().replace(/[:-]|\.\d{3}/g, '')
357
- const date = timestamp.substring(0, 8)
358
- const credentialScope = [date, region, service, 'aws4_request'].join('/')
359
- const algorithm = 'AWS4-HMAC-SHA256'
360
-
361
- if (signQuery) {
362
- // Query string signing (for presigned URLs)
363
- return signWithQueryStringAsync({
364
- urlObj,
365
- method,
366
- body,
367
- accessKeyId,
368
- secretAccessKey,
369
- sessionToken,
370
- service,
371
- region,
372
- timestamp,
373
- date,
374
- credentialScope,
375
- algorithm,
376
- expiresIn,
377
- cache: options.cache,
378
- })
379
- }
380
-
381
- // Header-based signing
382
- const path = urlObj.pathname || '/'
383
- const query = canonicalQueryString(urlObj.searchParams)
384
-
385
- const headers: Record<string, string> = {
386
- 'host': host,
387
- 'x-amz-date': timestamp,
388
- ...options.headers,
389
- }
390
-
391
- if (sessionToken) {
392
- headers['x-amz-security-token'] = sessionToken
393
- }
394
-
395
- // Add content-type for requests with body
396
- if (body && !headers['content-type']) {
397
- headers['content-type'] = 'application/x-amz-json-1.0'
398
- }
399
-
400
- // For S3, add content hash header
401
- const bodyHash = await hashAsync(body)
402
- if (service === 's3' && !headers['x-amz-content-sha256']) {
403
- headers['x-amz-content-sha256'] = bodyHash
404
- }
405
-
406
- const canonicalHeaders = getCanonicalHeaders(headers)
407
- const signedHeaders = getSignedHeaders(headers)
408
- const payloadHash = headers['x-amz-content-sha256'] || bodyHash
409
-
410
- const canonicalRequest = [
411
- method,
412
- encodePath(path),
413
- query,
414
- canonicalHeaders,
415
- signedHeaders,
416
- payloadHash,
417
- ].join('\n')
418
-
419
- // Step 2: Create string to sign
420
- const canonicalRequestHash = await hashAsync(canonicalRequest)
421
-
422
- const stringToSign = [
423
- algorithm,
424
- timestamp,
425
- credentialScope,
426
- canonicalRequestHash,
427
- ].join('\n')
428
-
429
- // Step 3: Calculate signature (with key caching for performance)
430
- const cache = options.cache ?? signingKeyCache
431
- const signature = await calculateSignatureAsync(
432
- secretAccessKey,
433
- date,
434
- region,
435
- service,
436
- stringToSign,
437
- cache,
438
- )
439
-
440
- // Step 4: Add authorization header
441
- const authorization = [
442
- `${algorithm} Credential=${accessKeyId}/${credentialScope}`,
443
- `SignedHeaders=${signedHeaders}`,
444
- `Signature=${signature}`,
445
- ].join(', ')
446
-
447
- headers['authorization'] = authorization
448
-
449
- return {
450
- url,
451
- method,
452
- headers,
453
- body: body || undefined,
454
- }
455
- }
456
-
457
- /**
458
- * Sign request using query string parameters (for presigned URLs)
459
- */
460
- function signWithQueryString(params: {
461
- urlObj: URL
462
- method: string
463
- body: string
464
- accessKeyId: string
465
- secretAccessKey: string
466
- sessionToken?: string
467
- service: string
468
- region: string
469
- timestamp: string
470
- date: string
471
- credentialScope: string
472
- algorithm: string
473
- expiresIn: number
474
- cache?: Map<string, Buffer | Uint8Array>
475
- }): SignedRequest {
476
- const {
477
- urlObj,
478
- method,
479
- body,
480
- accessKeyId,
481
- secretAccessKey,
482
- sessionToken,
483
- service,
484
- region,
485
- timestamp,
486
- date,
487
- credentialScope,
488
- algorithm,
489
- expiresIn,
490
- cache,
491
- } = params
492
-
493
- // Clone URL to avoid modifying original
494
- const signedUrl = new URL(urlObj.toString())
495
-
496
- // Add required query parameters
497
- signedUrl.searchParams.set('X-Amz-Algorithm', algorithm)
498
- signedUrl.searchParams.set('X-Amz-Credential', `${accessKeyId}/${credentialScope}`)
499
- signedUrl.searchParams.set('X-Amz-Date', timestamp)
500
- signedUrl.searchParams.set('X-Amz-Expires', String(expiresIn))
501
-
502
- // For S3, default to UNSIGNED-PAYLOAD
503
- const payloadHash = service === 's3' ? 'UNSIGNED-PAYLOAD' : hash(body)
504
- if (service === 's3') {
505
- signedUrl.searchParams.set('X-Amz-Content-Sha256', payloadHash)
506
- }
507
-
508
- // Signed headers (only host for query string signing)
509
- const signedHeaders = 'host'
510
- signedUrl.searchParams.set('X-Amz-SignedHeaders', signedHeaders)
511
-
512
- if (sessionToken) {
513
- signedUrl.searchParams.set('X-Amz-Security-Token', sessionToken)
514
- }
515
-
516
- // Build canonical request
517
- const path = encodePath(signedUrl.pathname || '/')
518
- const canonicalHeaders = `host:${signedUrl.hostname}\n`
519
- const query = canonicalQueryString(signedUrl.searchParams)
520
-
521
- const canonicalRequest = [
522
- method,
523
- path,
524
- query,
525
- canonicalHeaders,
526
- signedHeaders,
527
- payloadHash,
528
- ].join('\n')
529
-
530
- // Create string to sign
531
- const stringToSign = [
532
- algorithm,
533
- timestamp,
534
- credentialScope,
535
- hash(canonicalRequest),
536
- ].join('\n')
537
-
538
- // Calculate signature
539
- const signingCache = cache ?? signingKeyCache
540
- const signature = calculateSignature(
541
- secretAccessKey,
542
- date,
543
- region,
544
- service,
545
- stringToSign,
546
- signingCache,
547
- )
548
-
549
- // Add signature to URL
550
- signedUrl.searchParams.set('X-Amz-Signature', signature)
551
-
552
- return {
553
- url: signedUrl.toString(),
554
- method,
555
- headers: {},
556
- body: body || undefined,
557
- }
558
- }
559
-
560
- /**
561
- * Sign request using query string parameters (async - browser compatible)
562
- */
563
- async function signWithQueryStringAsync(params: {
564
- urlObj: URL
565
- method: string
566
- body: string
567
- accessKeyId: string
568
- secretAccessKey: string
569
- sessionToken?: string
570
- service: string
571
- region: string
572
- timestamp: string
573
- date: string
574
- credentialScope: string
575
- algorithm: string
576
- expiresIn: number
577
- cache?: Map<string, Buffer | Uint8Array>
578
- }): Promise<SignedRequest> {
579
- const {
580
- urlObj,
581
- method,
582
- body,
583
- accessKeyId,
584
- secretAccessKey,
585
- sessionToken,
586
- service,
587
- region,
588
- timestamp,
589
- date,
590
- credentialScope,
591
- algorithm,
592
- expiresIn,
593
- cache,
594
- } = params
595
-
596
- // Clone URL to avoid modifying original
597
- const signedUrl = new URL(urlObj.toString())
598
-
599
- // Add required query parameters
600
- signedUrl.searchParams.set('X-Amz-Algorithm', algorithm)
601
- signedUrl.searchParams.set('X-Amz-Credential', `${accessKeyId}/${credentialScope}`)
602
- signedUrl.searchParams.set('X-Amz-Date', timestamp)
603
- signedUrl.searchParams.set('X-Amz-Expires', String(expiresIn))
604
-
605
- // For S3, default to UNSIGNED-PAYLOAD
606
- const payloadHash = service === 's3' ? 'UNSIGNED-PAYLOAD' : await hashAsync(body)
607
- if (service === 's3') {
608
- signedUrl.searchParams.set('X-Amz-Content-Sha256', payloadHash)
609
- }
610
-
611
- // Signed headers (only host for query string signing)
612
- const signedHeaders = 'host'
613
- signedUrl.searchParams.set('X-Amz-SignedHeaders', signedHeaders)
614
-
615
- if (sessionToken) {
616
- signedUrl.searchParams.set('X-Amz-Security-Token', sessionToken)
617
- }
618
-
619
- // Build canonical request
620
- const path = encodePath(signedUrl.pathname || '/')
621
- const canonicalHeaders = `host:${signedUrl.hostname}\n`
622
- const query = canonicalQueryString(signedUrl.searchParams)
623
-
624
- const canonicalRequest = [
625
- method,
626
- path,
627
- query,
628
- canonicalHeaders,
629
- signedHeaders,
630
- payloadHash,
631
- ].join('\n')
632
-
633
- // Create string to sign
634
- const stringToSign = [
635
- algorithm,
636
- timestamp,
637
- credentialScope,
638
- await hashAsync(canonicalRequest),
639
- ].join('\n')
640
-
641
- // Calculate signature
642
- const signingCache = cache ?? signingKeyCache
643
- const signature = await calculateSignatureAsync(
644
- secretAccessKey,
645
- date,
646
- region,
647
- service,
648
- stringToSign,
649
- signingCache,
650
- )
651
-
652
- // Add signature to URL
653
- signedUrl.searchParams.set('X-Amz-Signature', signature)
654
-
655
- return {
656
- url: signedUrl.toString(),
657
- method,
658
- headers: {},
659
- body: body || undefined,
660
- }
661
- }
662
-
663
- /**
664
- * Generate a presigned URL for AWS requests (e.g., S3 GetObject, PutObject)
665
- */
666
- export function createPresignedUrl(options: PresignedUrlOptions): string {
667
- const {
668
- url,
669
- method = 'GET',
670
- expiresIn = 3600,
671
- ...rest
672
- } = options
673
-
674
- // Max expiration is 7 days for most services
675
- const clampedExpires = Math.min(expiresIn, 604800)
676
-
677
- const signed = signRequest({
678
- ...rest,
679
- url,
680
- method,
681
- signQuery: true,
682
- expiresIn: clampedExpires,
683
- })
684
-
685
- return signed.url
686
- }
687
-
688
- /**
689
- * Generate a presigned URL for AWS requests (async - browser compatible)
690
- */
691
- export async function createPresignedUrlAsync(options: PresignedUrlOptions): Promise<string> {
692
- const {
693
- url,
694
- method = 'GET',
695
- expiresIn = 3600,
696
- ...rest
697
- } = options
698
-
699
- // Max expiration is 7 days for most services
700
- const clampedExpires = Math.min(expiresIn, 604800)
701
-
702
- const signed = await signRequestAsync({
703
- ...rest,
704
- url,
705
- method,
706
- signQuery: true,
707
- expiresIn: clampedExpires,
708
- })
709
-
710
- return signed.url
711
- }
712
-
713
- /**
714
- * Create canonical headers string
715
- */
716
- function getCanonicalHeaders(headers: Record<string, string>): string {
717
- return Object.keys(headers)
718
- .sort()
719
- .map(key => `${key.toLowerCase()}:${headers[key].trim().replace(/\s+/g, ' ')}`)
720
- .join('\n') + '\n'
721
- }
722
-
723
- /**
724
- * Get signed headers string
725
- */
726
- function getSignedHeaders(headers: Record<string, string>): string {
727
- return Object.keys(headers)
728
- .sort()
729
- .map(key => key.toLowerCase())
730
- .join(';')
731
- }
732
-
733
- /**
734
- * Create canonical query string (sorted and encoded)
735
- */
736
- function canonicalQueryString(params: URLSearchParams): string {
737
- const sorted: Array<[string, string]> = []
738
-
739
- params.forEach((value, key) => {
740
- sorted.push([encodeRfc3986(key), encodeRfc3986(value)])
741
- })
742
-
743
- sorted.sort((a, b) => {
744
- if (a[0] < b[0]) return -1
745
- if (a[0] > b[0]) return 1
746
- if (a[1] < b[1]) return -1
747
- if (a[1] > b[1]) return 1
748
- return 0
749
- })
750
-
751
- return sorted.map(([k, v]) => `${k}=${v}`).join('&')
752
- }
753
-
754
- /**
755
- * Encode path for canonical request
756
- */
757
- function encodePath(path: string): string {
758
- return path
759
- .split('/')
760
- .map(segment => encodeRfc3986(segment))
761
- .join('/')
762
- }
763
-
764
- /**
765
- * RFC 3986 URI encoding (stricter than encodeURIComponent)
766
- */
767
- function encodeRfc3986(str: string): string {
768
- return encodeURIComponent(str).replace(/[!'()*]/g, c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)
769
- }
770
-
771
- /**
772
- * Calculate SHA256 hash (synchronous - Node.js/Bun only)
773
- */
774
- function hash(data: string): string {
775
- if (!nodeCrypto) {
776
- throw new Error('Synchronous hash not available in browser. Use signRequestAsync() instead.')
777
- }
778
- return nodeCrypto.createHash('sha256').update(data, 'utf8').digest('hex')
779
- }
780
-
781
- /**
782
- * Calculate HMAC SHA256 (synchronous - Node.js/Bun only)
783
- */
784
- function hmac(key: Buffer | string, data: string): Buffer {
785
- if (!nodeCrypto) {
786
- throw new Error('Synchronous hmac not available in browser. Use signRequestAsync() instead.')
787
- }
788
- return nodeCrypto.createHmac('sha256', key).update(data, 'utf8').digest()
789
- }
790
-
791
- /**
792
- * Calculate SHA256 hash (async - browser compatible)
793
- */
794
- async function hashAsync(data: string): Promise<string> {
795
- const encoder = new TextEncoder()
796
- const dataBuffer = encoder.encode(data)
797
- const hashBuffer = await crypto.subtle.digest('SHA-256', dataBuffer)
798
- return bufferToHex(new Uint8Array(hashBuffer))
799
- }
800
-
801
- /**
802
- * Calculate HMAC SHA256 (async - browser compatible)
803
- */
804
- async function hmacAsync(key: Uint8Array | string, data: string): Promise<Uint8Array> {
805
- const encoder = new TextEncoder()
806
- const keyBuffer = typeof key === 'string' ? encoder.encode(key) : key
807
- const dataBuffer = encoder.encode(data)
808
-
809
- const cryptoKey = await crypto.subtle.importKey(
810
- 'raw',
811
- keyBuffer.buffer as ArrayBuffer,
812
- { name: 'HMAC', hash: 'SHA-256' },
813
- false,
814
- ['sign'],
815
- )
816
-
817
- const signature = await crypto.subtle.sign('HMAC', cryptoKey, dataBuffer.buffer)
818
- return new Uint8Array(signature)
819
- }
820
-
821
- /**
822
- * Convert Uint8Array to hex string
823
- */
824
- function bufferToHex(buffer: Uint8Array): string {
825
- return Array.from(buffer)
826
- .map(b => b.toString(16).padStart(2, '0'))
827
- .join('')
828
- }
829
-
830
- /**
831
- * Calculate signature using AWS signing key derivation (sync - Node.js/Bun only)
832
- * Uses caching for signing keys to improve performance on repeated requests
833
- */
834
- function calculateSignature(
835
- secretAccessKey: string,
836
- date: string,
837
- region: string,
838
- service: string,
839
- stringToSign: string,
840
- cache: Map<string, Buffer | Uint8Array>,
841
- ): string {
842
- // Create cache key from signing parameters
843
- const cacheKey = `${secretAccessKey}:${date}:${region}:${service}`
844
-
845
- let kSigning = cache.get(cacheKey) as Buffer | undefined
846
-
847
- if (!kSigning) {
848
- // Derive signing key (expensive operation)
849
- const kDate = hmac(`AWS4${secretAccessKey}`, date)
850
- const kRegion = hmac(kDate, region)
851
- const kService = hmac(kRegion, service)
852
- kSigning = hmac(kService, 'aws4_request')
853
-
854
- // Limit cache size to prevent memory leaks
855
- if (cache.size >= MAX_CACHE_SIZE) {
856
- // Remove oldest entry (first key)
857
- const firstKey = cache.keys().next().value
858
- if (firstKey)
859
- cache.delete(firstKey)
860
- }
861
-
862
- cache.set(cacheKey, kSigning)
863
- }
864
-
865
- return hmac(kSigning, stringToSign).toString('hex')
866
- }
867
-
868
- /**
869
- * Calculate signature using AWS signing key derivation (async - browser compatible)
870
- * Uses caching for signing keys to improve performance on repeated requests
871
- */
872
- async function calculateSignatureAsync(
873
- secretAccessKey: string,
874
- date: string,
875
- region: string,
876
- service: string,
877
- stringToSign: string,
878
- cache: Map<string, Buffer | Uint8Array>,
879
- ): Promise<string> {
880
- // Create cache key from signing parameters
881
- const cacheKey = `${secretAccessKey}:${date}:${region}:${service}`
882
-
883
- let kSigning = cache.get(cacheKey) as Uint8Array | undefined
884
-
885
- if (!kSigning) {
886
- // Derive signing key (expensive operation)
887
- const kDate = await hmacAsync(`AWS4${secretAccessKey}`, date)
888
- const kRegion = await hmacAsync(kDate, region)
889
- const kService = await hmacAsync(kRegion, service)
890
- kSigning = await hmacAsync(kService, 'aws4_request')
891
-
892
- // Limit cache size to prevent memory leaks
893
- if (cache.size >= MAX_CACHE_SIZE) {
894
- // Remove oldest entry (first key)
895
- const firstKey = cache.keys().next().value
896
- if (firstKey)
897
- cache.delete(firstKey)
898
- }
899
-
900
- cache.set(cacheKey, kSigning)
901
- }
902
-
903
- const signature = await hmacAsync(kSigning, stringToSign)
904
- return bufferToHex(signature)
905
- }
906
-
907
- /**
908
- * Check if an error/status code is retryable
909
- */
910
- function isRetryable(status: number, retryableCodes: number[]): boolean {
911
- return retryableCodes.includes(status)
912
- }
913
-
914
- /**
915
- * Calculate delay with exponential backoff and jitter
916
- */
917
- function calculateBackoff(attempt: number, initialDelayMs: number, maxDelayMs: number): number {
918
- const exponentialDelay = initialDelayMs * Math.pow(2, attempt)
919
- const jitter = Math.random() * 0.3 * exponentialDelay // 0-30% jitter
920
- return Math.min(exponentialDelay + jitter, maxDelayMs)
921
- }
922
-
923
- /**
924
- * Sleep for specified milliseconds
925
- */
926
- function sleep(ms: number): Promise<void> {
927
- return new Promise(resolve => setTimeout(resolve, ms))
928
- }
929
-
930
- /**
931
- * Make a signed AWS API request with automatic retry
932
- */
933
- export async function makeAWSRequest(
934
- options: SignatureOptions,
935
- retryOptions?: RetryOptions,
936
- ): Promise<Response> {
937
- const {
938
- maxRetries = 3,
939
- initialDelayMs = 100,
940
- maxDelayMs = 5000,
941
- retryableStatusCodes = [429, 500, 502, 503, 504],
942
- timeoutMs = 30000,
943
- } = retryOptions ?? {}
944
-
945
- let lastError: Error | undefined
946
- let lastResponse: Response | undefined
947
-
948
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
949
- // Re-sign request on each attempt (timestamp changes)
950
- const signedRequest = signRequest(options)
951
-
952
- const fetchOptions: RequestInit = {
953
- method: signedRequest.method,
954
- headers: signedRequest.headers,
955
- }
956
-
957
- if (signedRequest.body) {
958
- fetchOptions.body = signedRequest.body
959
- }
960
-
961
- // Add timeout support via AbortController
962
- const controller = new AbortController()
963
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs)
964
- fetchOptions.signal = controller.signal
965
-
966
- try {
967
- const response = await fetch(signedRequest.url, fetchOptions)
968
- clearTimeout(timeoutId)
969
- lastResponse = response
970
-
971
- // Success - return immediately
972
- if (response.ok) {
973
- return response
974
- }
975
-
976
- // Check if we should retry
977
- if (attempt < maxRetries && isRetryable(response.status, retryableStatusCodes)) {
978
- const delay = calculateBackoff(attempt, initialDelayMs, maxDelayMs)
979
- await sleep(delay)
980
- continue
981
- }
982
-
983
- // Non-retryable error or max retries reached
984
- const errorText = await response.text()
985
- throw new Error(`AWS API request failed (${response.status}): ${errorText}`)
986
- } catch (error) {
987
- clearTimeout(timeoutId)
988
- lastError = error as Error
989
-
990
- // Handle timeout errors
991
- if (error instanceof Error && error.name === 'AbortError') {
992
- lastError = new Error(`Request timed out after ${timeoutMs}ms`)
993
- }
994
-
995
- // Network errors and timeouts are retryable
996
- if (attempt < maxRetries && !(error instanceof Error && error.message.includes('AWS API request failed'))) {
997
- const delay = calculateBackoff(attempt, initialDelayMs, maxDelayMs)
998
- await sleep(delay)
999
- continue
1000
- }
1001
-
1002
- throw lastError
1003
- }
1004
- }
1005
-
1006
- // Should not reach here, but just in case
1007
- throw lastError ?? new Error('Request failed after retries')
1008
- }
1009
-
1010
- /**
1011
- * Make a signed AWS API request without retry (for backwards compatibility)
1012
- */
1013
- export async function makeAWSRequestOnce(
1014
- options: SignatureOptions,
1015
- ): Promise<Response> {
1016
- return makeAWSRequest(options, { maxRetries: 0 })
1017
- }
1018
-
1019
- /**
1020
- * Make a signed AWS API request with automatic retry (async - browser compatible)
1021
- * Use this in browser environments where crypto.subtle is available
1022
- */
1023
- export async function makeAWSRequestAsync(
1024
- options: SignatureOptions,
1025
- retryOptions?: RetryOptions,
1026
- ): Promise<Response> {
1027
- const {
1028
- maxRetries = 3,
1029
- initialDelayMs = 100,
1030
- maxDelayMs = 5000,
1031
- retryableStatusCodes = [429, 500, 502, 503, 504],
1032
- timeoutMs = 30000,
1033
- } = retryOptions ?? {}
1034
-
1035
- let lastError: Error | undefined
1036
-
1037
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
1038
- // Re-sign request on each attempt (timestamp changes)
1039
- const signedRequest = await signRequestAsync(options)
1040
-
1041
- const fetchOptions: RequestInit = {
1042
- method: signedRequest.method,
1043
- headers: signedRequest.headers,
1044
- }
1045
-
1046
- if (signedRequest.body) {
1047
- fetchOptions.body = signedRequest.body
1048
- }
1049
-
1050
- // Add timeout support via AbortController
1051
- const controller = new AbortController()
1052
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs)
1053
- fetchOptions.signal = controller.signal
1054
-
1055
- try {
1056
- const response = await fetch(signedRequest.url, fetchOptions)
1057
- clearTimeout(timeoutId)
1058
-
1059
- // Success - return immediately
1060
- if (response.ok) {
1061
- return response
1062
- }
1063
-
1064
- // Check if we should retry
1065
- if (attempt < maxRetries && isRetryable(response.status, retryableStatusCodes)) {
1066
- const delay = calculateBackoff(attempt, initialDelayMs, maxDelayMs)
1067
- await sleep(delay)
1068
- continue
1069
- }
1070
-
1071
- // Non-retryable error or max retries reached
1072
- const errorText = await response.text()
1073
- throw new Error(`AWS API request failed (${response.status}): ${errorText}`)
1074
- } catch (error) {
1075
- clearTimeout(timeoutId)
1076
- lastError = error as Error
1077
-
1078
- // Handle timeout errors
1079
- if (error instanceof Error && error.name === 'AbortError') {
1080
- lastError = new Error(`Request timed out after ${timeoutMs}ms`)
1081
- }
1082
-
1083
- // Network errors and timeouts are retryable
1084
- if (attempt < maxRetries && !(error instanceof Error && error.message.includes('AWS API request failed'))) {
1085
- const delay = calculateBackoff(attempt, initialDelayMs, maxDelayMs)
1086
- await sleep(delay)
1087
- continue
1088
- }
1089
-
1090
- throw lastError
1091
- }
1092
- }
1093
-
1094
- // Should not reach here, but just in case
1095
- throw lastError ?? new Error('Request failed after retries')
1096
- }
1097
-
1098
- /**
1099
- * Parse XML response from AWS
1100
- */
1101
- export async function parseXMLResponse<T = any>(response: Response): Promise<T> {
1102
- const text = await response.text()
1103
-
1104
- // Simple XML parsing (for production, use a proper XML parser)
1105
- // This is a basic implementation for demonstration
1106
- const result: any = {}
1107
-
1108
- // Extract key-value pairs from XML
1109
- const regex = /<(\w+)>([^<]+)<\/\1>/g
1110
- let match
1111
- while ((match = regex.exec(text)) !== null) {
1112
- const [, key, value] = match
1113
- result[key] = value
1114
- }
1115
-
1116
- return result as T
1117
- }
1118
-
1119
- /**
1120
- * Parse JSON response from AWS
1121
- */
1122
- export async function parseJSONResponse<T = any>(response: Response): Promise<T> {
1123
- return await response.json() as T
1124
- }
1125
-
1126
- /**
1127
- * Clear the internal signing key cache
1128
- * Call this when credentials change or for testing
1129
- */
1130
- export function clearSigningKeyCache(): void {
1131
- signingKeyCache.clear()
1132
- }
1133
-
1134
- /**
1135
- * Get current cache size (for diagnostics)
1136
- */
1137
- export function getSigningKeyCacheSize(): number {
1138
- return signingKeyCache.size
1139
- }
1140
-
1141
- /**
1142
- * Check if Node.js crypto is available (for sync operations)
1143
- * Returns true in Node.js/Bun, false in browser
1144
- */
1145
- export function isNodeCryptoAvailable(): boolean {
1146
- return nodeCrypto !== undefined
1147
- }
1148
-
1149
- /**
1150
- * Check if Web Crypto API is available (for async operations)
1151
- * Returns true in modern browsers and Node.js 15+
1152
- */
1153
- export function isWebCryptoAvailable(): boolean {
1154
- return typeof crypto !== 'undefined' && typeof crypto.subtle !== 'undefined'
1155
- }