@gugananuvem/aws-local-simulator 1.0.11 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +349 -72
  2. package/package.json +12 -2
  3. package/src/config/config-loader.js +2 -0
  4. package/src/config/default-config.js +3 -0
  5. package/src/index.js +18 -2
  6. package/src/server.js +37 -31
  7. package/src/services/apigateway/index.js +10 -3
  8. package/src/services/apigateway/server.js +73 -0
  9. package/src/services/apigateway/simulator.js +13 -3
  10. package/src/services/athena/index.js +75 -0
  11. package/src/services/athena/server.js +101 -0
  12. package/src/services/athena/simulador.js +998 -0
  13. package/src/services/athena/simulator.js +346 -0
  14. package/src/services/cloudformation/index.js +106 -0
  15. package/src/services/cloudformation/server.js +417 -0
  16. package/src/services/cloudformation/simulador.js +1045 -0
  17. package/src/services/cloudtrail/index.js +84 -0
  18. package/src/services/cloudtrail/server.js +235 -0
  19. package/src/services/cloudtrail/simulador.js +719 -0
  20. package/src/services/cloudwatch/index.js +84 -0
  21. package/src/services/cloudwatch/server.js +366 -0
  22. package/src/services/cloudwatch/simulador.js +1173 -0
  23. package/src/services/cognito/index.js +5 -0
  24. package/src/services/cognito/server.js +54 -3
  25. package/src/services/cognito/simulator.js +273 -2
  26. package/src/services/config/index.js +96 -0
  27. package/src/services/config/server.js +215 -0
  28. package/src/services/config/simulador.js +1260 -0
  29. package/src/services/dynamodb/index.js +7 -3
  30. package/src/services/dynamodb/server.js +4 -2
  31. package/src/services/dynamodb/simulator.js +39 -29
  32. package/src/services/eventbridge/index.js +55 -51
  33. package/src/services/eventbridge/server.js +209 -0
  34. package/src/services/eventbridge/simulator.js +684 -0
  35. package/src/services/index.js +30 -4
  36. package/src/services/kms/index.js +75 -0
  37. package/src/services/kms/server.js +67 -0
  38. package/src/services/kms/simulator.js +324 -0
  39. package/src/services/lambda/handler-loader.js +13 -2
  40. package/src/services/lambda/index.js +7 -1
  41. package/src/services/lambda/server.js +32 -39
  42. package/src/services/lambda/simulator.js +78 -181
  43. package/src/services/parameter-store/index.js +80 -0
  44. package/src/services/parameter-store/server.js +50 -0
  45. package/src/services/parameter-store/simulator.js +201 -0
  46. package/src/services/s3/index.js +7 -3
  47. package/src/services/s3/server.js +20 -13
  48. package/src/services/s3/simulator.js +163 -407
  49. package/src/services/secret-manager/index.js +80 -0
  50. package/src/services/secret-manager/server.js +50 -0
  51. package/src/services/secret-manager/simulator.js +171 -0
  52. package/src/services/sns/index.js +55 -42
  53. package/src/services/sns/server.js +580 -0
  54. package/src/services/sns/simulator.js +1482 -0
  55. package/src/services/sqs/index.js +2 -4
  56. package/src/services/sqs/server.js +92 -18
  57. package/src/services/sqs/simulator.js +79 -298
  58. package/src/services/sts/index.js +37 -0
  59. package/src/services/sts/server.js +142 -0
  60. package/src/services/sts/simulator.js +69 -0
  61. package/src/services/xray/index.js +83 -0
  62. package/src/services/xray/server.js +308 -0
  63. package/src/services/xray/simulador.js +994 -0
  64. package/src/utils/cloudtrail-audit.js +129 -0
  65. package/src/utils/local-store.js +18 -2
@@ -2,128 +2,85 @@
2
2
  * Lambda Simulator - Simula execução de funções Lambda
3
3
  */
4
4
 
5
- const HandlerLoader = require('./handler-loader');
6
- const RouteRegistry = require('./route-registry');
7
- const logger = require('../../utils/logger');
5
+ const HandlerLoader = require("./handler-loader");
6
+ const logger = require("../../utils/logger");
7
+ const { CloudTrailAudit } = require("../../utils/cloudtrail-audit");
8
8
 
9
9
  class LambdaSimulator {
10
10
  constructor(config) {
11
11
  this.config = config;
12
- this.routeRegistry = new RouteRegistry();
13
- this.lambdas = new Map();
12
+ this.lambdas = new Map(); // functionName -> { handler, env, config }
14
13
  this.environment = { ...process.env };
14
+ this.audit = new CloudTrailAudit("lambda.amazonaws.com");
15
15
  }
16
16
 
17
17
  async initialize() {
18
- logger.debug('Inicializando Lambda Simulator...');
19
-
18
+ logger.debug("Inicializando Lambda Simulator...");
19
+
20
20
  if (this.config.lambdas && this.config.lambdas.length > 0) {
21
21
  for (const lambdaConfig of this.config.lambdas) {
22
22
  await this.registerLambda(lambdaConfig);
23
23
  }
24
24
  }
25
-
25
+
26
26
  logger.debug(`✅ ${this.lambdas.size} Lambdas registradas`);
27
27
  }
28
28
 
29
29
  async registerLambda(lambdaConfig) {
30
30
  try {
31
- const { path, handler: handlerPath, env = {}, type = 'auto' } = lambdaConfig;
32
-
33
- // Carrega o handler
31
+ const { name, handler: handlerPath, env = {}, type = "auto" } = lambdaConfig;
32
+
33
+ if (!name) {
34
+ logger.warn(`Lambda sem nome ignorada: ${JSON.stringify(lambdaConfig)}`);
35
+ return;
36
+ }
37
+
34
38
  const handler = await HandlerLoader.load(handlerPath, type);
35
-
36
- // Registra no route registry
37
- this.routeRegistry.register(path, handler, env);
38
-
39
- // Armazena metadata
40
- this.lambdas.set(path, {
41
- path,
42
- handler,
43
- handlerPath,
44
- handlerName: handler.name || 'anonymous',
45
- env,
46
- type,
47
- registeredAt: new Date().toISOString()
48
- });
49
-
50
- logger.debug(`✅ Lambda registrada: ${path} -> ${handler.name || 'anonymous'}`);
51
-
52
- } catch (error) {
53
- logger.error(`❌ Erro ao registrar Lambda ${lambdaConfig.path}:`, error);
54
- throw error;
39
+ if (handler != undefined) {
40
+ this.lambdas.set(name, {
41
+ name,
42
+ handler,
43
+ handlerPath,
44
+ handlerName: handler.name || "anonymous",
45
+ env,
46
+ type,
47
+ registeredAt: new Date().toISOString(),
48
+ });
49
+ }
50
+ logger.debug(`✅ Lambda registrada: ${name} -> ${handlerPath}`);
51
+ } catch (error) {
52
+ if (error.message.indexOf("Handler não encontrado") == -1){
53
+ logger.error(`Erro ao registrar Lambda ${lambdaConfig.name}:`, error);
54
+ throw error;
55
+ }else{
56
+ logger.error(`Erro ao registrar Lambda ${lambdaConfig.name}:`);
57
+ }
55
58
  }
56
59
  }
57
60
 
58
- async handleRequest(req, res) {
59
- const matchedRoute = this.routeRegistry.find(req.path);
60
-
61
- if (!matchedRoute) {
62
- return {
63
- error: {
64
- statusCode: 404,
65
- message: `Route not found: ${req.path}`,
66
- availableRoutes: this.listRoutes()
67
- },
68
- status: 404
69
- };
61
+ async invoke(functionName, event, invocationType = "RequestResponse") {
62
+ const lambda = this.lambdas.get(functionName);
63
+
64
+ if (!lambda) {
65
+ throw new Error(`Function not found: ${functionName}`);
70
66
  }
71
-
72
- // Aplica variáveis de ambiente específicas da rota
73
- this.applyEnvironment(matchedRoute.env);
74
-
75
- // Prepara evento Lambda
76
- const event = this.toLambdaEvent(req, matchedRoute.params);
77
-
78
- logger.debug(`🎯 Executando: ${matchedRoute.path} -> ${matchedRoute.handler.name || 'anonymous'}`);
79
-
80
- // Executa middlewares
81
- const middlewares = this.routeRegistry.getMiddlewares(matchedRoute);
82
- let handled = false;
83
- let result = null;
84
-
85
- const runMiddlewares = async (index) => {
86
- if (index >= middlewares.length) {
87
- // Executa handler
88
67
 
89
- result = await this.executeHandler(matchedRoute.handler, event);
90
- handled = true;
91
-
92
- console.log(`✅ Resposta: ${result.statusCode}`);
93
- res
94
- .status(result.statusCode || 200)
95
- .set(result.headers || {})
96
- .send(result.body ? JSON.parse(result.body) : null);
97
- return;
98
- }
99
-
100
- const middleware = middlewares[index];
101
- await new Promise((resolve, reject) => {
102
- middleware(event, {
103
- status: (code) => ({ json: (data) => {
104
- result = { statusCode: code, body: data };
105
- handled = true;
106
- resolve();
107
- }}),
108
- send: (data) => {
109
- result = { statusCode: 200, body: data };
110
- handled = true;
111
- resolve();
112
- },
113
- next: () => {
114
- runMiddlewares(index + 1).then(resolve).catch(reject);
115
- }
116
- });
117
- });
118
- };
119
-
120
- await runMiddlewares(0);
121
-
122
- if (!handled && result) {
123
- return this.formatResponse(result);
68
+ this.applyEnvironment(lambda.env);
69
+ logger.debug(`🎯 Invocando Lambda: ${functionName}`);
70
+
71
+ if (invocationType === "Event") {
72
+ this.executeHandler(lambda.handler, event).catch((err) => logger.error(`❌ Async Lambda error (${functionName}):`, err));
73
+ return { StatusCode: 202 };
124
74
  }
125
75
 
126
- return null;
76
+ const result = await this.executeHandler(lambda.handler, event);
77
+ this.audit.record({
78
+ eventName: "Invoke",
79
+ readOnly: false,
80
+ resources: [{ ARN: `arn:aws:lambda:local:000000000000:function:${functionName}`, type: "AWS::Lambda::Function" }],
81
+ requestParameters: { functionName, invocationType },
82
+ });
83
+ return { StatusCode: result.statusCode || 200, Payload: result };
127
84
  }
128
85
 
129
86
  async executeHandler(handler, event) {
@@ -132,67 +89,27 @@ class LambdaSimulator {
132
89
  const result = await handler(event, context);
133
90
  return result;
134
91
  } catch (error) {
135
- logger.error('❌ Erro no handler:', error);
92
+ logger.error("❌ Erro no handler:", error);
136
93
  return {
137
94
  statusCode: 500,
138
- body: {
139
- error: 'Internal Server Error',
140
- message: error.message,
141
- stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
142
- }
95
+ body: JSON.stringify({ error: "Internal Server Error", message: error.message }),
143
96
  };
144
97
  }
145
98
  }
146
99
 
147
- toLambdaEvent(req, params = {}) {
148
- return {
149
- httpMethod: req.method,
150
- path: req.path,
151
- headers: req.headers,
152
- queryStringParameters: req.query,
153
- pathParameters: params,
154
- body: req.body ? (typeof req.body === 'string' ? req.body : JSON.stringify(req.body)) : null,
155
- isBase64Encoded: false,
156
- requestContext: {
157
- path: req.path,
158
- stage: process.env.STAGE_NAME || 'dev',
159
- requestId: Math.random().toString(36).substring(7),
160
- identity: {
161
- sourceIp: req.ip,
162
- userAgent: req.headers['user-agent']
163
- }
164
- },
165
- stageVariables: {},
166
- resource: req.path
167
- };
168
- }
169
-
170
- formatResponse(result) {
171
- const statusCode = result.statusCode || 200;
172
- const body = result.body;
173
- const headers = result.headers || { 'Content-Type': 'application/json' };
174
-
175
- return {
176
- statusCode,
177
- headers,
178
- body: typeof body === 'string' ? body : JSON.stringify(body),
179
- isBase64Encoded: false
180
- };
181
- }
182
-
183
100
  createContext() {
184
101
  return {
185
102
  awsRequestId: Math.random().toString(36).substring(7),
186
- functionName: 'local-lambda',
187
- functionVersion: '$LATEST',
188
- invokedFunctionArn: 'arn:aws:lambda:local:function',
189
- memoryLimitInMB: '1024',
190
- logGroupName: '/aws/lambda/local-lambda',
191
- logStreamName: 'local-stream',
103
+ functionName: "local-lambda",
104
+ functionVersion: "$LATEST",
105
+ invokedFunctionArn: "arn:aws:lambda:local:000000000000:function:local-lambda",
106
+ memoryLimitInMB: "1024",
107
+ logGroupName: "/aws/lambda/local-lambda",
108
+ logStreamName: "local-stream",
192
109
  getRemainingTimeInMillis: () => 30000,
193
110
  callbackWaitsForEmptyEventLoop: true,
194
111
  identity: null,
195
- clientContext: null
112
+ clientContext: null,
196
113
  };
197
114
  }
198
115
 
@@ -213,22 +130,18 @@ class LambdaSimulator {
213
130
  }
214
131
 
215
132
  listLambdas() {
216
- return Array.from(this.lambdas.values()).map(l => ({
217
- path: l.path,
133
+ return Array.from(this.lambdas.values()).map((l) => ({
134
+ name: l.name,
218
135
  handlerName: l.handlerName,
219
136
  handlerPath: l.handlerPath,
220
137
  type: l.type,
221
138
  env: l.env,
222
- registeredAt: l.registeredAt
139
+ registeredAt: l.registeredAt,
223
140
  }));
224
141
  }
225
142
 
226
- getLambda(path) {
227
- return this.lambdas.get(path);
228
- }
229
-
230
- listRoutes() {
231
- return this.routeRegistry.list();
143
+ getLambda(name) {
144
+ return this.lambdas.get(name);
232
145
  }
233
146
 
234
147
  getLambdasCount() {
@@ -236,50 +149,34 @@ class LambdaSimulator {
236
149
  }
237
150
 
238
151
  async reloadLambdas() {
239
- logger.info('🔄 Recarregando Lambdas...');
240
-
241
- for (const [path, lambda] of this.lambdas.entries()) {
152
+ logger.info("🔄 Recarregando Lambdas...");
153
+
154
+ for (const [name, lambda] of this.lambdas.entries()) {
242
155
  try {
243
156
  const newHandler = await HandlerLoader.reload(lambda.handlerPath, lambda.type);
244
- this.routeRegistry.register(path, newHandler, lambda.env);
245
157
  lambda.handler = newHandler;
246
- lambda.handlerName = newHandler.name || 'anonymous';
247
- logger.debug(`✅ Lambda recarregada: ${path}`);
158
+ lambda.handlerName = newHandler.name || "anonymous";
159
+ logger.debug(`✅ Lambda recarregada: ${name}`);
248
160
  } catch (error) {
249
- logger.error(`❌ Erro ao recarregar Lambda ${path}:`, error);
161
+ logger.error(`❌ Erro ao recarregar Lambda ${name}:`, error);
250
162
  }
251
163
  }
252
-
164
+
253
165
  logger.info(`✅ ${this.lambdas.size} Lambdas recarregadas`);
254
166
  }
255
167
 
256
168
  getStats() {
257
- const lambdas = this.listLambdas();
258
169
  return {
259
- totalLambdas: lambdas.length,
260
- lambdas: lambdas.map(l => ({
261
- path: l.path,
262
- handler: l.handlerName
263
- })),
264
- routes: this.routeRegistry.getStats(),
265
- environment: Object.keys(this.environment).length
170
+ totalLambdas: this.lambdas.size,
171
+ lambdas: this.listLambdas().map((l) => ({ name: l.name, handler: l.handlerName })),
266
172
  };
267
173
  }
268
174
 
269
175
  async reset() {
270
- // Recarrega Lambdas
271
176
  await this.reloadLambdas();
272
-
273
- // Limpa variáveis de ambiente customizadas
274
- for (const key of Object.keys(this.environment)) {
275
- if (!process.env.hasOwnProperty(key) || key.startsWith('AWS_LOCAL_SIMULATOR_')) {
276
- delete process.env[key];
277
- }
278
- }
279
-
280
177
  this.environment = { ...process.env };
281
- logger.debug('Lambda: Estado resetado');
178
+ logger.debug("Lambda: Estado resetado");
282
179
  }
283
180
  }
284
181
 
285
- module.exports = LambdaSimulator;
182
+ module.exports = LambdaSimulator;
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @fileoverview Parameter Store Service
5
+ * Porta padrão: 4002
6
+ */
7
+
8
+ const http = require('http');
9
+ const path = require('path');
10
+ const { ParameterStoreSimulator } = require('./simulator');
11
+ const { ParameterStoreServer } = require('./server');
12
+ const LocalStore = require('../../utils/local-store');
13
+
14
+ class ParameterStoreService {
15
+ constructor(config) {
16
+ this.config = config;
17
+ this.logger = require('../../utils/logger');
18
+ this.name = 'parameter-store';
19
+ this.port = config?.ports?.parameterStore || config?.services?.parameterStore?.port || 4002;
20
+ this.store = null;
21
+ this.simulator = null;
22
+ this.httpServer = null;
23
+ this.isRunning = false;
24
+ }
25
+
26
+ async initialize() {
27
+ this.logger.debug(`Inicializando Parameter Store Service na porta ${this.port}...`);
28
+ const dataDir = process.env.AWS_LOCAL_SIMULATOR_DATA_DIR;
29
+ this.store = new LocalStore(path.join(dataDir, 'parameter-store'));
30
+ this.simulator = new ParameterStoreSimulator(this.store, this.logger, this.config);
31
+ await this.simulator.initialize();
32
+ this.app = new ParameterStoreServer(this.simulator, this.logger, this.config).getApp();
33
+ this.logger.debug('Parameter Store Service inicializado');
34
+ }
35
+
36
+ injectDependencies(server) {
37
+ const ct = server.getService('cloudtrail');
38
+ if (ct?.simulator) this.simulator.audit.setTrail(ct.simulator);
39
+ }
40
+
41
+ async start() {
42
+ if (this.isRunning) return;
43
+ return new Promise((resolve, reject) => {
44
+ this.httpServer = http.createServer(this.app);
45
+ this.httpServer.listen(this.port, () => {
46
+ this.isRunning = true;
47
+ this.logger.debug(`Parameter Store rodando na porta ${this.port}`);
48
+ resolve();
49
+ });
50
+ this.httpServer.on('error', reject);
51
+ });
52
+ }
53
+
54
+ async stop() {
55
+ if (!this.isRunning || !this.httpServer) return;
56
+ return new Promise((resolve) => {
57
+ this.httpServer.close(() => {
58
+ this.isRunning = false;
59
+ resolve();
60
+ });
61
+ });
62
+ }
63
+
64
+ async reset() {
65
+ await this.simulator.reset();
66
+ }
67
+
68
+ getStatus() {
69
+ return {
70
+ running: this.isRunning,
71
+ port: this.port,
72
+ endpoint: `http://localhost:${this.port}`,
73
+ parameters: this.simulator?.parameters.size || 0,
74
+ };
75
+ }
76
+
77
+ getSimulator() { return this.simulator; }
78
+ }
79
+
80
+ module.exports = { ParameterStoreService };
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ const express = require('express');
4
+ const cors = require('cors');
5
+
6
+ class ParameterStoreServer {
7
+ constructor(simulator, logger, config) {
8
+ this.simulator = simulator; this.logger = logger; this.config = config;
9
+ this.app = express();
10
+ this._setupMiddleware(); this._setupRoutes();
11
+ }
12
+ _setupMiddleware() {
13
+ if (this.config.cors?.enabled !== false) this.app.use(cors({ origin: this.config.cors?.origin || '*' }));
14
+ this.app.use(express.json({ limit: '5mb', type: ['application/json', 'application/x-amz-json-1.1'] }));
15
+ }
16
+ _getOperation(target) {
17
+ const map = {
18
+ 'AmazonSSM.PutParameter': 'putParameter',
19
+ 'AmazonSSM.GetParameter': 'getParameter',
20
+ 'AmazonSSM.GetParameters': 'getParameters',
21
+ 'AmazonSSM.GetParametersByPath': 'getParametersByPath',
22
+ 'AmazonSSM.DeleteParameter': 'deleteParameter',
23
+ 'AmazonSSM.DeleteParameters': 'deleteParameters',
24
+ 'AmazonSSM.DescribeParameters': 'describeParameters',
25
+ 'AmazonSSM.GetParameterHistory': 'getParameterHistory',
26
+ 'AmazonSSM.AddTagsToResource': 'addTagsToResource',
27
+ 'AmazonSSM.RemoveTagsFromResource': 'removeTagsFromResource',
28
+ };
29
+ return map[target];
30
+ }
31
+ _setupRoutes() {
32
+ this.app.get('/__admin/health', (req, res) => res.json({ status: 'healthy', service: 'parameter-store', timestamp: new Date().toISOString() }));
33
+ this.app.post('/', async (req, res) => {
34
+ const target = req.headers['x-amz-target'];
35
+ const operation = this._getOperation(target);
36
+ if (!operation) return res.status(400).json({ __type: 'InvalidAction', message: `Unknown: ${target}` });
37
+ try {
38
+ const result = await this.simulator[operation](req.body || {});
39
+ res.json(result || {});
40
+ } catch (err) {
41
+ this.logger.error(`ParameterStore ${target}: ${err.message}`, 'parameter-store');
42
+ const statusCodes = { ParameterNotFound: 400, ParameterAlreadyExists: 400, ParameterPatternMismatch: 400 };
43
+ res.status(statusCodes[err.code] || 500).json({ __type: err.code || 'InternalServerError', message: err.message });
44
+ }
45
+ });
46
+ }
47
+ getApp() { return this.app; }
48
+ }
49
+
50
+ module.exports = { ParameterStoreServer };
@@ -0,0 +1,201 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const { v4: uuidv4 } = require('uuid');
5
+ const { CloudTrailAudit } = require('../../utils/cloudtrail-audit');
6
+
7
+ /**
8
+ * Parameter Store (SSM) Simulator
9
+ */
10
+ class ParameterStoreSimulator {
11
+ constructor(store, logger, config) {
12
+ this.store = store; this.logger = logger; this.config = config;
13
+ this.parameters = new Map();
14
+ this.history = new Map();
15
+ this.audit = new CloudTrailAudit('ssm.amazonaws.com');
16
+ }
17
+
18
+ async initialize() {
19
+ try {
20
+ const params = await this.store.read('parameter-store/parameters');
21
+ if (Array.isArray(params)) for (const p of params) this.parameters.set(p.Name, p);
22
+ const history = await this.store.read('parameter-store/history');
23
+ if (Array.isArray(history)) for (const h of history) this.history.set(h.name, h.versions);
24
+ this.logger.info('ParameterStore: dados carregados', 'parameter-store');
25
+ } catch { this.logger.debug('ParameterStore: sem dados anteriores', 'parameter-store'); }
26
+ }
27
+
28
+ async _persist() {
29
+ await this.store.write('parameter-store/parameters', null, Array.from(this.parameters.values()));
30
+ const histArr = Array.from(this.history.entries()).map(([name, versions]) => ({ name, versions }));
31
+ await this.store.write('parameter-store/history', null, histArr);
32
+ }
33
+
34
+ _require(name) {
35
+ const p = this.parameters.get(name);
36
+ if (!p) { const err = new Error(`Parameter not found: ${name}`); err.code = 'ParameterNotFound'; throw err; }
37
+ return p;
38
+ }
39
+
40
+ async putParameter(params) {
41
+ const { Name, Value, Type = 'String', Description, Overwrite, Tags = [], KeyId, AllowedPattern, DataType = 'text' } = params;
42
+ if (this.parameters.has(Name) && !Overwrite) {
43
+ const err = new Error(`Parameter already exists: ${Name}`); err.code = 'ParameterAlreadyExists'; throw err;
44
+ }
45
+ if (AllowedPattern && !new RegExp(AllowedPattern).test(Value)) {
46
+ const err = new Error(`Value doesn't match pattern: ${AllowedPattern}`); err.code = 'ParameterPatternMismatch'; throw err;
47
+ }
48
+ const existing = this.parameters.get(Name);
49
+ const version = existing ? existing.Version + 1 : 1;
50
+ const storedValue = Type === 'SecureString' ? this._encrypt(Value) : Value;
51
+ const param = {
52
+ Name, Value: storedValue, Type, Description: Description || '', Version: version,
53
+ LastModifiedDate: new Date().toISOString(),
54
+ LastModifiedUser: 'local',
55
+ ARN: `arn:aws:ssm:local:000000000000:parameter${Name}`,
56
+ DataType, Tags: Tags || [], KeyId: Type === 'SecureString' ? (KeyId || 'aws/ssm') : undefined
57
+ };
58
+ this.parameters.set(Name, param);
59
+ // History
60
+ if (!this.history.has(Name)) this.history.set(Name, []);
61
+ this.history.get(Name).push({ ...param, Value });
62
+ await this._persist();
63
+ this.logger.info(`ParameterStore: parâmetro definido: ${Name}`, 'parameter-store');
64
+ this.audit.record({ eventName: 'PutParameter', readOnly: false, isDataEvent: true, resources: [{ ARN: param.ARN, type: 'AWS::SSM::Parameter' }], requestParameters: { name: Name, type: Type } });
65
+ return { Version: version, Tier: 'Standard' };
66
+ }
67
+
68
+ async getParameter(params) {
69
+ const { Name, WithDecryption } = params;
70
+ const param = this._require(Name);
71
+ const value = (WithDecryption && param.Type === 'SecureString') ? this._decrypt(param.Value) : param.Value;
72
+ this.audit.record({ eventName: 'GetParameter', readOnly: true, isDataEvent: true, resources: [{ ARN: param.ARN, type: 'AWS::SSM::Parameter' }], requestParameters: { name: Name } });
73
+ return { Parameter: { ...param, Value: value } };
74
+ }
75
+
76
+ async getParameters(params) {
77
+ const { Names, WithDecryption } = params;
78
+ const found = []; const invalid = [];
79
+ for (const name of Names) {
80
+ try {
81
+ const param = this._require(name);
82
+ const value = (WithDecryption && param.Type === 'SecureString') ? this._decrypt(param.Value) : param.Value;
83
+ found.push({ ...param, Value: value });
84
+ } catch { invalid.push(name); }
85
+ }
86
+ return { Parameters: found, InvalidParameters: invalid };
87
+ }
88
+
89
+ async getParametersByPath(params) {
90
+ const { Path, Recursive, WithDecryption, MaxResults = 10, NextToken, ParameterFilters = [] } = params;
91
+ let results = Array.from(this.parameters.values()).filter(p => {
92
+ if (Recursive) return p.Name.startsWith(Path);
93
+ const rest = p.Name.slice(Path.length);
94
+ return p.Name.startsWith(Path) && !rest.includes('/');
95
+ });
96
+ for (const filter of ParameterFilters) {
97
+ if (filter.Key === 'Type') results = results.filter(p => filter.Values.includes(p.Type));
98
+ }
99
+ let startIdx = 0;
100
+ if (NextToken) startIdx = parseInt(NextToken);
101
+ const slice = results.slice(startIdx, startIdx + MaxResults);
102
+ return {
103
+ Parameters: slice.map(p => ({
104
+ ...p, Value: (WithDecryption && p.Type === 'SecureString') ? this._decrypt(p.Value) : p.Value
105
+ })),
106
+ NextToken: results.length > startIdx + MaxResults ? String(startIdx + MaxResults) : undefined
107
+ };
108
+ }
109
+
110
+ async deleteParameter(params) {
111
+ const p = this._require(params.Name);
112
+ this.parameters.delete(params.Name);
113
+ await this._persist();
114
+ this.audit.record({ eventName: 'DeleteParameter', readOnly: false, resources: [{ ARN: p.ARN, type: 'AWS::SSM::Parameter' }], requestParameters: { name: params.Name } });
115
+ return {};
116
+ }
117
+
118
+ async deleteParameters(params) {
119
+ const { Names } = params;
120
+ const deleted = []; const invalid = [];
121
+ for (const name of Names) {
122
+ if (this.parameters.has(name)) { this.parameters.delete(name); deleted.push(name); }
123
+ else invalid.push(name);
124
+ }
125
+ await this._persist();
126
+ return { DeletedParameters: deleted, InvalidParameters: invalid };
127
+ }
128
+
129
+ async describeParameters(params) {
130
+ const { Filters = [], ParameterFilters = [], MaxResults = 50, NextToken } = params || {};
131
+ let results = Array.from(this.parameters.values());
132
+ for (const f of Filters) {
133
+ if (f.Key === 'Name') results = results.filter(p => f.Values.some(v => p.Name.includes(v)));
134
+ if (f.Key === 'Type') results = results.filter(p => f.Values.includes(p.Type));
135
+ }
136
+ let startIdx = 0;
137
+ if (NextToken) startIdx = parseInt(NextToken);
138
+ const slice = results.slice(startIdx, startIdx + MaxResults);
139
+ return {
140
+ Parameters: slice.map(({ Value, ...p }) => p),
141
+ NextToken: results.length > startIdx + MaxResults ? String(startIdx + MaxResults) : undefined
142
+ };
143
+ }
144
+
145
+ async getParameterHistory(params) {
146
+ const { Name, MaxResults = 50, NextToken } = params;
147
+ this._require(Name);
148
+ const history = this.history.get(Name) || [];
149
+ let startIdx = 0;
150
+ if (NextToken) startIdx = parseInt(NextToken);
151
+ const slice = history.slice(startIdx, startIdx + MaxResults);
152
+ return {
153
+ Parameters: slice,
154
+ NextToken: history.length > startIdx + MaxResults ? String(startIdx + MaxResults) : undefined
155
+ };
156
+ }
157
+
158
+ async addTagsToResource(params) {
159
+ const { ResourceType, ResourceId, Tags } = params;
160
+ if (ResourceType === 'Parameter') {
161
+ const param = this._require(ResourceId);
162
+ for (const tag of Tags) { const idx = param.Tags.findIndex(t => t.Key === tag.Key); if (idx >= 0) param.Tags[idx] = tag; else param.Tags.push(tag); }
163
+ await this._persist();
164
+ }
165
+ return {};
166
+ }
167
+
168
+ async removeTagsFromResource(params) {
169
+ const { ResourceType, ResourceId, TagKeys } = params;
170
+ if (ResourceType === 'Parameter') {
171
+ const param = this._require(ResourceId);
172
+ param.Tags = param.Tags.filter(t => !TagKeys.includes(t.Key));
173
+ await this._persist();
174
+ }
175
+ return {};
176
+ }
177
+
178
+ _encrypt(value) {
179
+ const key = crypto.createHash('sha256').update('local-ssm-key').digest();
180
+ const iv = crypto.randomBytes(12);
181
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
182
+ const enc = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]);
183
+ const tag = cipher.getAuthTag();
184
+ return Buffer.concat([iv, tag, enc]).toString('base64');
185
+ }
186
+
187
+ _decrypt(encrypted) {
188
+ try {
189
+ const key = crypto.createHash('sha256').update('local-ssm-key').digest();
190
+ const buf = Buffer.from(encrypted, 'base64');
191
+ const iv = buf.slice(0, 12); const tag = buf.slice(12, 28); const enc = buf.slice(28);
192
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
193
+ decipher.setAuthTag(tag);
194
+ return Buffer.concat([decipher.update(enc), decipher.final()]).toString('utf8');
195
+ } catch { return encrypted; }
196
+ }
197
+
198
+ async reset() { this.parameters.clear(); this.history.clear(); await this.store.clear('parameter-store'); }
199
+ }
200
+
201
+ module.exports = { ParameterStoreSimulator };