@gugananuvem/aws-local-simulator 1.0.14 → 1.0.16

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.
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  const express = require('express');
6
+ const cors = require('cors');
6
7
  const DynamoDBSimulator = require('./simulator');
7
8
  const logger = require('../../utils/logger');
8
9
 
@@ -17,6 +18,7 @@ class DynamoDBServer {
17
18
  }
18
19
 
19
20
  setupMiddlewares() {
21
+ this.app.use(cors());
20
22
  this.app.use(express.json({
21
23
  type: 'application/x-amz-json-1.0'
22
24
  }));
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  const express = require('express');
6
+ const cors = require('cors');
6
7
  const logger = require('../../utils/logger');
7
8
 
8
9
  class ECSServer {
@@ -16,6 +17,7 @@ class ECSServer {
16
17
  }
17
18
 
18
19
  setupMiddlewares() {
20
+ this.app.use(cors());
19
21
  this.app.use(express.json());
20
22
 
21
23
  if (logger.currentLogLevel === 'verboso') {
@@ -18,8 +18,8 @@ const cors = require('cors');
18
18
  function createEventBridgeServer(simulator, config, logger) {
19
19
  const app = express();
20
20
 
21
- if (config.cors?.enabled) {
22
- app.use(cors({ origin: config.cors.origin || '*' }));
21
+ if (config.cors?.enabled !== false) {
22
+ app.use(cors({ origin: config.cors?.origin || '*' }));
23
23
  }
24
24
 
25
25
  app.use(express.json({ limit: '10mb' }));
@@ -17,6 +17,12 @@ class LambdaSimulator {
17
17
  async initialize() {
18
18
  logger.debug("Inicializando Lambda Simulator...");
19
19
 
20
+ // Build global lambda defaults from config.global
21
+ const globalDefaults = this.config.global || {};
22
+ this.globalEnv = globalDefaults.env || {};
23
+ this.globalTimeout = globalDefaults.timeout || null;
24
+ this.globalMemorySize = globalDefaults.memorySize || null;
25
+
20
26
  if (this.config.lambdas && this.config.lambdas.length > 0) {
21
27
  for (const lambdaConfig of this.config.lambdas) {
22
28
  await this.registerLambda(lambdaConfig);
@@ -28,13 +34,22 @@ class LambdaSimulator {
28
34
 
29
35
  async registerLambda(lambdaConfig) {
30
36
  try {
31
- const { name, handler: handlerPath, env = {}, type = "auto" } = lambdaConfig;
37
+ const { name, handler: handlerPath, type = "auto" } = lambdaConfig;
32
38
 
33
39
  if (!name) {
34
40
  logger.warn(`Lambda sem nome ignorada: ${JSON.stringify(lambdaConfig)}`);
35
41
  return;
36
42
  }
37
43
 
44
+ // Merge: global env as base, lambda-specific env overrides
45
+ const env = {
46
+ ...(this.globalEnv || {}),
47
+ ...(lambdaConfig.env || {}),
48
+ };
49
+
50
+ const timeout = lambdaConfig.timeout ?? this.globalTimeout ?? 30;
51
+ const memorySize = lambdaConfig.memorySize ?? this.globalMemorySize ?? 128;
52
+
38
53
  const handler = await HandlerLoader.load(handlerPath, type);
39
54
  if (handler != undefined) {
40
55
  this.lambdas.set(name, {
@@ -43,6 +58,8 @@ class LambdaSimulator {
43
58
  handlerPath,
44
59
  handlerName: handler.name || "anonymous",
45
60
  env,
61
+ timeout,
62
+ memorySize,
46
63
  type,
47
64
  registeredAt: new Date().toISOString(),
48
65
  });
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  const express = require('express');
6
+ const cors = require('cors');
6
7
  const S3Simulator = require('./simulator');
7
8
  const logger = require('../../utils/logger');
8
9
 
@@ -17,6 +18,7 @@ class S3Server {
17
18
  }
18
19
 
19
20
  setupMiddlewares() {
21
+ this.app.use(cors());
20
22
  // Captura raw body como Buffer para qualquer content-type
21
23
  this.app.use(express.raw({ type: () => true, limit: '100mb' }));
22
24
  this.app.use(express.text({ limit: '100mb' }));
@@ -43,6 +45,13 @@ class S3Server {
43
45
  }
44
46
 
45
47
  setupRoutes() {
48
+ // Admin endpoints devem vir antes das rotas S3 para evitar conflito com /:bucket
49
+ this.setupAdminRoutes();
50
+
51
+ // Website static serving: GET /website/{bucket}/[path]
52
+ this.app.get('/website/:bucket', (req, res) => this._serveWebsite(req, res, ''));
53
+ this.app.get('/website/:bucket/*', (req, res) => this._serveWebsite(req, res, req.params[0]));
54
+
46
55
  // Listar buckets
47
56
  this.app.get('/', (req, res) => {
48
57
  const buckets = this.simulator.listBuckets();
@@ -52,6 +61,15 @@ class S3Server {
52
61
 
53
62
  // Criar bucket
54
63
  this.app.put('/:bucket', (req, res) => {
64
+ if (req.query.website !== undefined) {
65
+ let config = {};
66
+ try { config = JSON.parse(req.body?.toString() || '{}'); } catch (_) {}
67
+ const result = this.simulator.putWebsiteConfig(req.params.bucket, config);
68
+ if (result.error) {
69
+ return res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
70
+ }
71
+ return res.status(200).send();
72
+ }
55
73
  const result = this.simulator.createBucket(req.params.bucket);
56
74
  if (result.error) {
57
75
  res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
@@ -62,6 +80,13 @@ class S3Server {
62
80
 
63
81
  // Deletar bucket
64
82
  this.app.delete('/:bucket', (req, res) => {
83
+ if (req.query.website !== undefined) {
84
+ const result = this.simulator.deleteWebsiteConfig(req.params.bucket);
85
+ if (result.error) {
86
+ return res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
87
+ }
88
+ return res.status(204).send();
89
+ }
65
90
  const result = this.simulator.deleteBucket(req.params.bucket);
66
91
  if (result.error) {
67
92
  res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
@@ -153,8 +178,15 @@ class S3Server {
153
178
  }
154
179
  });
155
180
 
156
- // List objects
181
+ // Website config
157
182
  this.app.get('/:bucket', (req, res) => {
183
+ if (req.query.website !== undefined) {
184
+ const result = this.simulator.getWebsiteConfig(req.params.bucket);
185
+ if (result.error) {
186
+ return res.status(result.status).send(this.simulator.generateErrorResponse(result.error.code, result.error.message));
187
+ }
188
+ return res.set('Content-Type', 'application/xml').send(this._generateWebsiteConfigXml(result.websiteConfiguration));
189
+ }
158
190
  const bucket = req.params.bucket;
159
191
  const prefix = req.query.prefix || '';
160
192
  const delimiter = req.query.delimiter;
@@ -176,7 +208,7 @@ class S3Server {
176
208
  });
177
209
 
178
210
  // Admin endpoints
179
- this.setupAdminRoutes();
211
+ // (já registrados no início de setupRoutes)
180
212
  }
181
213
 
182
214
  setupAdminRoutes() {
@@ -213,6 +245,58 @@ class S3Server {
213
245
  });
214
246
  }
215
247
 
248
+ _serveWebsite(req, res, keyPath) {
249
+ const bucketName = req.params.bucket;
250
+ const bucket = this.simulator.getBucket(bucketName);
251
+
252
+ if (!bucket) {
253
+ return res.status(404).send('<html><body><h1>404 - Bucket not found</h1></body></html>');
254
+ }
255
+
256
+ if (!bucket.website) {
257
+ return res.status(403).send('<html><body><h1>403 - This bucket does not have static website hosting enabled</h1></body></html>');
258
+ }
259
+
260
+ const websiteConfig = bucket.websiteConfiguration || {};
261
+ const indexDoc = websiteConfig.IndexDocument?.Suffix || 'index.html';
262
+ const errorDoc = websiteConfig.ErrorDocument?.Key || 'error.html';
263
+
264
+ // Resolve a chave: path vazio ou terminando em '/' serve o indexDocument
265
+ let key = keyPath || '';
266
+ if (!key || key.endsWith('/')) {
267
+ key = key + indexDoc;
268
+ }
269
+
270
+ let result = this.simulator.getObject(bucketName, key, req.headers);
271
+
272
+ // Se não encontrou e não tem extensão, tenta com indexDoc dentro do path
273
+ if (result.error && !key.includes('.')) {
274
+ result = this.simulator.getObject(bucketName, key + '/' + indexDoc, req.headers);
275
+ }
276
+
277
+ if (result.error) {
278
+ const errorResult = this.simulator.getObject(bucketName, errorDoc, req.headers);
279
+ if (!errorResult.error) {
280
+ res.status(404).set('Content-Type', errorResult.contentType || 'text/html');
281
+ return res.send(errorResult.content);
282
+ }
283
+ return res.status(404).send('<html><body><h1>404 - Not Found</h1></body></html>');
284
+ }
285
+
286
+ res.set('Content-Type', result.contentType || 'text/html');
287
+ res.set('ETag', `"${result.etag}"`);
288
+ res.set('Last-Modified', result.lastModified);
289
+ res.send(result.content);
290
+ }
291
+
292
+ _generateWebsiteConfigXml(config) {
293
+ return `<?xml version="1.0" encoding="UTF-8"?>
294
+ <WebsiteConfiguration>
295
+ <IndexDocument><Suffix>${config.IndexDocument?.Suffix || 'index.html'}</Suffix></IndexDocument>
296
+ <ErrorDocument><Key>${config.ErrorDocument?.Key || 'error.html'}</Key></ErrorDocument>
297
+ </WebsiteConfiguration>`;
298
+ }
299
+
216
300
  start() {
217
301
  return new Promise((resolve) => {
218
302
  this.server = this.app.listen(this.port, () => {
@@ -25,12 +25,6 @@ class S3Simulator {
25
25
  }
26
26
 
27
27
  loadBuckets() {
28
- if (this.config.s3?.buckets) {
29
- for (const bucketName of this.config.s3.buckets) {
30
- this.createBucket(bucketName);
31
- }
32
- }
33
-
34
28
  const savedBuckets = this.store.read("__buckets__");
35
29
  if (savedBuckets && typeof savedBuckets === "object" && !Array.isArray(savedBuckets)) {
36
30
  for (const [name, data] of Object.entries(savedBuckets)) {
@@ -48,6 +42,9 @@ class S3Simulator {
48
42
  }
49
43
  this.buckets.set(name, {
50
44
  name,
45
+ region: data.region || "us-east-1",
46
+ website: !!(data.websiteConfiguration),
47
+ websiteConfiguration: data.websiteConfiguration || null,
51
48
  creationDate: new Date(data.creationDate),
52
49
  objects,
53
50
  objectCount: data.objectCount || 0,
@@ -56,6 +53,20 @@ class S3Simulator {
56
53
  }
57
54
  }
58
55
  }
56
+
57
+ // Cria buckets definidos na config que ainda não existem
58
+ if (this.config.s3?.buckets) {
59
+ for (const bucketDef of this.config.s3.buckets) {
60
+ // suporta string simples ou objeto { name, region, websiteConfiguration }
61
+ const bucketName = typeof bucketDef === "string" ? bucketDef : bucketDef.name;
62
+ const region = typeof bucketDef === "object" ? (bucketDef.region || "us-east-1") : "us-east-1";
63
+ const websiteConfig = typeof bucketDef === "object" ? (bucketDef.websiteConfiguration || null) : null;
64
+
65
+ if (!this.buckets.has(bucketName)) {
66
+ this.createBucket(bucketName, { region, websiteConfiguration: websiteConfig });
67
+ }
68
+ }
69
+ }
59
70
  }
60
71
 
61
72
  // ─── Helpers de conteúdo em arquivo ───────────────────────────────────────
@@ -91,7 +102,7 @@ class S3Simulator {
91
102
 
92
103
  // ─── Buckets ──────────────────────────────────────────────────────────────
93
104
 
94
- createBucket(bucketName) {
105
+ createBucket(bucketName, options = {}) {
95
106
  if (!this.isValidBucketName(bucketName)) {
96
107
  return { error: { code: "InvalidBucketName", message: "Bucket name is invalid" }, status: 400 };
97
108
  }
@@ -100,8 +111,18 @@ class S3Simulator {
100
111
  return { error: { code: "BucketAlreadyExists", message: "Bucket already exists" }, status: 409 };
101
112
  }
102
113
 
114
+ const { region = "us-east-1", websiteConfiguration = null } = options;
115
+
103
116
  const bucket = {
104
117
  name: bucketName,
118
+ region,
119
+ website: !!websiteConfiguration,
120
+ websiteConfiguration: websiteConfiguration
121
+ ? {
122
+ IndexDocument: { Suffix: websiteConfiguration.IndexDocument?.Suffix || "index.html" },
123
+ ErrorDocument: { Key: websiteConfiguration.ErrorDocument?.Key || "error.html" },
124
+ }
125
+ : null,
105
126
  creationDate: new Date(),
106
127
  objects: new Map(),
107
128
  objectCount: 0,
@@ -110,7 +131,7 @@ class S3Simulator {
110
131
 
111
132
  this.buckets.set(bucketName, bucket);
112
133
  this.persistBuckets();
113
- logger.debug(`✅ Bucket S3 criado: ${bucketName}`);
134
+ logger.debug(`✅ Bucket S3 criado: ${bucketName} (region: ${region}${websiteConfiguration ? ", website: enabled" : ""})`);
114
135
  this.audit.record({ eventName: "CreateBucket", readOnly: false, resources: [{ ARN: `arn:aws:s3:::${bucketName}`, type: "AWS::S3::Bucket" }], requestParameters: { bucketName } });
115
136
  return { bucket };
116
137
  }
@@ -141,6 +162,8 @@ class S3Simulator {
141
162
  getBucketsInfo() {
142
163
  return Array.from(this.buckets.values()).map((bucket) => ({
143
164
  name: bucket.name,
165
+ region: bucket.region,
166
+ website: bucket.website,
144
167
  creationDate: bucket.creationDate,
145
168
  objectCount: bucket.objectCount,
146
169
  totalSize: bucket.totalSize,
@@ -154,6 +177,9 @@ class S3Simulator {
154
177
  }
155
178
  return {
156
179
  name: bucket.name,
180
+ region: bucket.region,
181
+ website: bucket.website,
182
+ websiteConfiguration: bucket.websiteConfiguration,
157
183
  creationDate: bucket.creationDate,
158
184
  objectCount: bucket.objectCount,
159
185
  totalSize: bucket.totalSize,
@@ -371,6 +397,46 @@ class S3Simulator {
371
397
  }));
372
398
  }
373
399
 
400
+ // ─── Website Config ───────────────────────────────────────────────────────
401
+
402
+ getWebsiteConfig(bucketName) {
403
+ const bucket = this.buckets.get(bucketName);
404
+ if (!bucket) {
405
+ return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
406
+ }
407
+ if (!bucket.websiteConfiguration) {
408
+ return { error: { code: "NoSuchWebsiteConfiguration", message: "The specified bucket does not have a website configuration" }, status: 404 };
409
+ }
410
+ return { websiteConfiguration: bucket.websiteConfiguration };
411
+ }
412
+
413
+ putWebsiteConfig(bucketName, config) {
414
+ const bucket = this.buckets.get(bucketName);
415
+ if (!bucket) {
416
+ return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
417
+ }
418
+ bucket.website = true;
419
+ bucket.websiteConfiguration = {
420
+ IndexDocument: { Suffix: config.IndexDocument?.Suffix || "index.html" },
421
+ ErrorDocument: { Key: config.ErrorDocument?.Key || "error.html" },
422
+ };
423
+ this.persistBuckets();
424
+ logger.debug(`🌐 Website config atualizada: ${bucketName}`);
425
+ return { success: true };
426
+ }
427
+
428
+ deleteWebsiteConfig(bucketName) {
429
+ const bucket = this.buckets.get(bucketName);
430
+ if (!bucket) {
431
+ return { error: { code: "NoSuchBucket", message: "Bucket does not exist" }, status: 404 };
432
+ }
433
+ bucket.website = false;
434
+ bucket.websiteConfiguration = null;
435
+ this.persistBuckets();
436
+ logger.debug(`🗑️ Website config removida: ${bucketName}`);
437
+ return { success: true };
438
+ }
439
+
374
440
  // ─── Persistência ─────────────────────────────────────────────────────────
375
441
 
376
442
  persistBucket(bucketName) {
@@ -395,6 +461,9 @@ class S3Simulator {
395
461
  }
396
462
  bucketsObj[name] = {
397
463
  creationDate: bucket.creationDate.toISOString(),
464
+ region: bucket.region || "us-east-1",
465
+ website: bucket.website || false,
466
+ websiteConfiguration: bucket.websiteConfiguration || null,
398
467
  objects: objectsObj,
399
468
  objectCount: bucket.objectCount,
400
469
  totalSize: bucket.totalSize,
@@ -40,17 +40,22 @@ class SQSService {
40
40
  const logger = require('../../utils/logger');
41
41
 
42
42
  for (const queueConfig of this.config.sqs.queues) {
43
- if (queueConfig.lambdaPath && this.lambdaService) {
44
- const handler = this.lambdaService.getHandler(queueConfig.lambdaPath);
45
- if (handler) {
43
+ if (typeof queueConfig === 'object' && queueConfig.lambdaName && this.lambdaService) {
44
+ const lambdaSimulator = this.lambdaService.simulator;
45
+ const lambda = lambdaSimulator?.getLambda(queueConfig.lambdaName);
46
+
47
+ if (lambda) {
48
+ const lambdaName = queueConfig.lambdaName;
49
+ const handler = async (event) => lambdaSimulator.invoke(lambdaName, event);
50
+
46
51
  this.simulator.attachLambdaToQueue(
47
52
  queueConfig.name,
48
53
  handler,
49
54
  { batchSize: queueConfig.batchSize || 10 }
50
55
  );
51
- logger.debug(`🔗 Fila ${queueConfig.name} -> Lambda ${queueConfig.lambdaPath}`);
56
+ logger.debug(`🔗 Fila ${queueConfig.name} -> Lambda ${lambdaName}`);
52
57
  } else {
53
- logger.warn(`⚠️ Lambda não encontrada para fila ${queueConfig.name}: ${queueConfig.lambdaPath}`);
58
+ logger.warn(`⚠️ Lambda não encontrada para fila ${queueConfig.name}: ${queueConfig.lambdaName}`);
54
59
  }
55
60
  }
56
61
  }
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  const express = require('express');
6
+ const cors = require('cors');
6
7
  const crypto = require('crypto');
7
8
  const SQSSimulator = require('./simulator');
8
9
  const logger = require('../../utils/logger');
@@ -19,6 +20,7 @@ class SQSServer {
19
20
  }
20
21
 
21
22
  setupMiddlewares() {
23
+ this.app.use(cors());
22
24
  this.app.use(express.raw({ type: '*/*', limit: '10mb' }));
23
25
  this.app.use((req, res, next) => {
24
26
  if (req.body && Buffer.isBuffer(req.body)) {
@@ -1,4 +1,5 @@
1
1
  const express = require('express');
2
+ const cors = require('cors');
2
3
  const crypto = require('crypto');
3
4
  const STSSimulator = require('./simulator');
4
5
  const logger = require('../../utils/logger');
@@ -14,6 +15,7 @@ class STSServer {
14
15
  }
15
16
 
16
17
  setupMiddlewares() {
18
+ this.app.use(cors());
17
19
  this.app.use(express.raw({ type: '*/*', limit: '10mb' }));
18
20
  this.app.use((req, res, next) => {
19
21
  if (req.body && Buffer.isBuffer(req.body)) {