@gugananuvem/aws-local-simulator 1.0.25 → 1.0.27
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/package.json +28 -83
- 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 +38 -11
- package/src/services/dynamodb/simulator.js +129 -50
- 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
|
@@ -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();
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
const LocalStore = require("../../utils/local-store");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const fs = require("fs");
|
|
5
4
|
const HandlerLoader = require("./handler-loader");
|
|
6
5
|
const logger = require("../../utils/logger");
|
|
7
6
|
const { CloudTrailAudit } = require("../../utils/cloudtrail-audit");
|
|
@@ -9,6 +8,8 @@ const { CloudTrailAudit } = require("../../utils/cloudtrail-audit");
|
|
|
9
8
|
class LambdaSimulator {
|
|
10
9
|
constructor(config) {
|
|
11
10
|
this.config = config;
|
|
11
|
+
this.dataDir = path.join(process.env.AWS_LOCAL_SIMULATOR_DATA_DIR, "lambda");
|
|
12
|
+
this.store = new LocalStore(this.dataDir);
|
|
12
13
|
this.lambdas = new Map(); // functionName -> { handler, env, config }
|
|
13
14
|
this.environment = { ...process.env };
|
|
14
15
|
this.audit = new CloudTrailAudit("lambda.amazonaws.com");
|
|
@@ -23,15 +24,27 @@ class LambdaSimulator {
|
|
|
23
24
|
this.globalTimeout = globalDefaults.timeout || null;
|
|
24
25
|
this.globalMemorySize = globalDefaults.memorySize || null;
|
|
25
26
|
|
|
27
|
+
// Carrega do config.lambdas (fixo)
|
|
26
28
|
if (this.config.lambdas && this.config.lambdas.length > 0) {
|
|
27
29
|
for (const lambdaConfig of this.config.lambdas) {
|
|
28
30
|
await this.registerLambda(lambdaConfig);
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
// Carrega lambdas dinâmicas do disco
|
|
35
|
+
const savedLambdas = this.store.read("__functions__");
|
|
36
|
+
if (savedLambdas && Array.isArray(savedLambdas)) {
|
|
37
|
+
for (const lambdaConfig of savedLambdas) {
|
|
38
|
+
if (!this.lambdas.has(lambdaConfig.name)) {
|
|
39
|
+
await this.registerLambda(lambdaConfig);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
logger.debug(`✅ ${this.lambdas.size} Lambdas registradas`);
|
|
33
45
|
}
|
|
34
46
|
|
|
47
|
+
|
|
35
48
|
async registerLambda(lambdaConfig) {
|
|
36
49
|
try {
|
|
37
50
|
const { name, handler: handlerPath, type = "auto" } = lambdaConfig;
|
|
@@ -51,6 +64,14 @@ class LambdaSimulator {
|
|
|
51
64
|
const memorySize = lambdaConfig.memorySize ?? this.globalMemorySize ?? 128;
|
|
52
65
|
|
|
53
66
|
const handler = await HandlerLoader.load(handlerPath, type);
|
|
67
|
+
let codeSize = 0;
|
|
68
|
+
try {
|
|
69
|
+
const info = await HandlerLoader.getInfo(handlerPath);
|
|
70
|
+
codeSize = info.size;
|
|
71
|
+
} catch (err) {
|
|
72
|
+
// Ignore size errors
|
|
73
|
+
}
|
|
74
|
+
|
|
54
75
|
if (handler != undefined) {
|
|
55
76
|
this.lambdas.set(name, {
|
|
56
77
|
name,
|
|
@@ -60,6 +81,7 @@ class LambdaSimulator {
|
|
|
60
81
|
env,
|
|
61
82
|
timeout,
|
|
62
83
|
memorySize,
|
|
84
|
+
codeSize,
|
|
63
85
|
type,
|
|
64
86
|
registeredAt: new Date().toISOString(),
|
|
65
87
|
});
|
|
@@ -147,16 +169,32 @@ class LambdaSimulator {
|
|
|
147
169
|
}
|
|
148
170
|
|
|
149
171
|
listLambdas() {
|
|
150
|
-
return Array.from(this.lambdas.values()).map((l) =>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
172
|
+
return Array.from(this.lambdas.values()).map((l) => {
|
|
173
|
+
let code = "";
|
|
174
|
+
try {
|
|
175
|
+
if (fs.existsSync(l.handlerPath)) {
|
|
176
|
+
code = fs.readFileSync(l.handlerPath, "utf8");
|
|
177
|
+
}
|
|
178
|
+
} catch (err) {
|
|
179
|
+
logger.error(`Erro ao ler código da lambda ${l.name}:`, err);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
name: l.name,
|
|
184
|
+
handlerName: l.handlerName,
|
|
185
|
+
handlerPath: l.handlerPath,
|
|
186
|
+
type: l.type,
|
|
187
|
+
env: l.env,
|
|
188
|
+
timeout: l.timeout,
|
|
189
|
+
memorySize: l.memorySize,
|
|
190
|
+
codeSize: l.codeSize,
|
|
191
|
+
registeredAt: l.registeredAt,
|
|
192
|
+
code: code
|
|
193
|
+
};
|
|
194
|
+
});
|
|
158
195
|
}
|
|
159
196
|
|
|
197
|
+
|
|
160
198
|
getLambda(name) {
|
|
161
199
|
return this.lambdas.get(name);
|
|
162
200
|
}
|
|
@@ -182,6 +220,91 @@ class LambdaSimulator {
|
|
|
182
220
|
logger.info(`✅ ${this.lambdas.size} Lambdas recarregadas`);
|
|
183
221
|
}
|
|
184
222
|
|
|
223
|
+
async createFunction(lambdaConfig) {
|
|
224
|
+
const { name, code, runtime, handler, timeout, memorySize, environment } = lambdaConfig;
|
|
225
|
+
|
|
226
|
+
if (!name) throw new Error("Function name is required");
|
|
227
|
+
|
|
228
|
+
// Define o caminho do arquivo (dentro do dataDir/functions)
|
|
229
|
+
const functionsDir = path.join(this.dataDir, "functions");
|
|
230
|
+
if (!fs.existsSync(functionsDir)) fs.mkdirSync(functionsDir, { recursive: true });
|
|
231
|
+
|
|
232
|
+
const fileName = `${name}.js`;
|
|
233
|
+
const filePath = path.join(functionsDir, fileName);
|
|
234
|
+
|
|
235
|
+
// Salva o código no disco
|
|
236
|
+
fs.writeFileSync(filePath, code || "// Hello Lambda");
|
|
237
|
+
|
|
238
|
+
// Registra a lambda
|
|
239
|
+
const config = {
|
|
240
|
+
name,
|
|
241
|
+
handler: filePath,
|
|
242
|
+
runtime: runtime || "nodejs18.x",
|
|
243
|
+
timeout: timeout || 30,
|
|
244
|
+
memorySize: memorySize || 128,
|
|
245
|
+
env: environment || {},
|
|
246
|
+
type: "commonjs"
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
await this.registerLambda(config);
|
|
250
|
+
this.persistLambdas();
|
|
251
|
+
|
|
252
|
+
return this.lambdas.get(name);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async updateFunction(name, lambdaConfig) {
|
|
256
|
+
const lambda = this.lambdas.get(name);
|
|
257
|
+
if (!lambda) throw new Error(`Function not found: ${name}`);
|
|
258
|
+
|
|
259
|
+
const { code, runtime, handler, timeout, memorySize, environment } = lambdaConfig;
|
|
260
|
+
|
|
261
|
+
// Se houver código novo, sobrescreve o arquivo
|
|
262
|
+
if (code !== undefined) {
|
|
263
|
+
fs.writeFileSync(lambda.handlerPath, code);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Atualiza a configuração
|
|
267
|
+
const updatedConfig = {
|
|
268
|
+
name,
|
|
269
|
+
handler: lambda.handlerPath,
|
|
270
|
+
runtime: runtime || lambda.runtime,
|
|
271
|
+
timeout: timeout || lambda.timeout,
|
|
272
|
+
memorySize: memorySize || lambda.memorySize,
|
|
273
|
+
env: environment || lambda.env,
|
|
274
|
+
type: lambda.type
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
await this.registerLambda(updatedConfig);
|
|
278
|
+
this.persistLambdas();
|
|
279
|
+
|
|
280
|
+
return this.lambdas.get(name);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async deleteFunction(name) {
|
|
284
|
+
|
|
285
|
+
if (this.lambdas.has(name)) {
|
|
286
|
+
this.lambdas.delete(name);
|
|
287
|
+
this.persistLambdas();
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
persistLambdas() {
|
|
294
|
+
const functionsToSave = Array.from(this.lambdas.values())
|
|
295
|
+
.filter(l => l.handlerPath.includes(this.dataDir)) // Apenas as dinâmicas
|
|
296
|
+
.map(l => ({
|
|
297
|
+
name: l.name,
|
|
298
|
+
handler: l.handlerPath,
|
|
299
|
+
type: l.type,
|
|
300
|
+
env: l.env,
|
|
301
|
+
timeout: l.timeout,
|
|
302
|
+
memorySize: l.memorySize,
|
|
303
|
+
}));
|
|
304
|
+
|
|
305
|
+
this.store.write("__functions__", functionsToSave);
|
|
306
|
+
}
|
|
307
|
+
|
|
185
308
|
getStats() {
|
|
186
309
|
return {
|
|
187
310
|
totalLambdas: this.lambdas.size,
|
|
@@ -189,6 +312,7 @@ class LambdaSimulator {
|
|
|
189
312
|
};
|
|
190
313
|
}
|
|
191
314
|
|
|
315
|
+
|
|
192
316
|
async reset() {
|
|
193
317
|
await this.reloadLambdas();
|
|
194
318
|
this.environment = { ...process.env };
|
|
@@ -50,7 +50,7 @@ class ParameterStoreSimulator {
|
|
|
50
50
|
const storedValue = Type === 'SecureString' ? this._encrypt(Value) : Value;
|
|
51
51
|
const param = {
|
|
52
52
|
Name, Value: storedValue, Type, Description: Description || '', Version: version,
|
|
53
|
-
LastModifiedDate:
|
|
53
|
+
LastModifiedDate: Math.floor(Date.now() / 1000),
|
|
54
54
|
LastModifiedUser: 'local',
|
|
55
55
|
ARN: `arn:aws:ssm:local:000000000000:parameter${Name}`,
|
|
56
56
|
DataType, Tags: Tags || [], KeyId: Type === 'SecureString' ? (KeyId || 'aws/ssm') : undefined
|
|
@@ -23,6 +23,26 @@ class S3Server {
|
|
|
23
23
|
this.app.use(express.raw({ type: () => true, limit: '100mb' }));
|
|
24
24
|
this.app.use(express.text({ limit: '100mb' }));
|
|
25
25
|
|
|
26
|
+
// Suporte a Virtual Host Style addressing (bucket.localhost:4566)
|
|
27
|
+
this.app.use((req, res, next) => {
|
|
28
|
+
const host = req.headers.host || '';
|
|
29
|
+
// Se o host contém múltiplos pontos e não é apenas um IP ou localhost puro
|
|
30
|
+
if (host.includes('.') && !host.startsWith('localhost') && !host.startsWith('127.0.0.1')) {
|
|
31
|
+
const parts = host.split('.');
|
|
32
|
+
// O primeiro fragmento é o nome do bucket se houver mais de 2 partes (bucket.localhost:port)
|
|
33
|
+
// ou se a segunda parte for localhost
|
|
34
|
+
if (parts.length >= 2) {
|
|
35
|
+
const bucket = parts[0];
|
|
36
|
+
// Evita processar se for admin ou se já estiver no path style (heurística simples)
|
|
37
|
+
if (bucket !== 'localhost' && !req.path.startsWith('/__admin/')) {
|
|
38
|
+
req.url = `/${bucket}${req.url}`;
|
|
39
|
+
logger.verboso(`S3: Virtual Host Style detected. Rewriting to ${req.url}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
next();
|
|
44
|
+
});
|
|
45
|
+
|
|
26
46
|
// Logging de requisições
|
|
27
47
|
if (logger.currentLogLevel === 'verboso') {
|
|
28
48
|
this.app.use((req, res, next) => {
|
|
@@ -36,6 +56,7 @@ class S3Server {
|
|
|
36
56
|
}
|
|
37
57
|
}
|
|
38
58
|
|
|
59
|
+
|
|
39
60
|
async initialize() {
|
|
40
61
|
if (!this.simulator) {
|
|
41
62
|
this.simulator = new S3Simulator(this.config);
|
|
@@ -489,10 +489,13 @@ class S3Simulator {
|
|
|
489
489
|
// ─── Utilitários ──────────────────────────────────────────────────────────
|
|
490
490
|
|
|
491
491
|
isValidBucketName(bucketName) {
|
|
492
|
-
|
|
492
|
+
// S3 oficial é rigoroso, mas para simulador local vamos ser mais tolerantes
|
|
493
|
+
// permitindo underscores e letras maiúsculas que são comuns em testes locais
|
|
494
|
+
const regex = /^[a-zA-Z0-9][a-zA-Z0-9._-]{1,61}[a-zA-Z0-9]$/;
|
|
493
495
|
return regex.test(bucketName) && !bucketName.includes("..") && !bucketName.includes(".-") && !bucketName.includes("-.");
|
|
494
496
|
}
|
|
495
497
|
|
|
498
|
+
|
|
496
499
|
extractMetadata(headers) {
|
|
497
500
|
const metadata = {};
|
|
498
501
|
for (const [key, value] of Object.entries(headers)) {
|
|
@@ -37,7 +37,8 @@ class SecretManagerServer {
|
|
|
37
37
|
if (!operation) return res.status(400).json({ __type: 'UnknownOperationException', message: `Unknown: ${target}` });
|
|
38
38
|
try {
|
|
39
39
|
const result = await this.simulator[operation](req.body || {});
|
|
40
|
-
res.
|
|
40
|
+
res.setHeader('Content-Type', 'application/x-amz-json-1.1');
|
|
41
|
+
res.send(JSON.stringify(result || {}));
|
|
41
42
|
} catch (err) {
|
|
42
43
|
this.logger.error(`SecretsManager ${target}: ${err.message}`, 'secret-manager');
|
|
43
44
|
res.status(err.code === 'ResourceNotFoundException' ? 404 : 400).json({ __type: err.code || 'InternalServiceError', Message: err.message });
|
|
@@ -17,7 +17,18 @@ class SecretManagerSimulator {
|
|
|
17
17
|
async initialize() {
|
|
18
18
|
try {
|
|
19
19
|
const secrets = await this.store.read('secret-manager/secrets');
|
|
20
|
-
if (Array.isArray(secrets))
|
|
20
|
+
if (Array.isArray(secrets)) {
|
|
21
|
+
for (const s of secrets) {
|
|
22
|
+
if (typeof s.CreatedDate === 'string') s.CreatedDate = Math.floor(new Date(s.CreatedDate).getTime() / 1000);
|
|
23
|
+
if (typeof s.LastChangedDate === 'string') s.LastChangedDate = Math.floor(new Date(s.LastChangedDate).getTime() / 1000);
|
|
24
|
+
if (s._versions) {
|
|
25
|
+
for (const v of Object.values(s._versions)) {
|
|
26
|
+
if (typeof v.CreatedDate === 'string') v.CreatedDate = Math.floor(new Date(v.CreatedDate).getTime() / 1000);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
this.secrets.set(s.Name, s);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
21
32
|
this.logger.info('SecretsManager: dados carregados', 'secret-manager');
|
|
22
33
|
} catch { this.logger.debug('SecretsManager: sem dados anteriores', 'secret-manager'); }
|
|
23
34
|
}
|
|
@@ -38,12 +49,12 @@ class SecretManagerSimulator {
|
|
|
38
49
|
ARN: `arn:aws:secretsmanager:local:000000000000:secret:${Name}-${secretId.slice(0, 6)}`,
|
|
39
50
|
Name, Description: Description || '', Tags,
|
|
40
51
|
KmsKeyId: KmsKeyId || 'aws/secretsmanager',
|
|
41
|
-
CreatedDate:
|
|
42
|
-
LastChangedDate:
|
|
52
|
+
CreatedDate: Math.floor(Date.now() / 1000),
|
|
53
|
+
LastChangedDate: Math.floor(Date.now() / 1000),
|
|
43
54
|
LastAccessedDate: null,
|
|
44
55
|
RotationEnabled: false,
|
|
45
56
|
VersionsToStages: { [secretId]: ['AWSCURRENT'] },
|
|
46
|
-
_versions: { [secretId]: { SecretString, SecretBinary, CreatedDate:
|
|
57
|
+
_versions: { [secretId]: { SecretString, SecretBinary, CreatedDate: Math.floor(Date.now() / 1000) } }
|
|
47
58
|
};
|
|
48
59
|
this.secrets.set(Name, secret);
|
|
49
60
|
await this._persist();
|
|
@@ -55,7 +66,7 @@ class SecretManagerSimulator {
|
|
|
55
66
|
async getSecretValue(params) {
|
|
56
67
|
const { SecretId, VersionId, VersionStage = 'AWSCURRENT' } = params;
|
|
57
68
|
const secret = this._requireSecret(SecretId);
|
|
58
|
-
secret.LastAccessedDate =
|
|
69
|
+
secret.LastAccessedDate = Math.floor(Date.now() / 1000);
|
|
59
70
|
let versionId = VersionId;
|
|
60
71
|
if (!versionId) {
|
|
61
72
|
versionId = Object.entries(secret.VersionsToStages).find(([, stages]) => stages.includes(VersionStage))?.[0];
|
|
@@ -81,9 +92,9 @@ class SecretManagerSimulator {
|
|
|
81
92
|
secret.VersionsToStages[vid] = stages.filter(s => s !== 'AWSCURRENT').concat(['AWSPREVIOUS']);
|
|
82
93
|
}
|
|
83
94
|
}
|
|
84
|
-
secret._versions[versionId] = { SecretString, SecretBinary, CreatedDate:
|
|
95
|
+
secret._versions[versionId] = { SecretString, SecretBinary, CreatedDate: Math.floor(Date.now() / 1000) };
|
|
85
96
|
secret.VersionsToStages[versionId] = VersionStages;
|
|
86
|
-
secret.LastChangedDate =
|
|
97
|
+
secret.LastChangedDate = Math.floor(Date.now() / 1000);
|
|
87
98
|
await this._persist();
|
|
88
99
|
return { ARN: secret.ARN, Name: secret.Name, VersionId: versionId, VersionStages };
|
|
89
100
|
}
|
|
@@ -103,8 +114,8 @@ class SecretManagerSimulator {
|
|
|
103
114
|
async deleteSecret(params) {
|
|
104
115
|
const { SecretId, RecoveryWindowInDays = 30, ForceDeleteWithoutRecovery } = params;
|
|
105
116
|
const secret = this._requireSecret(SecretId);
|
|
106
|
-
const deletionDate = ForceDeleteWithoutRecovery ?
|
|
107
|
-
secret.DeletedDate =
|
|
117
|
+
const deletionDate = ForceDeleteWithoutRecovery ? Math.floor(Date.now() / 1000) : Math.floor((Date.now() + RecoveryWindowInDays * 86400000) / 1000);
|
|
118
|
+
secret.DeletedDate = Math.floor(Date.now() / 1000);
|
|
108
119
|
secret.DeletionDate = deletionDate;
|
|
109
120
|
if (ForceDeleteWithoutRecovery) this.secrets.delete(secret.Name);
|
|
110
121
|
await this._persist();
|
|
@@ -146,7 +157,7 @@ class SecretManagerSimulator {
|
|
|
146
157
|
secret.RotationEnabled = true;
|
|
147
158
|
secret.RotationLambdaARN = RotationLambdaARN;
|
|
148
159
|
secret.RotationRules = RotationRules;
|
|
149
|
-
secret.LastRotatedDate =
|
|
160
|
+
secret.LastRotatedDate = Math.floor(Date.now() / 1000);
|
|
150
161
|
await this._persist();
|
|
151
162
|
return { ARN: secret.ARN, Name: secret.Name };
|
|
152
163
|
}
|
|
@@ -81,27 +81,39 @@ function createSNSServer(simulator, config, logger) {
|
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
app.get('/__admin/topics', (_req, res) => {
|
|
84
|
-
res.json(
|
|
84
|
+
res.json(Array.from(simulator.topics.values()));
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
+
app.post('/__admin/topics', async (req, res) => {
|
|
88
|
+
try {
|
|
89
|
+
const topic = await simulator.createTopic({ Name: req.body.name });
|
|
90
|
+
res.status(201).json(topic);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
res.status(400).json({ error: err.message });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
|
|
87
97
|
app.get('/__admin/subscriptions', (_req, res) => {
|
|
88
|
-
res.json(
|
|
98
|
+
res.json(Array.from(simulator.subscriptions.values()));
|
|
89
99
|
});
|
|
90
100
|
|
|
91
101
|
app.get('/__admin/subscriptions/:topicName', (req, res) => {
|
|
92
102
|
const arn = `arn:aws:sns:us-east-1:123456789012:${req.params.topicName}`;
|
|
93
103
|
const subs = Array.from(simulator.subscriptions.values()).filter(s => s.TopicArn === arn);
|
|
94
|
-
res.json(
|
|
104
|
+
res.json(subs);
|
|
95
105
|
});
|
|
96
106
|
|
|
107
|
+
|
|
97
108
|
app.get('/__admin/publish-log', (_req, res) => {
|
|
98
|
-
res.json(
|
|
109
|
+
res.json(simulator.publishLog);
|
|
99
110
|
});
|
|
100
111
|
|
|
101
112
|
app.get('/__admin/platform-apps', (_req, res) => {
|
|
102
|
-
res.json(
|
|
113
|
+
res.json(Array.from(simulator.platformApps.values()));
|
|
103
114
|
});
|
|
104
115
|
|
|
116
|
+
|
|
105
117
|
app.delete('/__admin/topics/:topicName', async (req, res) => {
|
|
106
118
|
try {
|
|
107
119
|
const arn = `arn:aws:sns:us-east-1:123456789012:${req.params.topicName}`;
|
|
@@ -112,6 +124,21 @@ function createSNSServer(simulator, config, logger) {
|
|
|
112
124
|
}
|
|
113
125
|
});
|
|
114
126
|
|
|
127
|
+
app.post('/__admin/topics/:topicName/publish', async (req, res) => {
|
|
128
|
+
try {
|
|
129
|
+
const arn = `arn:aws:sns:us-east-1:123456789012:${req.params.topicName}`;
|
|
130
|
+
const result = await simulator.publish({
|
|
131
|
+
TopicArn: arn,
|
|
132
|
+
Subject: req.body.subject,
|
|
133
|
+
Message: req.body.message
|
|
134
|
+
});
|
|
135
|
+
res.json(result);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
res.status(400).json({ error: err.message });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
|
|
115
142
|
app.post('/__admin/reset', async (_req, res) => {
|
|
116
143
|
await simulator.reset();
|
|
117
144
|
res.json({ message: 'SNS data reset complete' });
|
|
@@ -187,7 +187,10 @@ class SQSServer {
|
|
|
187
187
|
return this.generateReceiveMessageResponse(result.messages);
|
|
188
188
|
case 'DeleteMessage':
|
|
189
189
|
return this.generateDeleteMessageResponse();
|
|
190
|
+
case 'SetQueueAttributes':
|
|
191
|
+
return this.generateSetQueueAttributesResponse();
|
|
190
192
|
case 'GetQueueUrl':
|
|
193
|
+
|
|
191
194
|
return this.generateGetQueueUrlResponse(result.queueUrl);
|
|
192
195
|
case 'ListQueues':
|
|
193
196
|
return this.generateListQueuesResponse(result.queues);
|
|
@@ -196,6 +199,14 @@ class SQSServer {
|
|
|
196
199
|
}
|
|
197
200
|
}
|
|
198
201
|
|
|
202
|
+
generateSetQueueAttributesResponse() {
|
|
203
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
204
|
+
<SetQueueAttributesResponse xmlns="http://queue.amazonaws.com/doc/2012-11-05/">
|
|
205
|
+
<ResponseMetadata><RequestId>${crypto.randomUUID()}</RequestId></ResponseMetadata>
|
|
206
|
+
</SetQueueAttributesResponse>`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
|
|
199
210
|
generateCreateQueueResponse(queueUrl) {
|
|
200
211
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
201
212
|
<CreateQueueResponse xmlns="http://queue.amazonaws.com/doc/2012-11-05/">
|