@gugananuvem/aws-local-simulator 1.0.33 → 1.0.34

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 (79) hide show
  1. package/README.md +834 -834
  2. package/aws-config +153 -153
  3. package/bin/aws-local-simulator.js +63 -63
  4. package/package.json +3 -2
  5. package/src/config/config-loader.js +114 -114
  6. package/src/config/default-config.js +79 -79
  7. package/src/config/env-loader.js +68 -68
  8. package/src/index.js +146 -146
  9. package/src/index.mjs +123 -123
  10. package/src/server.js +463 -463
  11. package/src/services/apigateway/index.js +75 -75
  12. package/src/services/apigateway/server.js +607 -607
  13. package/src/services/apigateway/simulator.js +1405 -1405
  14. package/src/services/athena/index.js +75 -75
  15. package/src/services/athena/server.js +101 -101
  16. package/src/services/athena/simulador.js +998 -998
  17. package/src/services/athena/simulator.js +346 -346
  18. package/src/services/cloudformation/index.js +106 -106
  19. package/src/services/cloudformation/server.js +417 -417
  20. package/src/services/cloudformation/simulador.js +1020 -1020
  21. package/src/services/cloudtrail/index.js +84 -84
  22. package/src/services/cloudtrail/server.js +235 -235
  23. package/src/services/cloudtrail/simulador.js +719 -719
  24. package/src/services/cloudwatch/index.js +84 -84
  25. package/src/services/cloudwatch/server.js +366 -366
  26. package/src/services/cloudwatch/simulador.js +1173 -1173
  27. package/src/services/cognito/index.js +79 -79
  28. package/src/services/cognito/server.js +297 -297
  29. package/src/services/cognito/simulator.js +1992 -1761
  30. package/src/services/config/index.js +96 -96
  31. package/src/services/config/server.js +215 -215
  32. package/src/services/config/simulador.js +1260 -1260
  33. package/src/services/dynamodb/index.js +74 -74
  34. package/src/services/dynamodb/server.js +139 -139
  35. package/src/services/dynamodb/simulator.js +1005 -994
  36. package/src/services/dynamodb/sqlite-store.js +722 -0
  37. package/src/services/ecs/index.js +65 -65
  38. package/src/services/ecs/server.js +235 -235
  39. package/src/services/ecs/simulator.js +844 -844
  40. package/src/services/eventbridge/index.js +89 -89
  41. package/src/services/eventbridge/server.js +209 -209
  42. package/src/services/eventbridge/simulator.js +684 -684
  43. package/src/services/index.js +45 -45
  44. package/src/services/kms/index.js +75 -75
  45. package/src/services/kms/server.js +81 -81
  46. package/src/services/kms/simulator.js +344 -344
  47. package/src/services/lambda/handler-loader.js +183 -183
  48. package/src/services/lambda/index.js +81 -81
  49. package/src/services/lambda/route-registry.js +274 -274
  50. package/src/services/lambda/server.js +191 -191
  51. package/src/services/lambda/simulator.js +364 -364
  52. package/src/services/parameter-store/index.js +80 -80
  53. package/src/services/parameter-store/server.js +50 -50
  54. package/src/services/parameter-store/simulator.js +201 -201
  55. package/src/services/s3/index.js +73 -73
  56. package/src/services/s3/server.js +350 -350
  57. package/src/services/s3/simulator.js +568 -568
  58. package/src/services/secret-manager/index.js +80 -80
  59. package/src/services/secret-manager/server.js +51 -51
  60. package/src/services/secret-manager/simulator.js +182 -182
  61. package/src/services/sns/index.js +89 -89
  62. package/src/services/sns/server.js +607 -607
  63. package/src/services/sns/simulator.js +1482 -1482
  64. package/src/services/sqs/index.js +98 -98
  65. package/src/services/sqs/server.js +360 -360
  66. package/src/services/sqs/simulator.js +509 -509
  67. package/src/services/sts/index.js +37 -37
  68. package/src/services/sts/server.js +144 -144
  69. package/src/services/sts/simulator.js +69 -69
  70. package/src/services/xray/index.js +83 -83
  71. package/src/services/xray/server.js +308 -308
  72. package/src/services/xray/simulador.js +994 -994
  73. package/src/template/aws-config-template.js +87 -87
  74. package/src/template/aws-config-template.mjs +90 -90
  75. package/src/template/config-template.json +203 -203
  76. package/src/utils/aws-config.js +91 -91
  77. package/src/utils/cloudtrail-audit.js +129 -129
  78. package/src/utils/local-store.js +83 -83
  79. package/src/utils/logger.js +59 -59
@@ -1,351 +1,351 @@
1
- /**
2
- * S3 Server - Servidor HTTP para S3
3
- */
4
-
5
- const express = require('express');
6
- const cors = require('cors');
7
- const S3Simulator = require('./simulator');
8
- const logger = require('../../utils/logger');
9
-
10
- class S3Server {
11
- constructor(port, config) {
12
- this.port = port;
13
- this.config = config;
14
- this.app = express();
15
- this.simulator = null;
16
- this.server = null;
17
- this.setupMiddlewares();
18
- }
19
-
20
- setupMiddlewares() {
21
- this.app.use(cors());
22
- // Captura raw body como Buffer para qualquer content-type
23
- this.app.use(express.raw({ type: () => true, limit: '100mb' }));
24
- this.app.use(express.text({ limit: '100mb' }));
25
-
26
- // Suporte a Virtual Host Style addressing (bucket.localhost:4566)
27
- this.app.use((req, res, next) => {
28
- const host = req.headers.host || '';
29
- // Se o host contém múltiplos pontos e não é apenas um IP ou localhost puro
30
- if (host.includes('.') && !host.startsWith('localhost') && !host.startsWith('127.0.0.1')) {
31
- const parts = host.split('.');
32
- // O primeiro fragmento é o nome do bucket se houver mais de 2 partes (bucket.localhost:port)
33
- // ou se a segunda parte for localhost
34
- if (parts.length >= 2) {
35
- const bucket = parts[0];
36
- // Evita processar se for admin ou se já estiver no path style (heurística simples)
37
- if (bucket !== 'localhost' && !req.path.startsWith('/__admin/')) {
38
- req.url = `/${bucket}${req.url}`;
39
- logger.verboso(`S3: Virtual Host Style detected. Rewriting to ${req.url}`);
40
- }
41
- }
42
- }
43
- next();
44
- });
45
-
46
- // Logging de requisições
47
- if (logger.currentLogLevel === 'verboso') {
48
- this.app.use((req, res, next) => {
49
- const start = Date.now();
50
- res.on('finish', () => {
51
- const duration = Date.now() - start;
52
- logger.verboso(`S3: ${req.method} ${req.path} - ${duration}ms`);
53
- });
54
- next();
55
- });
56
- }
57
- }
58
-
59
-
60
- async initialize() {
61
- if (!this.simulator) {
62
- this.simulator = new S3Simulator(this.config);
63
- await this.simulator.initialize();
64
- }
65
- this.setupRoutes();
66
- }
67
-
68
- setupRoutes() {
69
- // Admin endpoints devem vir antes das rotas S3 para evitar conflito com /:bucket
70
- this.setupAdminRoutes();
71
-
72
- // Website static serving: GET /website/{bucket}/[path]
73
- this.app.get('/website/:bucket', (req, res) => this._serveWebsite(req, res, ''));
74
- this.app.get('/website/:bucket/*', (req, res) => this._serveWebsite(req, res, req.params[0]));
75
-
76
- // Listar buckets
77
- this.app.get('/', (req, res) => {
78
- const buckets = this.simulator.listBuckets();
79
- res.set('Content-Type', 'application/xml');
80
- res.send(this.simulator.generateListBucketsResponse(buckets));
81
- });
82
-
83
- // Criar bucket
84
- this.app.put('/:bucket', (req, res) => {
85
- if (req.query.website !== undefined) {
86
- let config = {};
87
- try { config = JSON.parse(req.body?.toString() || '{}'); } catch (_) {}
88
- const result = this.simulator.putWebsiteConfig(req.params.bucket, config);
89
- if (result.error) {
90
- return res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
91
- }
92
- return res.status(200).send();
93
- }
94
- const result = this.simulator.createBucket(req.params.bucket);
95
- if (result.error) {
96
- res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
97
- } else {
98
- res.status(200).send();
99
- }
100
- });
101
-
102
- // Deletar bucket
103
- this.app.delete('/:bucket', (req, res) => {
104
- if (req.query.website !== undefined) {
105
- const result = this.simulator.deleteWebsiteConfig(req.params.bucket);
106
- if (result.error) {
107
- return res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
108
- }
109
- return res.status(204).send();
110
- }
111
- const result = this.simulator.deleteBucket(req.params.bucket);
112
- if (result.error) {
113
- res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
114
- } else {
115
- res.status(204).send();
116
- }
117
- });
118
-
119
- // Head bucket
120
- this.app.head('/:bucket', (req, res) => {
121
- const bucket = this.simulator.getBucket(req.params.bucket);
122
- if (!bucket) {
123
- res.status(404).send();
124
- } else {
125
- res.status(200).send();
126
- }
127
- });
128
-
129
- // Upload object
130
- this.app.put('/:bucket/*', (req, res) => {
131
- try {
132
- const bucket = req.params.bucket;
133
- const key = req.params[0];
134
- const result = this.simulator.putObject(bucket, key, req.body, req.headers);
135
-
136
- if (result.error) {
137
- res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
138
- } else {
139
- res.set('ETag', `"${result.etag}"`);
140
- res.status(200).send();
141
- }
142
- } catch (err) {
143
- logger.error('S3 putObject error:', err);
144
- res.status(500).send(this.simulator.generateErrorResponse('InternalError', err.message));
145
- }
146
- });
147
-
148
- // Get object
149
- this.app.get('/:bucket/*', (req, res) => {
150
- const bucket = req.params.bucket;
151
- const key = req.params[0];
152
- const result = this.simulator.getObject(bucket, key, req.headers);
153
-
154
- if (result.error) {
155
- res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
156
- } else {
157
- res.set('ETag', `"${result.etag}"`);
158
- res.set('Last-Modified', result.lastModified);
159
- res.set('Content-Type', result.contentType);
160
- res.set('Content-Length', result.size);
161
-
162
- if (result.metadata) {
163
- Object.entries(result.metadata).forEach(([k, v]) => {
164
- res.set(`x-amz-meta-${k}`, v);
165
- });
166
- }
167
-
168
- res.send(result.content);
169
- }
170
- });
171
-
172
- // Head object
173
- this.app.head('/:bucket/*', (req, res) => {
174
- const bucket = req.params.bucket;
175
- const key = req.params[0];
176
- const result = this.simulator.headObject(bucket, key);
177
-
178
- if (result.error) {
179
- res.status(result.status).send();
180
- } else {
181
- res.set('ETag', `"${result.etag}"`);
182
- res.set('Last-Modified', result.lastModified);
183
- res.set('Content-Type', result.contentType);
184
- res.set('Content-Length', result.size);
185
- res.status(200).send();
186
- }
187
- });
188
-
189
- // Delete object
190
- this.app.delete('/:bucket/*', (req, res) => {
191
- const bucket = req.params.bucket;
192
- const key = req.params[0];
193
- const result = this.simulator.deleteObject(bucket, key);
194
-
195
- if (result.error) {
196
- res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
197
- } else {
198
- res.status(204).send();
199
- }
200
- });
201
-
202
- // Website config
203
- this.app.get('/:bucket', (req, res) => {
204
- if (req.query.website !== undefined) {
205
- const result = this.simulator.getWebsiteConfig(req.params.bucket);
206
- if (result.error) {
207
- return res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
208
- }
209
- return res.set('Content-Type', 'application/xml').send(this._generateWebsiteConfigXml(result.websiteConfiguration));
210
- }
211
- const bucket = req.params.bucket;
212
- const prefix = req.query.prefix || '';
213
- const delimiter = req.query.delimiter;
214
- const maxKeys = parseInt(req.query['max-keys']) || 1000;
215
- const listType = req.query['list-type'];
216
-
217
- const result = this.simulator.listObjects(bucket, { prefix, delimiter, maxKeys });
218
-
219
- if (result.error) {
220
- res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
221
- } else {
222
- res.set('Content-Type', 'application/xml');
223
- if (listType === '2') {
224
- res.send(this.simulator.generateListObjectsV2Response(result));
225
- } else {
226
- res.send(this.simulator.generateListObjectsResponse(result));
227
- }
228
- }
229
- });
230
-
231
- // Admin endpoints
232
- // (já registrados no início de setupRoutes)
233
- }
234
-
235
- setupAdminRoutes() {
236
- this.app.get('/__admin/buckets', (req, res) => {
237
- res.json(this.simulator.getBucketsInfo());
238
- });
239
-
240
- this.app.get('/__admin/buckets/:bucket', (req, res) => {
241
- const info = this.simulator.getBucketInfo(req.params.bucket);
242
- if (info.error) {
243
- res.status(404).json(info.error);
244
- } else {
245
- res.json(info);
246
- }
247
- });
248
-
249
- this.app.get('/__admin/buckets/:bucket/objects', (req, res) => {
250
- const objects = this.simulator.listAllObjects(req.params.bucket);
251
- res.json(objects);
252
- });
253
-
254
- this.app.delete('/__admin/buckets/:bucket', (req, res) => {
255
- this.simulator.clearBucket(req.params.bucket);
256
- res.json({ message: `Bucket ${req.params.bucket} cleared` });
257
- });
258
-
259
- this.app.delete('/__admin/buckets/:bucket/objects/:key', (req, res) => {
260
- this.simulator.deleteObject(req.params.bucket, req.params.key);
261
- res.json({ message: 'Object deleted' });
262
- });
263
-
264
- this.app.get('/__admin/stats', (req, res) => {
265
- res.json(this.simulator.getStats());
266
- });
267
- }
268
-
269
- _serveWebsite(req, res, keyPath) {
270
- const bucketName = req.params.bucket;
271
- const bucket = this.simulator.getBucket(bucketName);
272
-
273
- if (!bucket) {
274
- return res.status(404).send('<html><body><h1>404 - Bucket not found</h1></body></html>');
275
- }
276
-
277
- if (!bucket.website) {
278
- return res.status(403).send('<html><body><h1>403 - This bucket does not have static website hosting enabled</h1></body></html>');
279
- }
280
-
281
- const websiteConfig = bucket.websiteConfiguration || {};
282
- const indexDoc = websiteConfig.IndexDocument?.Suffix || 'index.html';
283
- const errorDoc = websiteConfig.ErrorDocument?.Key || 'error.html';
284
-
285
- // Resolve a chave: path vazio ou terminando em '/' serve o indexDocument
286
- let key = keyPath || '';
287
- if (!key || key.endsWith('/')) {
288
- key = key + indexDoc;
289
- }
290
-
291
- let result = this.simulator.getObject(bucketName, key, req.headers);
292
-
293
- // Se não encontrou e não tem extensão, tenta com indexDoc dentro do path
294
- if (result.error && !key.includes('.')) {
295
- result = this.simulator.getObject(bucketName, key + '/' + indexDoc, req.headers);
296
- }
297
-
298
- if (result.error) {
299
- const errorResult = this.simulator.getObject(bucketName, errorDoc, req.headers);
300
- if (!errorResult.error) {
301
- res.status(404).set('Content-Type', errorResult.contentType || 'text/html');
302
- return res.send(errorResult.content);
303
- }
304
- return res.status(404).send('<html><body><h1>404 - Not Found</h1></body></html>');
305
- }
306
-
307
- res.set('Content-Type', result.contentType || 'text/html');
308
- res.set('ETag', `"${result.etag}"`);
309
- res.set('Last-Modified', result.lastModified);
310
- res.send(result.content);
311
- }
312
-
313
- _generateWebsiteConfigXml(config) {
314
- return `<?xml version="1.0" encoding="UTF-8"?>
315
- <WebsiteConfiguration>
316
- <IndexDocument><Suffix>${config.IndexDocument?.Suffix || 'index.html'}</Suffix></IndexDocument>
317
- <ErrorDocument><Key>${config.ErrorDocument?.Key || 'error.html'}</Key></ErrorDocument>
318
- </WebsiteConfiguration>`;
319
- }
320
-
321
- start() {
322
- return new Promise((resolve) => {
323
- this.server = this.app.listen(this.port, () => {
324
- logger.info(`🗄️ S3 rodando em http://localhost:${this.port}`);
325
- resolve();
326
- });
327
- });
328
- }
329
-
330
- stop() {
331
- return new Promise((resolve) => {
332
- if (this.server) {
333
- this.server.close(() => resolve());
334
- } else {
335
- resolve();
336
- }
337
- });
338
- }
339
-
340
- getStatus() {
341
- return {
342
- running: !!this.server,
343
- port: this.port,
344
- endpoint: `http://localhost:${this.port}`,
345
- bucketsCount: this.simulator?.getBucketsCount() || 0,
346
- objectsCount: this.simulator?.getTotalObjectsCount() || 0
347
- };
348
- }
349
- }
350
-
1
+ /**
2
+ * S3 Server - Servidor HTTP para S3
3
+ */
4
+
5
+ const express = require('express');
6
+ const cors = require('cors');
7
+ const S3Simulator = require('./simulator');
8
+ const logger = require('../../utils/logger');
9
+
10
+ class S3Server {
11
+ constructor(port, config) {
12
+ this.port = port;
13
+ this.config = config;
14
+ this.app = express();
15
+ this.simulator = null;
16
+ this.server = null;
17
+ this.setupMiddlewares();
18
+ }
19
+
20
+ setupMiddlewares() {
21
+ this.app.use(cors());
22
+ // Captura raw body como Buffer para qualquer content-type
23
+ this.app.use(express.raw({ type: () => true, limit: '100mb' }));
24
+ this.app.use(express.text({ limit: '100mb' }));
25
+
26
+ // Suporte a Virtual Host Style addressing (bucket.localhost:4566)
27
+ this.app.use((req, res, next) => {
28
+ const host = req.headers.host || '';
29
+ // Se o host contém múltiplos pontos e não é apenas um IP ou localhost puro
30
+ if (host.includes('.') && !host.startsWith('localhost') && !host.startsWith('127.0.0.1')) {
31
+ const parts = host.split('.');
32
+ // O primeiro fragmento é o nome do bucket se houver mais de 2 partes (bucket.localhost:port)
33
+ // ou se a segunda parte for localhost
34
+ if (parts.length >= 2) {
35
+ const bucket = parts[0];
36
+ // Evita processar se for admin ou se já estiver no path style (heurística simples)
37
+ if (bucket !== 'localhost' && !req.path.startsWith('/__admin/')) {
38
+ req.url = `/${bucket}${req.url}`;
39
+ logger.verboso(`S3: Virtual Host Style detected. Rewriting to ${req.url}`);
40
+ }
41
+ }
42
+ }
43
+ next();
44
+ });
45
+
46
+ // Logging de requisições
47
+ if (logger.currentLogLevel === 'verboso') {
48
+ this.app.use((req, res, next) => {
49
+ const start = Date.now();
50
+ res.on('finish', () => {
51
+ const duration = Date.now() - start;
52
+ logger.verboso(`S3: ${req.method} ${req.path} - ${duration}ms`);
53
+ });
54
+ next();
55
+ });
56
+ }
57
+ }
58
+
59
+
60
+ async initialize() {
61
+ if (!this.simulator) {
62
+ this.simulator = new S3Simulator(this.config);
63
+ await this.simulator.initialize();
64
+ }
65
+ this.setupRoutes();
66
+ }
67
+
68
+ setupRoutes() {
69
+ // Admin endpoints devem vir antes das rotas S3 para evitar conflito com /:bucket
70
+ this.setupAdminRoutes();
71
+
72
+ // Website static serving: GET /website/{bucket}/[path]
73
+ this.app.get('/website/:bucket', (req, res) => this._serveWebsite(req, res, ''));
74
+ this.app.get('/website/:bucket/*', (req, res) => this._serveWebsite(req, res, req.params[0]));
75
+
76
+ // Listar buckets
77
+ this.app.get('/', (req, res) => {
78
+ const buckets = this.simulator.listBuckets();
79
+ res.set('Content-Type', 'application/xml');
80
+ res.send(this.simulator.generateListBucketsResponse(buckets));
81
+ });
82
+
83
+ // Criar bucket
84
+ this.app.put('/:bucket', (req, res) => {
85
+ if (req.query.website !== undefined) {
86
+ let config = {};
87
+ try { config = JSON.parse(req.body?.toString() || '{}'); } catch (_) {}
88
+ const result = this.simulator.putWebsiteConfig(req.params.bucket, config);
89
+ if (result.error) {
90
+ return res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
91
+ }
92
+ return res.status(200).send();
93
+ }
94
+ const result = this.simulator.createBucket(req.params.bucket);
95
+ if (result.error) {
96
+ res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
97
+ } else {
98
+ res.status(200).send();
99
+ }
100
+ });
101
+
102
+ // Deletar bucket
103
+ this.app.delete('/:bucket', (req, res) => {
104
+ if (req.query.website !== undefined) {
105
+ const result = this.simulator.deleteWebsiteConfig(req.params.bucket);
106
+ if (result.error) {
107
+ return res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
108
+ }
109
+ return res.status(204).send();
110
+ }
111
+ const result = this.simulator.deleteBucket(req.params.bucket);
112
+ if (result.error) {
113
+ res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
114
+ } else {
115
+ res.status(204).send();
116
+ }
117
+ });
118
+
119
+ // Head bucket
120
+ this.app.head('/:bucket', (req, res) => {
121
+ const bucket = this.simulator.getBucket(req.params.bucket);
122
+ if (!bucket) {
123
+ res.status(404).send();
124
+ } else {
125
+ res.status(200).send();
126
+ }
127
+ });
128
+
129
+ // Upload object
130
+ this.app.put('/:bucket/*', (req, res) => {
131
+ try {
132
+ const bucket = req.params.bucket;
133
+ const key = req.params[0];
134
+ const result = this.simulator.putObject(bucket, key, req.body, req.headers);
135
+
136
+ if (result.error) {
137
+ res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
138
+ } else {
139
+ res.set('ETag', `"${result.etag}"`);
140
+ res.status(200).send();
141
+ }
142
+ } catch (err) {
143
+ logger.error('S3 putObject error:', err);
144
+ res.status(500).send(this.simulator.generateErrorResponse('InternalError', err.message));
145
+ }
146
+ });
147
+
148
+ // Get object
149
+ this.app.get('/:bucket/*', (req, res) => {
150
+ const bucket = req.params.bucket;
151
+ const key = req.params[0];
152
+ const result = this.simulator.getObject(bucket, key, req.headers);
153
+
154
+ if (result.error) {
155
+ res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
156
+ } else {
157
+ res.set('ETag', `"${result.etag}"`);
158
+ res.set('Last-Modified', result.lastModified);
159
+ res.set('Content-Type', result.contentType);
160
+ res.set('Content-Length', result.size);
161
+
162
+ if (result.metadata) {
163
+ Object.entries(result.metadata).forEach(([k, v]) => {
164
+ res.set(`x-amz-meta-${k}`, v);
165
+ });
166
+ }
167
+
168
+ res.send(result.content);
169
+ }
170
+ });
171
+
172
+ // Head object
173
+ this.app.head('/:bucket/*', (req, res) => {
174
+ const bucket = req.params.bucket;
175
+ const key = req.params[0];
176
+ const result = this.simulator.headObject(bucket, key);
177
+
178
+ if (result.error) {
179
+ res.status(result.status).send();
180
+ } else {
181
+ res.set('ETag', `"${result.etag}"`);
182
+ res.set('Last-Modified', result.lastModified);
183
+ res.set('Content-Type', result.contentType);
184
+ res.set('Content-Length', result.size);
185
+ res.status(200).send();
186
+ }
187
+ });
188
+
189
+ // Delete object
190
+ this.app.delete('/:bucket/*', (req, res) => {
191
+ const bucket = req.params.bucket;
192
+ const key = req.params[0];
193
+ const result = this.simulator.deleteObject(bucket, key);
194
+
195
+ if (result.error) {
196
+ res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
197
+ } else {
198
+ res.status(204).send();
199
+ }
200
+ });
201
+
202
+ // Website config
203
+ this.app.get('/:bucket', (req, res) => {
204
+ if (req.query.website !== undefined) {
205
+ const result = this.simulator.getWebsiteConfig(req.params.bucket);
206
+ if (result.error) {
207
+ return res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
208
+ }
209
+ return res.set('Content-Type', 'application/xml').send(this._generateWebsiteConfigXml(result.websiteConfiguration));
210
+ }
211
+ const bucket = req.params.bucket;
212
+ const prefix = req.query.prefix || '';
213
+ const delimiter = req.query.delimiter;
214
+ const maxKeys = parseInt(req.query['max-keys']) || 1000;
215
+ const listType = req.query['list-type'];
216
+
217
+ const result = this.simulator.listObjects(bucket, { prefix, delimiter, maxKeys });
218
+
219
+ if (result.error) {
220
+ res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
221
+ } else {
222
+ res.set('Content-Type', 'application/xml');
223
+ if (listType === '2') {
224
+ res.send(this.simulator.generateListObjectsV2Response(result));
225
+ } else {
226
+ res.send(this.simulator.generateListObjectsResponse(result));
227
+ }
228
+ }
229
+ });
230
+
231
+ // Admin endpoints
232
+ // (já registrados no início de setupRoutes)
233
+ }
234
+
235
+ setupAdminRoutes() {
236
+ this.app.get('/__admin/buckets', (req, res) => {
237
+ res.json(this.simulator.getBucketsInfo());
238
+ });
239
+
240
+ this.app.get('/__admin/buckets/:bucket', (req, res) => {
241
+ const info = this.simulator.getBucketInfo(req.params.bucket);
242
+ if (info.error) {
243
+ res.status(404).json(info.error);
244
+ } else {
245
+ res.json(info);
246
+ }
247
+ });
248
+
249
+ this.app.get('/__admin/buckets/:bucket/objects', (req, res) => {
250
+ const objects = this.simulator.listAllObjects(req.params.bucket);
251
+ res.json(objects);
252
+ });
253
+
254
+ this.app.delete('/__admin/buckets/:bucket', (req, res) => {
255
+ this.simulator.clearBucket(req.params.bucket);
256
+ res.json({ message: `Bucket ${req.params.bucket} cleared` });
257
+ });
258
+
259
+ this.app.delete('/__admin/buckets/:bucket/objects/:key', (req, res) => {
260
+ this.simulator.deleteObject(req.params.bucket, req.params.key);
261
+ res.json({ message: 'Object deleted' });
262
+ });
263
+
264
+ this.app.get('/__admin/stats', (req, res) => {
265
+ res.json(this.simulator.getStats());
266
+ });
267
+ }
268
+
269
+ _serveWebsite(req, res, keyPath) {
270
+ const bucketName = req.params.bucket;
271
+ const bucket = this.simulator.getBucket(bucketName);
272
+
273
+ if (!bucket) {
274
+ return res.status(404).send('<html><body><h1>404 - Bucket not found</h1></body></html>');
275
+ }
276
+
277
+ if (!bucket.website) {
278
+ return res.status(403).send('<html><body><h1>403 - This bucket does not have static website hosting enabled</h1></body></html>');
279
+ }
280
+
281
+ const websiteConfig = bucket.websiteConfiguration || {};
282
+ const indexDoc = websiteConfig.IndexDocument?.Suffix || 'index.html';
283
+ const errorDoc = websiteConfig.ErrorDocument?.Key || 'error.html';
284
+
285
+ // Resolve a chave: path vazio ou terminando em '/' serve o indexDocument
286
+ let key = keyPath || '';
287
+ if (!key || key.endsWith('/')) {
288
+ key = key + indexDoc;
289
+ }
290
+
291
+ let result = this.simulator.getObject(bucketName, key, req.headers);
292
+
293
+ // Se não encontrou e não tem extensão, tenta com indexDoc dentro do path
294
+ if (result.error && !key.includes('.')) {
295
+ result = this.simulator.getObject(bucketName, key + '/' + indexDoc, req.headers);
296
+ }
297
+
298
+ if (result.error) {
299
+ const errorResult = this.simulator.getObject(bucketName, errorDoc, req.headers);
300
+ if (!errorResult.error) {
301
+ res.status(404).set('Content-Type', errorResult.contentType || 'text/html');
302
+ return res.send(errorResult.content);
303
+ }
304
+ return res.status(404).send('<html><body><h1>404 - Not Found</h1></body></html>');
305
+ }
306
+
307
+ res.set('Content-Type', result.contentType || 'text/html');
308
+ res.set('ETag', `"${result.etag}"`);
309
+ res.set('Last-Modified', result.lastModified);
310
+ res.send(result.content);
311
+ }
312
+
313
+ _generateWebsiteConfigXml(config) {
314
+ return `<?xml version="1.0" encoding="UTF-8"?>
315
+ <WebsiteConfiguration>
316
+ <IndexDocument><Suffix>${config.IndexDocument?.Suffix || 'index.html'}</Suffix></IndexDocument>
317
+ <ErrorDocument><Key>${config.ErrorDocument?.Key || 'error.html'}</Key></ErrorDocument>
318
+ </WebsiteConfiguration>`;
319
+ }
320
+
321
+ start() {
322
+ return new Promise((resolve) => {
323
+ this.server = this.app.listen(this.port, () => {
324
+ logger.info(`🗄️ S3 rodando em http://localhost:${this.port}`);
325
+ resolve();
326
+ });
327
+ });
328
+ }
329
+
330
+ stop() {
331
+ return new Promise((resolve) => {
332
+ if (this.server) {
333
+ this.server.close(() => resolve());
334
+ } else {
335
+ resolve();
336
+ }
337
+ });
338
+ }
339
+
340
+ getStatus() {
341
+ return {
342
+ running: !!this.server,
343
+ port: this.port,
344
+ endpoint: `http://localhost:${this.port}`,
345
+ bucketsCount: this.simulator?.getBucketsCount() || 0,
346
+ objectsCount: this.simulator?.getTotalObjectsCount() || 0
347
+ };
348
+ }
349
+ }
350
+
351
351
  module.exports = S3Server;