@gugananuvem/aws-local-simulator 1.0.34 → 1.0.39

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 (150) hide show
  1. package/README.md +662 -658
  2. package/package.json +11 -4
  3. package/src/config/default-config.js +71 -0
  4. package/src/config/env-loader.js +4 -2
  5. package/src/middleware/authorization.js +271 -0
  6. package/src/middleware/cloudtrail-logger.js +333 -0
  7. package/src/middleware/error-handler.js +138 -0
  8. package/src/middleware/index.js +79 -0
  9. package/src/middleware/policy-enforcement.js +407 -0
  10. package/src/server.js +74 -4
  11. package/src/services/acm/index.js +66 -0
  12. package/src/services/acm/models/certificate.js +202 -0
  13. package/src/services/acm/server.js +224 -0
  14. package/src/services/acm/simulator.js +293 -0
  15. package/src/services/alb/index.js +104 -0
  16. package/src/services/alb/models/loadBalancer.js +88 -0
  17. package/src/services/alb/models/targetGroup.js +128 -0
  18. package/src/services/alb/server.js +160 -0
  19. package/src/services/alb/simulator.js +262 -0
  20. package/src/services/apigateway/index.js +22 -0
  21. package/src/services/apigateway/server.js +247 -35
  22. package/src/services/apigateway/simulator.js +780 -26
  23. package/src/services/asg/index.js +100 -0
  24. package/src/services/asg/models/autoScalingGroup.js +110 -0
  25. package/src/services/asg/models/launchTemplate.js +41 -0
  26. package/src/services/asg/models/scalingPolicy.js +51 -0
  27. package/src/services/asg/server.js +236 -0
  28. package/src/services/asg/simulator.js +410 -0
  29. package/src/services/aurora/cluster-manager.js +731 -0
  30. package/src/services/aurora/data-api.js +514 -0
  31. package/src/services/aurora/db-engine.js +270 -0
  32. package/src/services/aurora/index.js +159 -0
  33. package/src/services/aurora/rds-api.js +189 -0
  34. package/src/services/aurora/server.js +213 -0
  35. package/src/services/aurora/sql-translator.js +334 -0
  36. package/src/services/cloudformation/index.js +2 -2
  37. package/src/services/cloudformation/server.js +1 -0
  38. package/src/services/cloudfront/cache-engine.js +225 -0
  39. package/src/services/cloudfront/geo-restrictions.js +377 -0
  40. package/src/services/cloudfront/index.js +220 -0
  41. package/src/services/cloudfront/models/behavior.js +158 -0
  42. package/src/services/cloudfront/models/distribution.js +284 -0
  43. package/src/services/cloudfront/models/index.js +99 -0
  44. package/src/services/cloudfront/models/invalidation.js +109 -0
  45. package/src/services/cloudfront/models/origin.js +155 -0
  46. package/src/services/cloudfront/origin-router.js +552 -0
  47. package/src/services/cloudfront/origin-validator.js +398 -0
  48. package/src/services/cloudfront/server.js +512 -0
  49. package/src/services/cloudfront/signed-url-generator.js +377 -0
  50. package/src/services/cloudfront/simulator.js +425 -0
  51. package/src/services/cloudfront/store.js +326 -0
  52. package/src/services/cloudshell/env-builder.js +27 -0
  53. package/src/services/cloudshell/index.js +48 -0
  54. package/src/services/cloudshell/routes.js +60 -0
  55. package/src/services/cloudshell/session-manager.js +226 -0
  56. package/src/services/cloudshell/static/.gitkeep +0 -0
  57. package/src/services/cloudshell/static/index.html +406 -0
  58. package/src/services/cloudshell/websocket-handler.js +107 -0
  59. package/src/services/cloudtrail/index.js +2 -3
  60. package/src/services/cloudtrail/server.js +68 -0
  61. package/src/services/cognito/server.js +4 -1
  62. package/src/services/dynamodb/server.js +4 -1
  63. package/src/services/dynamodb/simulator.js +2 -1
  64. package/src/services/ec2/index.js +101 -0
  65. package/src/services/ec2/instanceServer.js +311 -0
  66. package/src/services/ec2/models/ami.js +39 -0
  67. package/src/services/ec2/models/instance.js +98 -0
  68. package/src/services/ec2/models/networkInterface.js +74 -0
  69. package/src/services/ec2/models/securityGroup.js +71 -0
  70. package/src/services/ec2/server.js +350 -0
  71. package/src/services/ec2/simulator.js +436 -0
  72. package/src/services/eventbridge/server.js +36 -0
  73. package/src/services/eventbridge/simulator.js +230 -45
  74. package/src/services/iam/index.js +65 -0
  75. package/src/services/iam/middleware.js +99 -0
  76. package/src/services/iam/server.js +331 -0
  77. package/src/services/iam/simulator.js +1432 -0
  78. package/src/services/index.js +28 -0
  79. package/src/services/kinesis-analytics/index.js +105 -0
  80. package/src/services/kinesis-analytics/models/application.js +325 -0
  81. package/src/services/kinesis-analytics/models/index.js +12 -0
  82. package/src/services/kinesis-analytics/server.js +250 -0
  83. package/src/services/kinesis-analytics/simulator.js +407 -0
  84. package/src/services/kinesis-analytics/store.js +176 -0
  85. package/src/services/kinesis-data-streams/index.js +106 -0
  86. package/src/services/kinesis-data-streams/models/index.js +12 -0
  87. package/src/services/kinesis-data-streams/models/record.js +70 -0
  88. package/src/services/kinesis-data-streams/models/stream.js +280 -0
  89. package/src/services/kinesis-data-streams/server.js +255 -0
  90. package/src/services/kinesis-data-streams/simulator.js +514 -0
  91. package/src/services/kinesis-data-streams/store.js +163 -0
  92. package/src/services/kinesis-firehose/index.js +106 -0
  93. package/src/services/kinesis-firehose/models/delivery-stream.js +265 -0
  94. package/src/services/kinesis-firehose/models/index.js +10 -0
  95. package/src/services/kinesis-firehose/server.js +215 -0
  96. package/src/services/kinesis-firehose/simulator.js +388 -0
  97. package/src/services/kinesis-firehose/store.js +163 -0
  98. package/src/services/kms/server.js +4 -1
  99. package/src/services/lambda/handler-loader.js +50 -5
  100. package/src/services/lambda/index.js +26 -0
  101. package/src/services/lambda/server.js +18 -12
  102. package/src/services/lambda/simulator.js +73 -17
  103. package/src/services/opensearch/index.js +105 -0
  104. package/src/services/opensearch/models/document.js +115 -0
  105. package/src/services/opensearch/models/index.js +73 -0
  106. package/src/services/opensearch/server.js +310 -0
  107. package/src/services/opensearch/simulator.js +605 -0
  108. package/src/services/opensearch/store.js +162 -0
  109. package/src/services/organizations/control-tower-simulator.js +321 -0
  110. package/src/services/organizations/control-tower-store.js +182 -0
  111. package/src/services/organizations/iam-integration.js +413 -0
  112. package/src/services/organizations/index.js +172 -0
  113. package/src/services/organizations/models/index.js +13 -0
  114. package/src/services/organizations/models/invitation.js +48 -0
  115. package/src/services/organizations/models/organization.js +566 -0
  116. package/src/services/organizations/models/role.js +244 -0
  117. package/src/services/organizations/models/scp.js +131 -0
  118. package/src/services/organizations/server.js +736 -0
  119. package/src/services/organizations/simulator.js +593 -0
  120. package/src/services/organizations/store.js +235 -0
  121. package/src/services/organizations/vpc-integration.js +453 -0
  122. package/src/services/route53/index.js +67 -0
  123. package/src/services/route53/models/healthCheck.js +101 -0
  124. package/src/services/route53/models/hostedZone.js +37 -0
  125. package/src/services/route53/models/resourceRecord.js +106 -0
  126. package/src/services/route53/server.js +841 -0
  127. package/src/services/route53/simulator.js +350 -0
  128. package/src/services/s3/index.js +62 -0
  129. package/src/services/s3/presigned-url-generator.js +337 -0
  130. package/src/services/s3/server.js +231 -0
  131. package/src/services/s3/simulator.js +551 -4
  132. package/src/services/sns/index.js +31 -0
  133. package/src/services/sns/server.js +9 -0
  134. package/src/services/sqs/index.js +38 -0
  135. package/src/services/sqs/server.js +57 -4
  136. package/src/services/sqs/simulator.js +423 -33
  137. package/src/services/sts/simulator.js +209 -17
  138. package/src/services/vpc/README.md +459 -0
  139. package/src/services/vpc/ec2ResponseFormatter.js +431 -0
  140. package/src/services/vpc/index.js +90 -0
  141. package/src/services/vpc/models/internetGateway.js +71 -0
  142. package/src/services/vpc/models/routeTable.js +160 -0
  143. package/src/services/vpc/models/subnet.js +48 -0
  144. package/src/services/vpc/models/vpc.js +37 -0
  145. package/src/services/vpc/models/vpcEndpoint.js +100 -0
  146. package/src/services/vpc/models/vpcGateway.js +134 -0
  147. package/src/services/vpc/server.js +836 -0
  148. package/src/services/vpc/simulator.js +613 -0
  149. package/src/utils/policy-cache.js +215 -0
  150. package/aws-config +0 -154
@@ -11,17 +11,46 @@ class SQSSimulator {
11
11
  constructor(config, lambdaService) {
12
12
  this.config = config;
13
13
  this.lambdaService = lambdaService;
14
+ this.snsService = null;
15
+ this.region = "local";
16
+ this.accountId = "000000000000";
14
17
  this.dataDir = path.join(process.env.AWS_LOCAL_SIMULATOR_DATA_DIR, 'sqs');
15
18
  this.store = new LocalStore(this.dataDir);
16
19
  this.queues = new Map();
17
20
  }
18
21
 
22
+ setLambdaService(lambdaService) {
23
+ this.lambdaService = lambdaService;
24
+ }
25
+
26
+ setSnsService(snsService) {
27
+ this.snsService = snsService;
28
+ }
29
+
30
+ setRegion(region) {
31
+ this.region = region;
32
+ }
33
+
34
+ setAccountId(accountId) {
35
+ this.accountId = accountId;
36
+ }
37
+
19
38
  async initialize() {
20
39
  logger.debug('Inicializando SQS Simulator...');
21
40
  this.loadQueues();
41
+ this.reattachLambdaHandlers();
22
42
  logger.debug(`✅ SQS Simulator inicializado com ${this.queues.size} filas`);
23
43
  }
24
44
 
45
+ reattachLambdaHandlers() {
46
+ if (!this.lambdaService?.simulator) return;
47
+ for (const [queueName, queue] of this.queues.entries()) {
48
+ if (!queue.handler && queue.lambdaName) {
49
+ this.attachLambdaToQueue(queueName, queue.lambdaName, { batchSize: queue.batchSize });
50
+ }
51
+ }
52
+ }
53
+
25
54
  loadQueues() {
26
55
  // Carrega filas da configuraÃ§ÃŖo
27
56
  if (this.config.sqs?.queues) {
@@ -31,7 +60,11 @@ class SQSSimulator {
31
60
  // Restore persisted messages for this queue
32
61
  const queue = this.queues.get(name);
33
62
  if (queue) {
34
- queue.messages = this.store.read(name) || [];
63
+ const savedMessages = this.store.read(name) || [];
64
+ queue.messages = savedMessages.map(msg => ({
65
+ ...msg,
66
+ messageReceiveTracking: new Map() // Reinitialize tracking map
67
+ }));
35
68
  queue.messageCount = queue.messages.length;
36
69
  }
37
70
  }
@@ -43,7 +76,23 @@ class SQSSimulator {
43
76
  for (const [name, data] of Object.entries(savedQueues)) {
44
77
  if (!this.queues.has(name)) {
45
78
  const messages = this.store.read(name) || [];
46
- this.queues.set(name, { ...data, messages });
79
+ const queue = {
80
+ ...data,
81
+ messages,
82
+ messageReceiveTracking: new Map(),
83
+ handler: null,
84
+ lambdaName: data.lambdaName || null
85
+ };
86
+ this.queues.set(name, queue);
87
+ } else {
88
+ // Merge with existing queue, preserving RedrivePolicy from saved state
89
+ const queue = this.queues.get(name);
90
+ if (data.redrivePolicy) {
91
+ queue.redrivePolicy = data.redrivePolicy;
92
+ }
93
+ if (data.isDLQ !== undefined) {
94
+ queue.isDLQ = data.isDLQ;
95
+ }
47
96
  }
48
97
  }
49
98
  }
@@ -54,27 +103,56 @@ class SQSSimulator {
54
103
  return { error: { code: 'QueueAlreadyExists', message: 'Queue already exists' }, status: 409 };
55
104
  }
56
105
 
106
+ const region = attributes.Region || this.region || "local";
107
+ const accountId = attributes.AccountId || this.accountId || "000000000000";
108
+
57
109
  const queue = {
58
110
  name: queueName,
59
111
  url: `http://localhost:${this.config.ports.sqs}/queue/${queueName}`,
60
- arn: `arn:aws:sqs:local:000000000000:${queueName}`,
112
+ arn: `arn:aws:sqs:${region}:${accountId}:${queueName}`,
61
113
  messages: [],
62
114
  handler: null,
115
+ lambdaName: null,
63
116
  batchSize: parseInt(attributes.BatchSize || 10),
64
117
  visibilityTimeout: parseInt(attributes.VisibilityTimeout || 30),
65
118
  delaySeconds: parseInt(attributes.DelaySeconds || 0),
66
119
  messageRetentionPeriod: parseInt(attributes.MessageRetentionPeriod || 345600),
120
+
121
+ // DLQ Support
122
+ redrivePolicy: null, // Will be set if RedrivePolicy is provided
123
+ isDLQ: false, // Flag to identify if this is a Dead Letter Queue
124
+
67
125
  createdAt: new Date().toISOString(),
68
- messageCount: 0
126
+ messageCount: 0,
127
+
128
+ // Track message receive counts for DLQ handling
129
+ messageReceiveTracking: new Map() // messageId -> { receiveCount, firstReceivedAt, lastReceivedAt, receiptHandles: Set }
69
130
  };
70
131
 
132
+ this.queues.set(queueName, queue);
133
+
134
+ // Parse RedrivePolicy if provided
135
+ if (attributes.RedrivePolicy) {
136
+ try {
137
+ const policy = typeof attributes.RedrivePolicy === 'string'
138
+ ? JSON.parse(attributes.RedrivePolicy)
139
+ : attributes.RedrivePolicy;
140
+ queue.redrivePolicy = {
141
+ deadLetterTargetArn: policy.deadLetterTargetArn,
142
+ maxReceiveCount: parseInt(policy.maxReceiveCount || 3)
143
+ };
144
+ logger.debug(`✅ Fila ${queueName} configurada com DLQ: ${queue.redrivePolicy.deadLetterTargetArn}, maxReceiveCount: ${queue.redrivePolicy.maxReceiveCount}`);
145
+ } catch (e) {
146
+ logger.warn(`âš ī¸ RedrivePolicy invÃĄlida para ${queueName}: ${e.message}`);
147
+ }
148
+ }
149
+
71
150
  if (attributes.LambdaName) {
151
+ queue.lambdaName = attributes.LambdaName;
72
152
  this.attachLambdaToQueue(queueName, attributes.LambdaName, { batchSize: queue.batchSize });
73
153
  }
74
154
 
75
- this.queues.set(queueName, queue);
76
155
  this.persistQueues();
77
- // Only initialize message store if it doesn't already exist
78
156
  if (!this.store.read(queueName)) {
79
157
  this.store.write(queueName, []);
80
158
  }
@@ -90,6 +168,8 @@ class SQSSimulator {
90
168
  return this.createQueueAction(req);
91
169
  case 'SetQueueAttributes':
92
170
  return this.setQueueAttributesAction(req);
171
+ case 'GetQueueAttributes':
172
+ return this.getQueueAttributesAction(req);
93
173
  case 'SendMessage':
94
174
  return this.sendMessageAction(req);
95
175
  case 'SendMessageBatch':
@@ -117,10 +197,12 @@ class SQSSimulator {
117
197
  }
118
198
 
119
199
  // Parses Attribute.N.Name and Attribute.N.Value from request body/query
200
+ // Also supports direct attribute keys like RedrivePolicy
120
201
  extractAttributes(req) {
121
202
  const attributes = {};
122
- const source = req.body.Action ? req.body : req.query;
203
+ const source = req.body && Object.keys(req.body).length > 0 ? req.body : req.query;
123
204
 
205
+ // First, look for Attribute.N.Name/Value pattern
124
206
  Object.keys(source).forEach(key => {
125
207
  if (key.startsWith('Attribute.') && key.endsWith('.Name')) {
126
208
  const index = key.split('.')[1];
@@ -129,6 +211,15 @@ class SQSSimulator {
129
211
  attributes[name] = value;
130
212
  }
131
213
  });
214
+
215
+ // Also support direct keys like RedrivePolicy, DelaySeconds, etc.
216
+ const directKeys = ['RedrivePolicy', 'DelaySeconds', 'VisibilityTimeout', 'MessageRetentionPeriod', 'BatchSize', 'LambdaName'];
217
+ directKeys.forEach(key => {
218
+ if (source[key] !== undefined) {
219
+ attributes[key] = source[key];
220
+ }
221
+ });
222
+
132
223
  return attributes;
133
224
  }
134
225
 
@@ -158,11 +249,31 @@ class SQSSimulator {
158
249
  if (attributes.VisibilityTimeout !== undefined) queue.visibilityTimeout = parseInt(attributes.VisibilityTimeout);
159
250
  if (attributes.MessageRetentionPeriod !== undefined) queue.messageRetentionPeriod = parseInt(attributes.MessageRetentionPeriod);
160
251
 
252
+ // Support for RedrivePolicy (Dead Letter Queue configuration)
253
+ if (attributes.RedrivePolicy !== undefined) {
254
+ if (attributes.RedrivePolicy) {
255
+ try {
256
+ const policy = typeof attributes.RedrivePolicy === 'string'
257
+ ? JSON.parse(attributes.RedrivePolicy)
258
+ : attributes.RedrivePolicy;
259
+ queue.redrivePolicy = {
260
+ deadLetterTargetArn: policy.deadLetterTargetArn,
261
+ maxReceiveCount: parseInt(policy.maxReceiveCount || 3)
262
+ };
263
+ logger.debug(`✅ RedrivePolicy configurada para ${queueName}: ${JSON.stringify(queue.redrivePolicy)}`);
264
+ } catch (e) {
265
+ logger.warn(`âš ī¸ RedrivePolicy invÃĄlida para ${queueName}: ${e.message}`);
266
+ return { error: { code: 'InvalidParameterValue', message: 'Invalid RedrivePolicy format' }, status: 400 };
267
+ }
268
+ } else {
269
+ queue.redrivePolicy = null;
270
+ logger.debug(`✅ RedrivePolicy removida de ${queueName}`);
271
+ }
272
+ }
273
+
161
274
  if (attributes.LambdaName !== undefined) {
162
275
  queue.lambdaName = attributes.LambdaName;
163
276
  if (attributes.LambdaName) {
164
- // Here we'd normally re-attach if we had a direct reference to the lambda service function
165
- // For now, we store the name and the server/index.js will handle re-attachment or we use the name to look it up
166
277
  this.attachLambdaToQueue(queueName, attributes.LambdaName, { batchSize: queue.batchSize });
167
278
  } else {
168
279
  queue.handler = null;
@@ -177,6 +288,36 @@ class SQSSimulator {
177
288
  return { success: true };
178
289
  }
179
290
 
291
+ getQueueAttributesAction(req) {
292
+ const queueName = this.resolveQueueName(req);
293
+ const queue = this.queues.get(queueName);
294
+
295
+ if (!queue) {
296
+ return { error: { code: 'QueueDoesNotExist', message: 'Queue does not exist' }, status: 400 };
297
+ }
298
+
299
+ // Build response with all attributes
300
+ const attributes = {
301
+ DelaySeconds: String(queue.delaySeconds),
302
+ VisibilityTimeout: String(queue.visibilityTimeout),
303
+ MessageRetentionPeriod: String(queue.messageRetentionPeriod),
304
+ ApproximateNumberOfMessages: String(queue.messageCount),
305
+ ApproximateNumberOfMessagesNotVisible: String(
306
+ queue.messages.filter(m => m.invisibleUntil && m.invisibleUntil > Date.now()).length
307
+ ),
308
+ CreatedTimestamp: String(Math.floor(new Date(queue.createdAt).getTime() / 1000)),
309
+ QueueArn: queue.arn
310
+ };
311
+
312
+ // Add RedrivePolicy if configured
313
+ if (queue.redrivePolicy) {
314
+ attributes.RedrivePolicy = JSON.stringify(queue.redrivePolicy);
315
+ }
316
+
317
+ return { attributes };
318
+ }
319
+
320
+
180
321
  sendMessageAction(req) {
181
322
  const queueName = this.resolveQueueName(req);
182
323
  const queue = this.queues.get(queueName);
@@ -186,21 +327,62 @@ class SQSSimulator {
186
327
  }
187
328
 
188
329
  const messageBody = req.query.MessageBody || req.body.MessageBody;
330
+ return this.sendMessage({ QueueUrl: queue.url, MessageBody: messageBody });
331
+ }
332
+
333
+ sendMessage(params) {
334
+ const { QueueUrl, MessageBody } = params;
335
+ if (!QueueUrl || !MessageBody) {
336
+ return { error: { code: 'MissingParameter', message: 'QueueUrl and MessageBody are required' }, status: 400 };
337
+ }
338
+
339
+ let queueName;
340
+ const match = QueueUrl.match(/\/queue\/([^/?]+)/);
341
+ if (match) {
342
+ queueName = match[1];
343
+ } else {
344
+ const arnMatch = QueueUrl.match(/:([^:]+)$/);
345
+ queueName = arnMatch ? arnMatch[1] : QueueUrl;
346
+ }
347
+
348
+ const queue = this.queues.get(queueName);
349
+ if (!queue) {
350
+ return { error: { code: 'QueueDoesNotExist', message: 'Queue does not exist' }, status: 400 };
351
+ }
352
+
189
353
  const messageId = crypto.randomUUID();
190
354
  const receiptHandle = crypto.randomUUID();
191
- const md5 = crypto.createHash('md5').update(messageBody).digest('hex');
355
+ const md5 = crypto.createHash('md5').update(MessageBody).digest('hex');
356
+ const nowMs = Date.now();
192
357
 
193
358
  const message = {
194
359
  MessageId: messageId,
195
360
  ReceiptHandle: receiptHandle,
196
- Body: messageBody,
361
+ Body: MessageBody,
197
362
  MD5OfBody: md5,
198
- Attributes: {},
199
- timestamp: Date.now()
363
+ Attributes: {
364
+ ApproximateReceiveCount: "0", // Will be incremented when received
365
+ SentTimestamp: String(nowMs),
366
+ ApproximateFirstReceiveTimestamp: null,
367
+ SenderId: "AIDAJDPLRKLG7UEXAMPLE",
368
+ AWSTraceHeader: null
369
+ },
370
+ timestamp: nowMs,
371
+ receiveCount: 0, // Track internally how many times received
372
+ invisibleUntil: null // When message becomes visible again
200
373
  };
201
374
 
202
375
  queue.messages.push(message);
203
376
  queue.messageCount++;
377
+
378
+ // Initialize receive tracking for this message
379
+ queue.messageReceiveTracking.set(messageId, {
380
+ receiveCount: 0,
381
+ firstReceivedAt: null,
382
+ lastReceivedAt: null,
383
+ receiptHandles: new Set()
384
+ });
385
+
204
386
  this.persistQueue(queueName);
205
387
 
206
388
  logger.verboso(`📨 Mensagem enviada para ${queueName}: ${messageId}`);
@@ -276,14 +458,54 @@ class SQSSimulator {
276
458
  }
277
459
 
278
460
  const maxNumberOfMessages = parseInt(req.query.MaxNumberOfMessages || req.body.MaxNumberOfMessages || 1);
279
- const messages = queue.messages.splice(0, maxNumberOfMessages);
461
+ const nowMs = Date.now();
462
+
463
+ // Collect visible messages (don't remove yet)
464
+ const messagesToReturn = [];
465
+ for (let i = 0; i < queue.messages.length && messagesToReturn.length < maxNumberOfMessages; i++) {
466
+ const msg = queue.messages[i];
467
+ if (!msg.invisibleUntil || msg.invisibleUntil <= nowMs) {
468
+ messagesToReturn.push(msg);
469
+ }
470
+ }
280
471
 
281
- if (messages.length > 0) {
282
- queue.messageCount -= messages.length;
283
- this.persistQueue(queueName);
472
+ // Process each message
473
+ for (const message of messagesToReturn) {
474
+ // Generate new ReceiptHandle for this receive (unique per receive)
475
+ const newReceiptHandle = crypto.randomUUID();
476
+ message.ReceiptHandle = newReceiptHandle;
477
+
478
+ // Increment receive count
479
+ message.receiveCount++;
480
+ message.Attributes.ApproximateReceiveCount = String(message.receiveCount);
481
+
482
+ // Set approximate first receive timestamp on first receive
483
+ if (!message.Attributes.ApproximateFirstReceiveTimestamp || message.Attributes.ApproximateFirstReceiveTimestamp === "null") {
484
+ message.Attributes.ApproximateFirstReceiveTimestamp = String(nowMs);
485
+ }
486
+
487
+ // Set visibility timeout (make invisible for next visibility timeout seconds)
488
+ message.invisibleUntil = nowMs + (queue.visibilityTimeout * 1000);
489
+
490
+ // Update tracking
491
+ const tracking = queue.messageReceiveTracking.get(message.MessageId);
492
+ if (tracking) {
493
+ tracking.receiveCount = message.receiveCount;
494
+ tracking.lastReceivedAt = nowMs;
495
+ if (!tracking.firstReceivedAt) {
496
+ tracking.firstReceivedAt = nowMs;
497
+ }
498
+ tracking.receiptHandles.add(newReceiptHandle);
499
+ }
500
+
501
+ logger.verboso(`đŸ“Ŧ Mensagem recebida ${message.MessageId} da fila ${queueName} (receiveCount: ${message.receiveCount})`);
284
502
  }
285
503
 
286
- return { messages };
504
+ // Update message count
505
+ queue.messageCount = queue.messages.filter(m => !m.invisibleUntil || m.invisibleUntil <= nowMs).length;
506
+ this.persistQueue(queueName);
507
+
508
+ return { messages: messagesToReturn };
287
509
  }
288
510
 
289
511
  deleteMessageAction(req) {
@@ -295,11 +517,15 @@ class SQSSimulator {
295
517
  return { error: { code: 'QueueDoesNotExist', message: 'Queue does not exist' }, status: 400 };
296
518
  }
297
519
 
298
- const index = queue.messages.findIndex(m => m.ReceiptHandle === receiptHandle);
299
- if (index !== -1) {
300
- queue.messages.splice(index, 1);
520
+ // Find message by current ReceiptHandle (which was set at receive time)
521
+ const messageIndex = queue.messages.findIndex(m => m.ReceiptHandle === receiptHandle);
522
+ if (messageIndex !== -1) {
523
+ const deletedMessage = queue.messages.splice(messageIndex, 1)[0];
301
524
  queue.messageCount--;
525
+ logger.verboso(`đŸ—‘ī¸ Mensagem deletada: ${deletedMessage.MessageId} da fila ${queueName}`);
302
526
  this.persistQueue(queueName);
527
+ } else {
528
+ logger.warn(`âš ī¸ Mensagem com ReceiptHandle ${receiptHandle} nÃŖo encontrada em ${queueName}`);
303
529
  }
304
530
 
305
531
  return { success: true };
@@ -326,6 +552,16 @@ class SQSSimulator {
326
552
  return { queues };
327
553
  }
328
554
 
555
+ _extractLambdaFunctionName(lambdaRef) {
556
+ if (!lambdaRef || typeof lambdaRef === 'function') return null;
557
+ if (typeof lambdaRef !== 'string') return null;
558
+ const lambdaStr = lambdaRef.trim();
559
+ const arnMatch = lambdaStr.match(/arn:aws:lambda:[^:]+:[^:]+:function:([^:]+)/);
560
+ if (arnMatch) return arnMatch[1];
561
+ if (lambdaStr.startsWith('function:')) return lambdaStr.slice(9);
562
+ return lambdaStr;
563
+ }
564
+
329
565
  attachLambdaToQueue(queueName, handler, options = {}) {
330
566
  const queue = this.queues.get(queueName);
331
567
 
@@ -334,31 +570,131 @@ class SQSSimulator {
334
570
  return this.attachLambdaToQueue(queueName, handler, options);
335
571
  }
336
572
 
337
- queue.handler = handler;
573
+ let resolvedHandler = handler;
574
+
575
+ if (typeof handler === 'string') {
576
+ const functionName = this._extractLambdaFunctionName(handler);
577
+ queue.lambdaName = functionName || handler;
578
+
579
+ if (this.lambdaService?.simulator) {
580
+ const lambdaSim = this.lambdaService.simulator;
581
+ if (functionName) {
582
+ resolvedHandler = async (event) => {
583
+ return lambdaSim.invoke(functionName, event, "Event");
584
+ };
585
+ logger.debug(`🔗 Lambda "${functionName}" (resolved via ARN/nome) associada à fila ${queueName}`);
586
+ } else {
587
+ resolvedHandler = async (event) => {
588
+ return lambdaSim.invoke(handler, event, "Event");
589
+ };
590
+ logger.debug(`🔗 Lambda "${handler}" associada à fila ${queueName}`);
591
+ }
592
+ } else {
593
+ logger.warn(`âš ī¸ Lambda service nÃŖo disponível para resolver ${handler} na fila ${queueName}`);
594
+ resolvedHandler = null;
595
+ }
596
+ } else if (typeof handler === 'function') {
597
+ queue.lambdaName = options?.lambdaName || queue.lambdaName;
598
+ logger.debug(`🔗 Handler direto associado à fila ${queueName}`);
599
+ }
600
+
601
+ queue.handler = resolvedHandler;
338
602
  queue.batchSize = options.batchSize || 10;
339
603
  this.persistQueue(queueName);
340
604
 
341
- logger.debug(`🔗 Lambda associada à fila ${queueName}`);
342
-
343
605
  return queue;
344
606
  }
345
607
 
608
+ async moveMessageToDLQ(queue, message) {
609
+ if (!queue.redrivePolicy) {
610
+ logger.debug(`âš ī¸ Nenhuma DLQ configurada para ${queue.name}, mensagem ${message.MessageId} serÃĄ descartada`);
611
+ return;
612
+ }
613
+
614
+ const dlqArn = queue.redrivePolicy.deadLetterTargetArn;
615
+ const dlqName = dlqArn.split(':').pop();
616
+ const dlq = this.queues.get(dlqName);
617
+
618
+ if (!dlq) {
619
+ logger.error(`❌ DLQ nÃŖo encontrada: ${dlqName}`);
620
+ return;
621
+ }
622
+
623
+ // Create new message in DLQ with original body and metadata
624
+ const dlqMessage = {
625
+ MessageId: crypto.randomUUID(),
626
+ ReceiptHandle: crypto.randomUUID(),
627
+ Body: message.Body,
628
+ MD5OfBody: message.MD5OfBody,
629
+ Attributes: {
630
+ ...message.Attributes,
631
+ ApproximateReceiveCount: "0", // Reset in DLQ
632
+ ApproximateFirstReceiveTimestamp: null
633
+ },
634
+ timestamp: Date.now(),
635
+ receiveCount: 0,
636
+ invisibleUntil: null,
637
+ originalMessageId: message.MessageId // Reference to original
638
+ };
639
+
640
+ dlq.messages.push(dlqMessage);
641
+ dlq.messageCount++;
642
+
643
+ dlq.messageReceiveTracking.set(dlqMessage.MessageId, {
644
+ receiveCount: 0,
645
+ firstReceivedAt: null,
646
+ lastReceivedAt: null,
647
+ receiptHandles: new Set()
648
+ });
649
+
650
+ logger.warn(`🔄 Mensagem ${message.MessageId} movida para DLQ: ${dlqName}`);
651
+ this.persistQueue(dlqName);
652
+ }
653
+
654
+ async checkAndMoveToDLQ(queue, message) {
655
+ if (!queue.redrivePolicy) return false;
656
+
657
+ const maxReceiveCount = queue.redrivePolicy.maxReceiveCount;
658
+ const receiveCount = parseInt(message.Attributes.ApproximateReceiveCount || 0);
659
+
660
+ if (receiveCount >= maxReceiveCount) {
661
+ await this.moveMessageToDLQ(queue, message);
662
+ return true;
663
+ }
664
+
665
+ return false;
666
+ }
667
+
346
668
  async processQueueMessages(queue) {
347
669
  if (!queue.handler || queue.messages.length === 0) return;
348
670
 
349
- const messagesToProcess = queue.messages.splice(0, queue.batchSize);
671
+ const visibleMessages = queue.messages.filter(m =>
672
+ !m.invisibleUntil || m.invisibleUntil <= Date.now()
673
+ );
674
+
675
+ if (visibleMessages.length === 0) return;
676
+
677
+ const messagesToProcess = visibleMessages.splice(0, queue.batchSize);
678
+ const nowMs = Date.now();
350
679
 
351
680
  const event = {
352
681
  Records: messagesToProcess.map(msg => ({
353
682
  messageId: msg.MessageId,
354
683
  receiptHandle: msg.ReceiptHandle,
355
684
  body: msg.Body,
356
- attributes: msg.Attributes,
685
+ attributes: {
686
+ ApproximateReceiveCount: msg.Attributes.ApproximateReceiveCount || "0",
687
+ SentTimestamp: String(msg.Attributes.SentTimestamp || msg.timestamp || nowMs),
688
+ SenderId: msg.Attributes.SenderId || "AIDAJDPLRKLG7UEXAMPLE",
689
+ ApproximateFirstReceiveTimestamp: msg.Attributes.ApproximateFirstReceiveTimestamp || String(nowMs),
690
+ AWSTraceHeader: msg.Attributes?.AWSTraceHeader,
691
+ },
357
692
  messageAttributes: {},
358
693
  md5OfBody: msg.MD5OfBody,
694
+ md5OfMessageAttributes: null,
359
695
  eventSource: 'aws:sqs',
360
696
  eventSourceARN: queue.arn,
361
- awsRegion: 'local'
697
+ awsRegion: this.region || 'local'
362
698
  }))
363
699
  };
364
700
 
@@ -368,16 +704,69 @@ class SQSSimulator {
368
704
  const result = await queue.handler(event);
369
705
 
370
706
  if (result && result.batchItemFailures && result.batchItemFailures.length > 0) {
707
+ // Only failed messages are returned to the queue
371
708
  const failedIds = new Set(result.batchItemFailures.map(f => f.itemIdentifier));
372
709
  const failedMessages = messagesToProcess.filter(msg => failedIds.has(msg.MessageId));
373
- queue.messages.unshift(...failedMessages);
374
- logger.warn(`âš ī¸ ${failedMessages.length} mensagens falharam`);
710
+ const successMessages = messagesToProcess.filter(msg => !failedIds.has(msg.MessageId));
711
+
712
+ // Remove successfully processed messages from queue
713
+ for (const successMsg of successMessages) {
714
+ const idx = queue.messages.findIndex(m => m.MessageId === successMsg.MessageId);
715
+ if (idx !== -1) {
716
+ queue.messages.splice(idx, 1);
717
+ }
718
+ }
719
+
720
+ // Return failed messages to queue (will be reprocessed after visibility timeout)
721
+ for (const msg of failedMessages) {
722
+ // Check if should go to DLQ
723
+ const shouldMoveToDLQ = await this.checkAndMoveToDLQ(queue, msg);
724
+ if (!shouldMoveToDLQ) {
725
+ // Message already in queue, just reset invisibility for retry
726
+ msg.invisibleUntil = 0;
727
+ logger.verboso(`â†Šī¸ Mensagem ${msg.MessageId} retornada à fila ${queue.name} (tentativa ${msg.receiveCount + 1})`);
728
+ } else {
729
+ // Message was moved to DLQ, remove it from current queue
730
+ const idx = queue.messages.findIndex(m => m.MessageId === msg.MessageId);
731
+ if (idx !== -1) {
732
+ queue.messages.splice(idx, 1);
733
+ }
734
+ }
735
+ }
736
+
737
+ // Update message count
738
+ queue.messageCount = queue.messages.filter(m => !m.invisibleUntil || m.invisibleUntil <= nowMs).length;
739
+
740
+ logger.warn(`âš ī¸ ${failedMessages.length} mensagens falharam na fila ${queue.name}, ${successMessages.length} processadas com sucesso`);
375
741
  } else {
376
- queue.messageCount -= messagesToProcess.length;
742
+ // If no explicit batchItemFailures, treat all as successful (delete them)
743
+ for (const msg of messagesToProcess) {
744
+ const idx = queue.messages.findIndex(m => m.MessageId === msg.MessageId);
745
+ if (idx !== -1) {
746
+ queue.messages.splice(idx, 1);
747
+ }
748
+ }
749
+ logger.verboso(`✅ ${messagesToProcess.length} mensagens da fila ${queue.name} processadas com sucesso`);
750
+ queue.messageCount = queue.messages.filter(m => !m.invisibleUntil || m.invisibleUntil <= nowMs).length;
377
751
  }
378
752
  } catch (error) {
379
- logger.error(`❌ Erro ao processar mensagens:`, error);
380
- queue.messages.unshift(...messagesToProcess);
753
+ logger.error(`❌ Erro ao processar mensagens da fila ${queue.name}:`, error);
754
+
755
+ // On exception, return all messages to queue for retry
756
+ for (const msg of messagesToProcess) {
757
+ const shouldMoveToDLQ = await this.checkAndMoveToDLQ(queue, msg);
758
+ if (!shouldMoveToDLQ) {
759
+ // Message already in queue, just reset invisibility for retry
760
+ msg.invisibleUntil = 0;
761
+ } else {
762
+ // Message was moved to DLQ, remove it from current queue
763
+ const idx = queue.messages.findIndex(m => m.MessageId === msg.MessageId);
764
+ if (idx !== -1) {
765
+ queue.messages.splice(idx, 1);
766
+ }
767
+ }
768
+ }
769
+ queue.messageCount = queue.messages.filter(m => !m.invisibleUntil || m.invisibleUntil <= nowMs).length;
381
770
  }
382
771
 
383
772
  this.persistQueue(queue.name);
@@ -395,10 +784,11 @@ class SQSSimulator {
395
784
  delaySeconds: queue.delaySeconds,
396
785
  messageRetentionPeriod: queue.messageRetentionPeriod,
397
786
  lambdaName: queue.lambdaName,
787
+ redrivePolicy: queue.redrivePolicy, // Persist RedrivePolicy
788
+ isDLQ: queue.isDLQ,
398
789
  createdAt: queue.createdAt,
399
790
  messageCount: queue.messageCount
400
791
  };
401
-
402
792
  }
403
793
  this.store.write('__queues__', queuesObj);
404
794
  }