@gugananuvem/aws-local-simulator 1.0.22 → 1.0.26

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.
@@ -87,10 +87,35 @@ class CognitoSimulator {
87
87
  this.loadIdentityPools();
88
88
  this.loadUsers();
89
89
  this.loadSessions();
90
+ this._watchUsersFile();
90
91
 
91
92
  logger.debug(`✅ Cognito Simulator inicializado com ${this.userPools.size} user pools, ${this.identityPools.size} identity pools, ${this.users.size} usuários`);
92
93
  }
93
94
 
95
+ _watchUsersFile() {
96
+ const fs = require("fs");
97
+ const usersFilePath = this.store.getFilePath("__users__");
98
+ if (!fs.existsSync(usersFilePath)) return;
99
+
100
+ let reloadTimeout = null;
101
+ fs.watch(usersFilePath, (eventType) => {
102
+ if (eventType !== "change") return;
103
+ // Debounce para evitar múltiplos reloads em edições rápidas
104
+ clearTimeout(reloadTimeout);
105
+ reloadTimeout = setTimeout(() => {
106
+ try {
107
+ this.users.clear();
108
+ this.loadUsers();
109
+ logger.info(`🔄 Cognito users recarregados do disco (${this.users.size} usuários)`);
110
+ } catch (err) {
111
+ logger.warn(`⚠️ Erro ao recarregar users: ${err.message}`);
112
+ }
113
+ }, 200);
114
+ });
115
+
116
+ logger.debug(`👁️ Watching: ${usersFilePath}`);
117
+ }
118
+
94
119
  // ============ User Pool Operations ============
95
120
 
96
121
  createUserPool(params) {
@@ -102,8 +127,8 @@ class CognitoSimulator {
102
127
  Name: PoolName,
103
128
  Arn: `arn:aws:cognito:local:000000000000:userpool/${poolId}`,
104
129
  Status: "ACTIVE",
105
- CreationDate: new Date().toISOString(),
106
- LastModifiedDate: new Date().toISOString(),
130
+ CreationDate: Math.floor(Date.now() / 1000),
131
+ LastModifiedDate: Math.floor(Date.now() / 1000),
107
132
  Policies: Policies || {
108
133
  PasswordPolicy: {
109
134
  MinimumLength: 8,
@@ -146,6 +171,22 @@ class CognitoSimulator {
146
171
  };
147
172
  }
148
173
 
174
+ updateUserPool(params) {
175
+ const { UserPoolId, LambdaConfig, MfaConfiguration, AutoVerifiedAttributes, Policies } = params;
176
+ const userPool = this.userPools.get(UserPoolId);
177
+ if (!userPool) throw new Error(`User pool ${UserPoolId} not found`);
178
+
179
+ if (LambdaConfig !== undefined) userPool.LambdaConfig = LambdaConfig;
180
+ if (MfaConfiguration !== undefined) userPool.MfaConfiguration = MfaConfiguration;
181
+ if (AutoVerifiedAttributes !== undefined) userPool.AutoVerifiedAttributes = AutoVerifiedAttributes;
182
+ if (Policies !== undefined) userPool.Policies = Policies;
183
+ userPool.LastModifiedDate = Math.floor(Date.now() / 1000);
184
+
185
+ this.persistUserPools();
186
+ logger.debug(`✅ User Pool atualizado: ${UserPoolId}`);
187
+ return {};
188
+ }
189
+
149
190
  listUserPools(params = {}) {
150
191
  const { MaxResults = 60, NextToken } = params;
151
192
  let userPools = Array.from(this.userPools.values());
@@ -361,7 +402,7 @@ class CognitoSimulator {
361
402
  // 2. Aplica nova senha e confirma usuário
362
403
  user.Password = this.hashPassword(newPassword);
363
404
  user.UserStatus = "CONFIRMED";
364
- user.LastModifiedDate = new Date().toISOString();
405
+ user.LastModifiedDate = Math.floor(Date.now() / 1000);
365
406
  this.persistUsers();
366
407
  this.customAuthSessions.delete(params.Session);
367
408
 
@@ -399,8 +440,8 @@ class CognitoSimulator {
399
440
  AccessToken: accessToken,
400
441
  IdToken: idToken,
401
442
  RefreshToken: refreshToken,
402
- CreatedAt: new Date().toISOString(),
403
- ExpiresAt: new Date(Date.now() + 3600000).toISOString(),
443
+ CreatedAt: Math.floor(Date.now() / 1000),
444
+ ExpiresAt: Math.floor((Date.now() + 3600000) / 1000),
404
445
  };
405
446
  this.sessions.set(sessionId, authSession);
406
447
  this.accessTokens.set(accessToken, authSession);
@@ -482,8 +523,8 @@ class CognitoSimulator {
482
523
  AccessToken: accessToken,
483
524
  IdToken: idToken,
484
525
  RefreshToken: refreshToken,
485
- CreatedAt: new Date().toISOString(),
486
- ExpiresAt: new Date(Date.now() + 3600000).toISOString(),
526
+ CreatedAt: Math.floor(Date.now() / 1000),
527
+ ExpiresAt: Math.floor((Date.now() + 3600000) / 1000),
487
528
  };
488
529
 
489
530
  this.sessions.set(sessionId, authSession);
@@ -684,8 +725,8 @@ class CognitoSimulator {
684
725
  AllowedOAuthScopes: AllowedOAuthScopes || ["openid", "email", "profile"],
685
726
  CallbackURLs: CallbackURLs || [],
686
727
  LogoutURLs: LogoutURLs || [],
687
- CreatedDate: new Date().toISOString(),
688
- LastModifiedDate: new Date().toISOString(),
728
+ CreatedDate: Math.floor(Date.now() / 1000),
729
+ LastModifiedDate: Math.floor(Date.now() / 1000),
689
730
  };
690
731
 
691
732
  userPool.Clients.set(clientId, client);
@@ -739,8 +780,8 @@ class CognitoSimulator {
739
780
  Attributes: this.normalizeUserAttributes(UserAttributes || []),
740
781
  Enabled: true,
741
782
  UserStatus: "UNCONFIRMED",
742
- CreatedDate: new Date().toISOString(),
743
- LastModifiedDate: new Date().toISOString(),
783
+ CreatedDate: Math.floor(Date.now() / 1000),
784
+ LastModifiedDate: Math.floor(Date.now() / 1000),
744
785
  Password: this.hashPassword(Password),
745
786
  ConfirmationCode: confirmationCode,
746
787
  MfaOptions: [],
@@ -1460,7 +1501,16 @@ class CognitoSimulator {
1460
1501
  const saved = this.store.read("__userpools__");
1461
1502
  if (saved) {
1462
1503
  for (const [id, data] of Object.entries(saved)) {
1504
+ // Sanitize dates
1505
+ if (typeof data.CreationDate === 'string') data.CreationDate = Math.floor(new Date(data.CreationDate).getTime() / 1000);
1506
+ if (typeof data.LastModifiedDate === 'string') data.LastModifiedDate = Math.floor(new Date(data.LastModifiedDate).getTime() / 1000);
1507
+
1463
1508
  data.Clients = new Map(Object.entries(data.Clients || {}));
1509
+ for (const client of data.Clients.values()) {
1510
+ if (typeof client.CreatedDate === 'string') client.CreatedDate = Math.floor(new Date(client.CreatedDate).getTime() / 1000);
1511
+ if (typeof client.LastModifiedDate === 'string') client.LastModifiedDate = Math.floor(new Date(client.LastModifiedDate).getTime() / 1000);
1512
+ }
1513
+
1464
1514
  data.Groups = new Map(Object.entries(data.Groups || {}));
1465
1515
  data.IdentityProviders = new Map(Object.entries(data.IdentityProviders || {}));
1466
1516
  data.ResourceServers = new Map(Object.entries(data.ResourceServers || {}));
@@ -1569,6 +1619,8 @@ class CognitoSimulator {
1569
1619
  const saved = this.store.read("__users__");
1570
1620
  if (saved) {
1571
1621
  for (const [id, user] of Object.entries(saved)) {
1622
+ if (typeof user.CreatedDate === 'string') user.CreatedDate = Math.floor(new Date(user.CreatedDate).getTime() / 1000);
1623
+ if (typeof user.LastModifiedDate === 'string') user.LastModifiedDate = Math.floor(new Date(user.LastModifiedDate).getTime() / 1000);
1572
1624
  this.users.set(id, user);
1573
1625
  }
1574
1626
  }
@@ -25,9 +25,38 @@ class DynamoDBSimulator {
25
25
  async initialize() {
26
26
  logger.debug("Inicializando DynamoDB Simulator...");
27
27
  this.loadTables();
28
+ this._watchTablesFile();
28
29
  logger.debug(`✅ DynamoDB Simulator inicializado com ${this.tables.size} tabelas`);
29
30
  }
30
31
 
32
+ _watchTablesFile() {
33
+ const fs = require("fs");
34
+ const tablesFilePath = this.store.getFilePath("__tables__");
35
+ if (!fs.existsSync(tablesFilePath)) return;
36
+
37
+ let reloadTimeout = null;
38
+ fs.watch(tablesFilePath, (eventType) => {
39
+ if (eventType !== "change") return;
40
+ clearTimeout(reloadTimeout);
41
+ reloadTimeout = setTimeout(() => {
42
+ try {
43
+ this.tables.clear();
44
+ const savedTables = this.store.read("__tables__");
45
+ if (savedTables) {
46
+ for (const [name, definition] of Object.entries(savedTables)) {
47
+ this.tables.set(name, definition);
48
+ }
49
+ }
50
+ logger.info(`🔄 DynamoDB schema recarregado (${this.tables.size} tabelas)`);
51
+ } catch (err) {
52
+ logger.warn(`⚠️ Erro ao recarregar schema: ${err.message}`);
53
+ }
54
+ }, 200);
55
+ });
56
+
57
+ logger.debug(`👁️ Watching: ${tablesFilePath}`);
58
+ }
59
+
31
60
  loadTables() {
32
61
  // Carrega tabelas existentes do disco PRIMEIRO para evitar sobrescrever definições persistidas
33
62
  const savedTables = this.store.read("__tables__");
@@ -196,6 +225,19 @@ class DynamoDBSimulator {
196
225
  })),
197
226
  ItemCount: items.length,
198
227
  TableSizeBytes: JSON.stringify(items).length,
228
+ GlobalSecondaryIndexes: Object.entries(table.globalSecondaryIndexes || {}).map(([indexName, gsi]) => ({
229
+ IndexName: indexName,
230
+ IndexStatus: "ACTIVE",
231
+ KeySchema: [
232
+ { AttributeName: gsi.hashKey, KeyType: "HASH" },
233
+ ...(gsi.rangeKey ? [{ AttributeName: gsi.rangeKey, KeyType: "RANGE" }] : []),
234
+ ],
235
+ Projection: { ProjectionType: "ALL" },
236
+ ProvisionedThroughput: {
237
+ ReadCapacityUnits: 5,
238
+ WriteCapacityUnits: 5,
239
+ },
240
+ })),
199
241
  ProvisionedThroughput: {
200
242
  ReadCapacityUnits: 5,
201
243
  WriteCapacityUnits: 5,
@@ -350,8 +392,12 @@ class DynamoDBSimulator {
350
392
  response.Attributes = this.marshallItem(oldItem, table);
351
393
  break;
352
394
  case "ALL_NEW":
395
+ case "UPDATED_NEW":
353
396
  response.Attributes = this.marshallItem(updatedItem, table);
354
397
  break;
398
+ case "UPDATED_OLD":
399
+ response.Attributes = this.marshallItem(oldItem, table);
400
+ break;
355
401
  default:
356
402
  break;
357
403
  }
@@ -469,7 +515,7 @@ class DynamoDBSimulator {
469
515
  }
470
516
 
471
517
  query(params) {
472
- const { TableName, KeyConditionExpression, ExpressionAttributeValues, IndexName } = params;
518
+ const { TableName, KeyConditionExpression, ExpressionAttributeValues, ExpressionAttributeNames = {}, IndexName } = params;
473
519
  const table = this.tables.get(TableName);
474
520
 
475
521
  if (!table) {
@@ -478,7 +524,7 @@ class DynamoDBSimulator {
478
524
 
479
525
  let items = this.store.read(TableName);
480
526
 
481
- // Resolve hash key e range key: usa GSI se IndexName estiver presente, caso contrário usa a tabela principal
527
+ // Resolve hash key e range key
482
528
  let hashKey;
483
529
  let rangeKey;
484
530
 
@@ -495,43 +541,56 @@ class DynamoDBSimulator {
495
541
  rangeKey = table.rangeKey;
496
542
  }
497
543
 
498
- // Filtra pela chave de partição
499
- const hashValueMatch = KeyConditionExpression.match(new RegExp(`${hashKey}\\s*=\\s*([^\\s]+)`));
500
-
501
- if (hashValueMatch) {
502
- const hashValuePlaceholder = hashValueMatch[1];
503
- const rawHashValue = ExpressionAttributeValues[hashValuePlaceholder];
504
- const hashValue = rawHashValue && typeof rawHashValue === 'object' ? Object.values(rawHashValue)[0] : rawHashValue;
505
- items = items.filter((item) => item[hashKey] === hashValue);
506
- }
507
-
508
- // Filtra pela chave de ordenação se existir
509
- if (rangeKey) {
510
- const rangeConditionMatch = KeyConditionExpression.match(new RegExp(`${rangeKey}\\s*(=|>|<|>=|<=)\\s*([^\\s]+)`));
544
+ // Helper para resolver nomes de atributos (que podem ser placeholders como #n0)
545
+ const resolveAttributeName = (name) => {
546
+ if (name.startsWith("#")) return ExpressionAttributeNames[name] || name;
547
+ return name;
548
+ };
511
549
 
512
- if (rangeConditionMatch) {
513
- const operator = rangeConditionMatch[1];
514
- const rangeValuePlaceholder = rangeConditionMatch[2];
515
- const rawRangeValue = ExpressionAttributeValues[rangeValuePlaceholder];
516
- const rangeValue = rawRangeValue && typeof rawRangeValue === 'object' ? Object.values(rawRangeValue)[0] : rawRangeValue;
550
+ // Helper para extrair valor de placeholder (ex: :v0)
551
+ const resolveValue = (placeholder) => {
552
+ const rawValue = ExpressionAttributeValues[placeholder];
553
+ if (rawValue === undefined) return undefined;
554
+ // Se for formato DynamoDB { S: "..." }, desmembra. Se for nativo (DocumentClient), usa direto.
555
+ if (rawValue !== null && typeof rawValue === 'object' && !Array.isArray(rawValue)) {
556
+ const keys = Object.keys(rawValue);
557
+ if (keys.length === 1 && ["S", "N", "BOOL", "NULL", "M", "L", "SS", "NS", "BS"].includes(keys[0])) {
558
+ return this.normalizeValue(rawValue, table);
559
+ }
560
+ }
561
+ return rawValue;
562
+ };
517
563
 
518
- items = items.filter((item) => {
519
- const itemValue = item[rangeKey];
520
- switch (operator) {
521
- case "=":
522
- return itemValue === rangeValue;
523
- case ">":
524
- return itemValue > rangeValue;
525
- case "<":
526
- return itemValue < rangeValue;
527
- case ">=":
528
- return itemValue >= rangeValue;
529
- case "<=":
530
- return itemValue <= rangeValue;
531
- default:
532
- return true;
564
+ // Filtra pela KeyConditionExpression
565
+ // DynamoDB Query KeyConditionExpression tem formato restrito: PartitionKey = :val AND (SortKey operator :val)
566
+ if (KeyConditionExpression) {
567
+ const parts = KeyConditionExpression.split(/\s+AND\s+/i);
568
+
569
+ for (const part of parts) {
570
+ const match = part.match(/([^\s]+)\s*(=|>|<|>=|<=|BEGINS_WITH|BETWEEN)\s*([^\s]+)(?:\s+AND\s+([^\s]+))?/i);
571
+ if (match) {
572
+ const attrPlaceholder = match[1];
573
+ const operator = match[2].toUpperCase();
574
+ const valPlaceholder = match[3];
575
+
576
+ const attributeName = resolveAttributeName(attrPlaceholder);
577
+ const expectedValue = resolveValue(valPlaceholder);
578
+
579
+ if (operator === "=") {
580
+ items = items.filter(item => item[attributeName] === expectedValue);
581
+ } else if (operator === ">") {
582
+ items = items.filter(item => item[attributeName] > expectedValue);
583
+ } else if (operator === "<") {
584
+ items = items.filter(item => item[attributeName] < expectedValue);
585
+ } else if (operator === ">=") {
586
+ items = items.filter(item => item[attributeName] >= expectedValue);
587
+ } else if (operator === "<=") {
588
+ items = items.filter(item => item[attributeName] <= expectedValue);
589
+ } else if (operator === "BEGINS_WITH") {
590
+ const val = expectedValue;
591
+ items = items.filter(item => String(item[attributeName] || "").startsWith(String(val)));
533
592
  }
534
- });
593
+ }
535
594
  }
536
595
  }
537
596
 
@@ -545,7 +604,7 @@ class DynamoDBSimulator {
545
604
  }
546
605
 
547
606
  scan(params) {
548
- const { TableName, FilterExpression, ExpressionAttributeValues, Limit } = params;
607
+ const { TableName, FilterExpression, ExpressionAttributeValues, ExpressionAttributeNames = {}, Limit } = params;
549
608
  const table = this.tables.get(TableName);
550
609
 
551
610
  if (!table) {
@@ -556,7 +615,7 @@ class DynamoDBSimulator {
556
615
 
557
616
  // Aplica filtro se existir
558
617
  if (FilterExpression) {
559
- items = this.applyFilter(items, FilterExpression, ExpressionAttributeValues, table);
618
+ items = this.applyFilter(items, FilterExpression, ExpressionAttributeValues, ExpressionAttributeNames, table);
560
619
  }
561
620
 
562
621
  // Aplica limite
@@ -573,6 +632,7 @@ class DynamoDBSimulator {
573
632
  };
574
633
  }
575
634
 
635
+
576
636
  // Métodos auxiliares
577
637
  normalizeItem(item, table) {
578
638
  const normalized = { ...item };
@@ -687,21 +747,71 @@ class DynamoDBSimulator {
687
747
  }
688
748
  }
689
749
 
690
- applyFilter(items, expression, values, table) {
691
- // Implementação simplificada
692
- return items.filter((item) => {
693
- const match = expression.match(/([^\s]+)\s*=\s*([^\s]+)/);
694
- if (match) {
695
- const [, attribute, placeholder] = match;
696
- const rawValue = values[placeholder];
697
- const expectedValue = rawValue && typeof rawValue === 'object' ? Object.values(rawValue)[0] : rawValue;
698
- const actualValue = item[attribute];
699
- return actualValue === expectedValue;
750
+ applyFilter(items, expression, values, names, table) {
751
+ if (!expression) return items;
752
+
753
+ const resolveAttributeName = (name) => {
754
+ if (name.startsWith("#")) return names[name] || name;
755
+ return name;
756
+ };
757
+
758
+ const resolveValue = (placeholder) => {
759
+ const rawValue = values[placeholder];
760
+ if (rawValue === undefined) return undefined;
761
+ if (rawValue !== null && typeof rawValue === 'object' && !Array.isArray(rawValue)) {
762
+ const keys = Object.keys(rawValue);
763
+ if (keys.length === 1 && ["S", "N", "BOOL", "NULL", "M", "L", "SS", "NS", "BS"].includes(keys[0])) {
764
+ return this.normalizeValue(rawValue, table);
765
+ }
700
766
  }
701
- return true;
767
+ return rawValue;
768
+ };
769
+
770
+ const conditions = expression.split(/\s+AND\s+/i);
771
+
772
+ return items.filter((item) => {
773
+ return conditions.every(cond => {
774
+ // Regex para match de funções como contains(#n, :v) ou begins_with(#n, :v)
775
+ const funcMatch = cond.match(/(contains|begins_with)\s*\(\s*([^\s,]+)\s*,\s*([^\s,)]+)\s*\)/i);
776
+ if (funcMatch) {
777
+ const func = funcMatch[1].toLowerCase();
778
+ const attrName = resolveAttributeName(funcMatch[2]);
779
+ const expectedVal = resolveValue(funcMatch[3]);
780
+ const actualVal = item[attrName];
781
+
782
+ if (func === 'contains') {
783
+ if (Array.isArray(actualVal)) return actualVal.includes(expectedVal);
784
+ return String(actualVal || "").includes(String(expectedVal));
785
+ }
786
+ if (func === 'begins_with') {
787
+ return String(actualVal || "").startsWith(String(expectedVal));
788
+ }
789
+ }
790
+
791
+ // Regex para operadores básicos
792
+ const opMatch = cond.match(/([^\s]+)\s*(=|<>|<|<=|>|>=)\s*([^\s]+)/);
793
+ if (opMatch) {
794
+ const attrName = resolveAttributeName(opMatch[1]);
795
+ const operator = opMatch[2];
796
+ const expectedVal = resolveValue(opMatch[3]);
797
+ const actualVal = item[attrName];
798
+
799
+ switch (operator) {
800
+ case "=": return actualVal === expectedVal;
801
+ case "<>": return actualVal !== expectedVal;
802
+ case "<": return actualVal < expectedVal;
803
+ case "<=": return actualVal <= expectedVal;
804
+ case ">": return actualVal > expectedVal;
805
+ case ">=": return actualVal >= expectedVal;
806
+ default: return true;
807
+ }
808
+ }
809
+ return true;
810
+ });
702
811
  });
703
812
  }
704
813
 
814
+
705
815
  persistTables() {
706
816
  const tablesObj = {};
707
817
  for (const [name, table] of this.tables.entries()) {
@@ -45,7 +45,21 @@ class KMSServer {
45
45
 
46
46
  _setupRoutes() {
47
47
  this.app.get('/__admin/health', (req, res) => res.json({ status: 'healthy', service: 'kms', timestamp: new Date().toISOString() }));
48
- this.app.get('/__admin/keys', async (req, res) => { const r = await this.simulator.listKeys({}); res.json(r); });
48
+ this.app.get('/__admin/keys', async (req, res) => {
49
+ const keys = this.simulator.listKeysFull();
50
+ res.json(keys);
51
+ });
52
+
53
+ this.app.post('/__admin/keys', async (req, res) => {
54
+ try {
55
+ const result = await this.simulator.createKey(req.body);
56
+ res.status(201).json(result);
57
+ } catch (err) {
58
+ res.status(400).json({ __type: err.code || 'KMSInternalException', message: err.message });
59
+ }
60
+ });
61
+
62
+
49
63
 
50
64
  this.app.post('/', async (req, res) => {
51
65
  const target = req.headers['x-amz-target'];
@@ -65,44 +65,56 @@ class KMSSimulator {
65
65
  }
66
66
 
67
67
  async createKey(params) {
68
- const { Description, KeyUsage = 'ENCRYPT_DECRYPT', KeySpec = 'SYMMETRIC_DEFAULT', Tags = [], MultiRegion = false } = params || {};
68
+ const Description = params.Description || params.description || '';
69
+ const KeyUsage = params.KeyUsage || params.keyUsage || 'ENCRYPT_DECRYPT';
70
+ const KeySpec = params.KeySpec || params.keySpec || 'SYMMETRIC_DEFAULT';
71
+ const Tags = params.Tags || params.tags || [];
72
+ const MultiRegion = params.MultiRegion || params.multiRegion || false;
73
+
69
74
  const keyId = uuidv4();
70
75
  const keyArn = `arn:aws:kms:local:000000000000:key/${keyId}`;
71
76
  let keyMaterial;
72
77
  let publicKey = null;
73
78
  let privateKey = null;
74
79
 
75
- if (KeySpec === 'SYMMETRIC_DEFAULT') {
76
- keyMaterial = crypto.randomBytes(32);
77
- } else if (KeySpec.startsWith('RSA_')) {
78
- const bits = KeySpec === 'RSA_2048' ? 2048 : KeySpec === 'RSA_3072' ? 3072 : 4096;
79
- const pair = crypto.generateKeyPairSync('rsa', {
80
- modulusLength: bits,
81
- publicKeyEncoding: { type: 'spki', format: 'pem' },
82
- privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
83
- });
84
- publicKey = pair.publicKey;
85
- privateKey = pair.privateKey;
86
- keyMaterial = Buffer.from(privateKey);
87
- } else if (KeySpec.startsWith('ECC_')) {
88
- const curve = KeySpec.includes('P256') ? 'prime256v1' : KeySpec.includes('P384') ? 'secp384r1' : 'secp521r1';
89
- const pair = crypto.generateKeyPairSync('ec', {
90
- namedCurve: curve,
91
- publicKeyEncoding: { type: 'spki', format: 'pem' },
92
- privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
93
- });
94
- publicKey = pair.publicKey;
95
- privateKey = pair.privateKey;
96
- keyMaterial = Buffer.from(privateKey);
97
- } else {
98
- keyMaterial = crypto.randomBytes(32);
80
+ this.logger.debug(`KMS: Criando chave ${keyId} (Spec: ${KeySpec})`, 'kms');
81
+
82
+ try {
83
+ if (KeySpec === 'SYMMETRIC_DEFAULT') {
84
+ keyMaterial = crypto.randomBytes(32);
85
+ } else if (KeySpec.startsWith('RSA_')) {
86
+ const bits = KeySpec === 'RSA_2048' ? 2048 : KeySpec === 'RSA_3072' ? 3072 : 4096;
87
+ const pair = crypto.generateKeyPairSync('rsa', {
88
+ modulusLength: bits,
89
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
90
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
91
+ });
92
+ publicKey = pair.publicKey;
93
+ privateKey = pair.privateKey;
94
+ keyMaterial = Buffer.from(privateKey);
95
+ } else if (KeySpec.startsWith('ECC_')) {
96
+ const curve = KeySpec.includes('P256') ? 'prime256v1' : KeySpec.includes('P384') ? 'secp384r1' : 'secp521r1';
97
+ const pair = crypto.generateKeyPairSync('ec', {
98
+ namedCurve: curve,
99
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
100
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
101
+ });
102
+ publicKey = pair.publicKey;
103
+ privateKey = pair.privateKey;
104
+ keyMaterial = Buffer.from(privateKey);
105
+ } else {
106
+ keyMaterial = crypto.randomBytes(32);
107
+ }
108
+ } catch (err) {
109
+ this.logger.error(`KMS: Erro ao gerar material da chave: ${err.message}`, 'kms');
110
+ throw err;
99
111
  }
100
112
 
101
113
  this.keyMaterial.set(keyId, keyMaterial);
102
114
  const key = {
103
115
  KeyId: keyId,
104
116
  KeyArn: keyArn,
105
- Description: Description || '',
117
+ Description,
106
118
  KeyUsage,
107
119
  KeySpec,
108
120
  KeyState: 'Enabled',
@@ -115,22 +127,30 @@ class KMSSimulator {
115
127
  };
116
128
  this.keys.set(keyId, key);
117
129
  await this._persistKeys();
118
- this.logger.info(`KMS: chave criada: ${keyId}`, 'kms');
130
+ this.logger.info(`KMS: chave criada com sucesso: ${keyId}`, 'kms');
119
131
  this.audit.record({ eventName: 'CreateKey', readOnly: false, resources: [{ ARN: keyArn, type: 'AWS::KMS::Key' }], requestParameters: { description: Description, keyUsage: KeyUsage, keySpec: KeySpec } });
120
132
  return { KeyMetadata: this._sanitizeKey(key) };
121
133
  }
122
134
 
123
135
  async describeKey(params) {
124
- const key = this._requireKey(params.KeyId);
136
+ const keyId = params.KeyId || params.keyId;
137
+ const key = this._requireKey(keyId);
125
138
  return { KeyMetadata: this._sanitizeKey(key) };
126
139
  }
127
140
 
128
141
  async listKeys(params) {
129
142
  const { Limit = 100 } = params || {};
130
143
  const keys = Array.from(this.keys.values()).slice(0, Limit);
144
+ // Para o Dashboard, retornamos mais detalhes se for solicitado via admin
145
+ // Mas para o SDK padrão, retornamos apenas o que o SDK espera
131
146
  return { Keys: keys.map(k => ({ KeyId: k.KeyId, KeyArn: k.KeyArn })) };
132
147
  }
133
148
 
149
+ listKeysFull() {
150
+ return Array.from(this.keys.values()).map(k => this._sanitizeKey(k));
151
+ }
152
+
153
+
134
154
  async enableKey(params) {
135
155
  const key = this._requireKey(params.KeyId);
136
156
  key.KeyState = 'Enabled'; key.Enabled = true;
@@ -80,6 +80,30 @@ class LambdaServer {
80
80
  this.app.get('/__admin/functions', (req, res) => {
81
81
  res.json(this.simulator.listLambdas());
82
82
  });
83
+
84
+ this.app.post('/__admin/functions', async (req, res) => {
85
+ try {
86
+ const lambda = await this.simulator.createFunction(req.body);
87
+ res.status(201).json(lambda);
88
+ } catch (err) {
89
+ res.status(400).json({ error: err.message });
90
+ }
91
+ });
92
+
93
+ this.app.put('/__admin/functions/:name', async (req, res) => {
94
+ try {
95
+ const lambda = await this.simulator.updateFunction(req.params.name, req.body);
96
+ res.json(lambda);
97
+ } catch (err) {
98
+ res.status(400).json({ error: err.message });
99
+ }
100
+ });
101
+
102
+ this.app.delete('/__admin/functions/:name', async (req, res) => {
103
+ const deleted = await this.simulator.deleteFunction(req.params.name);
104
+ if (deleted) res.json({ message: 'Lambda deleted' });
105
+ else res.status(404).json({ error: 'Lambda not found' });
106
+ });
83
107
 
84
108
  this.app.post('/__admin/reload', async (req, res) => {
85
109
  await this.simulator.reloadLambdas();