@gugananuvem/aws-local-simulator 1.0.12 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +235 -11
  2. package/package.json +12 -2
  3. package/src/config/default-config.js +1 -0
  4. package/src/index.js +18 -2
  5. package/src/server.js +36 -32
  6. package/src/services/apigateway/index.js +5 -0
  7. package/src/services/apigateway/server.js +20 -0
  8. package/src/services/apigateway/simulator.js +13 -3
  9. package/src/services/athena/index.js +75 -0
  10. package/src/services/athena/server.js +101 -0
  11. package/src/services/athena/simulador.js +998 -0
  12. package/src/services/athena/simulator.js +346 -0
  13. package/src/services/cloudformation/index.js +106 -0
  14. package/src/services/cloudformation/server.js +417 -0
  15. package/src/services/cloudformation/simulador.js +1045 -0
  16. package/src/services/cloudtrail/index.js +84 -0
  17. package/src/services/cloudtrail/server.js +235 -0
  18. package/src/services/cloudtrail/simulador.js +719 -0
  19. package/src/services/cloudwatch/index.js +84 -0
  20. package/src/services/cloudwatch/server.js +366 -0
  21. package/src/services/cloudwatch/simulador.js +1173 -0
  22. package/src/services/cognito/index.js +5 -0
  23. package/src/services/cognito/simulator.js +4 -0
  24. package/src/services/config/index.js +96 -0
  25. package/src/services/config/server.js +215 -0
  26. package/src/services/config/simulador.js +1260 -0
  27. package/src/services/dynamodb/index.js +7 -3
  28. package/src/services/dynamodb/server.js +4 -2
  29. package/src/services/dynamodb/simulator.js +39 -29
  30. package/src/services/eventbridge/index.js +55 -51
  31. package/src/services/eventbridge/server.js +209 -0
  32. package/src/services/eventbridge/simulator.js +684 -0
  33. package/src/services/index.js +30 -4
  34. package/src/services/kms/index.js +75 -0
  35. package/src/services/kms/server.js +67 -0
  36. package/src/services/kms/simulator.js +324 -0
  37. package/src/services/lambda/index.js +5 -0
  38. package/src/services/lambda/simulator.js +48 -38
  39. package/src/services/parameter-store/index.js +80 -0
  40. package/src/services/parameter-store/server.js +50 -0
  41. package/src/services/parameter-store/simulator.js +201 -0
  42. package/src/services/s3/index.js +7 -3
  43. package/src/services/s3/server.js +20 -13
  44. package/src/services/s3/simulator.js +163 -407
  45. package/src/services/secret-manager/index.js +80 -0
  46. package/src/services/secret-manager/server.js +50 -0
  47. package/src/services/secret-manager/simulator.js +171 -0
  48. package/src/services/sns/index.js +55 -42
  49. package/src/services/sns/server.js +580 -0
  50. package/src/services/sns/simulator.js +1482 -0
  51. package/src/services/sqs/index.js +2 -4
  52. package/src/services/sqs/server.js +4 -2
  53. package/src/services/xray/index.js +83 -0
  54. package/src/services/xray/server.js +308 -0
  55. package/src/services/xray/simulador.js +994 -0
  56. package/src/utils/cloudtrail-audit.js +129 -0
  57. package/src/utils/local-store.js +18 -2
@@ -3,9 +3,11 @@
3
3
  */
4
4
 
5
5
  const crypto = require("crypto");
6
+ const fs = require("fs");
7
+ const path = require("path");
6
8
  const LocalStore = require("../../utils/local-store");
7
9
  const logger = require("../../utils/logger");
8
- const path = require("path");
10
+ const { CloudTrailAudit } = require("../../utils/cloudtrail-audit");
9
11
 
10
12
  class S3Simulator {
11
13
  constructor(config) {
@@ -13,6 +15,7 @@ class S3Simulator {
13
15
  this.dataDir = path.join(process.env.AWS_LOCAL_SIMULATOR_DATA_DIR, "s3");
14
16
  this.store = new LocalStore(this.dataDir);
15
17
  this.buckets = new Map();
18
+ this.audit = new CloudTrailAudit("s3.amazonaws.com");
16
19
  }
17
20
 
18
21
  async initialize() {
@@ -22,22 +25,31 @@ class S3Simulator {
22
25
  }
23
26
 
24
27
  loadBuckets() {
25
- // Carrega buckets da configuração
26
28
  if (this.config.s3?.buckets) {
27
29
  for (const bucketName of this.config.s3.buckets) {
28
30
  this.createBucket(bucketName);
29
31
  }
30
32
  }
31
33
 
32
- // Carrega buckets existentes do disco
33
34
  const savedBuckets = this.store.read("__buckets__");
34
- if (savedBuckets) {
35
+ if (savedBuckets && typeof savedBuckets === "object" && !Array.isArray(savedBuckets)) {
35
36
  for (const [name, data] of Object.entries(savedBuckets)) {
36
37
  if (!this.buckets.has(name)) {
38
+ const objects = new Map();
39
+ for (const [key, meta] of Object.entries(data.objects || {})) {
40
+ objects.set(key, {
41
+ key: meta.key,
42
+ size: meta.size,
43
+ etag: meta.etag,
44
+ contentType: meta.contentType,
45
+ metadata: meta.metadata,
46
+ lastModified: new Date(meta.lastModified),
47
+ });
48
+ }
37
49
  this.buckets.set(name, {
38
50
  name,
39
51
  creationDate: new Date(data.creationDate),
40
- objects: new Map(Object.entries(data.objects || {})),
52
+ objects,
41
53
  objectCount: data.objectCount || 0,
42
54
  totalSize: data.totalSize || 0,
43
55
  });
@@ -46,6 +58,39 @@ class S3Simulator {
46
58
  }
47
59
  }
48
60
 
61
+ // ─── Helpers de conteúdo em arquivo ───────────────────────────────────────
62
+
63
+ _objectFilePath(bucketName, key) {
64
+ const safePath = key.split("/").map((part) =>
65
+ part.replace(/[<>:"|?*\\]/g, "_")
66
+ ).join(path.sep);
67
+ return path.join(this.dataDir, bucketName, safePath);
68
+ }
69
+
70
+ _writeObjectContent(bucketName, key, content) {
71
+ const filePath = this._objectFilePath(bucketName, key);
72
+ const dir = path.dirname(filePath);
73
+ if (!fs.existsSync(dir)) {
74
+ require("mkdirp").sync(dir);
75
+ }
76
+ fs.writeFileSync(filePath, content);
77
+ }
78
+
79
+ _readObjectContent(bucketName, key) {
80
+ const filePath = this._objectFilePath(bucketName, key);
81
+ if (!fs.existsSync(filePath)) return null;
82
+ return fs.readFileSync(filePath);
83
+ }
84
+
85
+ _deleteObjectContent(bucketName, key) {
86
+ const filePath = this._objectFilePath(bucketName, key);
87
+ if (fs.existsSync(filePath)) {
88
+ fs.unlinkSync(filePath);
89
+ }
90
+ }
91
+
92
+ // ─── Buckets ──────────────────────────────────────────────────────────────
93
+
49
94
  createBucket(bucketName) {
50
95
  if (!this.isValidBucketName(bucketName)) {
51
96
  return { error: { code: "InvalidBucketName", message: "Bucket name is invalid" }, status: 400 };
@@ -65,185 +110,27 @@ class S3Simulator {
65
110
 
66
111
  this.buckets.set(bucketName, bucket);
67
112
  this.persistBuckets();
68
-
69
113
  logger.debug(`✅ Bucket S3 criado: ${bucketName}`);
70
-
114
+ this.audit.record({ eventName: "CreateBucket", readOnly: false, resources: [{ ARN: `arn:aws:s3:::${bucketName}`, type: "AWS::S3::Bucket" }], requestParameters: { bucketName } });
71
115
  return { bucket };
72
116
  }
73
117
 
74
118
  deleteBucket(bucketName) {
75
119
  const bucket = this.buckets.get(bucketName);
76
-
77
120
  if (!bucket) {
78
121
  return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
79
122
  }
80
-
81
123
  if (bucket.objects.size > 0) {
82
124
  return { error: { code: "BucketNotEmpty", message: "Bucket is not empty" }, status: 409 };
83
125
  }
84
-
85
126
  this.buckets.delete(bucketName);
86
127
  this.store.delete(bucketName);
87
128
  this.persistBuckets();
88
-
89
129
  logger.debug(`🗑️ Bucket S3 deletado: ${bucketName}`);
90
-
91
- return { success: true };
92
- }
93
-
94
- putObject(bucketName, key, content, headers) {
95
- const bucket = this.buckets.get(bucketName);
96
-
97
- if (!bucket) {
98
- return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
99
- }
100
-
101
- // Normaliza o conteúdo
102
- let body = content;
103
- if (Buffer.isBuffer(content)) {
104
- body = content;
105
- } else if (typeof content === "object") {
106
- body = Buffer.from(JSON.stringify(content));
107
- } else if (typeof content === "string") {
108
- body = Buffer.from(content);
109
- }
110
-
111
- const contentType = headers["content-type"] || "application/octet-stream";
112
- const metadata = this.extractMetadata(headers);
113
- const etag = crypto.createHash("md5").update(body).digest("hex");
114
-
115
- const object = {
116
- key,
117
- size: body.length,
118
- etag,
119
- contentType,
120
- metadata,
121
- lastModified: new Date(),
122
- content: body,
123
- };
124
-
125
- // Atualiza ou adiciona objeto
126
- const oldObject = bucket.objects.get(key);
127
- if (oldObject) {
128
- bucket.totalSize -= oldObject.size;
129
- } else {
130
- bucket.objectCount++;
131
- }
132
-
133
- bucket.objects.set(key, object);
134
- bucket.totalSize += body.length;
135
-
136
- this.persistBucket(bucketName);
137
-
138
- logger.verboso(`📤 Upload S3: ${bucketName}/${key} (${body.length} bytes)`);
139
-
140
- return { etag };
141
- }
142
-
143
- getObject(bucketName, key, headers) {
144
- const bucket = this.buckets.get(bucketName);
145
-
146
- if (!bucket) {
147
- return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
148
- }
149
-
150
- const object = bucket.objects.get(key);
151
- if (!object) {
152
- return { error: { code: "NoSuchKey", message: "The specified key does not exist" }, status: 404 };
153
- }
154
-
155
- let content = object.content;
156
- let start = 0;
157
- let end = content.length - 1;
158
-
159
- // Suporte a Range headers
160
- if (headers.range) {
161
- const range = headers.range.match(/bytes=(\d+)-(\d+)?/);
162
- if (range) {
163
- start = parseInt(range[1], 10);
164
- end = range[2] ? parseInt(range[2], 10) : content.length - 1;
165
- content = content.slice(start, end + 1);
166
- }
167
- }
168
-
169
- return {
170
- content,
171
- etag: object.etag,
172
- lastModified: object.lastModified.toUTCString(),
173
- contentType: object.contentType,
174
- size: content.length,
175
- metadata: object.metadata,
176
- };
177
- }
178
-
179
- deleteObject(bucketName, key) {
180
- const bucket = this.buckets.get(bucketName);
181
-
182
- if (!bucket) {
183
- return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
184
- }
185
-
186
- const object = bucket.objects.get(key);
187
- if (object) {
188
- bucket.objects.delete(key);
189
- bucket.objectCount--;
190
- bucket.totalSize -= object.size;
191
- this.persistBucket(bucketName);
192
- logger.verboso(`🗑️ Delete S3: ${bucketName}/${key}`);
193
- }
194
-
130
+ this.audit.record({ eventName: "DeleteBucket", readOnly: false, resources: [{ ARN: `arn:aws:s3:::${bucketName}`, type: "AWS::S3::Bucket" }] });
195
131
  return { success: true };
196
132
  }
197
133
 
198
- listObjects(bucketName, options = {}) {
199
- const bucket = this.buckets.get(bucketName);
200
-
201
- if (!bucket) {
202
- return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
203
- }
204
-
205
- const { prefix = "", delimiter, maxKeys = 1000 } = options;
206
-
207
- let objects = Array.from(bucket.objects.values())
208
- .filter((obj) => obj.key.startsWith(prefix))
209
- .sort((a, b) => a.key.localeCompare(b.key));
210
-
211
- const commonPrefixes = new Set();
212
-
213
- if (delimiter) {
214
- const filteredObjects = [];
215
- for (const obj of objects) {
216
- const afterPrefix = obj.key.substring(prefix.length);
217
- const delimiterIndex = afterPrefix.indexOf(delimiter);
218
-
219
- if (delimiterIndex !== -1) {
220
- const prefixPath = prefix + afterPrefix.substring(0, delimiterIndex + 1);
221
- commonPrefixes.add(prefixPath);
222
- } else {
223
- filteredObjects.push(obj);
224
- }
225
- }
226
- objects = filteredObjects;
227
- }
228
-
229
- const contents = objects.slice(0, maxKeys).map((obj) => ({
230
- Key: obj.key,
231
- LastModified: obj.lastModified.toISOString(),
232
- ETag: `"${obj.etag}"`,
233
- Size: obj.size,
234
- StorageClass: "STANDARD",
235
- }));
236
-
237
- return {
238
- name: bucketName,
239
- prefix,
240
- maxKeys,
241
- isTruncated: objects.length > maxKeys,
242
- contents,
243
- commonPrefixes: Array.from(commonPrefixes),
244
- };
245
- }
246
-
247
134
  listBuckets() {
248
135
  return Array.from(this.buckets.values()).map((bucket) => ({
249
136
  Name: bucket.name,
@@ -265,7 +152,6 @@ class S3Simulator {
265
152
  if (!bucket) {
266
153
  return { error: { code: "NoSuchBucket", message: "Bucket not found" } };
267
154
  }
268
-
269
155
  return {
270
156
  name: bucket.name,
271
157
  creationDate: bucket.creationDate,
@@ -282,78 +168,8 @@ class S3Simulator {
282
168
  };
283
169
  }
284
170
 
285
- clearBucket(bucketName) {
286
- const bucket = this.buckets.get(bucketName);
287
- if (bucket) {
288
- bucket.objects.clear();
289
- bucket.objectCount = 0;
290
- bucket.totalSize = 0;
291
- this.persistBucket(bucketName);
292
- }
293
- }
294
-
295
- getBucketsCount() {
296
- return this.buckets.size;
297
- }
298
-
299
- isValidBucketName(bucketName) {
300
- const regex = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
301
- return regex.test(bucketName) && !bucketName.includes("..") && !bucketName.includes(".-") && !bucketName.includes("-.");
302
- }
303
-
304
- extractMetadata(headers) {
305
- const metadata = {};
306
- for (const [key, value] of Object.entries(headers)) {
307
- if (key.startsWith("x-amz-meta-")) {
308
- const metaKey = key.replace("x-amz-meta-", "");
309
- metadata[metaKey] = value;
310
- }
311
- }
312
- return metadata;
313
- }
314
- persistBuckets() {
315
- const bucketsObj = {};
316
- for (const [name, bucket] of this.buckets.entries()) {
317
- const objectsObj = {};
318
- for (const [key, obj] of bucket.objects.entries()) {
319
- objectsObj[key] = {
320
- key: obj.key,
321
- size: obj.size,
322
- etag: obj.etag,
323
- contentType: obj.contentType,
324
- metadata: obj.metadata,
325
- lastModified: obj.lastModified,
326
- content: obj.content,
327
- };
328
- }
329
-
330
- bucketsObj[name] = {
331
- creationDate: bucket.creationDate.toISOString(),
332
- objects: objectsObj,
333
- objectCount: bucket.objectCount,
334
- totalSize: bucket.totalSize,
335
- };
336
- }
337
- this.store.write("__buckets__", bucketsObj);
338
- }
339
-
340
- isValidBucketName(bucketName) {
341
- const regex = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
342
- return regex.test(bucketName) && !bucketName.includes("..") && !bucketName.includes(".-") && !bucketName.includes("-.");
343
- }
344
-
345
- async reset() {
346
- for (const [name] of this.buckets) {
347
- const bucket = this.buckets.get(name);
348
- if (bucket) {
349
- bucket.objects.clear();
350
- bucket.objectCount = 0;
351
- bucket.totalSize = 0;
352
- this.store.write(name, {});
353
- }
354
- }
355
- this.persistBuckets();
356
- logger.debug("S3: Todos os dados resetados");
171
+ getBucket(bucketName) {
172
+ return this.buckets.get(bucketName);
357
173
  }
358
174
 
359
175
  getBucketsCount() {
@@ -368,81 +184,23 @@ class S3Simulator {
368
184
  return total;
369
185
  }
370
186
 
371
- getBucket(bucketName) {
372
- return this.buckets.get(bucketName);
373
- }
374
-
375
- listBuckets() {
376
- return Array.from(this.buckets.values()).map((bucket) => ({
377
- Name: bucket.name,
378
- CreationDate: bucket.creationDate.toISOString(),
379
- }));
380
- }
381
-
382
- getBucketsInfo() {
383
- return Array.from(this.buckets.values()).map((bucket) => ({
384
- name: bucket.name,
385
- creationDate: bucket.creationDate,
386
- objectCount: bucket.objectCount,
387
- totalSize: bucket.totalSize,
388
- }));
389
- }
390
-
391
- getBucketInfo(bucketName) {
187
+ clearBucket(bucketName) {
392
188
  const bucket = this.buckets.get(bucketName);
393
- if (!bucket) {
394
- return { error: { code: "NoSuchBucket", message: "Bucket not found" } };
189
+ if (bucket) {
190
+ for (const key of bucket.objects.keys()) {
191
+ this._deleteObjectContent(bucketName, key);
192
+ }
193
+ bucket.objects.clear();
194
+ bucket.objectCount = 0;
195
+ bucket.totalSize = 0;
196
+ this.persistBucket(bucketName);
395
197
  }
396
-
397
- return {
398
- name: bucket.name,
399
- creationDate: bucket.creationDate,
400
- objectCount: bucket.objectCount,
401
- totalSize: bucket.totalSize,
402
- objects: Array.from(bucket.objects.values())
403
- .slice(0, 20)
404
- .map((obj) => ({
405
- key: obj.key,
406
- size: obj.size,
407
- etag: obj.etag,
408
- lastModified: obj.lastModified,
409
- })),
410
- };
411
198
  }
412
199
 
413
- listAllObjects(bucketName) {
414
- const bucket = this.buckets.get(bucketName);
415
- if (!bucket) return [];
416
- return Array.from(bucket.objects.values()).map((obj) => ({
417
- key: obj.key,
418
- size: obj.size,
419
- etag: obj.etag,
420
- lastModified: obj.lastModified,
421
- }));
422
- }
423
-
424
- headObject(bucketName, key) {
425
- const bucket = this.buckets.get(bucketName);
426
- if (!bucket) {
427
- return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
428
- }
429
-
430
- const object = bucket.objects.get(key);
431
- if (!object) {
432
- return { error: { code: "NoSuchKey", message: "Key does not exist" }, status: 404 };
433
- }
434
-
435
- return {
436
- etag: object.etag,
437
- lastModified: object.lastModified.toUTCString(),
438
- contentType: object.contentType,
439
- size: object.size,
440
- };
441
- }
200
+ // ─── Objects ──────────────────────────────────────────────────────────────
442
201
 
443
202
  putObject(bucketName, key, content, headers) {
444
203
  const bucket = this.buckets.get(bucketName);
445
-
446
204
  if (!bucket) {
447
205
  return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
448
206
  }
@@ -460,16 +218,6 @@ class S3Simulator {
460
218
  const metadata = this.extractMetadata(headers);
461
219
  const etag = crypto.createHash("md5").update(body).digest("hex");
462
220
 
463
- const object = {
464
- key,
465
- size: body.length,
466
- etag,
467
- contentType,
468
- metadata,
469
- lastModified: new Date(),
470
- content: body,
471
- };
472
-
473
221
  const oldObject = bucket.objects.get(key);
474
222
  if (oldObject) {
475
223
  bucket.totalSize -= oldObject.size;
@@ -477,19 +225,27 @@ class S3Simulator {
477
225
  bucket.objectCount++;
478
226
  }
479
227
 
480
- bucket.objects.set(key, object);
228
+ // Apenas metadados no Map — conteúdo vai para arquivo
229
+ bucket.objects.set(key, {
230
+ key,
231
+ size: body.length,
232
+ etag,
233
+ contentType,
234
+ metadata,
235
+ lastModified: new Date(),
236
+ });
481
237
  bucket.totalSize += body.length;
482
238
 
239
+ this._writeObjectContent(bucketName, key, body);
483
240
  this.persistBucket(bucketName);
484
241
 
485
242
  logger.verboso(`📤 Upload S3: ${bucketName}/${key} (${body.length} bytes)`);
486
-
243
+ this.audit.record({ eventName: "PutObject", readOnly: false, isDataEvent: true, resources: [{ ARN: `arn:aws:s3:::${bucketName}/${key}`, type: "AWS::S3::Object" }], requestParameters: { bucketName, key, contentType }, responseElements: { etag } });
487
244
  return { etag };
488
245
  }
489
246
 
490
247
  getObject(bucketName, key, headers) {
491
248
  const bucket = this.buckets.get(bucketName);
492
-
493
249
  if (!bucket) {
494
250
  return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
495
251
  }
@@ -499,20 +255,21 @@ class S3Simulator {
499
255
  return { error: { code: "NoSuchKey", message: "The specified key does not exist" }, status: 404 };
500
256
  }
501
257
 
502
- let content = object.content;
503
- let start = 0;
504
- let end = content.length - 1;
258
+ let content = this._readObjectContent(bucketName, key);
259
+ if (!content) {
260
+ return { error: { code: "NoSuchKey", message: "Object content not found on disk" }, status: 404 };
261
+ }
505
262
 
506
- if (headers.range) {
263
+ if (headers && headers.range) {
507
264
  const range = headers.range.match(/bytes=(\d+)-(\d+)?/);
508
265
  if (range) {
509
- start = parseInt(range[1], 10);
510
- end = range[2] ? parseInt(range[2], 10) : content.length - 1;
511
- content = content.slice(start, end + 1);
266
+ const start = parseInt(range[1], 10);
267
+ const end = range[2] ? parseInt(range[2], 10) : content.length - 1;
268
+ content = content.subarray(start, end + 1);
512
269
  }
513
270
  }
514
271
 
515
- return {
272
+ const result = {
516
273
  content,
517
274
  etag: object.etag,
518
275
  lastModified: object.lastModified.toUTCString(),
@@ -520,51 +277,64 @@ class S3Simulator {
520
277
  size: content.length,
521
278
  metadata: object.metadata,
522
279
  };
280
+ this.audit.record({ eventName: "GetObject", readOnly: true, isDataEvent: true, resources: [{ ARN: `arn:aws:s3:::${bucketName}/${key}`, type: "AWS::S3::Object" }], requestParameters: { bucketName, key } });
281
+ return result;
523
282
  }
524
283
 
525
- deleteObject(bucketName, key) {
284
+ headObject(bucketName, key) {
526
285
  const bucket = this.buckets.get(bucketName);
527
-
528
286
  if (!bucket) {
529
287
  return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
530
288
  }
289
+ const object = bucket.objects.get(key);
290
+ if (!object) {
291
+ return { error: { code: "NoSuchKey", message: "Key does not exist" }, status: 404 };
292
+ }
293
+ return {
294
+ etag: object.etag,
295
+ lastModified: object.lastModified.toUTCString(),
296
+ contentType: object.contentType,
297
+ size: object.size,
298
+ };
299
+ }
531
300
 
301
+ deleteObject(bucketName, key) {
302
+ const bucket = this.buckets.get(bucketName);
303
+ if (!bucket) {
304
+ return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
305
+ }
532
306
  const object = bucket.objects.get(key);
533
307
  if (object) {
534
308
  bucket.objects.delete(key);
535
309
  bucket.objectCount--;
536
310
  bucket.totalSize -= object.size;
311
+ this._deleteObjectContent(bucketName, key);
537
312
  this.persistBucket(bucketName);
538
313
  logger.verboso(`🗑️ Delete S3: ${bucketName}/${key}`);
314
+ this.audit.record({ eventName: "DeleteObject", readOnly: false, isDataEvent: true, resources: [{ ARN: `arn:aws:s3:::${bucketName}/${key}`, type: "AWS::S3::Object" }], requestParameters: { bucketName, key } });
539
315
  }
540
-
541
316
  return { success: true };
542
317
  }
543
318
 
544
319
  listObjects(bucketName, options = {}) {
545
320
  const bucket = this.buckets.get(bucketName);
546
-
547
321
  if (!bucket) {
548
322
  return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
549
323
  }
550
324
 
551
325
  const { prefix = "", delimiter, maxKeys = 1000 } = options;
552
-
553
326
  let objects = Array.from(bucket.objects.values())
554
327
  .filter((obj) => obj.key.startsWith(prefix))
555
328
  .sort((a, b) => a.key.localeCompare(b.key));
556
329
 
557
330
  const commonPrefixes = new Set();
558
-
559
331
  if (delimiter) {
560
332
  const filteredObjects = [];
561
333
  for (const obj of objects) {
562
334
  const afterPrefix = obj.key.substring(prefix.length);
563
335
  const delimiterIndex = afterPrefix.indexOf(delimiter);
564
-
565
336
  if (delimiterIndex !== -1) {
566
- const prefixPath = prefix + afterPrefix.substring(0, delimiterIndex + 1);
567
- commonPrefixes.add(prefixPath);
337
+ commonPrefixes.add(prefix + afterPrefix.substring(0, delimiterIndex + 1));
568
338
  } else {
569
339
  filteredObjects.push(obj);
570
340
  }
@@ -590,20 +360,28 @@ class S3Simulator {
590
360
  };
591
361
  }
592
362
 
593
- extractMetadata(headers) {
594
- const metadata = {};
595
- for (const [key, value] of Object.entries(headers)) {
596
- if (key.startsWith("x-amz-meta-")) {
597
- const metaKey = key.replace("x-amz-meta-", "");
598
- metadata[metaKey] = value;
599
- }
600
- }
601
- return metadata;
363
+ listAllObjects(bucketName) {
364
+ const bucket = this.buckets.get(bucketName);
365
+ if (!bucket) return [];
366
+ return Array.from(bucket.objects.values()).map((obj) => ({
367
+ key: obj.key,
368
+ size: obj.size,
369
+ etag: obj.etag,
370
+ lastModified: obj.lastModified,
371
+ }));
602
372
  }
603
373
 
374
+ // ─── Persistência ─────────────────────────────────────────────────────────
375
+
604
376
  persistBucket(bucketName) {
605
377
  const bucket = this.buckets.get(bucketName);
606
- if (bucket) {
378
+ if (!bucket) return;
379
+ this.persistBuckets();
380
+ }
381
+
382
+ persistBuckets() {
383
+ const bucketsObj = {};
384
+ for (const [name, bucket] of this.buckets.entries()) {
607
385
  const objectsObj = {};
608
386
  for (const [key, obj] of bucket.objects.entries()) {
609
387
  objectsObj[key] = {
@@ -613,22 +391,47 @@ class S3Simulator {
613
391
  contentType: obj.contentType,
614
392
  metadata: obj.metadata,
615
393
  lastModified: obj.lastModified,
616
- content: obj.content,
617
394
  };
618
395
  }
619
- this.store.write(bucketName, objectsObj);
620
- this.persistBuckets();
396
+ bucketsObj[name] = {
397
+ creationDate: bucket.creationDate.toISOString(),
398
+ objects: objectsObj,
399
+ objectCount: bucket.objectCount,
400
+ totalSize: bucket.totalSize,
401
+ };
621
402
  }
403
+ this.store.write("__buckets__", bucketsObj);
622
404
  }
623
405
 
624
- clearBucket(bucketName) {
625
- const bucket = this.buckets.get(bucketName);
626
- if (bucket) {
406
+ async reset() {
407
+ for (const [name, bucket] of this.buckets) {
408
+ for (const key of bucket.objects.keys()) {
409
+ this._deleteObjectContent(name, key);
410
+ }
627
411
  bucket.objects.clear();
628
412
  bucket.objectCount = 0;
629
413
  bucket.totalSize = 0;
630
- this.persistBucket(bucketName);
414
+ this.store.write(name, {});
415
+ }
416
+ this.persistBuckets();
417
+ logger.debug("S3: Todos os dados resetados");
418
+ }
419
+
420
+ // ─── Utilitários ──────────────────────────────────────────────────────────
421
+
422
+ isValidBucketName(bucketName) {
423
+ const regex = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
424
+ return regex.test(bucketName) && !bucketName.includes("..") && !bucketName.includes(".-") && !bucketName.includes("-.");
425
+ }
426
+
427
+ extractMetadata(headers) {
428
+ const metadata = {};
429
+ for (const [key, value] of Object.entries(headers)) {
430
+ if (key.startsWith("x-amz-meta-")) {
431
+ metadata[key.replace("x-amz-meta-", "")] = value;
432
+ }
631
433
  }
434
+ return metadata;
632
435
  }
633
436
 
634
437
  getStats() {
@@ -639,6 +442,8 @@ class S3Simulator {
639
442
  };
640
443
  }
641
444
 
445
+ // ─── XML Responses ────────────────────────────────────────────────────────
446
+
642
447
  generateListBucketsResponse(buckets) {
643
448
  return `<?xml version="1.0" encoding="UTF-8"?>
644
449
  <ListAllMyBucketsResult>
@@ -647,16 +452,7 @@ class S3Simulator {
647
452
  <DisplayName>local-simulator</DisplayName>
648
453
  </Owner>
649
454
  <Buckets>
650
- ${buckets
651
- .map(
652
- (bucket) => `
653
- <Bucket>
654
- <Name>${bucket.Name}</Name>
655
- <CreationDate>${bucket.CreationDate}</CreationDate>
656
- </Bucket>
657
- `,
658
- )
659
- .join("")}
455
+ ${buckets.map((b) => `<Bucket><Name>${b.Name}</Name><CreationDate>${b.CreationDate}</CreationDate></Bucket>`).join("")}
660
456
  </Buckets>
661
457
  </ListAllMyBucketsResult>`;
662
458
  }
@@ -668,28 +464,8 @@ class S3Simulator {
668
464
  <Prefix>${data.prefix}</Prefix>
669
465
  <MaxKeys>${data.maxKeys}</MaxKeys>
670
466
  <IsTruncated>${data.isTruncated}</IsTruncated>
671
- ${data.contents
672
- .map(
673
- (obj) => `
674
- <Contents>
675
- <Key>${obj.Key}</Key>
676
- <LastModified>${obj.LastModified}</LastModified>
677
- <ETag>${obj.ETag}</ETag>
678
- <Size>${obj.Size}</Size>
679
- <StorageClass>${obj.StorageClass}</StorageClass>
680
- </Contents>
681
- `,
682
- )
683
- .join("")}
684
- ${data.commonPrefixes
685
- .map(
686
- (prefix) => `
687
- <CommonPrefixes>
688
- <Prefix>${prefix}</Prefix>
689
- </CommonPrefixes>
690
- `,
691
- )
692
- .join("")}
467
+ ${data.contents.map((obj) => `<Contents><Key>${obj.Key}</Key><LastModified>${obj.LastModified}</LastModified><ETag>${obj.ETag}</ETag><Size>${obj.Size}</Size><StorageClass>${obj.StorageClass}</StorageClass></Contents>`).join("")}
468
+ ${data.commonPrefixes.map((p) => `<CommonPrefixes><Prefix>${p}</Prefix></CommonPrefixes>`).join("")}
693
469
  </ListBucketResult>`;
694
470
  }
695
471
 
@@ -701,28 +477,8 @@ class S3Simulator {
701
477
  <MaxKeys>${data.maxKeys}</MaxKeys>
702
478
  <IsTruncated>${data.isTruncated}</IsTruncated>
703
479
  <KeyCount>${data.contents.length}</KeyCount>
704
- ${data.contents
705
- .map(
706
- (obj) => `
707
- <Contents>
708
- <Key>${obj.Key}</Key>
709
- <LastModified>${obj.LastModified}</LastModified>
710
- <ETag>${obj.ETag}</ETag>
711
- <Size>${obj.Size}</Size>
712
- <StorageClass>${obj.StorageClass}</StorageClass>
713
- </Contents>
714
- `,
715
- )
716
- .join("")}
717
- ${data.commonPrefixes
718
- .map(
719
- (prefix) => `
720
- <CommonPrefixes>
721
- <Prefix>${prefix}</Prefix>
722
- </CommonPrefixes>
723
- `,
724
- )
725
- .join("")}
480
+ ${data.contents.map((obj) => `<Contents><Key>${obj.Key}</Key><LastModified>${obj.LastModified}</LastModified><ETag>${obj.ETag}</ETag><Size>${obj.Size}</Size><StorageClass>${obj.StorageClass}</StorageClass></Contents>`).join("")}
481
+ ${data.commonPrefixes.map((p) => `<CommonPrefixes><Prefix>${p}</Prefix></CommonPrefixes>`).join("")}
726
482
  </ListBucketResult>`;
727
483
  }
728
484