@stacksjs/ts-cloud 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 (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/s3.ts DELETED
@@ -1,2318 +0,0 @@
1
- /**
2
- * AWS S3 Operations
3
- * Direct API calls without AWS CLI dependency
4
- */
5
-
6
- import * as crypto from 'node:crypto'
7
- import { AWSClient } from './client'
8
- import { readdir, stat } from 'node:fs/promises'
9
- import { join } from 'node:path'
10
- import { readFileSync } from 'node:fs'
11
-
12
- export interface S3SyncOptions {
13
- source: string
14
- bucket: string
15
- prefix?: string
16
- delete?: boolean
17
- acl?: 'private' | 'public-read' | 'public-read-write' | 'authenticated-read'
18
- cacheControl?: string
19
- contentType?: string
20
- metadata?: Record<string, string>
21
- exclude?: string[]
22
- include?: string[]
23
- dryRun?: boolean
24
- }
25
-
26
- export interface S3CopyOptions {
27
- source: string
28
- bucket: string
29
- key: string
30
- acl?: 'private' | 'public-read' | 'public-read-write' | 'authenticated-read'
31
- cacheControl?: string
32
- contentType?: string
33
- metadata?: Record<string, string>
34
- }
35
-
36
- export interface S3ListOptions {
37
- bucket: string
38
- prefix?: string
39
- maxKeys?: number
40
- }
41
-
42
- export interface S3Object {
43
- Key: string
44
- LastModified: string
45
- Size: number
46
- ETag?: string
47
- }
48
-
49
- /**
50
- * S3 client using direct API calls
51
- */
52
- export class S3Client {
53
- private client: AWSClient
54
- private region: string
55
-
56
- constructor(region: string = 'us-east-1', profile?: string) {
57
- this.region = region
58
- this.client = new AWSClient()
59
- }
60
-
61
- /**
62
- * Get AWS credentials from environment or credentials file
63
- */
64
- private getCredentials(): { accessKeyId: string, secretAccessKey: string, sessionToken?: string } {
65
- // 1. Check environment variables first
66
- const envAccessKey = process.env.AWS_ACCESS_KEY_ID
67
- const envSecretKey = process.env.AWS_SECRET_ACCESS_KEY
68
- const envSessionToken = process.env.AWS_SESSION_TOKEN
69
-
70
- if (envAccessKey && envSecretKey) {
71
- return { accessKeyId: envAccessKey, secretAccessKey: envSecretKey, sessionToken: envSessionToken }
72
- }
73
-
74
- // 2. Try to load from ~/.aws/credentials file
75
- const fileCreds = this.loadCredentialsFromFile()
76
- if (fileCreds) {
77
- return fileCreds
78
- }
79
-
80
- throw new Error('AWS credentials not found. Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables, or configure ~/.aws/credentials.')
81
- }
82
-
83
- /**
84
- * Load credentials from ~/.aws/credentials file
85
- */
86
- private loadCredentialsFromFile(): { accessKeyId: string, secretAccessKey: string, sessionToken?: string } | null {
87
- try {
88
- const { existsSync, readFileSync } = require('node:fs')
89
- const { homedir } = require('node:os')
90
- const { join: pathJoin } = require('node:path')
91
-
92
- const profile = process.env.AWS_PROFILE || 'default'
93
- const credentialsPath = process.env.AWS_SHARED_CREDENTIALS_FILE || pathJoin(homedir(), '.aws', 'credentials')
94
-
95
- if (!existsSync(credentialsPath)) {
96
- return null
97
- }
98
-
99
- const content = readFileSync(credentialsPath, 'utf-8')
100
- const lines = content.split('\n')
101
- let currentProfile = ''
102
- let accessKeyId = ''
103
- let secretAccessKey = ''
104
- let sessionToken: string | undefined
105
-
106
- for (const line of lines) {
107
- const trimmed = line.trim()
108
- if (!trimmed || trimmed.startsWith('#')) continue
109
-
110
- const profileMatch = trimmed.match(/^\[([^\]]+)\]$/)
111
- if (profileMatch) {
112
- currentProfile = profileMatch[1]
113
- continue
114
- }
115
-
116
- if (currentProfile === profile) {
117
- const [key, ...valueParts] = trimmed.split('=')
118
- const value = valueParts.join('=').trim()
119
-
120
- if (key.trim() === 'aws_access_key_id') {
121
- accessKeyId = value
122
- } else if (key.trim() === 'aws_secret_access_key') {
123
- secretAccessKey = value
124
- } else if (key.trim() === 'aws_session_token') {
125
- sessionToken = value
126
- }
127
- }
128
- }
129
-
130
- if (accessKeyId && secretAccessKey) {
131
- return { accessKeyId, secretAccessKey, sessionToken }
132
- }
133
- } catch {
134
- // Failed to read credentials file
135
- }
136
-
137
- return null
138
- }
139
-
140
- /**
141
- * List all S3 buckets in the account
142
- */
143
- async listBuckets(): Promise<{ Buckets: Array<{ Name: string, CreationDate?: string }> }> {
144
- const result = await this.client.request({
145
- service: 's3',
146
- region: this.region,
147
- method: 'GET',
148
- path: '/',
149
- })
150
-
151
- const buckets: Array<{ Name: string, CreationDate?: string }> = []
152
- const bucketList = result?.ListAllMyBucketsResult?.Buckets?.Bucket
153
-
154
- if (bucketList) {
155
- const list = Array.isArray(bucketList) ? bucketList : [bucketList]
156
- for (const b of list) {
157
- buckets.push({
158
- Name: b.Name,
159
- CreationDate: b.CreationDate,
160
- })
161
- }
162
- }
163
-
164
- return { Buckets: buckets }
165
- }
166
-
167
- /**
168
- * Create an S3 bucket
169
- */
170
- async createBucket(bucket: string, options?: { acl?: string }): Promise<void> {
171
- const headers: Record<string, string> = {}
172
- if (options?.acl) {
173
- headers['x-amz-acl'] = options.acl
174
- }
175
-
176
- // For us-east-1, don't include LocationConstraint
177
- // For other regions, include it in the body
178
- let body: string | undefined
179
- if (this.region !== 'us-east-1') {
180
- body = `<?xml version="1.0" encoding="UTF-8"?>
181
- <CreateBucketConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
182
- <LocationConstraint>${this.region}</LocationConstraint>
183
- </CreateBucketConfiguration>`
184
- headers['Content-Type'] = 'application/xml'
185
- }
186
-
187
- await this.client.request({
188
- service: 's3',
189
- region: this.region,
190
- method: 'PUT',
191
- path: `/${bucket}`,
192
- headers,
193
- body,
194
- })
195
- }
196
-
197
- /**
198
- * Delete an S3 bucket (must be empty)
199
- */
200
- async deleteBucket(bucket: string): Promise<void> {
201
- await this.client.request({
202
- service: 's3',
203
- region: this.region,
204
- method: 'DELETE',
205
- path: `/${bucket}`,
206
- })
207
- }
208
-
209
- /**
210
- * Empty and delete an S3 bucket
211
- */
212
- async emptyAndDeleteBucket(bucket: string): Promise<void> {
213
- // First list and delete all objects
214
- let hasMore = true
215
- while (hasMore) {
216
- const objects = await this.listAllObjects({ bucket })
217
- if (objects.length === 0) {
218
- hasMore = false
219
- break
220
- }
221
-
222
- // Delete in batches of 1000 (S3 limit)
223
- const keys = objects.map(obj => obj.Key)
224
- for (let i = 0; i < keys.length; i += 1000) {
225
- const batch = keys.slice(i, i + 1000)
226
- await this.deleteObjects(bucket, batch)
227
- }
228
- }
229
-
230
- // Now delete the bucket
231
- await this.deleteBucket(bucket)
232
- }
233
-
234
- /**
235
- * List all objects in a bucket (handles pagination)
236
- */
237
- async listAllObjects(options: S3ListOptions): Promise<S3Object[]> {
238
- const allObjects: S3Object[] = []
239
- let continuationToken: string | undefined
240
-
241
- do {
242
- const params: Record<string, any> = {
243
- 'list-type': '2',
244
- 'max-keys': '1000',
245
- }
246
-
247
- if (options.prefix) {
248
- params.prefix = options.prefix
249
- }
250
-
251
- if (continuationToken) {
252
- params['continuation-token'] = continuationToken
253
- }
254
-
255
- const result = await this.client.request({
256
- service: 's3',
257
- region: this.region,
258
- method: 'GET',
259
- path: `/${options.bucket}`,
260
- queryParams: params,
261
- })
262
-
263
- const contents = result?.ListBucketResult?.Contents
264
- if (contents) {
265
- const list = Array.isArray(contents) ? contents : [contents]
266
- for (const obj of list) {
267
- allObjects.push({
268
- Key: obj.Key,
269
- LastModified: obj.LastModified || '',
270
- Size: Number.parseInt(obj.Size || '0'),
271
- ETag: obj.ETag,
272
- })
273
- }
274
- }
275
-
276
- // Check for more results
277
- const isTruncated = result?.ListBucketResult?.IsTruncated
278
- continuationToken = isTruncated === 'true' || isTruncated === true
279
- ? result?.ListBucketResult?.NextContinuationToken
280
- : undefined
281
-
282
- } while (continuationToken)
283
-
284
- return allObjects
285
- }
286
-
287
- /**
288
- * List objects in S3 bucket
289
- */
290
- async list(options: S3ListOptions): Promise<S3Object[]> {
291
- // Use path-style URL without query params for simpler signing
292
- const result = await this.client.request({
293
- service: 's3',
294
- region: this.region,
295
- method: 'GET',
296
- path: `/${options.bucket}`,
297
- })
298
-
299
- // Parse S3 XML response
300
- const objects: S3Object[] = []
301
-
302
- // Handle ListBucketResult structure from XML parsing
303
- const contents = result?.ListBucketResult?.Contents
304
- if (contents) {
305
- const items = Array.isArray(contents) ? contents : [contents]
306
- for (const item of items) {
307
- // Filter by prefix if specified
308
- if (options.prefix && !item.Key?.startsWith(options.prefix)) {
309
- continue
310
- }
311
- objects.push({
312
- Key: item.Key || '',
313
- LastModified: item.LastModified || '',
314
- Size: Number.parseInt(item.Size || '0'),
315
- ETag: item.ETag,
316
- })
317
- // Respect maxKeys
318
- if (options.maxKeys && objects.length >= options.maxKeys) {
319
- break
320
- }
321
- }
322
- }
323
-
324
- return objects
325
- }
326
-
327
- /**
328
- * Put object to S3 bucket
329
- */
330
- async putObject(options: {
331
- bucket: string
332
- key: string
333
- body: string | Buffer | Uint8Array
334
- acl?: string
335
- cacheControl?: string
336
- contentType?: string
337
- metadata?: Record<string, string>
338
- }): Promise<void> {
339
- const headers: Record<string, string> = {}
340
-
341
- if (options.acl) {
342
- headers['x-amz-acl'] = options.acl
343
- }
344
-
345
- if (options.cacheControl) {
346
- headers['Cache-Control'] = options.cacheControl
347
- }
348
-
349
- if (options.contentType) {
350
- headers['Content-Type'] = options.contentType
351
- }
352
-
353
- if (options.metadata) {
354
- for (const [key, value] of Object.entries(options.metadata)) {
355
- headers[`x-amz-meta-${key}`] = value
356
- }
357
- }
358
-
359
- // Normalize body to Buffer for binary data
360
- // Uint8Array needs to be converted to Buffer for proper handling
361
- const normalizedBody = options.body instanceof Uint8Array && !Buffer.isBuffer(options.body)
362
- ? Buffer.from(options.body)
363
- : options.body
364
-
365
- // For binary data (Buffer/Uint8Array), use direct binary upload
366
- if (Buffer.isBuffer(normalizedBody) || (normalizedBody as any) instanceof Uint8Array) {
367
- const binaryBody = Buffer.isBuffer(normalizedBody) ? normalizedBody : Buffer.from(normalizedBody)
368
- // Actually, for S3 we need to send raw binary, not base64
369
- // Let's use Bun's fetch which handles Buffer natively
370
- const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials()
371
- const host = `${options.bucket}.s3.${this.region}.amazonaws.com`
372
- const url = `https://${host}/${options.key}`
373
-
374
- const now = new Date()
375
- const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '')
376
- const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, '')
377
-
378
- const payloadHash = crypto.createHash('sha256').update(binaryBody).digest('hex')
379
-
380
- const requestHeaders: Record<string, string> = {
381
- 'host': host,
382
- 'x-amz-date': amzDate,
383
- 'x-amz-content-sha256': payloadHash,
384
- ...headers,
385
- }
386
-
387
- if (sessionToken) {
388
- requestHeaders['x-amz-security-token'] = sessionToken
389
- }
390
-
391
- // Create canonical request
392
- const canonicalHeaders = Object.keys(requestHeaders)
393
- .sort()
394
- .map(key => `${key.toLowerCase()}:${requestHeaders[key].trim()}\n`)
395
- .join('')
396
-
397
- const signedHeaders = Object.keys(requestHeaders)
398
- .sort()
399
- .map(key => key.toLowerCase())
400
- .join(';')
401
-
402
- const canonicalRequest = [
403
- 'PUT',
404
- `/${options.key}`,
405
- '',
406
- canonicalHeaders,
407
- signedHeaders,
408
- payloadHash,
409
- ].join('\n')
410
-
411
- // Create string to sign
412
- const algorithm = 'AWS4-HMAC-SHA256'
413
- const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`
414
- const stringToSign = [
415
- algorithm,
416
- amzDate,
417
- credentialScope,
418
- crypto.createHash('sha256').update(canonicalRequest).digest('hex'),
419
- ].join('\n')
420
-
421
- // Calculate signature
422
- const kDate = crypto.createHmac('sha256', `AWS4${secretAccessKey}`).update(dateStamp).digest()
423
- const kRegion = crypto.createHmac('sha256', kDate).update(this.region).digest()
424
- const kService = crypto.createHmac('sha256', kRegion).update('s3').digest()
425
- const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest()
426
- const signature = crypto.createHmac('sha256', kSigning).update(stringToSign).digest('hex')
427
-
428
- const authorizationHeader = `${algorithm} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
429
-
430
- const response = await fetch(url, {
431
- method: 'PUT',
432
- headers: {
433
- ...requestHeaders,
434
- 'Authorization': authorizationHeader,
435
- },
436
- body: binaryBody,
437
- })
438
-
439
- if (!response.ok) {
440
- const errorText = await response.text()
441
- throw new Error(`S3 PUT failed: ${response.status} ${errorText}`)
442
- }
443
-
444
- return
445
- }
446
-
447
- await this.client.request({
448
- service: 's3',
449
- region: this.region,
450
- method: 'PUT',
451
- path: `/${options.key}`,
452
- bucket: options.bucket, // Use virtual-hosted style
453
- headers,
454
- body: options.body as string,
455
- })
456
- }
457
-
458
- /**
459
- * Get object from S3 bucket
460
- * Returns raw content as string (not parsed as XML)
461
- */
462
- async getObject(bucket: string, key: string): Promise<string> {
463
- const result = await this.client.request({
464
- service: 's3',
465
- region: this.region,
466
- method: 'GET',
467
- path: `/${bucket}/${key}`,
468
- rawResponse: true,
469
- })
470
-
471
- return result
472
- }
473
-
474
- /**
475
- * Copy object within S3 (server-side copy)
476
- */
477
- async copyObject(options: {
478
- sourceBucket: string
479
- sourceKey: string
480
- destinationBucket: string
481
- destinationKey: string
482
- contentType?: string
483
- metadata?: Record<string, string>
484
- metadataDirective?: 'COPY' | 'REPLACE'
485
- }): Promise<void> {
486
- const headers: Record<string, string> = {
487
- 'x-amz-copy-source': `/${options.sourceBucket}/${options.sourceKey}`,
488
- }
489
-
490
- if (options.metadataDirective) {
491
- headers['x-amz-metadata-directive'] = options.metadataDirective
492
- }
493
-
494
- if (options.contentType) {
495
- headers['Content-Type'] = options.contentType
496
- }
497
-
498
- if (options.metadata) {
499
- for (const [key, value] of Object.entries(options.metadata)) {
500
- headers[`x-amz-meta-${key}`] = value
501
- }
502
- }
503
-
504
- await this.client.request({
505
- service: 's3',
506
- region: this.region,
507
- method: 'PUT',
508
- path: `/${options.destinationBucket}/${options.destinationKey}`,
509
- headers,
510
- })
511
- }
512
-
513
- /**
514
- * Delete object from S3
515
- */
516
- async deleteObject(bucket: string, key: string): Promise<void> {
517
- await this.client.request({
518
- service: 's3',
519
- region: this.region,
520
- method: 'DELETE',
521
- path: `/${bucket}/${key}`,
522
- })
523
- }
524
-
525
- /**
526
- * Delete multiple objects from S3
527
- */
528
- async deleteObjects(bucket: string, keys: string[]): Promise<void> {
529
- const deleteXml = `<?xml version="1.0" encoding="UTF-8"?>
530
- <Delete>
531
- ${keys.map(key => `<Object><Key>${key}</Key></Object>`).join('\n ')}
532
- </Delete>`
533
-
534
- // S3 DeleteObjects requires Content-MD5 header
535
- const contentMd5 = crypto.createHash('md5').update(deleteXml).digest('base64')
536
-
537
- await this.client.request({
538
- service: 's3',
539
- region: this.region,
540
- method: 'POST',
541
- path: `/${bucket}`,
542
- queryParams: { delete: '' },
543
- body: deleteXml,
544
- headers: {
545
- 'Content-Type': 'application/xml',
546
- 'Content-MD5': contentMd5,
547
- },
548
- })
549
- }
550
-
551
- /**
552
- * Check if bucket exists
553
- */
554
- async bucketExists(bucket: string): Promise<boolean> {
555
- try {
556
- await this.client.request({
557
- service: 's3',
558
- region: this.region,
559
- method: 'HEAD',
560
- path: `/${bucket}`,
561
- })
562
- return true
563
- }
564
- catch {
565
- return false
566
- }
567
- }
568
-
569
- /**
570
- * Copy file to S3
571
- */
572
- async copy(options: S3CopyOptions): Promise<void> {
573
- // Read file and upload
574
- const fileContent = readFileSync(options.source)
575
-
576
- await this.putObject({
577
- bucket: options.bucket,
578
- key: options.key,
579
- body: fileContent,
580
- acl: options.acl,
581
- cacheControl: options.cacheControl,
582
- contentType: options.contentType,
583
- metadata: options.metadata,
584
- })
585
- }
586
-
587
- /**
588
- * Sync local directory to S3 bucket
589
- * Note: This is a simplified version. For production use, implement proper sync logic
590
- */
591
- async sync(options: S3SyncOptions): Promise<void> {
592
- const files = await this.listFilesRecursive(options.source)
593
-
594
- for (const file of files) {
595
- // Skip excluded files
596
- if (options.exclude && options.exclude.some(pattern => file.includes(pattern))) {
597
- continue
598
- }
599
-
600
- // Check included files
601
- if (options.include && !options.include.some(pattern => file.includes(pattern))) {
602
- continue
603
- }
604
-
605
- const relativePath = file.substring(options.source.length + 1)
606
- const s3Key = options.prefix ? `${options.prefix}/${relativePath}` : relativePath
607
-
608
- if (!options.dryRun) {
609
- const fileContent = readFileSync(file)
610
-
611
- await this.putObject({
612
- bucket: options.bucket,
613
- key: s3Key,
614
- body: fileContent,
615
- acl: options.acl,
616
- cacheControl: options.cacheControl,
617
- contentType: options.contentType,
618
- metadata: options.metadata,
619
- })
620
- }
621
- }
622
- }
623
-
624
- /**
625
- * Delete object from S3 (alias for deleteObject)
626
- */
627
- async delete(bucket: string, key: string): Promise<void> {
628
- await this.deleteObject(bucket, key)
629
- }
630
-
631
- /**
632
- * Delete all objects in a prefix
633
- */
634
- async deletePrefix(bucket: string, prefix: string): Promise<void> {
635
- const objects = await this.list({ bucket, prefix })
636
- const keys = objects.map(obj => obj.Key)
637
-
638
- if (keys.length > 0) {
639
- await this.deleteObjects(bucket, keys)
640
- }
641
- }
642
-
643
- /**
644
- * Get bucket size
645
- */
646
- async getBucketSize(bucket: string, prefix?: string): Promise<number> {
647
- const objects = await this.list({ bucket, prefix })
648
- return objects.reduce((total, obj) => total + obj.Size, 0)
649
- }
650
-
651
- /**
652
- * List files recursively in a directory
653
- */
654
- private async listFilesRecursive(dir: string): Promise<string[]> {
655
- const files: string[] = []
656
- const entries = await readdir(dir, { withFileTypes: true })
657
-
658
- for (const entry of entries) {
659
- const fullPath = join(dir, entry.name)
660
-
661
- if (entry.isDirectory()) {
662
- const subFiles = await this.listFilesRecursive(fullPath)
663
- files.push(...subFiles)
664
- }
665
- else {
666
- files.push(fullPath)
667
- }
668
- }
669
-
670
- return files
671
- }
672
-
673
- /**
674
- * Put bucket policy for an S3 bucket
675
- * Uses path-style URLs to avoid redirect issues
676
- */
677
- async putBucketPolicy(bucket: string, policy: object | string): Promise<void> {
678
- const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials()
679
- const host = `s3.${this.region}.amazonaws.com`
680
- const policyString = typeof policy === 'string' ? policy : JSON.stringify(policy)
681
-
682
- const now = new Date()
683
- const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '')
684
- const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, '')
685
-
686
- const payloadHash = crypto.createHash('sha256').update(policyString).digest('hex')
687
-
688
- // Use path-style URL: s3.region.amazonaws.com/bucket?policy
689
- const canonicalUri = '/' + bucket
690
- const canonicalQuerystring = 'policy='
691
-
692
- const requestHeaders: Record<string, string> = {
693
- 'host': host,
694
- 'x-amz-date': amzDate,
695
- 'x-amz-content-sha256': payloadHash,
696
- 'content-type': 'application/json',
697
- }
698
-
699
- if (sessionToken) {
700
- requestHeaders['x-amz-security-token'] = sessionToken
701
- }
702
-
703
- const canonicalHeaders = Object.keys(requestHeaders)
704
- .sort()
705
- .map(key => `${key.toLowerCase()}:${requestHeaders[key].trim()}\n`)
706
- .join('')
707
-
708
- const signedHeaders = Object.keys(requestHeaders)
709
- .sort()
710
- .map(key => key.toLowerCase())
711
- .join(';')
712
-
713
- const canonicalRequest = [
714
- 'PUT',
715
- canonicalUri,
716
- canonicalQuerystring,
717
- canonicalHeaders,
718
- signedHeaders,
719
- payloadHash,
720
- ].join('\n')
721
-
722
- const algorithm = 'AWS4-HMAC-SHA256'
723
- const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`
724
- const stringToSign = [
725
- algorithm,
726
- amzDate,
727
- credentialScope,
728
- crypto.createHash('sha256').update(canonicalRequest).digest('hex'),
729
- ].join('\n')
730
-
731
- const kDate = crypto.createHmac('sha256', 'AWS4' + secretAccessKey).update(dateStamp).digest()
732
- const kRegion = crypto.createHmac('sha256', kDate).update(this.region).digest()
733
- const kService = crypto.createHmac('sha256', kRegion).update('s3').digest()
734
- const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest()
735
- const signature = crypto.createHmac('sha256', kSigning).update(stringToSign).digest('hex')
736
-
737
- const authHeader = `${algorithm} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
738
-
739
- const url = `https://${host}${canonicalUri}?${canonicalQuerystring}`
740
-
741
- const response = await fetch(url, {
742
- method: 'PUT',
743
- headers: {
744
- ...requestHeaders,
745
- 'Authorization': authHeader,
746
- },
747
- body: policyString,
748
- })
749
-
750
- if (!response.ok) {
751
- const text = await response.text()
752
- throw new Error(`Failed to put bucket policy: ${response.status} ${text}`)
753
- }
754
- }
755
-
756
- /**
757
- * Get bucket policy for an S3 bucket
758
- * Uses path-style URLs to avoid redirect issues
759
- */
760
- async getBucketPolicy(bucket: string): Promise<object | null> {
761
- const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials()
762
- const host = `s3.${this.region}.amazonaws.com`
763
-
764
- const now = new Date()
765
- const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '')
766
- const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, '')
767
-
768
- const payloadHash = crypto.createHash('sha256').update('').digest('hex')
769
-
770
- // Use path-style URL: s3.region.amazonaws.com/bucket?policy
771
- const canonicalUri = '/' + bucket
772
- const canonicalQuerystring = 'policy='
773
-
774
- const requestHeaders: Record<string, string> = {
775
- 'host': host,
776
- 'x-amz-date': amzDate,
777
- 'x-amz-content-sha256': payloadHash,
778
- }
779
-
780
- if (sessionToken) {
781
- requestHeaders['x-amz-security-token'] = sessionToken
782
- }
783
-
784
- const canonicalHeaders = Object.keys(requestHeaders)
785
- .sort()
786
- .map(key => `${key.toLowerCase()}:${requestHeaders[key].trim()}\n`)
787
- .join('')
788
-
789
- const signedHeaders = Object.keys(requestHeaders)
790
- .sort()
791
- .map(key => key.toLowerCase())
792
- .join(';')
793
-
794
- const canonicalRequest = [
795
- 'GET',
796
- canonicalUri,
797
- canonicalQuerystring,
798
- canonicalHeaders,
799
- signedHeaders,
800
- payloadHash,
801
- ].join('\n')
802
-
803
- const algorithm = 'AWS4-HMAC-SHA256'
804
- const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`
805
- const stringToSign = [
806
- algorithm,
807
- amzDate,
808
- credentialScope,
809
- crypto.createHash('sha256').update(canonicalRequest).digest('hex'),
810
- ].join('\n')
811
-
812
- const kDate = crypto.createHmac('sha256', 'AWS4' + secretAccessKey).update(dateStamp).digest()
813
- const kRegion = crypto.createHmac('sha256', kDate).update(this.region).digest()
814
- const kService = crypto.createHmac('sha256', kRegion).update('s3').digest()
815
- const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest()
816
- const signature = crypto.createHmac('sha256', kSigning).update(stringToSign).digest('hex')
817
-
818
- const authHeader = `${algorithm} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
819
-
820
- const url = `https://${host}${canonicalUri}?${canonicalQuerystring}`
821
-
822
- const response = await fetch(url, {
823
- method: 'GET',
824
- headers: {
825
- ...requestHeaders,
826
- 'Authorization': authHeader,
827
- },
828
- })
829
-
830
- if (response.status === 404) {
831
- return null
832
- }
833
-
834
- if (!response.ok) {
835
- const text = await response.text()
836
- throw new Error(`Failed to get bucket policy: ${response.status} ${text}`)
837
- }
838
-
839
- const text = await response.text()
840
- return JSON.parse(text)
841
- }
842
-
843
- /**
844
- * Delete bucket policy
845
- */
846
- async deleteBucketPolicy(bucket: string): Promise<void> {
847
- await this.client.request({
848
- service: 's3',
849
- region: this.region,
850
- method: 'DELETE',
851
- path: `/${bucket}`,
852
- queryParams: { policy: '' },
853
- })
854
- }
855
-
856
- /**
857
- * Head bucket - check if bucket exists and you have access
858
- */
859
- async headBucket(bucket: string): Promise<{ exists: boolean; region?: string }> {
860
- try {
861
- const result = await this.client.request({
862
- service: 's3',
863
- region: this.region,
864
- method: 'HEAD',
865
- path: `/${bucket}`,
866
- returnHeaders: true,
867
- })
868
- return { exists: true, region: result?.headers?.['x-amz-bucket-region'] }
869
- } catch (e: any) {
870
- if (e.statusCode === 404) {
871
- return { exists: false }
872
- }
873
- throw e
874
- }
875
- }
876
-
877
- /**
878
- * Head object - get object metadata without downloading
879
- */
880
- async headObject(bucket: string, key: string): Promise<{
881
- ContentLength?: number
882
- ContentType?: string
883
- ETag?: string
884
- LastModified?: string
885
- Metadata?: Record<string, string>
886
- } | null> {
887
- try {
888
- const result = await this.client.request({
889
- service: 's3',
890
- region: this.region,
891
- method: 'HEAD',
892
- path: `/${bucket}/${key}`,
893
- returnHeaders: true,
894
- })
895
- return {
896
- ContentLength: result?.headers?.['content-length'] ? parseInt(result.headers['content-length']) : undefined,
897
- ContentType: result?.headers?.['content-type'],
898
- ETag: result?.headers?.['etag'],
899
- LastModified: result?.headers?.['last-modified'],
900
- }
901
- } catch (e: any) {
902
- if (e.statusCode === 404) {
903
- return null
904
- }
905
- throw e
906
- }
907
- }
908
-
909
- /**
910
- * Get object as Buffer
911
- */
912
- async getObjectBuffer(bucket: string, key: string): Promise<Buffer> {
913
- const content = await this.getObject(bucket, key)
914
- return Buffer.from(content)
915
- }
916
-
917
- /**
918
- * Get object as JSON
919
- */
920
- async getObjectJson<T = any>(bucket: string, key: string): Promise<T> {
921
- const content = await this.getObject(bucket, key)
922
- return JSON.parse(content)
923
- }
924
-
925
- /**
926
- * Put JSON object
927
- */
928
- async putObjectJson(bucket: string, key: string, data: any, options?: {
929
- acl?: string
930
- cacheControl?: string
931
- metadata?: Record<string, string>
932
- }): Promise<void> {
933
- await this.putObject({
934
- bucket,
935
- key,
936
- body: JSON.stringify(data),
937
- contentType: 'application/json',
938
- ...options,
939
- })
940
- }
941
-
942
- /**
943
- * Get bucket versioning configuration
944
- */
945
- async getBucketVersioning(bucket: string): Promise<{ Status?: 'Enabled' | 'Suspended' }> {
946
- const result = await this.client.request({
947
- service: 's3',
948
- region: this.region,
949
- method: 'GET',
950
- path: `/${bucket}`,
951
- queryParams: { versioning: '' },
952
- })
953
- return {
954
- Status: result?.VersioningConfiguration?.Status,
955
- }
956
- }
957
-
958
- /**
959
- * Put bucket versioning configuration
960
- */
961
- async putBucketVersioning(bucket: string, status: 'Enabled' | 'Suspended'): Promise<void> {
962
- const body = `<?xml version="1.0" encoding="UTF-8"?>
963
- <VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
964
- <Status>${status}</Status>
965
- </VersioningConfiguration>`
966
-
967
- await this.client.request({
968
- service: 's3',
969
- region: this.region,
970
- method: 'PUT',
971
- path: `/${bucket}`,
972
- queryParams: { versioning: '' },
973
- headers: { 'Content-Type': 'application/xml' },
974
- body,
975
- })
976
- }
977
-
978
- /**
979
- * Get bucket lifecycle configuration
980
- */
981
- async getBucketLifecycleConfiguration(bucket: string): Promise<any> {
982
- try {
983
- const result = await this.client.request({
984
- service: 's3',
985
- region: this.region,
986
- method: 'GET',
987
- path: `/${bucket}`,
988
- queryParams: { lifecycle: '' },
989
- })
990
- return result?.LifecycleConfiguration
991
- } catch (e: any) {
992
- if (e.statusCode === 404) {
993
- return null
994
- }
995
- throw e
996
- }
997
- }
998
-
999
- /**
1000
- * Put bucket lifecycle configuration
1001
- */
1002
- async putBucketLifecycleConfiguration(bucket: string, rules: Array<{
1003
- ID: string
1004
- Status: 'Enabled' | 'Disabled'
1005
- Filter?: { Prefix?: string }
1006
- Expiration?: { Days?: number; Date?: string }
1007
- Transitions?: Array<{ Days?: number; StorageClass: string }>
1008
- NoncurrentVersionExpiration?: { NoncurrentDays: number }
1009
- }>): Promise<void> {
1010
- const rulesXml = rules.map(rule => {
1011
- let ruleXml = `<Rule><ID>${rule.ID}</ID><Status>${rule.Status}</Status>`
1012
-
1013
- if (rule.Filter) {
1014
- ruleXml += `<Filter><Prefix>${rule.Filter.Prefix || ''}</Prefix></Filter>`
1015
- } else {
1016
- ruleXml += '<Filter><Prefix></Prefix></Filter>'
1017
- }
1018
-
1019
- if (rule.Expiration) {
1020
- if (rule.Expiration.Days) {
1021
- ruleXml += `<Expiration><Days>${rule.Expiration.Days}</Days></Expiration>`
1022
- } else if (rule.Expiration.Date) {
1023
- ruleXml += `<Expiration><Date>${rule.Expiration.Date}</Date></Expiration>`
1024
- }
1025
- }
1026
-
1027
- if (rule.Transitions) {
1028
- for (const t of rule.Transitions) {
1029
- ruleXml += `<Transition><Days>${t.Days}</Days><StorageClass>${t.StorageClass}</StorageClass></Transition>`
1030
- }
1031
- }
1032
-
1033
- if (rule.NoncurrentVersionExpiration) {
1034
- ruleXml += `<NoncurrentVersionExpiration><NoncurrentDays>${rule.NoncurrentVersionExpiration.NoncurrentDays}</NoncurrentDays></NoncurrentVersionExpiration>`
1035
- }
1036
-
1037
- ruleXml += '</Rule>'
1038
- return ruleXml
1039
- }).join('')
1040
-
1041
- const body = `<?xml version="1.0" encoding="UTF-8"?>
1042
- <LifecycleConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
1043
- ${rulesXml}
1044
- </LifecycleConfiguration>`
1045
-
1046
- await this.client.request({
1047
- service: 's3',
1048
- region: this.region,
1049
- method: 'PUT',
1050
- path: `/${bucket}`,
1051
- queryParams: { lifecycle: '' },
1052
- headers: { 'Content-Type': 'application/xml' },
1053
- body,
1054
- })
1055
- }
1056
-
1057
- /**
1058
- * Delete bucket lifecycle configuration
1059
- */
1060
- async deleteBucketLifecycleConfiguration(bucket: string): Promise<void> {
1061
- await this.client.request({
1062
- service: 's3',
1063
- region: this.region,
1064
- method: 'DELETE',
1065
- path: `/${bucket}`,
1066
- queryParams: { lifecycle: '' },
1067
- })
1068
- }
1069
-
1070
- /**
1071
- * Get bucket CORS configuration
1072
- */
1073
- async getBucketCors(bucket: string): Promise<any> {
1074
- try {
1075
- const result = await this.client.request({
1076
- service: 's3',
1077
- region: this.region,
1078
- method: 'GET',
1079
- path: `/${bucket}`,
1080
- queryParams: { cors: '' },
1081
- })
1082
- return result?.CORSConfiguration
1083
- } catch (e: any) {
1084
- if (e.statusCode === 404) {
1085
- return null
1086
- }
1087
- throw e
1088
- }
1089
- }
1090
-
1091
- /**
1092
- * Put bucket CORS configuration
1093
- */
1094
- async putBucketCors(bucket: string, rules: Array<{
1095
- AllowedOrigins: string[]
1096
- AllowedMethods: string[]
1097
- AllowedHeaders?: string[]
1098
- ExposeHeaders?: string[]
1099
- MaxAgeSeconds?: number
1100
- }>): Promise<void> {
1101
- const rulesXml = rules.map(rule => {
1102
- let ruleXml = '<CORSRule>'
1103
- for (const origin of rule.AllowedOrigins) {
1104
- ruleXml += `<AllowedOrigin>${origin}</AllowedOrigin>`
1105
- }
1106
- for (const method of rule.AllowedMethods) {
1107
- ruleXml += `<AllowedMethod>${method}</AllowedMethod>`
1108
- }
1109
- if (rule.AllowedHeaders) {
1110
- for (const header of rule.AllowedHeaders) {
1111
- ruleXml += `<AllowedHeader>${header}</AllowedHeader>`
1112
- }
1113
- }
1114
- if (rule.ExposeHeaders) {
1115
- for (const header of rule.ExposeHeaders) {
1116
- ruleXml += `<ExposeHeader>${header}</ExposeHeader>`
1117
- }
1118
- }
1119
- if (rule.MaxAgeSeconds) {
1120
- ruleXml += `<MaxAgeSeconds>${rule.MaxAgeSeconds}</MaxAgeSeconds>`
1121
- }
1122
- ruleXml += '</CORSRule>'
1123
- return ruleXml
1124
- }).join('')
1125
-
1126
- const body = `<?xml version="1.0" encoding="UTF-8"?>
1127
- <CORSConfiguration>
1128
- ${rulesXml}
1129
- </CORSConfiguration>`
1130
-
1131
- await this.client.request({
1132
- service: 's3',
1133
- region: this.region,
1134
- method: 'PUT',
1135
- path: `/${bucket}`,
1136
- queryParams: { cors: '' },
1137
- headers: { 'Content-Type': 'application/xml' },
1138
- body,
1139
- })
1140
- }
1141
-
1142
- /**
1143
- * Delete bucket CORS configuration
1144
- */
1145
- async deleteBucketCors(bucket: string): Promise<void> {
1146
- await this.client.request({
1147
- service: 's3',
1148
- region: this.region,
1149
- method: 'DELETE',
1150
- path: `/${bucket}`,
1151
- queryParams: { cors: '' },
1152
- })
1153
- }
1154
-
1155
- /**
1156
- * Get bucket encryption configuration
1157
- */
1158
- async getBucketEncryption(bucket: string): Promise<any> {
1159
- try {
1160
- const result = await this.client.request({
1161
- service: 's3',
1162
- region: this.region,
1163
- method: 'GET',
1164
- path: `/${bucket}`,
1165
- queryParams: { encryption: '' },
1166
- })
1167
- return result?.ServerSideEncryptionConfiguration
1168
- } catch (e: any) {
1169
- if (e.statusCode === 404) {
1170
- return null
1171
- }
1172
- throw e
1173
- }
1174
- }
1175
-
1176
- /**
1177
- * Put bucket encryption configuration
1178
- */
1179
- async putBucketEncryption(bucket: string, sseAlgorithm: 'AES256' | 'aws:kms', kmsKeyId?: string): Promise<void> {
1180
- let ruleXml = `<ApplyServerSideEncryptionByDefault><SSEAlgorithm>${sseAlgorithm}</SSEAlgorithm>`
1181
- if (kmsKeyId) {
1182
- ruleXml += `<KMSMasterKeyID>${kmsKeyId}</KMSMasterKeyID>`
1183
- }
1184
- ruleXml += '</ApplyServerSideEncryptionByDefault>'
1185
-
1186
- const body = `<?xml version="1.0" encoding="UTF-8"?>
1187
- <ServerSideEncryptionConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
1188
- <Rule>${ruleXml}</Rule>
1189
- </ServerSideEncryptionConfiguration>`
1190
-
1191
- await this.client.request({
1192
- service: 's3',
1193
- region: this.region,
1194
- method: 'PUT',
1195
- path: `/${bucket}`,
1196
- queryParams: { encryption: '' },
1197
- headers: { 'Content-Type': 'application/xml' },
1198
- body,
1199
- })
1200
- }
1201
-
1202
- /**
1203
- * Delete bucket encryption configuration
1204
- */
1205
- async deleteBucketEncryption(bucket: string): Promise<void> {
1206
- await this.client.request({
1207
- service: 's3',
1208
- region: this.region,
1209
- method: 'DELETE',
1210
- path: `/${bucket}`,
1211
- queryParams: { encryption: '' },
1212
- })
1213
- }
1214
-
1215
- /**
1216
- * Get bucket tagging
1217
- */
1218
- async getBucketTagging(bucket: string): Promise<Array<{ Key: string; Value: string }>> {
1219
- try {
1220
- const result = await this.client.request({
1221
- service: 's3',
1222
- region: this.region,
1223
- method: 'GET',
1224
- path: `/${bucket}`,
1225
- queryParams: { tagging: '' },
1226
- })
1227
- const tagSet = result?.Tagging?.TagSet?.Tag
1228
- if (!tagSet) return []
1229
- return Array.isArray(tagSet) ? tagSet : [tagSet]
1230
- } catch (e: any) {
1231
- if (e.statusCode === 404) {
1232
- return []
1233
- }
1234
- throw e
1235
- }
1236
- }
1237
-
1238
- /**
1239
- * Put bucket tagging
1240
- */
1241
- async putBucketTagging(bucket: string, tags: Array<{ Key: string; Value: string }>): Promise<void> {
1242
- const tagsXml = tags.map(t => `<Tag><Key>${t.Key}</Key><Value>${t.Value}</Value></Tag>`).join('')
1243
-
1244
- const body = `<?xml version="1.0" encoding="UTF-8"?>
1245
- <Tagging>
1246
- <TagSet>${tagsXml}</TagSet>
1247
- </Tagging>`
1248
-
1249
- await this.client.request({
1250
- service: 's3',
1251
- region: this.region,
1252
- method: 'PUT',
1253
- path: `/${bucket}`,
1254
- queryParams: { tagging: '' },
1255
- headers: { 'Content-Type': 'application/xml' },
1256
- body,
1257
- })
1258
- }
1259
-
1260
- /**
1261
- * Delete bucket tagging
1262
- */
1263
- async deleteBucketTagging(bucket: string): Promise<void> {
1264
- await this.client.request({
1265
- service: 's3',
1266
- region: this.region,
1267
- method: 'DELETE',
1268
- path: `/${bucket}`,
1269
- queryParams: { tagging: '' },
1270
- })
1271
- }
1272
-
1273
- /**
1274
- * Get object tagging
1275
- */
1276
- async getObjectTagging(bucket: string, key: string): Promise<Array<{ Key: string; Value: string }>> {
1277
- try {
1278
- const result = await this.client.request({
1279
- service: 's3',
1280
- region: this.region,
1281
- method: 'GET',
1282
- path: `/${bucket}/${key}`,
1283
- queryParams: { tagging: '' },
1284
- })
1285
- const tagSet = result?.Tagging?.TagSet?.Tag
1286
- if (!tagSet) return []
1287
- return Array.isArray(tagSet) ? tagSet : [tagSet]
1288
- } catch (e: any) {
1289
- if (e.statusCode === 404) {
1290
- return []
1291
- }
1292
- throw e
1293
- }
1294
- }
1295
-
1296
- /**
1297
- * Put object tagging
1298
- */
1299
- async putObjectTagging(bucket: string, key: string, tags: Array<{ Key: string; Value: string }>): Promise<void> {
1300
- const tagsXml = tags.map(t => `<Tag><Key>${t.Key}</Key><Value>${t.Value}</Value></Tag>`).join('')
1301
-
1302
- const body = `<?xml version="1.0" encoding="UTF-8"?>
1303
- <Tagging>
1304
- <TagSet>${tagsXml}</TagSet>
1305
- </Tagging>`
1306
-
1307
- await this.client.request({
1308
- service: 's3',
1309
- region: this.region,
1310
- method: 'PUT',
1311
- path: `/${bucket}/${key}`,
1312
- queryParams: { tagging: '' },
1313
- headers: { 'Content-Type': 'application/xml' },
1314
- body,
1315
- })
1316
- }
1317
-
1318
- /**
1319
- * Delete object tagging
1320
- */
1321
- async deleteObjectTagging(bucket: string, key: string): Promise<void> {
1322
- await this.client.request({
1323
- service: 's3',
1324
- region: this.region,
1325
- method: 'DELETE',
1326
- path: `/${bucket}/${key}`,
1327
- queryParams: { tagging: '' },
1328
- })
1329
- }
1330
-
1331
- /**
1332
- * Get bucket ACL
1333
- */
1334
- async getBucketAcl(bucket: string): Promise<any> {
1335
- const result = await this.client.request({
1336
- service: 's3',
1337
- region: this.region,
1338
- method: 'GET',
1339
- path: `/${bucket}`,
1340
- queryParams: { acl: '' },
1341
- })
1342
- return result?.AccessControlPolicy
1343
- }
1344
-
1345
- /**
1346
- * Put bucket ACL (canned ACL)
1347
- */
1348
- async putBucketAcl(bucket: string, acl: 'private' | 'public-read' | 'public-read-write' | 'authenticated-read'): Promise<void> {
1349
- await this.client.request({
1350
- service: 's3',
1351
- region: this.region,
1352
- method: 'PUT',
1353
- path: `/${bucket}`,
1354
- queryParams: { acl: '' },
1355
- headers: { 'x-amz-acl': acl },
1356
- })
1357
- }
1358
-
1359
- /**
1360
- * Get object ACL
1361
- */
1362
- async getObjectAcl(bucket: string, key: string): Promise<any> {
1363
- const result = await this.client.request({
1364
- service: 's3',
1365
- region: this.region,
1366
- method: 'GET',
1367
- path: `/${bucket}/${key}`,
1368
- queryParams: { acl: '' },
1369
- })
1370
- return result?.AccessControlPolicy
1371
- }
1372
-
1373
- /**
1374
- * Put object ACL (canned ACL)
1375
- */
1376
- async putObjectAcl(bucket: string, key: string, acl: 'private' | 'public-read' | 'public-read-write' | 'authenticated-read'): Promise<void> {
1377
- await this.client.request({
1378
- service: 's3',
1379
- region: this.region,
1380
- method: 'PUT',
1381
- path: `/${bucket}/${key}`,
1382
- queryParams: { acl: '' },
1383
- headers: { 'x-amz-acl': acl },
1384
- })
1385
- }
1386
-
1387
- /**
1388
- * Get bucket location
1389
- */
1390
- async getBucketLocation(bucket: string): Promise<string> {
1391
- const result = await this.client.request({
1392
- service: 's3',
1393
- region: this.region,
1394
- method: 'GET',
1395
- path: `/${bucket}`,
1396
- queryParams: { location: '' },
1397
- })
1398
- // Empty string means us-east-1
1399
- return result?.LocationConstraint || 'us-east-1'
1400
- }
1401
-
1402
- /**
1403
- * Get bucket logging configuration
1404
- */
1405
- async getBucketLogging(bucket: string): Promise<any> {
1406
- const result = await this.client.request({
1407
- service: 's3',
1408
- region: this.region,
1409
- method: 'GET',
1410
- path: `/${bucket}`,
1411
- queryParams: { logging: '' },
1412
- })
1413
- return result?.BucketLoggingStatus
1414
- }
1415
-
1416
- /**
1417
- * Put bucket logging configuration
1418
- */
1419
- async putBucketLogging(bucket: string, targetBucket: string, targetPrefix: string): Promise<void> {
1420
- const body = `<?xml version="1.0" encoding="UTF-8"?>
1421
- <BucketLoggingStatus xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
1422
- <LoggingEnabled>
1423
- <TargetBucket>${targetBucket}</TargetBucket>
1424
- <TargetPrefix>${targetPrefix}</TargetPrefix>
1425
- </LoggingEnabled>
1426
- </BucketLoggingStatus>`
1427
-
1428
- await this.client.request({
1429
- service: 's3',
1430
- region: this.region,
1431
- method: 'PUT',
1432
- path: `/${bucket}`,
1433
- queryParams: { logging: '' },
1434
- headers: { 'Content-Type': 'application/xml' },
1435
- body,
1436
- })
1437
- }
1438
-
1439
- /**
1440
- * Get bucket notification configuration
1441
- */
1442
- async getBucketNotificationConfiguration(bucket: string): Promise<any> {
1443
- const result = await this.client.request({
1444
- service: 's3',
1445
- region: this.region,
1446
- method: 'GET',
1447
- path: `/${bucket}`,
1448
- queryParams: { notification: '' },
1449
- })
1450
- return result?.NotificationConfiguration
1451
- }
1452
-
1453
- /**
1454
- * Put bucket notification configuration
1455
- */
1456
- async putBucketNotificationConfiguration(bucket: string, config: {
1457
- LambdaFunctionConfigurations?: Array<{
1458
- Id?: string
1459
- LambdaFunctionArn: string
1460
- Events: string[]
1461
- Filter?: { Key?: { FilterRules: Array<{ Name: string; Value: string }> } }
1462
- }>
1463
- TopicConfigurations?: Array<{
1464
- Id?: string
1465
- TopicArn: string
1466
- Events: string[]
1467
- Filter?: { Key?: { FilterRules: Array<{ Name: string; Value: string }> } }
1468
- }>
1469
- QueueConfigurations?: Array<{
1470
- Id?: string
1471
- QueueArn: string
1472
- Events: string[]
1473
- Filter?: { Key?: { FilterRules: Array<{ Name: string; Value: string }> } }
1474
- }>
1475
- }): Promise<void> {
1476
- let configXml = ''
1477
-
1478
- if (config.LambdaFunctionConfigurations) {
1479
- for (const c of config.LambdaFunctionConfigurations) {
1480
- configXml += '<CloudFunctionConfiguration>'
1481
- if (c.Id) configXml += `<Id>${c.Id}</Id>`
1482
- configXml += `<CloudFunction>${c.LambdaFunctionArn}</CloudFunction>`
1483
- for (const event of c.Events) {
1484
- configXml += `<Event>${event}</Event>`
1485
- }
1486
- if (c.Filter?.Key?.FilterRules) {
1487
- configXml += '<Filter><S3Key>'
1488
- for (const rule of c.Filter.Key.FilterRules) {
1489
- configXml += `<FilterRule><Name>${rule.Name}</Name><Value>${rule.Value}</Value></FilterRule>`
1490
- }
1491
- configXml += '</S3Key></Filter>'
1492
- }
1493
- configXml += '</CloudFunctionConfiguration>'
1494
- }
1495
- }
1496
-
1497
- if (config.TopicConfigurations) {
1498
- for (const c of config.TopicConfigurations) {
1499
- configXml += '<TopicConfiguration>'
1500
- if (c.Id) configXml += `<Id>${c.Id}</Id>`
1501
- configXml += `<Topic>${c.TopicArn}</Topic>`
1502
- for (const event of c.Events) {
1503
- configXml += `<Event>${event}</Event>`
1504
- }
1505
- configXml += '</TopicConfiguration>'
1506
- }
1507
- }
1508
-
1509
- if (config.QueueConfigurations) {
1510
- for (const c of config.QueueConfigurations) {
1511
- configXml += '<QueueConfiguration>'
1512
- if (c.Id) configXml += `<Id>${c.Id}</Id>`
1513
- configXml += `<Queue>${c.QueueArn}</Queue>`
1514
- for (const event of c.Events) {
1515
- configXml += `<Event>${event}</Event>`
1516
- }
1517
- configXml += '</QueueConfiguration>'
1518
- }
1519
- }
1520
-
1521
- const body = `<?xml version="1.0" encoding="UTF-8"?>
1522
- <NotificationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
1523
- ${configXml}
1524
- </NotificationConfiguration>`
1525
-
1526
- await this.client.request({
1527
- service: 's3',
1528
- region: this.region,
1529
- method: 'PUT',
1530
- path: `/${bucket}`,
1531
- queryParams: { notification: '' },
1532
- headers: { 'Content-Type': 'application/xml' },
1533
- body,
1534
- })
1535
- }
1536
-
1537
- /**
1538
- * Get bucket website configuration
1539
- */
1540
- async getBucketWebsite(bucket: string): Promise<any> {
1541
- try {
1542
- const result = await this.client.request({
1543
- service: 's3',
1544
- region: this.region,
1545
- method: 'GET',
1546
- path: `/${bucket}`,
1547
- queryParams: { website: '' },
1548
- })
1549
- return result?.WebsiteConfiguration
1550
- } catch (e: any) {
1551
- if (e.statusCode === 404) {
1552
- return null
1553
- }
1554
- throw e
1555
- }
1556
- }
1557
-
1558
- /**
1559
- * Put bucket website configuration
1560
- */
1561
- async putBucketWebsite(bucket: string, config: {
1562
- IndexDocument: string
1563
- ErrorDocument?: string
1564
- RedirectAllRequestsTo?: { HostName: string; Protocol?: string }
1565
- }): Promise<void> {
1566
- let configXml = ''
1567
-
1568
- if (config.RedirectAllRequestsTo) {
1569
- configXml = `<RedirectAllRequestsTo>
1570
- <HostName>${config.RedirectAllRequestsTo.HostName}</HostName>
1571
- ${config.RedirectAllRequestsTo.Protocol ? `<Protocol>${config.RedirectAllRequestsTo.Protocol}</Protocol>` : ''}
1572
- </RedirectAllRequestsTo>`
1573
- } else {
1574
- configXml = `<IndexDocument><Suffix>${config.IndexDocument}</Suffix></IndexDocument>`
1575
- if (config.ErrorDocument) {
1576
- configXml += `<ErrorDocument><Key>${config.ErrorDocument}</Key></ErrorDocument>`
1577
- }
1578
- }
1579
-
1580
- const body = `<?xml version="1.0" encoding="UTF-8"?>
1581
- <WebsiteConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
1582
- ${configXml}
1583
- </WebsiteConfiguration>`
1584
-
1585
- await this.client.request({
1586
- service: 's3',
1587
- region: this.region,
1588
- method: 'PUT',
1589
- path: `/${bucket}`,
1590
- queryParams: { website: '' },
1591
- headers: { 'Content-Type': 'application/xml' },
1592
- body,
1593
- })
1594
- }
1595
-
1596
- /**
1597
- * Delete bucket website configuration
1598
- */
1599
- async deleteBucketWebsite(bucket: string): Promise<void> {
1600
- await this.client.request({
1601
- service: 's3',
1602
- region: this.region,
1603
- method: 'DELETE',
1604
- path: `/${bucket}`,
1605
- queryParams: { website: '' },
1606
- })
1607
- }
1608
-
1609
- /**
1610
- * Get bucket replication configuration
1611
- */
1612
- async getBucketReplication(bucket: string): Promise<any> {
1613
- try {
1614
- const result = await this.client.request({
1615
- service: 's3',
1616
- region: this.region,
1617
- method: 'GET',
1618
- path: `/${bucket}`,
1619
- queryParams: { replication: '' },
1620
- })
1621
- return result?.ReplicationConfiguration
1622
- } catch (e: any) {
1623
- if (e.statusCode === 404) {
1624
- return null
1625
- }
1626
- throw e
1627
- }
1628
- }
1629
-
1630
- /**
1631
- * Delete bucket replication configuration
1632
- */
1633
- async deleteBucketReplication(bucket: string): Promise<void> {
1634
- await this.client.request({
1635
- service: 's3',
1636
- region: this.region,
1637
- method: 'DELETE',
1638
- path: `/${bucket}`,
1639
- queryParams: { replication: '' },
1640
- })
1641
- }
1642
-
1643
- /**
1644
- * Get public access block configuration
1645
- */
1646
- async getPublicAccessBlock(bucket: string): Promise<any> {
1647
- try {
1648
- const result = await this.client.request({
1649
- service: 's3',
1650
- region: this.region,
1651
- method: 'GET',
1652
- path: `/${bucket}`,
1653
- queryParams: { publicAccessBlock: '' },
1654
- })
1655
- return result?.PublicAccessBlockConfiguration
1656
- } catch (e: any) {
1657
- if (e.statusCode === 404) {
1658
- return null
1659
- }
1660
- throw e
1661
- }
1662
- }
1663
-
1664
- /**
1665
- * Put public access block configuration
1666
- */
1667
- async putPublicAccessBlock(bucket: string, config: {
1668
- BlockPublicAcls?: boolean
1669
- IgnorePublicAcls?: boolean
1670
- BlockPublicPolicy?: boolean
1671
- RestrictPublicBuckets?: boolean
1672
- }): Promise<void> {
1673
- const body = `<?xml version="1.0" encoding="UTF-8"?>
1674
- <PublicAccessBlockConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
1675
- <BlockPublicAcls>${config.BlockPublicAcls ?? true}</BlockPublicAcls>
1676
- <IgnorePublicAcls>${config.IgnorePublicAcls ?? true}</IgnorePublicAcls>
1677
- <BlockPublicPolicy>${config.BlockPublicPolicy ?? true}</BlockPublicPolicy>
1678
- <RestrictPublicBuckets>${config.RestrictPublicBuckets ?? true}</RestrictPublicBuckets>
1679
- </PublicAccessBlockConfiguration>`
1680
-
1681
- await this.client.request({
1682
- service: 's3',
1683
- region: this.region,
1684
- method: 'PUT',
1685
- path: `/${bucket}`,
1686
- queryParams: { publicAccessBlock: '' },
1687
- headers: { 'Content-Type': 'application/xml' },
1688
- body,
1689
- })
1690
- }
1691
-
1692
- /**
1693
- * Delete public access block configuration
1694
- */
1695
- async deletePublicAccessBlock(bucket: string): Promise<void> {
1696
- await this.client.request({
1697
- service: 's3',
1698
- region: this.region,
1699
- method: 'DELETE',
1700
- path: `/${bucket}`,
1701
- queryParams: { publicAccessBlock: '' },
1702
- })
1703
- }
1704
-
1705
- /**
1706
- * Generate a presigned URL for GET
1707
- */
1708
- generatePresignedGetUrl(bucket: string, key: string, expiresInSeconds: number = 3600): string {
1709
- const { accessKeyId, secretAccessKey } = this.getCredentials()
1710
- const host = `${bucket}.s3.${this.region}.amazonaws.com`
1711
- const now = new Date()
1712
- const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '')
1713
- const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, '')
1714
- const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`
1715
- const credential = `${accessKeyId}/${credentialScope}`
1716
-
1717
- const queryParams = new URLSearchParams({
1718
- 'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
1719
- 'X-Amz-Credential': credential,
1720
- 'X-Amz-Date': amzDate,
1721
- 'X-Amz-Expires': expiresInSeconds.toString(),
1722
- 'X-Amz-SignedHeaders': 'host',
1723
- })
1724
-
1725
- const canonicalRequest = [
1726
- 'GET',
1727
- `/${key}`,
1728
- queryParams.toString(),
1729
- `host:${host}\n`,
1730
- 'host',
1731
- 'UNSIGNED-PAYLOAD',
1732
- ].join('\n')
1733
-
1734
- const stringToSign = [
1735
- 'AWS4-HMAC-SHA256',
1736
- amzDate,
1737
- credentialScope,
1738
- crypto.createHash('sha256').update(canonicalRequest).digest('hex'),
1739
- ].join('\n')
1740
-
1741
- const kDate = crypto.createHmac('sha256', `AWS4${secretAccessKey}`).update(dateStamp).digest()
1742
- const kRegion = crypto.createHmac('sha256', kDate).update(this.region).digest()
1743
- const kService = crypto.createHmac('sha256', kRegion).update('s3').digest()
1744
- const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest()
1745
- const signature = crypto.createHmac('sha256', kSigning).update(stringToSign).digest('hex')
1746
-
1747
- queryParams.append('X-Amz-Signature', signature)
1748
-
1749
- return `https://${host}/${key}?${queryParams.toString()}`
1750
- }
1751
-
1752
- /**
1753
- * Generate a presigned URL for PUT
1754
- */
1755
- generatePresignedPutUrl(bucket: string, key: string, contentType: string, expiresInSeconds: number = 3600): string {
1756
- const { accessKeyId, secretAccessKey } = this.getCredentials()
1757
- const host = `${bucket}.s3.${this.region}.amazonaws.com`
1758
- const now = new Date()
1759
- const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '')
1760
- const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, '')
1761
- const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`
1762
- const credential = `${accessKeyId}/${credentialScope}`
1763
-
1764
- const queryParams = new URLSearchParams({
1765
- 'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
1766
- 'X-Amz-Credential': credential,
1767
- 'X-Amz-Date': amzDate,
1768
- 'X-Amz-Expires': expiresInSeconds.toString(),
1769
- 'X-Amz-SignedHeaders': 'content-type;host',
1770
- })
1771
-
1772
- const canonicalRequest = [
1773
- 'PUT',
1774
- `/${key}`,
1775
- queryParams.toString(),
1776
- `content-type:${contentType}\nhost:${host}\n`,
1777
- 'content-type;host',
1778
- 'UNSIGNED-PAYLOAD',
1779
- ].join('\n')
1780
-
1781
- const stringToSign = [
1782
- 'AWS4-HMAC-SHA256',
1783
- amzDate,
1784
- credentialScope,
1785
- crypto.createHash('sha256').update(canonicalRequest).digest('hex'),
1786
- ].join('\n')
1787
-
1788
- const kDate = crypto.createHmac('sha256', `AWS4${secretAccessKey}`).update(dateStamp).digest()
1789
- const kRegion = crypto.createHmac('sha256', kDate).update(this.region).digest()
1790
- const kService = crypto.createHmac('sha256', kRegion).update('s3').digest()
1791
- const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest()
1792
- const signature = crypto.createHmac('sha256', kSigning).update(stringToSign).digest('hex')
1793
-
1794
- queryParams.append('X-Amz-Signature', signature)
1795
-
1796
- return `https://${host}/${key}?${queryParams.toString()}`
1797
- }
1798
-
1799
- /**
1800
- * Initiate multipart upload
1801
- */
1802
- async createMultipartUpload(bucket: string, key: string, options?: {
1803
- contentType?: string
1804
- metadata?: Record<string, string>
1805
- }): Promise<{ UploadId: string }> {
1806
- const headers: Record<string, string> = {}
1807
- if (options?.contentType) {
1808
- headers['Content-Type'] = options.contentType
1809
- }
1810
- if (options?.metadata) {
1811
- for (const [k, v] of Object.entries(options.metadata)) {
1812
- headers[`x-amz-meta-${k}`] = v
1813
- }
1814
- }
1815
-
1816
- const result = await this.client.request({
1817
- service: 's3',
1818
- region: this.region,
1819
- method: 'POST',
1820
- path: `/${bucket}/${key}`,
1821
- queryParams: { uploads: '' },
1822
- headers,
1823
- })
1824
-
1825
- return { UploadId: result?.InitiateMultipartUploadResult?.UploadId }
1826
- }
1827
-
1828
- /**
1829
- * Upload a part in multipart upload
1830
- */
1831
- async uploadPart(bucket: string, key: string, uploadId: string, partNumber: number, body: Buffer): Promise<{ ETag: string }> {
1832
- const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials()
1833
- const host = `${bucket}.s3.${this.region}.amazonaws.com`
1834
- const url = `https://${host}/${key}?partNumber=${partNumber}&uploadId=${encodeURIComponent(uploadId)}`
1835
-
1836
- const now = new Date()
1837
- const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '')
1838
- const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, '')
1839
-
1840
- const payloadHash = crypto.createHash('sha256').update(body).digest('hex')
1841
-
1842
- const requestHeaders: Record<string, string> = {
1843
- 'host': host,
1844
- 'x-amz-date': amzDate,
1845
- 'x-amz-content-sha256': payloadHash,
1846
- }
1847
-
1848
- if (sessionToken) {
1849
- requestHeaders['x-amz-security-token'] = sessionToken
1850
- }
1851
-
1852
- const canonicalHeaders = Object.keys(requestHeaders)
1853
- .sort()
1854
- .map(k => `${k.toLowerCase()}:${requestHeaders[k].trim()}\n`)
1855
- .join('')
1856
-
1857
- const signedHeaders = Object.keys(requestHeaders)
1858
- .sort()
1859
- .map(k => k.toLowerCase())
1860
- .join(';')
1861
-
1862
- const canonicalRequest = [
1863
- 'PUT',
1864
- `/${key}`,
1865
- `partNumber=${partNumber}&uploadId=${encodeURIComponent(uploadId)}`,
1866
- canonicalHeaders,
1867
- signedHeaders,
1868
- payloadHash,
1869
- ].join('\n')
1870
-
1871
- const algorithm = 'AWS4-HMAC-SHA256'
1872
- const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`
1873
- const stringToSign = [
1874
- algorithm,
1875
- amzDate,
1876
- credentialScope,
1877
- crypto.createHash('sha256').update(canonicalRequest).digest('hex'),
1878
- ].join('\n')
1879
-
1880
- const kDate = crypto.createHmac('sha256', `AWS4${secretAccessKey}`).update(dateStamp).digest()
1881
- const kRegion = crypto.createHmac('sha256', kDate).update(this.region).digest()
1882
- const kService = crypto.createHmac('sha256', kRegion).update('s3').digest()
1883
- const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest()
1884
- const signature = crypto.createHmac('sha256', kSigning).update(stringToSign).digest('hex')
1885
-
1886
- const authHeader = `${algorithm} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
1887
-
1888
- const response = await fetch(url, {
1889
- method: 'PUT',
1890
- headers: {
1891
- ...requestHeaders,
1892
- 'Authorization': authHeader,
1893
- },
1894
- body,
1895
- })
1896
-
1897
- if (!response.ok) {
1898
- const text = await response.text()
1899
- throw new Error(`Upload part failed: ${response.status} ${text}`)
1900
- }
1901
-
1902
- return { ETag: response.headers.get('etag') || '' }
1903
- }
1904
-
1905
- /**
1906
- * Complete multipart upload
1907
- */
1908
- async completeMultipartUpload(bucket: string, key: string, uploadId: string, parts: Array<{ PartNumber: number; ETag: string }>): Promise<void> {
1909
- const partsXml = parts
1910
- .sort((a, b) => a.PartNumber - b.PartNumber)
1911
- .map(p => `<Part><PartNumber>${p.PartNumber}</PartNumber><ETag>${p.ETag}</ETag></Part>`)
1912
- .join('')
1913
-
1914
- const body = `<?xml version="1.0" encoding="UTF-8"?>
1915
- <CompleteMultipartUpload>${partsXml}</CompleteMultipartUpload>`
1916
-
1917
- await this.client.request({
1918
- service: 's3',
1919
- region: this.region,
1920
- method: 'POST',
1921
- path: `/${bucket}/${key}`,
1922
- queryParams: { uploadId },
1923
- headers: { 'Content-Type': 'application/xml' },
1924
- body,
1925
- })
1926
- }
1927
-
1928
- /**
1929
- * Abort multipart upload
1930
- */
1931
- async abortMultipartUpload(bucket: string, key: string, uploadId: string): Promise<void> {
1932
- await this.client.request({
1933
- service: 's3',
1934
- region: this.region,
1935
- method: 'DELETE',
1936
- path: `/${bucket}/${key}`,
1937
- queryParams: { uploadId },
1938
- })
1939
- }
1940
-
1941
- /**
1942
- * List multipart uploads
1943
- */
1944
- async listMultipartUploads(bucket: string): Promise<Array<{ Key: string; UploadId: string; Initiated: string }>> {
1945
- const result = await this.client.request({
1946
- service: 's3',
1947
- region: this.region,
1948
- method: 'GET',
1949
- path: `/${bucket}`,
1950
- queryParams: { uploads: '' },
1951
- })
1952
-
1953
- const uploads = result?.ListMultipartUploadsResult?.Upload
1954
- if (!uploads) return []
1955
- const list = Array.isArray(uploads) ? uploads : [uploads]
1956
- return list.map((u: any) => ({
1957
- Key: u.Key,
1958
- UploadId: u.UploadId,
1959
- Initiated: u.Initiated,
1960
- }))
1961
- }
1962
-
1963
- /**
1964
- * Restore object from Glacier
1965
- */
1966
- async restoreObject(bucket: string, key: string, days: number, tier: 'Standard' | 'Bulk' | 'Expedited' = 'Standard'): Promise<void> {
1967
- const body = `<?xml version="1.0" encoding="UTF-8"?>
1968
- <RestoreRequest>
1969
- <Days>${days}</Days>
1970
- <GlacierJobParameters>
1971
- <Tier>${tier}</Tier>
1972
- </GlacierJobParameters>
1973
- </RestoreRequest>`
1974
-
1975
- await this.client.request({
1976
- service: 's3',
1977
- region: this.region,
1978
- method: 'POST',
1979
- path: `/${bucket}/${key}`,
1980
- queryParams: { restore: '' },
1981
- headers: { 'Content-Type': 'application/xml' },
1982
- body,
1983
- })
1984
- }
1985
-
1986
- /**
1987
- * Select object content (S3 Select)
1988
- */
1989
- async selectObjectContent(bucket: string, key: string, expression: string, inputFormat: 'CSV' | 'JSON' | 'Parquet', outputFormat: 'CSV' | 'JSON' = 'JSON'): Promise<string> {
1990
- let inputSerialization = ''
1991
- if (inputFormat === 'CSV') {
1992
- inputSerialization = '<CSV><FileHeaderInfo>USE</FileHeaderInfo></CSV>'
1993
- } else if (inputFormat === 'JSON') {
1994
- inputSerialization = '<JSON><Type>DOCUMENT</Type></JSON>'
1995
- } else {
1996
- inputSerialization = '<Parquet/>'
1997
- }
1998
-
1999
- let outputSerialization = ''
2000
- if (outputFormat === 'CSV') {
2001
- outputSerialization = '<CSV/>'
2002
- } else {
2003
- outputSerialization = '<JSON/>'
2004
- }
2005
-
2006
- const body = `<?xml version="1.0" encoding="UTF-8"?>
2007
- <SelectObjectContentRequest>
2008
- <Expression>${expression}</Expression>
2009
- <ExpressionType>SQL</ExpressionType>
2010
- <InputSerialization>${inputSerialization}</InputSerialization>
2011
- <OutputSerialization>${outputSerialization}</OutputSerialization>
2012
- </SelectObjectContentRequest>`
2013
-
2014
- const result = await this.client.request({
2015
- service: 's3',
2016
- region: this.region,
2017
- method: 'POST',
2018
- path: `/${bucket}/${key}`,
2019
- queryParams: { select: '', 'select-type': '2' },
2020
- headers: { 'Content-Type': 'application/xml' },
2021
- body,
2022
- rawResponse: true,
2023
- })
2024
-
2025
- return result
2026
- }
2027
-
2028
- /**
2029
- * Generate a presigned URL for S3 object access
2030
- * Allows temporary access to private objects without authentication
2031
- */
2032
- async getSignedUrl(options: {
2033
- bucket: string
2034
- key: string
2035
- expiresIn?: number
2036
- operation?: 'getObject' | 'putObject'
2037
- }): Promise<string> {
2038
- const { bucket, key, expiresIn = 3600, operation = 'getObject' } = options
2039
- const { accessKeyId, secretAccessKey, sessionToken } = this.getCredentials()
2040
-
2041
- const now = new Date()
2042
- const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '')
2043
- const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, '')
2044
-
2045
- const host = `${bucket}.s3.${this.region}.amazonaws.com`
2046
- const method = operation === 'putObject' ? 'PUT' : 'GET'
2047
- const algorithm = 'AWS4-HMAC-SHA256'
2048
- const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`
2049
- const credential = `${accessKeyId}/${credentialScope}`
2050
-
2051
- // Build query parameters
2052
- const queryParams: Record<string, string> = {
2053
- 'X-Amz-Algorithm': algorithm,
2054
- 'X-Amz-Credential': credential,
2055
- 'X-Amz-Date': amzDate,
2056
- 'X-Amz-Expires': expiresIn.toString(),
2057
- 'X-Amz-SignedHeaders': 'host',
2058
- }
2059
-
2060
- if (sessionToken) {
2061
- queryParams['X-Amz-Security-Token'] = sessionToken
2062
- }
2063
-
2064
- // Sort and encode query string
2065
- const sortedParams = Object.keys(queryParams).sort()
2066
- const canonicalQuerystring = sortedParams
2067
- .map(k => `${encodeURIComponent(k)}=${encodeURIComponent(queryParams[k])}`)
2068
- .join('&')
2069
-
2070
- // Canonical request
2071
- const canonicalUri = '/' + key
2072
- const canonicalHeaders = `host:${host}\n`
2073
- const signedHeaders = 'host'
2074
- const payloadHash = 'UNSIGNED-PAYLOAD'
2075
-
2076
- const canonicalRequest = [
2077
- method,
2078
- canonicalUri,
2079
- canonicalQuerystring,
2080
- canonicalHeaders,
2081
- signedHeaders,
2082
- payloadHash,
2083
- ].join('\n')
2084
-
2085
- // String to sign
2086
- const stringToSign = [
2087
- algorithm,
2088
- amzDate,
2089
- credentialScope,
2090
- crypto.createHash('sha256').update(canonicalRequest).digest('hex'),
2091
- ].join('\n')
2092
-
2093
- // Calculate signature
2094
- const kDate = crypto.createHmac('sha256', `AWS4${secretAccessKey}`).update(dateStamp).digest()
2095
- const kRegion = crypto.createHmac('sha256', kDate).update(this.region).digest()
2096
- const kService = crypto.createHmac('sha256', kRegion).update('s3').digest()
2097
- const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest()
2098
- const signature = crypto.createHmac('sha256', kSigning).update(stringToSign).digest('hex')
2099
-
2100
- // Build presigned URL
2101
- const presignedUrl = `https://${host}${canonicalUri}?${canonicalQuerystring}&X-Amz-Signature=${signature}`
2102
-
2103
- return presignedUrl
2104
- }
2105
-
2106
- /**
2107
- * List objects in a bucket with pagination support
2108
- */
2109
- async listObjects(options: {
2110
- bucket: string
2111
- prefix?: string
2112
- maxKeys?: number
2113
- continuationToken?: string
2114
- }): Promise<{
2115
- objects: S3Object[]
2116
- nextContinuationToken?: string
2117
- }> {
2118
- const { bucket, prefix, maxKeys = 1000, continuationToken } = options
2119
-
2120
- // Build query parameters for ListObjectsV2
2121
- const queryParams: Record<string, string> = {
2122
- 'list-type': '2',
2123
- 'max-keys': maxKeys.toString(),
2124
- }
2125
-
2126
- if (prefix) queryParams.prefix = prefix
2127
- if (continuationToken) queryParams['continuation-token'] = continuationToken
2128
-
2129
- const result = await this.client.request({
2130
- service: 's3',
2131
- region: this.region,
2132
- method: 'GET',
2133
- path: `/${bucket}`,
2134
- queryParams,
2135
- })
2136
-
2137
- // Parse S3 XML response
2138
- const objects: S3Object[] = []
2139
- const listResult = result?.ListBucketResult
2140
-
2141
- if (listResult?.Contents) {
2142
- const items = Array.isArray(listResult.Contents) ? listResult.Contents : [listResult.Contents]
2143
- for (const item of items) {
2144
- objects.push({
2145
- Key: item.Key || '',
2146
- LastModified: item.LastModified || '',
2147
- Size: Number.parseInt(item.Size || '0'),
2148
- ETag: item.ETag,
2149
- })
2150
- }
2151
- }
2152
-
2153
- return {
2154
- objects,
2155
- nextContinuationToken: listResult?.NextContinuationToken,
2156
- }
2157
- }
2158
-
2159
- /**
2160
- * Empty a bucket by deleting all objects (required before bucket deletion)
2161
- */
2162
- async emptyBucket(bucket: string): Promise<{ deletedCount: number }> {
2163
- let deletedCount = 0
2164
-
2165
- // List all objects in the bucket (handles pagination internally)
2166
- const objects = await this.listAllObjects({ bucket })
2167
-
2168
- if (objects.length > 0) {
2169
- // Delete objects in batches of 1000
2170
- for (let i = 0; i < objects.length; i += 1000) {
2171
- const batch = objects.slice(i, i + 1000)
2172
- const keys = batch.map((obj: S3Object) => obj.Key)
2173
- await this.deleteObjects(bucket, keys)
2174
- deletedCount += keys.length
2175
- }
2176
- }
2177
-
2178
- // Also delete any object versions if versioning is enabled
2179
- try {
2180
- let keyMarker: string | undefined
2181
- let versionIdMarker: string | undefined
2182
-
2183
- do {
2184
- const versionsResult = await this.listObjectVersions({
2185
- bucket,
2186
- keyMarker,
2187
- versionIdMarker,
2188
- maxKeys: 1000,
2189
- })
2190
-
2191
- const versionsToDelete: Array<{ Key: string; VersionId?: string }> = []
2192
-
2193
- if (versionsResult.versions) {
2194
- for (const version of versionsResult.versions) {
2195
- versionsToDelete.push({ Key: version.Key, VersionId: version.VersionId })
2196
- }
2197
- }
2198
-
2199
- if (versionsResult.deleteMarkers) {
2200
- for (const marker of versionsResult.deleteMarkers) {
2201
- versionsToDelete.push({ Key: marker.Key, VersionId: marker.VersionId })
2202
- }
2203
- }
2204
-
2205
- if (versionsToDelete.length > 0) {
2206
- await this.deleteObjectVersions(bucket, versionsToDelete)
2207
- deletedCount += versionsToDelete.length
2208
- }
2209
-
2210
- keyMarker = versionsResult.nextKeyMarker
2211
- versionIdMarker = versionsResult.nextVersionIdMarker
2212
- } while (keyMarker)
2213
- }
2214
- catch {
2215
- // Versioning might not be enabled, ignore errors
2216
- }
2217
-
2218
- return { deletedCount }
2219
- }
2220
-
2221
- /**
2222
- * List object versions in a bucket
2223
- */
2224
- async listObjectVersions(options: {
2225
- bucket: string
2226
- prefix?: string
2227
- keyMarker?: string
2228
- versionIdMarker?: string
2229
- maxKeys?: number
2230
- }): Promise<{
2231
- versions: Array<{ Key: string; VersionId: string; IsLatest: boolean }>
2232
- deleteMarkers: Array<{ Key: string; VersionId: string; IsLatest: boolean }>
2233
- nextKeyMarker?: string
2234
- nextVersionIdMarker?: string
2235
- }> {
2236
- const { bucket, prefix, keyMarker, versionIdMarker, maxKeys = 1000 } = options
2237
-
2238
- const queryParams: Record<string, string> = {
2239
- versions: '',
2240
- 'max-keys': maxKeys.toString(),
2241
- }
2242
-
2243
- if (prefix) queryParams.prefix = prefix
2244
- if (keyMarker) queryParams['key-marker'] = keyMarker
2245
- if (versionIdMarker) queryParams['version-id-marker'] = versionIdMarker
2246
-
2247
- const result = await this.client.request({
2248
- service: 's3',
2249
- region: this.region,
2250
- method: 'GET',
2251
- path: `/${bucket}`,
2252
- queryParams,
2253
- })
2254
-
2255
- const versions: Array<{ Key: string; VersionId: string; IsLatest: boolean }> = []
2256
- const deleteMarkers: Array<{ Key: string; VersionId: string; IsLatest: boolean }> = []
2257
-
2258
- // Parse versions
2259
- if (result.Version) {
2260
- const versionList = Array.isArray(result.Version) ? result.Version : [result.Version]
2261
- for (const v of versionList) {
2262
- versions.push({
2263
- Key: v.Key,
2264
- VersionId: v.VersionId,
2265
- IsLatest: v.IsLatest === 'true',
2266
- })
2267
- }
2268
- }
2269
-
2270
- // Parse delete markers
2271
- if (result.DeleteMarker) {
2272
- const markerList = Array.isArray(result.DeleteMarker) ? result.DeleteMarker : [result.DeleteMarker]
2273
- for (const m of markerList) {
2274
- deleteMarkers.push({
2275
- Key: m.Key,
2276
- VersionId: m.VersionId,
2277
- IsLatest: m.IsLatest === 'true',
2278
- })
2279
- }
2280
- }
2281
-
2282
- return {
2283
- versions,
2284
- deleteMarkers,
2285
- nextKeyMarker: result.NextKeyMarker,
2286
- nextVersionIdMarker: result.NextVersionIdMarker,
2287
- }
2288
- }
2289
-
2290
- /**
2291
- * Delete specific object versions
2292
- */
2293
- async deleteObjectVersions(
2294
- bucket: string,
2295
- objects: Array<{ Key: string; VersionId?: string }>,
2296
- ): Promise<void> {
2297
- const deleteXml = `<?xml version="1.0" encoding="UTF-8"?>
2298
- <Delete>
2299
- <Quiet>true</Quiet>
2300
- ${objects.map(obj => `<Object><Key>${obj.Key}</Key>${obj.VersionId ? `<VersionId>${obj.VersionId}</VersionId>` : ''}</Object>`).join('\n ')}
2301
- </Delete>`
2302
-
2303
- const contentMd5 = crypto.createHash('md5').update(deleteXml).digest('base64')
2304
-
2305
- await this.client.request({
2306
- service: 's3',
2307
- region: this.region,
2308
- method: 'POST',
2309
- path: `/${bucket}`,
2310
- queryParams: { delete: '' },
2311
- body: deleteXml,
2312
- headers: {
2313
- 'Content-Type': 'application/xml',
2314
- 'Content-MD5': contentMd5,
2315
- },
2316
- })
2317
- }
2318
- }