@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.
- package/README.md +45 -0
- package/aws-config +154 -0
- package/package.json +30 -84
- package/src/config/default-config.js +15 -4
- package/src/server.js +257 -21
- package/src/services/apigateway/server.js +37 -0
- package/src/services/apigateway/simulator.js +148 -4
- package/src/services/cognito/server.js +8 -14
- package/src/services/cognito/simulator.js +63 -11
- package/src/services/dynamodb/simulator.js +159 -49
- package/src/services/kms/server.js +15 -1
- package/src/services/kms/simulator.js +48 -28
- package/src/services/lambda/server.js +24 -0
- package/src/services/lambda/simulator.js +136 -12
- package/src/services/parameter-store/simulator.js +1 -1
- package/src/services/s3/server.js +21 -0
- package/src/services/s3/simulator.js +4 -1
- package/src/services/secret-manager/server.js +2 -1
- package/src/services/secret-manager/simulator.js +21 -10
- package/src/services/sns/server.js +32 -5
- package/src/services/sqs/server.js +11 -0
- package/src/services/sqs/simulator.js +74 -6
|
@@ -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:
|
|
106
|
-
LastModifiedDate:
|
|
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 =
|
|
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:
|
|
403
|
-
ExpiresAt:
|
|
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:
|
|
486
|
-
ExpiresAt:
|
|
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:
|
|
688
|
-
LastModifiedDate:
|
|
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:
|
|
743
|
-
LastModifiedDate:
|
|
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
|
|
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
|
-
//
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
if (
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
|
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) => {
|
|
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
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
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
|
|
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();
|