@gugananuvem/aws-local-simulator 1.0.20 → 1.0.21

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gugananuvem/aws-local-simulator",
3
- "version": "1.0.20",
3
+ "version": "1.0.21",
4
4
  "description": "Simulador local completo para serviços AWS",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -120,6 +120,6 @@
120
120
  "optional": true
121
121
  }
122
122
  },
123
- "buildDate": "2026-04-22T02:31:17.105Z",
123
+ "buildDate": "2026-04-22T17:40:52.588Z",
124
124
  "published": true
125
125
  }
@@ -1,4 +1,4 @@
1
- 'use strict';
1
+ "use strict";
2
2
 
3
3
  /**
4
4
  * @fileoverview CloudFormation Simulator
@@ -14,9 +14,9 @@
14
14
  * - Persistência via LocalStore
15
15
  */
16
16
 
17
- const { randomUUID } = require('crypto');
18
- const yaml = require('js-yaml');
19
- const { CloudTrailAudit } = require('../../utils/cloudtrail-audit');
17
+ const { randomUUID } = require("crypto");
18
+ const yaml = require("js-yaml");
19
+ const { CloudTrailAudit } = require("../../utils/cloudtrail-audit");
20
20
 
21
21
  // ─── Erros tipados ───────────────────────────────────────────────────────────
22
22
 
@@ -29,43 +29,38 @@ class CloudFormationError extends Error {
29
29
  }
30
30
 
31
31
  const Errors = {
32
- AlreadyExists: (name) =>
33
- new CloudFormationError('AlreadyExistsException', `Stack [${name}] already exists`, 400),
34
- DoesNotExist: (name) =>
35
- new CloudFormationError('ValidationError', `Stack with id ${name} does not exist`, 400),
36
- ChangeSetNotFound: (name) =>
37
- new CloudFormationError('ChangeSetNotFoundException', `ChangeSet [${name}] does not exist`, 404),
38
- InvalidTemplate: (msg) =>
39
- new CloudFormationError('ValidationError', `Template format error: ${msg}`, 400),
40
- InvalidAction: (msg) =>
41
- new CloudFormationError('ValidationError', msg, 400),
32
+ AlreadyExists: (name) => new CloudFormationError("AlreadyExistsException", `Stack [${name}] already exists`, 400),
33
+ DoesNotExist: (name) => new CloudFormationError("ValidationError", `Stack with id ${name} does not exist`, 400),
34
+ ChangeSetNotFound: (name) => new CloudFormationError("ChangeSetNotFoundException", `ChangeSet [${name}] does not exist`, 404),
35
+ InvalidTemplate: (msg) => new CloudFormationError("ValidationError", `Template format error: ${msg}`, 400),
36
+ InvalidAction: (msg) => new CloudFormationError("ValidationError", msg, 400),
42
37
  };
43
38
 
44
39
  // ─── Constantes ──────────────────────────────────────────────────────────────
45
40
 
46
- const REGION = 'us-east-1';
47
- const ACCOUNT = '000000000000';
41
+ const REGION = "us-east-1";
42
+ const ACCOUNT = "000000000000";
48
43
 
49
44
  const StackStatus = {
50
- CREATE_IN_PROGRESS: 'CREATE_IN_PROGRESS',
51
- CREATE_COMPLETE: 'CREATE_COMPLETE',
52
- CREATE_FAILED: 'CREATE_FAILED',
53
- UPDATE_IN_PROGRESS: 'UPDATE_IN_PROGRESS',
54
- UPDATE_COMPLETE: 'UPDATE_COMPLETE',
55
- UPDATE_FAILED: 'UPDATE_FAILED',
56
- DELETE_IN_PROGRESS: 'DELETE_IN_PROGRESS',
57
- DELETE_COMPLETE: 'DELETE_COMPLETE',
58
- DELETE_FAILED: 'DELETE_FAILED',
59
- ROLLBACK_IN_PROGRESS: 'ROLLBACK_IN_PROGRESS',
60
- ROLLBACK_COMPLETE: 'ROLLBACK_COMPLETE',
45
+ CREATE_IN_PROGRESS: "CREATE_IN_PROGRESS",
46
+ CREATE_COMPLETE: "CREATE_COMPLETE",
47
+ CREATE_FAILED: "CREATE_FAILED",
48
+ UPDATE_IN_PROGRESS: "UPDATE_IN_PROGRESS",
49
+ UPDATE_COMPLETE: "UPDATE_COMPLETE",
50
+ UPDATE_FAILED: "UPDATE_FAILED",
51
+ DELETE_IN_PROGRESS: "DELETE_IN_PROGRESS",
52
+ DELETE_COMPLETE: "DELETE_COMPLETE",
53
+ DELETE_FAILED: "DELETE_FAILED",
54
+ ROLLBACK_IN_PROGRESS: "ROLLBACK_IN_PROGRESS",
55
+ ROLLBACK_COMPLETE: "ROLLBACK_COMPLETE",
61
56
  };
62
57
 
63
58
  const ChangeSetStatus = {
64
- CREATE_PENDING: 'CREATE_PENDING',
65
- CREATE_IN_PROGRESS: 'CREATE_IN_PROGRESS',
66
- CREATE_COMPLETE: 'CREATE_COMPLETE',
67
- DELETE_COMPLETE: 'DELETE_COMPLETE',
68
- FAILED: 'FAILED',
59
+ CREATE_PENDING: "CREATE_PENDING",
60
+ CREATE_IN_PROGRESS: "CREATE_IN_PROGRESS",
61
+ CREATE_COMPLETE: "CREATE_COMPLETE",
62
+ DELETE_COMPLETE: "DELETE_COMPLETE",
63
+ FAILED: "FAILED",
69
64
  };
70
65
 
71
66
  // ─── Helpers ─────────────────────────────────────────────────────────────────
@@ -88,30 +83,30 @@ function changeSetArn(stackName, changeSetName) {
88
83
  * Faz parse básico do template (JSON ou YAML string → objeto)
89
84
  */
90
85
  function parseTemplate(template) {
91
- if (!template) throw Errors.InvalidTemplate('Template body is required');
92
- if (typeof template === 'object') return template;
86
+ if (!template) throw Errors.InvalidTemplate("Template body is required");
87
+ if (typeof template === "object") return template;
93
88
  try {
94
89
  return JSON.parse(template);
95
90
  } catch (_) {
96
91
  try {
97
92
  // Schema customizado que trata tags AWS como !Ref, !Sub, !If, etc.
98
93
  const awsSchema = yaml.DEFAULT_SCHEMA.extend([
99
- new yaml.Type('!Ref', { kind: 'scalar', construct: d => ({ Ref: d }) }),
100
- new yaml.Type('!Sub', { kind: 'scalar', construct: d => ({ 'Fn::Sub': d }) }),
101
- new yaml.Type('!Sub', { kind: 'sequence', construct: d => ({ 'Fn::Sub': d }) }),
102
- new yaml.Type('!If', { kind: 'sequence', construct: d => ({ 'Fn::If': d }) }),
103
- new yaml.Type('!Equals', { kind: 'sequence', construct: d => ({ 'Fn::Equals': d }) }),
104
- new yaml.Type('!Not', { kind: 'sequence', construct: d => ({ 'Fn::Not': d }) }),
105
- new yaml.Type('!And', { kind: 'sequence', construct: d => ({ 'Fn::And': d }) }),
106
- new yaml.Type('!Or', { kind: 'sequence', construct: d => ({ 'Fn::Or': d }) }),
107
- new yaml.Type('!Select', { kind: 'sequence', construct: d => ({ 'Fn::Select': d }) }),
108
- new yaml.Type('!Split', { kind: 'sequence', construct: d => ({ 'Fn::Split': d }) }),
109
- new yaml.Type('!Join', { kind: 'sequence', construct: d => ({ 'Fn::Join': d }) }),
110
- new yaml.Type('!GetAtt', { kind: 'scalar', construct: d => ({ 'Fn::GetAtt': d.split('.') }) }),
111
- new yaml.Type('!FindInMap', { kind: 'sequence', construct: d => ({ 'Fn::FindInMap': d }) }),
112
- new yaml.Type('!Base64', { kind: 'scalar', construct: d => ({ 'Fn::Base64': d }) }),
113
- new yaml.Type('!Cidr', { kind: 'sequence', construct: d => ({ 'Fn::Cidr': d }) }),
114
- new yaml.Type('!ImportValue',{ kind: 'scalar', construct: d => ({ 'Fn::ImportValue': d }) }),
94
+ new yaml.Type("!Ref", { kind: "scalar", construct: (d) => ({ Ref: d }) }),
95
+ new yaml.Type("!Sub", { kind: "scalar", construct: (d) => ({ "Fn::Sub": d }) }),
96
+ new yaml.Type("!Sub", { kind: "sequence", construct: (d) => ({ "Fn::Sub": d }) }),
97
+ new yaml.Type("!If", { kind: "sequence", construct: (d) => ({ "Fn::If": d }) }),
98
+ new yaml.Type("!Equals", { kind: "sequence", construct: (d) => ({ "Fn::Equals": d }) }),
99
+ new yaml.Type("!Not", { kind: "sequence", construct: (d) => ({ "Fn::Not": d }) }),
100
+ new yaml.Type("!And", { kind: "sequence", construct: (d) => ({ "Fn::And": d }) }),
101
+ new yaml.Type("!Or", { kind: "sequence", construct: (d) => ({ "Fn::Or": d }) }),
102
+ new yaml.Type("!Select", { kind: "sequence", construct: (d) => ({ "Fn::Select": d }) }),
103
+ new yaml.Type("!Split", { kind: "sequence", construct: (d) => ({ "Fn::Split": d }) }),
104
+ new yaml.Type("!Join", { kind: "sequence", construct: (d) => ({ "Fn::Join": d }) }),
105
+ new yaml.Type("!GetAtt", { kind: "scalar", construct: (d) => ({ "Fn::GetAtt": d.split(".") }) }),
106
+ new yaml.Type("!FindInMap", { kind: "sequence", construct: (d) => ({ "Fn::FindInMap": d }) }),
107
+ new yaml.Type("!Base64", { kind: "scalar", construct: (d) => ({ "Fn::Base64": d }) }),
108
+ new yaml.Type("!Cidr", { kind: "sequence", construct: (d) => ({ "Fn::Cidr": d }) }),
109
+ new yaml.Type("!ImportValue", { kind: "scalar", construct: (d) => ({ "Fn::ImportValue": d }) }),
115
110
  ]);
116
111
  return yaml.load(template, { schema: awsSchema });
117
112
  } catch (yamlErr) {
@@ -130,10 +125,10 @@ function extractResources(parsedTemplate, stackName) {
130
125
  resources.push({
131
126
  LogicalResourceId: logicalId,
132
127
  PhysicalResourceId: `${stackName}-${logicalId}-${randomUUID().slice(0, 8)}`,
133
- ResourceType: resource.Type || 'AWS::CloudFormation::WaitConditionHandle',
134
- ResourceStatus: 'CREATE_COMPLETE',
128
+ ResourceType: resource.Type || "AWS::CloudFormation::WaitConditionHandle",
129
+ ResourceStatus: "CREATE_COMPLETE",
135
130
  Timestamp: now(),
136
- DriftInformation: { StackResourceDriftStatus: 'NOT_CHECKED' },
131
+ DriftInformation: { StackResourceDriftStatus: "NOT_CHECKED" },
137
132
  });
138
133
  }
139
134
  return resources;
@@ -147,18 +142,16 @@ function resolveParameters(templateParams, inputParams) {
147
142
  const templateDefs = templateParams || {};
148
143
  const inputMap = {};
149
144
 
150
- for (const param of (inputParams || [])) {
145
+ for (const param of inputParams || []) {
151
146
  inputMap[param.ParameterKey] = param;
152
147
  }
153
148
 
154
149
  for (const [key, def] of Object.entries(templateDefs)) {
155
150
  const input = inputMap[key];
156
151
  if (input) {
157
- resolved[key] = input.UsePreviousValue
158
- ? def.Default || ''
159
- : (input.ParameterValue || def.Default || '');
152
+ resolved[key] = input.UsePreviousValue ? def.Default || "" : input.ParameterValue || def.Default || "";
160
153
  } else {
161
- resolved[key] = def.Default || '';
154
+ resolved[key] = def.Default || "";
162
155
  }
163
156
  }
164
157
 
@@ -179,7 +172,7 @@ function resolveOutputs(parsedTemplate, stackName) {
179
172
  outputs.push({
180
173
  OutputKey: key,
181
174
  OutputValue: def.Value || `${stackName}-${key}-output`,
182
- Description: def.Description || '',
175
+ Description: def.Description || "",
183
176
  ExportName: def.Export?.Name || undefined,
184
177
  });
185
178
  }
@@ -208,7 +201,7 @@ class CloudFormationSimulator {
208
201
  /** @type {Map<string, Object>} changeSetArn → changeSet */
209
202
  this.changeSets = new Map();
210
203
 
211
- this.audit = new CloudTrailAudit('cloudformation.amazonaws.com');
204
+ this.audit = new CloudTrailAudit("cloudformation.amazonaws.com");
212
205
 
213
206
  // Simuladores injetados via injectDependencies
214
207
  this.s3Simulator = null;
@@ -224,21 +217,21 @@ class CloudFormationSimulator {
224
217
 
225
218
  async load() {
226
219
  try {
227
- const data = await this.store.read('cloudformation', 'data');
220
+ const data = await this.store.read("cloudformation", "data");
228
221
  if (data) {
229
222
  if (data.stacks) this.stacks = new Map(Object.entries(data.stacks));
230
223
  if (data.stackResources) this.stackResources = new Map(Object.entries(data.stackResources));
231
224
  if (data.changeSets) this.changeSets = new Map(Object.entries(data.changeSets));
232
- this.logger.info('[CloudFormation] Loaded persisted data');
225
+ this.logger.info("[CloudFormation] Loaded persisted data");
233
226
  }
234
227
  } catch (_) {
235
- this.logger.debug('[CloudFormation] No persisted data found, starting fresh');
228
+ this.logger.debug("[CloudFormation] No persisted data found, starting fresh");
236
229
  }
237
230
  }
238
231
 
239
232
  async save() {
240
233
  try {
241
- await this.store.write('cloudformation', 'data', {
234
+ await this.store.write("cloudformation", "data", {
242
235
  stacks: Object.fromEntries(this.stacks),
243
236
  stackResources: Object.fromEntries(this.stackResources),
244
237
  changeSets: Object.fromEntries(this.changeSets),
@@ -252,8 +245,10 @@ class CloudFormationSimulator {
252
245
  this.stacks.clear();
253
246
  this.stackResources.clear();
254
247
  this.changeSets.clear();
255
- try { await this.store.clear('cloudformation'); } catch (_) {}
256
- this.logger.info('[CloudFormation] Reset complete');
248
+ try {
249
+ await this.store.clear("cloudformation");
250
+ } catch (_) {}
251
+ this.logger.info("[CloudFormation] Reset complete");
257
252
  }
258
253
 
259
254
  // ─── Stacks ────────────────────────────────────────────────────
@@ -268,16 +263,16 @@ class CloudFormationSimulator {
268
263
  Parameters = [],
269
264
  Capabilities = [],
270
265
  Tags = [],
271
- OnFailure = 'ROLLBACK',
266
+ OnFailure = "ROLLBACK",
272
267
  TimeoutInMinutes,
273
268
  NotificationARNs = [],
274
269
  RoleARN,
275
270
  DisableRollback = false,
276
271
  }) {
277
- if (!StackName) throw Errors.InvalidAction('StackName is required');
272
+ if (!StackName) throw Errors.InvalidAction("StackName is required");
278
273
  if (this.stacks.has(StackName)) throw Errors.AlreadyExists(StackName);
279
274
 
280
- const template = parseTemplate(TemplateBody || '{}');
275
+ const template = parseTemplate(TemplateBody || "{}");
281
276
  const stackId = randomUUID();
282
277
  const arn = stackArn(StackName, stackId);
283
278
  const resolvedParams = resolveParameters(template.Parameters, Parameters);
@@ -288,7 +283,7 @@ class CloudFormationSimulator {
288
283
  StackId: arn,
289
284
  StackName,
290
285
  StackStatus: StackStatus.CREATE_COMPLETE,
291
- StackStatusReason: 'Stack created successfully',
286
+ StackStatusReason: "Stack created successfully",
292
287
  CreationTime: now(),
293
288
  LastUpdatedTime: now(),
294
289
  Parameters: resolvedParams,
@@ -296,13 +291,13 @@ class CloudFormationSimulator {
296
291
  Capabilities,
297
292
  Tags,
298
293
  NotificationARNs,
299
- RoleARN: RoleARN || '',
294
+ RoleARN: RoleARN || "",
300
295
  TimeoutInMinutes: TimeoutInMinutes || 0,
301
296
  DisableRollback,
302
297
  OnFailure,
303
298
  TemplateBody: TemplateBody || JSON.stringify(template),
304
299
  EnableTerminationProtection: false,
305
- DriftInformation: { StackDriftStatus: 'NOT_CHECKED' },
300
+ DriftInformation: { StackDriftStatus: "NOT_CHECKED" },
306
301
  };
307
302
 
308
303
  this.stacks.set(StackName, stack);
@@ -315,10 +310,10 @@ class CloudFormationSimulator {
315
310
  await this._provisionResources(StackName, template, resolvedParams);
316
311
 
317
312
  this.audit.record({
318
- eventName: 'CreateStack',
313
+ eventName: "CreateStack",
319
314
  readOnly: false,
320
- resources: [{ ARN: arn, type: 'AWS::CloudFormation::Stack' }],
321
- requestParameters: { stackName: StackName }
315
+ resources: [{ ARN: arn, type: "AWS::CloudFormation::Stack" }],
316
+ requestParameters: { stackName: StackName },
322
317
  });
323
318
 
324
319
  return { StackId: arn };
@@ -333,13 +328,13 @@ class CloudFormationSimulator {
333
328
  // Helper para resolver !Ref de parâmetros
334
329
  const resolveRef = (value) => {
335
330
  if (!value) return value;
336
- if (typeof value === 'object' && value.Ref) {
337
- const param = resolvedParams.find(p => p.ParameterKey === value.Ref);
331
+ if (typeof value === "object" && value.Ref) {
332
+ const param = resolvedParams.find((p) => p.ParameterKey === value.Ref);
338
333
  return param ? param.ParameterValue : value.Ref;
339
334
  }
340
- if (typeof value === 'object' && value['Fn::Sub']) {
341
- return value['Fn::Sub'].replace(/\$\{([^}]+)\}/g, (_, key) => {
342
- const param = resolvedParams.find(p => p.ParameterKey === key);
335
+ if (typeof value === "object" && value["Fn::Sub"]) {
336
+ return value["Fn::Sub"].replace(/\$\{([^}]+)\}/g, (_, key) => {
337
+ const param = resolvedParams.find((p) => p.ParameterKey === key);
343
338
  return param ? param.ParameterValue : key;
344
339
  });
345
340
  }
@@ -354,8 +349,7 @@ class CloudFormationSimulator {
354
349
  let physicalId = null;
355
350
 
356
351
  switch (resource.Type) {
357
-
358
- case 'AWS::S3::Bucket': {
352
+ case "AWS::S3::Bucket": {
359
353
  if (!this.s3Simulator) break;
360
354
  const bucketName = resolveRef(props.BucketName) || `${stackName}-${logicalId}`.toLowerCase();
361
355
  this.s3Simulator.createBucket(bucketName);
@@ -364,7 +358,7 @@ class CloudFormationSimulator {
364
358
  break;
365
359
  }
366
360
 
367
- case 'AWS::SQS::Queue': {
361
+ case "AWS::SQS::Queue": {
368
362
  if (!this.sqsSimulator) break;
369
363
  const queueName = resolveRef(props.QueueName) || `${stackName}-${logicalId}`;
370
364
  this.sqsSimulator.createQueue(queueName);
@@ -373,35 +367,56 @@ class CloudFormationSimulator {
373
367
  break;
374
368
  }
375
369
 
376
- case 'AWS::DynamoDB::Table': {
370
+ case "AWS::DynamoDB::Table": {
377
371
  if (!this.dynamoSimulator) break;
378
372
  const tableName = resolveRef(props.TableName) || `${stackName}-${logicalId}`;
379
- const attrDefs = (props.AttributeDefinitions || []).map(a => ({
373
+ const attrDefs = (props.AttributeDefinitions || []).map((a) => ({
380
374
  AttributeName: resolveRef(a.AttributeName),
381
375
  AttributeType: a.AttributeType,
382
376
  }));
383
- const keySchema = (props.KeySchema || []).map(k => ({
377
+ const keySchema = (props.KeySchema || []).map((k) => ({
384
378
  AttributeName: resolveRef(k.AttributeName),
385
379
  KeyType: k.KeyType,
386
380
  }));
381
+
382
+ // ⭐ NOVO: Processar GlobalSecondaryIndexes
383
+ const gsis = (props.GlobalSecondaryIndexes || []).map((gsi) => ({
384
+ IndexName: gsi.IndexName,
385
+ KeySchema: (gsi.KeySchema || []).map((k) => ({
386
+ AttributeName: resolveRef(k.AttributeName),
387
+ KeyType: k.KeyType,
388
+ })),
389
+ Projection: gsi.Projection || { ProjectionType: "ALL" },
390
+ // Opcional: ProvisionedThroughput (se não for PAY_PER_REQUEST)
391
+ ...(props.BillingMode !== "PAY_PER_REQUEST" && gsi.ProvisionedThroughput
392
+ ? {
393
+ ProvisionedThroughput: {
394
+ ReadCapacityUnits: gsi.ProvisionedThroughput?.ReadCapacityUnits || 5,
395
+ WriteCapacityUnits: gsi.ProvisionedThroughput?.WriteCapacityUnits || 5,
396
+ },
397
+ }
398
+ : {}),
399
+ }));
400
+
387
401
  await this.dynamoSimulator.createTable({
388
402
  TableName: tableName,
389
403
  AttributeDefinitions: attrDefs,
390
404
  KeySchema: keySchema,
391
- BillingMode: props.BillingMode || 'PAY_PER_REQUEST',
405
+ GlobalSecondaryIndexes: gsis, // ⭐ NOVO: passa os GSIs
406
+ BillingMode: props.BillingMode || "PAY_PER_REQUEST",
392
407
  Tags: props.Tags || [],
393
408
  });
394
409
  physicalId = tableName;
395
- this.logger.info(`[CloudFormation] Provisionada DynamoDB table: ${tableName}`);
410
+ this.logger.info(`[CloudFormation] Provisionada DynamoDB table: ${tableName} com ${gsis.length} GSIs`);
396
411
  break;
397
412
  }
398
413
 
399
- case 'AWS::Athena::WorkGroup': {
414
+ case "AWS::Athena::WorkGroup": {
400
415
  if (!this.athenaSimulator) break;
401
416
  const wgName = resolveRef(props.Name) || `${stackName}-${logicalId}`;
402
417
  await this.athenaSimulator.createWorkGroup({
403
418
  Name: wgName,
404
- Description: resolveRef(props.Description) || '',
419
+ Description: resolveRef(props.Description) || "",
405
420
  Configuration: props.WorkGroupConfiguration || {},
406
421
  });
407
422
  physicalId = wgName;
@@ -409,12 +424,12 @@ class CloudFormationSimulator {
409
424
  break;
410
425
  }
411
426
 
412
- case 'AWS::KMS::Key': {
427
+ case "AWS::KMS::Key": {
413
428
  if (!this.kmsSimulator) break;
414
429
  const result = await this.kmsSimulator.createKey({
415
430
  Description: resolveRef(props.Description) || `${stackName}-${logicalId}`,
416
- KeyUsage: props.KeyUsage || 'ENCRYPT_DECRYPT',
417
- KeySpec: props.KeySpec || 'SYMMETRIC_DEFAULT',
431
+ KeyUsage: props.KeyUsage || "ENCRYPT_DECRYPT",
432
+ KeySpec: props.KeySpec || "SYMMETRIC_DEFAULT",
418
433
  Tags: props.Tags || [],
419
434
  });
420
435
  physicalId = result.KeyMetadata.KeyId;
@@ -422,13 +437,13 @@ class CloudFormationSimulator {
422
437
  break;
423
438
  }
424
439
 
425
- case 'AWS::SecretsManager::Secret': {
440
+ case "AWS::SecretsManager::Secret": {
426
441
  if (!this.secretsSimulator) break;
427
442
  const secretName = resolveRef(props.Name) || `${stackName}-${logicalId}`;
428
443
  await this.secretsSimulator.createSecret({
429
444
  Name: secretName,
430
- Description: resolveRef(props.Description) || '',
431
- SecretString: resolveRef(props.SecretString) || '{}',
445
+ Description: resolveRef(props.Description) || "",
446
+ SecretString: resolveRef(props.SecretString) || "{}",
432
447
  Tags: props.Tags || [],
433
448
  });
434
449
  physicalId = secretName;
@@ -436,14 +451,14 @@ class CloudFormationSimulator {
436
451
  break;
437
452
  }
438
453
 
439
- case 'AWS::SSM::Parameter': {
454
+ case "AWS::SSM::Parameter": {
440
455
  if (!this.parameterStoreSimulator) break;
441
456
  const paramName = resolveRef(props.Name) || `/${stackName}/${logicalId}`;
442
457
  await this.parameterStoreSimulator.putParameter({
443
458
  Name: paramName,
444
- Value: resolveRef(props.Value) || '',
445
- Type: props.Type || 'String',
446
- Description: resolveRef(props.Description) || '',
459
+ Value: resolveRef(props.Value) || "",
460
+ Type: props.Type || "String",
461
+ Description: resolveRef(props.Description) || "",
447
462
  Tags: props.Tags || [],
448
463
  Overwrite: true,
449
464
  });
@@ -458,10 +473,9 @@ class CloudFormationSimulator {
458
473
 
459
474
  // Atualiza o PhysicalResourceId com o nome real do recurso
460
475
  if (physicalId) {
461
- const entry = stackResourceList.find(r => r.LogicalResourceId === logicalId);
476
+ const entry = stackResourceList.find((r) => r.LogicalResourceId === logicalId);
462
477
  if (entry) entry.PhysicalResourceId = physicalId;
463
478
  }
464
-
465
479
  } catch (err) {
466
480
  this.logger.warn(`[CloudFormation] Erro ao provisionar ${logicalId} (${resource.Type}): ${err.message}`);
467
481
  }
@@ -471,29 +485,18 @@ class CloudFormationSimulator {
471
485
  /**
472
486
  * UpdateStack
473
487
  */
474
- async updateStack({
475
- StackName,
476
- TemplateBody,
477
- UsePreviousTemplate = false,
478
- Parameters = [],
479
- Capabilities = [],
480
- Tags,
481
- RoleARN,
482
- NotificationARNs,
483
- }) {
488
+ async updateStack({ StackName, TemplateBody, UsePreviousTemplate = false, Parameters = [], Capabilities = [], Tags, RoleARN, NotificationARNs }) {
484
489
  const stack = this._getStack(StackName);
485
490
 
486
- const templateBody = UsePreviousTemplate
487
- ? stack.TemplateBody
488
- : (TemplateBody || stack.TemplateBody);
491
+ const templateBody = UsePreviousTemplate ? stack.TemplateBody : TemplateBody || stack.TemplateBody;
489
492
 
490
493
  const template = parseTemplate(templateBody);
491
- const resolvedParams = resolveParameters(template.Parameters, Parameters.length ? Parameters : stack.Parameters.map(p => ({ ParameterKey: p.ParameterKey, UsePreviousValue: true })));
494
+ const resolvedParams = resolveParameters(template.Parameters, Parameters.length ? Parameters : stack.Parameters.map((p) => ({ ParameterKey: p.ParameterKey, UsePreviousValue: true })));
492
495
  const outputs = resolveOutputs(template, StackName);
493
496
  const resources = extractResources(template, StackName);
494
497
 
495
498
  stack.StackStatus = StackStatus.UPDATE_COMPLETE;
496
- stack.StackStatusReason = 'Stack updated successfully';
499
+ stack.StackStatusReason = "Stack updated successfully";
497
500
  stack.LastUpdatedTime = now();
498
501
  stack.TemplateBody = templateBody;
499
502
  stack.Parameters = resolvedParams;
@@ -523,11 +526,7 @@ class CloudFormationSimulator {
523
526
  const stack = this.stacks.get(StackName);
524
527
 
525
528
  if (stack.EnableTerminationProtection) {
526
- throw new CloudFormationError(
527
- 'ValidationError',
528
- `Stack [${StackName}] cannot be deleted while TerminationProtection is enabled`,
529
- 400
530
- );
529
+ throw new CloudFormationError("ValidationError", `Stack [${StackName}] cannot be deleted while TerminationProtection is enabled`, 400);
531
530
  }
532
531
 
533
532
  // Captura recursos antes de remover do map
@@ -555,49 +554,49 @@ class CloudFormationSimulator {
555
554
  const { ResourceType, PhysicalResourceId } = resource;
556
555
  try {
557
556
  switch (ResourceType) {
558
- case 'AWS::S3::Bucket':
557
+ case "AWS::S3::Bucket":
559
558
  if (this.s3Simulator && PhysicalResourceId) {
560
559
  this.s3Simulator.deleteBucket(PhysicalResourceId);
561
560
  this.logger.info(`[CloudFormation] Removido S3 bucket: ${PhysicalResourceId}`);
562
561
  }
563
562
  break;
564
563
 
565
- case 'AWS::SQS::Queue':
564
+ case "AWS::SQS::Queue":
566
565
  if (this.sqsSimulator && PhysicalResourceId) {
567
566
  this.sqsSimulator.deleteQueue(PhysicalResourceId);
568
567
  this.logger.info(`[CloudFormation] Removida SQS queue: ${PhysicalResourceId}`);
569
568
  }
570
569
  break;
571
570
 
572
- case 'AWS::DynamoDB::Table':
571
+ case "AWS::DynamoDB::Table":
573
572
  if (this.dynamoSimulator && PhysicalResourceId) {
574
573
  await this.dynamoSimulator.deleteTable({ TableName: PhysicalResourceId });
575
574
  this.logger.info(`[CloudFormation] Removida DynamoDB table: ${PhysicalResourceId}`);
576
575
  }
577
576
  break;
578
577
 
579
- case 'AWS::KMS::Key':
578
+ case "AWS::KMS::Key":
580
579
  if (this.kmsSimulator && PhysicalResourceId) {
581
580
  await this.kmsSimulator.scheduleKeyDeletion({ KeyId: PhysicalResourceId, PendingWindowInDays: 7 });
582
581
  this.logger.info(`[CloudFormation] Agendada exclusão KMS key: ${PhysicalResourceId}`);
583
582
  }
584
583
  break;
585
584
 
586
- case 'AWS::SecretsManager::Secret':
585
+ case "AWS::SecretsManager::Secret":
587
586
  if (this.secretsSimulator && PhysicalResourceId) {
588
587
  await this.secretsSimulator.deleteSecret({ SecretId: PhysicalResourceId, ForceDeleteWithoutRecovery: true });
589
588
  this.logger.info(`[CloudFormation] Removido Secret: ${PhysicalResourceId}`);
590
589
  }
591
590
  break;
592
591
 
593
- case 'AWS::SSM::Parameter':
592
+ case "AWS::SSM::Parameter":
594
593
  if (this.parameterStoreSimulator && PhysicalResourceId) {
595
594
  await this.parameterStoreSimulator.deleteParameter({ Name: PhysicalResourceId });
596
595
  this.logger.info(`[CloudFormation] Removido SSM Parameter: ${PhysicalResourceId}`);
597
596
  }
598
597
  break;
599
598
 
600
- case 'AWS::Athena::WorkGroup':
599
+ case "AWS::Athena::WorkGroup":
601
600
  if (this.athenaSimulator && PhysicalResourceId) {
602
601
  await this.athenaSimulator.deleteWorkGroup({ WorkGroup: PhysicalResourceId, RecursiveDeleteOption: true });
603
602
  this.logger.info(`[CloudFormation] Removido Athena WorkGroup: ${PhysicalResourceId}`);
@@ -621,7 +620,7 @@ class CloudFormationSimulator {
621
620
  const stack = this._getStack(StackName);
622
621
  return { Stacks: [this._formatStack(stack)] };
623
622
  }
624
- const stacks = Array.from(this.stacks.values()).map(s => this._formatStack(s));
623
+ const stacks = Array.from(this.stacks.values()).map((s) => this._formatStack(s));
625
624
  return { Stacks: stacks };
626
625
  }
627
626
 
@@ -632,15 +631,15 @@ class CloudFormationSimulator {
632
631
  let items = Array.from(this.stacks.values());
633
632
 
634
633
  if (StackStatusFilter.length > 0) {
635
- items = items.filter(s => StackStatusFilter.includes(s.StackStatus));
634
+ items = items.filter((s) => StackStatusFilter.includes(s.StackStatus));
636
635
  }
637
636
 
638
637
  let startIdx = 0;
639
638
  if (NextToken) {
640
- startIdx = parseInt(Buffer.from(NextToken, 'base64').toString('utf8'), 10) || 0;
639
+ startIdx = parseInt(Buffer.from(NextToken, "base64").toString("utf8"), 10) || 0;
641
640
  }
642
641
 
643
- const page = items.slice(startIdx, startIdx + 100).map(s => ({
642
+ const page = items.slice(startIdx, startIdx + 100).map((s) => ({
644
643
  StackId: s.StackId,
645
644
  StackName: s.StackName,
646
645
  StackStatus: s.StackStatus,
@@ -652,9 +651,7 @@ class CloudFormationSimulator {
652
651
  }));
653
652
 
654
653
  const hasMore = startIdx + 100 < items.length;
655
- const newNextToken = hasMore
656
- ? Buffer.from(String(startIdx + 100)).toString('base64')
657
- : undefined;
654
+ const newNextToken = hasMore ? Buffer.from(String(startIdx + 100)).toString("base64") : undefined;
658
655
 
659
656
  return { StackSummaries: page, NextToken: newNextToken };
660
657
  }
@@ -665,41 +662,37 @@ class CloudFormationSimulator {
665
662
  * ValidateTemplate
666
663
  */
667
664
  validateTemplate({ TemplateBody, TemplateURL }) {
668
- const body = TemplateBody || '{}';
665
+ const body = TemplateBody || "{}";
669
666
  const template = parseTemplate(body);
670
667
 
671
668
  const parameters = Object.entries(template.Parameters || {}).map(([key, def]) => ({
672
669
  ParameterKey: key,
673
- DefaultValue: def.Default || '',
670
+ DefaultValue: def.Default || "",
674
671
  NoEcho: def.NoEcho || false,
675
- Description: def.Description || '',
672
+ Description: def.Description || "",
676
673
  }));
677
674
 
678
675
  const capabilities = [];
679
676
  const resources = Object.values(template.Resources || {});
680
- const hasIAM = resources.some(r =>
681
- r.Type && (r.Type.includes('IAM') || r.Type.includes('Role'))
682
- );
683
- if (hasIAM) capabilities.push('CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM');
677
+ const hasIAM = resources.some((r) => r.Type && (r.Type.includes("IAM") || r.Type.includes("Role")));
678
+ if (hasIAM) capabilities.push("CAPABILITY_IAM", "CAPABILITY_NAMED_IAM");
684
679
 
685
680
  return {
686
681
  Parameters: parameters,
687
- Description: template.Description || '',
682
+ Description: template.Description || "",
688
683
  Capabilities: capabilities,
689
- CapabilitiesReason: capabilities.length > 0
690
- ? 'The following resource(s) require capabilities: [AWS::IAM::Role]'
691
- : '',
684
+ CapabilitiesReason: capabilities.length > 0 ? "The following resource(s) require capabilities: [AWS::IAM::Role]" : "",
692
685
  };
693
686
  }
694
687
 
695
688
  /**
696
689
  * GetTemplate
697
690
  */
698
- getTemplate({ StackName, TemplateStage = 'Original' }) {
691
+ getTemplate({ StackName, TemplateStage = "Original" }) {
699
692
  const stack = this._getStack(StackName);
700
693
  return {
701
- TemplateBody: stack.TemplateBody || '{}',
702
- StagesAvailable: ['Original', 'Processed'],
694
+ TemplateBody: stack.TemplateBody || "{}",
695
+ StagesAvailable: ["Original", "Processed"],
703
696
  };
704
697
  }
705
698
 
@@ -712,10 +705,10 @@ class CloudFormationSimulator {
712
705
  const resources = this.stackResources.get(StackName) || [];
713
706
  let filtered = resources;
714
707
  if (LogicalResourceId) {
715
- filtered = resources.filter(r => r.LogicalResourceId === LogicalResourceId);
708
+ filtered = resources.filter((r) => r.LogicalResourceId === LogicalResourceId);
716
709
  }
717
710
  return {
718
- StackResources: filtered.map(r => ({ ...r, StackName, StackId: this.stacks.get(StackName)?.StackId })),
711
+ StackResources: filtered.map((r) => ({ ...r, StackName, StackId: this.stacks.get(StackName)?.StackId })),
719
712
  };
720
713
  }
721
714
 
@@ -728,14 +721,12 @@ class CloudFormationSimulator {
728
721
 
729
722
  let startIdx = 0;
730
723
  if (NextToken) {
731
- startIdx = parseInt(Buffer.from(NextToken, 'base64').toString('utf8'), 10) || 0;
724
+ startIdx = parseInt(Buffer.from(NextToken, "base64").toString("utf8"), 10) || 0;
732
725
  }
733
726
 
734
727
  const page = resources.slice(startIdx, startIdx + 100);
735
728
  const hasMore = startIdx + 100 < resources.length;
736
- const newNextToken = hasMore
737
- ? Buffer.from(String(startIdx + 100)).toString('base64')
738
- : undefined;
729
+ const newNextToken = hasMore ? Buffer.from(String(startIdx + 100)).toString("base64") : undefined;
739
730
 
740
731
  return {
741
732
  StackResourceSummaries: page,
@@ -748,31 +739,19 @@ class CloudFormationSimulator {
748
739
  /**
749
740
  * CreateChangeSet
750
741
  */
751
- async createChangeSet({
752
- StackName,
753
- ChangeSetName,
754
- TemplateBody,
755
- UsePreviousTemplate = false,
756
- Parameters = [],
757
- Capabilities = [],
758
- Tags = [],
759
- Description = '',
760
- ChangeSetType = 'UPDATE',
761
- }) {
762
- if (!ChangeSetName) throw Errors.InvalidAction('ChangeSetName is required');
763
- if (!StackName) throw Errors.InvalidAction('StackName is required');
742
+ async createChangeSet({ StackName, ChangeSetName, TemplateBody, UsePreviousTemplate = false, Parameters = [], Capabilities = [], Tags = [], Description = "", ChangeSetType = "UPDATE" }) {
743
+ if (!ChangeSetName) throw Errors.InvalidAction("ChangeSetName is required");
744
+ if (!StackName) throw Errors.InvalidAction("StackName is required");
764
745
 
765
746
  // Para CREATE, a stack não deve existir; para UPDATE, deve existir
766
- if (ChangeSetType === 'UPDATE' && !this.stacks.has(StackName)) {
747
+ if (ChangeSetType === "UPDATE" && !this.stacks.has(StackName)) {
767
748
  throw Errors.DoesNotExist(StackName);
768
749
  }
769
750
 
770
751
  const csArn = changeSetArn(StackName, ChangeSetName);
771
752
 
772
753
  const existingStack = this.stacks.get(StackName);
773
- const templateBody = UsePreviousTemplate
774
- ? (existingStack?.TemplateBody || '{}')
775
- : (TemplateBody || '{}');
754
+ const templateBody = UsePreviousTemplate ? existingStack?.TemplateBody || "{}" : TemplateBody || "{}";
776
755
 
777
756
  const template = parseTemplate(templateBody);
778
757
  const newResources = extractResources(template, StackName);
@@ -787,7 +766,7 @@ class CloudFormationSimulator {
787
766
  StackName,
788
767
  StackId: existingStack?.StackId || stackArn(StackName, randomUUID()),
789
768
  Status: ChangeSetStatus.CREATE_COMPLETE,
790
- StatusReason: 'Complete',
769
+ StatusReason: "Complete",
791
770
  Description,
792
771
  ChangeSetType,
793
772
  CreationTime: now(),
@@ -796,7 +775,7 @@ class CloudFormationSimulator {
796
775
  Tags,
797
776
  TemplateBody: templateBody,
798
777
  Changes: changes,
799
- ExecutionStatus: 'AVAILABLE',
778
+ ExecutionStatus: "AVAILABLE",
800
779
  };
801
780
 
802
781
  this.changeSets.set(csArn, changeSet);
@@ -821,12 +800,8 @@ class CloudFormationSimulator {
821
800
  async executeChangeSet({ ChangeSetName, StackName, ClientRequestToken }) {
822
801
  const changeSet = this._getChangeSet(ChangeSetName, StackName);
823
802
 
824
- if (changeSet.ExecutionStatus !== 'AVAILABLE') {
825
- throw new CloudFormationError(
826
- 'InvalidChangeSetStatus',
827
- `ChangeSet [${ChangeSetName}] cannot be executed in its current status [${changeSet.ExecutionStatus}]`,
828
- 400
829
- );
803
+ if (changeSet.ExecutionStatus !== "AVAILABLE") {
804
+ throw new CloudFormationError("InvalidChangeSetStatus", `ChangeSet [${ChangeSetName}] cannot be executed in its current status [${changeSet.ExecutionStatus}]`, 400);
830
805
  }
831
806
 
832
807
  const template = parseTemplate(changeSet.TemplateBody);
@@ -834,7 +809,7 @@ class CloudFormationSimulator {
834
809
  const resolvedParams = resolveParameters(template.Parameters, changeSet.Parameters);
835
810
  const outputs = resolveOutputs(template, StackName);
836
811
 
837
- if (changeSet.ChangeSetType === 'CREATE') {
812
+ if (changeSet.ChangeSetType === "CREATE") {
838
813
  // Cria a stack
839
814
  const stackId = randomUUID();
840
815
  const arn = stackArn(StackName, stackId);
@@ -842,7 +817,7 @@ class CloudFormationSimulator {
842
817
  StackId: changeSet.StackId || arn,
843
818
  StackName,
844
819
  StackStatus: StackStatus.CREATE_COMPLETE,
845
- StackStatusReason: 'Stack created via ChangeSet',
820
+ StackStatusReason: "Stack created via ChangeSet",
846
821
  CreationTime: now(),
847
822
  LastUpdatedTime: now(),
848
823
  Parameters: resolvedParams,
@@ -852,7 +827,7 @@ class CloudFormationSimulator {
852
827
  NotificationARNs: [],
853
828
  TemplateBody: changeSet.TemplateBody,
854
829
  EnableTerminationProtection: false,
855
- DriftInformation: { StackDriftStatus: 'NOT_CHECKED' },
830
+ DriftInformation: { StackDriftStatus: "NOT_CHECKED" },
856
831
  };
857
832
  this.stacks.set(StackName, stack);
858
833
  } else {
@@ -860,7 +835,7 @@ class CloudFormationSimulator {
860
835
  const stack = this.stacks.get(StackName);
861
836
  if (stack) {
862
837
  stack.StackStatus = StackStatus.UPDATE_COMPLETE;
863
- stack.StackStatusReason = 'Stack updated via ChangeSet';
838
+ stack.StackStatusReason = "Stack updated via ChangeSet";
864
839
  stack.LastUpdatedTime = now();
865
840
  stack.TemplateBody = changeSet.TemplateBody;
866
841
  stack.Parameters = resolvedParams;
@@ -869,8 +844,8 @@ class CloudFormationSimulator {
869
844
  }
870
845
 
871
846
  this.stackResources.set(StackName, resources);
872
- changeSet.ExecutionStatus = 'EXECUTE_COMPLETE';
873
- changeSet.Status = 'UPDATE_COMPLETE';
847
+ changeSet.ExecutionStatus = "EXECUTE_COMPLETE";
848
+ changeSet.Status = "UPDATE_COMPLETE";
874
849
 
875
850
  this.logger.info(`[CloudFormation] Executed ChangeSet: ${ChangeSetName} on ${StackName}`);
876
851
  await this.save();
@@ -896,8 +871,8 @@ class CloudFormationSimulator {
896
871
  */
897
872
  listChangeSets({ StackName, NextToken } = {}) {
898
873
  const items = Array.from(this.changeSets.values())
899
- .filter(cs => cs.StackName === StackName)
900
- .map(cs => ({
874
+ .filter((cs) => cs.StackName === StackName)
875
+ .map((cs) => ({
901
876
  ChangeSetId: cs.ChangeSetId,
902
877
  ChangeSetName: cs.ChangeSetName,
903
878
  StackId: cs.StackId,
@@ -961,27 +936,27 @@ class CloudFormationSimulator {
961
936
  Capabilities: stack.Capabilities || [],
962
937
  Tags: stack.Tags || [],
963
938
  NotificationARNs: stack.NotificationARNs || [],
964
- RoleARN: stack.RoleARN || '',
939
+ RoleARN: stack.RoleARN || "",
965
940
  EnableTerminationProtection: stack.EnableTerminationProtection || false,
966
- DriftInformation: stack.DriftInformation || { StackDriftStatus: 'NOT_CHECKED' },
941
+ DriftInformation: stack.DriftInformation || { StackDriftStatus: "NOT_CHECKED" },
967
942
  };
968
943
  }
969
944
 
970
945
  _computeChanges(currentResources, newResources) {
971
946
  const changes = [];
972
- const currentMap = new Map(currentResources.map(r => [r.LogicalResourceId, r]));
973
- const newMap = new Map(newResources.map(r => [r.LogicalResourceId, r]));
947
+ const currentMap = new Map(currentResources.map((r) => [r.LogicalResourceId, r]));
948
+ const newMap = new Map(newResources.map((r) => [r.LogicalResourceId, r]));
974
949
 
975
950
  // Adições
976
951
  for (const [id, res] of newMap.entries()) {
977
952
  if (!currentMap.has(id)) {
978
953
  changes.push({
979
- Type: 'Resource',
954
+ Type: "Resource",
980
955
  ResourceChange: {
981
- Action: 'Add',
956
+ Action: "Add",
982
957
  LogicalResourceId: id,
983
958
  ResourceType: res.ResourceType,
984
- Replacement: 'False',
959
+ Replacement: "False",
985
960
  Scope: [],
986
961
  Details: [],
987
962
  },
@@ -993,13 +968,13 @@ class CloudFormationSimulator {
993
968
  for (const [id, res] of currentMap.entries()) {
994
969
  if (!newMap.has(id)) {
995
970
  changes.push({
996
- Type: 'Resource',
971
+ Type: "Resource",
997
972
  ResourceChange: {
998
- Action: 'Remove',
973
+ Action: "Remove",
999
974
  LogicalResourceId: id,
1000
975
  PhysicalResourceId: res.PhysicalResourceId,
1001
976
  ResourceType: res.ResourceType,
1002
- Replacement: 'False',
977
+ Replacement: "False",
1003
978
  Scope: [],
1004
979
  Details: [],
1005
980
  },
@@ -1013,14 +988,14 @@ class CloudFormationSimulator {
1013
988
  const current = currentMap.get(id);
1014
989
  if (current.ResourceType !== res.ResourceType) {
1015
990
  changes.push({
1016
- Type: 'Resource',
991
+ Type: "Resource",
1017
992
  ResourceChange: {
1018
- Action: 'Modify',
993
+ Action: "Modify",
1019
994
  LogicalResourceId: id,
1020
995
  PhysicalResourceId: current.PhysicalResourceId,
1021
996
  ResourceType: res.ResourceType,
1022
- Replacement: 'True',
1023
- Scope: ['Properties'],
997
+ Replacement: "True",
998
+ Scope: ["Properties"],
1024
999
  Details: [],
1025
1000
  },
1026
1001
  });